-
Notifications
You must be signed in to change notification settings - Fork 0
Release v0.2.0: Schematic Explorer, Theme System, E2E Tests #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,8 +17,6 @@ use tracing::info; | |
|
|
||
| use crate::state::AppState; | ||
|
|
||
| type SymbolRow = (String, String, String, i64, i64, Option<String>); | ||
|
|
||
| #[derive(Deserialize)] | ||
| struct AskRequest { | ||
| question: String, | ||
|
|
@@ -72,24 +70,35 @@ async fn ask_stream( | |
| }).into_response(); | ||
| } | ||
|
|
||
| // Build context from selected symbols | ||
| // Build context from selected symbols (batch query) | ||
| let mut context_parts = Vec::new(); | ||
| if !body.context_symbol_ids.is_empty() { | ||
| let conn = state.db.connection(); | ||
| for sid in &body.context_symbol_ids { | ||
| let result: Result<SymbolRow, _> = conn.query_row( | ||
| "SELECT s.name, s.kind, f.path, s.start_line, s.end_line, s.signature | ||
| FROM symbols s JOIN files f ON s.file_id = f.id WHERE s.id = ?1", | ||
| [sid], | ||
| |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, row.get(5)?)), | ||
| ); | ||
| if let Ok((name, kind, path, start, end, sig)) = result { | ||
| context_parts.push(format!( | ||
| "- {} `{}` in `{}` (lines {}-{}){}", | ||
| kind, name, path, start, end, | ||
| sig.map(|s| format!("\n Signature: {}", s)).unwrap_or_default() | ||
| )); | ||
| } | ||
| let placeholders: Vec<String> = body.context_symbol_ids.iter().enumerate().map(|(i, _)| format!("?{}", i + 1)).collect(); | ||
| let sql = format!( | ||
| "SELECT s.name, s.kind, f.path, s.start_line, s.end_line, s.signature \ | ||
| FROM symbols s JOIN files f ON s.file_id = f.id \ | ||
| WHERE s.id IN ({})", | ||
| placeholders.join(", ") | ||
| ); | ||
| let mut stmt = conn.prepare(&sql).unwrap(); | ||
| let params: Vec<&dyn rusqlite::types::ToSql> = body.context_symbol_ids.iter().map(|id| id as &dyn rusqlite::types::ToSql).collect(); | ||
| let rows = stmt.query_map(params.as_slice(), |row| { | ||
| Ok(( | ||
| row.get::<_, String>(0)?, | ||
| row.get::<_, String>(1)?, | ||
| row.get::<_, String>(2)?, | ||
| row.get::<_, i64>(3)?, | ||
| row.get::<_, i64>(4)?, | ||
| row.get::<_, Option<String>>(5)?, | ||
| )) | ||
| }).unwrap(); | ||
|
Comment on lines
+84
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of |
||
| for (name, kind, path, start, end, sig) in rows.flatten() { | ||
| context_parts.push(format!( | ||
| "- {} `{}` in `{}` (lines {}-{}){}", | ||
| kind, name, path, start, end, | ||
| sig.map(|s| format!("\n Signature: {}", s)).unwrap_or_default() | ||
| )); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,16 +17,29 @@ use crate::state::AppState; | |
| #[derive(Deserialize)] | ||
| pub struct FileListQuery { | ||
| pub language: Option<String>, | ||
| pub limit: Option<i64>, | ||
| pub offset: Option<i64>, | ||
| } | ||
|
|
||
| /// GET /api/v1/files — List all files, optional ?language= filter | ||
| /// GET /api/v1/files — List files with pagination, optional ?language= filter | ||
| async fn list_files( | ||
| State(state): State<AppState>, | ||
| Query(query): Query<FileListQuery>, | ||
| ) -> Result<Json<Vec<FileRow>>, ApiError> { | ||
| ) -> Result<Json<serde_json::Value>, ApiError> { | ||
| let limit = query.limit.unwrap_or(50).clamp(1, 200); | ||
| let offset = query.offset.unwrap_or(0).max(0); | ||
| let cache_key = format!("files:l={:?}:l={}:o={}", query.language, limit, offset); | ||
|
|
||
| if let Some(cached) = state.cache.json.get(&cache_key) { | ||
| return Ok(Json(cached)); | ||
| } | ||
|
|
||
| let repo = FileRepo::new(Arc::clone(&state.db)); | ||
| let files = repo.list(query.language.as_deref())?; | ||
| Ok(Json(files)) | ||
| let files = repo.list_paginated(query.language.as_deref(), limit, offset)?; | ||
| let value = serde_json::to_value(&files) | ||
| .map_err(|e| ApiError::from(codeilus_core::error::CodeilusError::Internal(e.to_string())))?; | ||
| state.cache.json.insert(cache_key, value.clone()); | ||
| Ok(Json(value)) | ||
| } | ||
|
|
||
| /// GET /api/v1/files/:id — Get a single file by ID | ||
|
|
@@ -83,24 +96,32 @@ async fn get_file_source( | |
| message: "No repository has been analyzed".to_string(), | ||
| })?; | ||
|
|
||
| // Resolve the file path relative to repo root | ||
| // Resolve the file path — handle both relative and absolute paths | ||
| let clean_path = file.path.strip_prefix("./").unwrap_or(&file.path); | ||
| let full_path = repo_root.join(clean_path); | ||
| let full_path = if std::path::Path::new(clean_path).is_absolute() { | ||
| std::path::PathBuf::from(clean_path) | ||
| } else { | ||
| repo_root.join(clean_path) | ||
| }; | ||
|
|
||
| // Canonicalize and verify the path stays within repo root (prevent path traversal) | ||
| // Canonicalize and verify the path exists and is safe | ||
| let canonical = full_path.canonicalize().map_err(|e| ApiError { | ||
| status: StatusCode::NOT_FOUND, | ||
| message: format!("Could not resolve file path: {}", e), | ||
| })?; | ||
| let canonical_root = repo_root.canonicalize().map_err(|e| ApiError { | ||
| status: StatusCode::INTERNAL_SERVER_ERROR, | ||
| message: format!("Could not resolve repo root: {}", e), | ||
| })?; | ||
| if !canonical.starts_with(&canonical_root) { | ||
| return Err(ApiError { | ||
| status: StatusCode::FORBIDDEN, | ||
| message: "Path traversal detected".to_string(), | ||
| }); | ||
|
|
||
| // For relative paths, verify within repo root (prevent traversal) | ||
| if !std::path::Path::new(clean_path).is_absolute() { | ||
| let canonical_root = repo_root.canonicalize().map_err(|e| ApiError { | ||
| status: StatusCode::INTERNAL_SERVER_ERROR, | ||
| message: format!("Could not resolve repo root: {}", e), | ||
| })?; | ||
| if !canonical.starts_with(&canonical_root) { | ||
| return Err(ApiError { | ||
| status: StatusCode::FORBIDDEN, | ||
| message: "Path traversal detected".to_string(), | ||
| }); | ||
| } | ||
| } | ||
|
Comment on lines
+114
to
125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic introduces a path traversal vulnerability. By providing an absolute path, an attacker can bypass the directory traversal check because the check // Verify path is safe and within repo root (prevent traversal)
let canonical_root = repo_root.canonicalize().map_err(|e| ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: format!("Could not resolve repo root: {}", e),
})?;
if !canonical.starts_with(&canonical_root) {
return Err(ApiError {
status: StatusCode::FORBIDDEN,
message: "Path traversal detected".to_string(),
});
} |
||
|
|
||
| let content = std::fs::read_to_string(&canonical).map_err(|e| { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The build command includes a hardcoded absolute path to a user's home directory. This will not work for other developers or in a CI environment. It should be replaced with a relative path or a placeholder variable to make the instructions more generic and portable.