From f82581a62a7b896f9119acf117525d73b4e94098 Mon Sep 17 00:00:00 2001 From: Amp Date: Tue, 15 Sep 2026 18:03:40 +0000 Subject: [PATCH] fix: skip scanner symlink cycles and map compile_batch panics Library and source scanners no longer follow directory or file symlinks, and skip target/.git/build trees. compile_batch reports worker panics as CompileError instead of unwinding. Amp-Thread-ID: https://ampcode.com/threads/T-01a0a631-cbad-764e-a59c-662f4c6e0902 Co-authored-by: Max Carter --- src/compiler.rs | 11 ++++++++++- src/detector.rs | 24 +++++++++++++++++++++++- src/scanner.rs | 39 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/compiler.rs b/src/compiler.rs index a5fa25c..ae2b730 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -245,7 +245,16 @@ pub fn compile_batch( scope.spawn(move || compile_to_c_with_lang(&path, &output_dir, lang)) }) .collect(); - handles.into_iter().map(|h| h.join().unwrap()).collect() + handles + .into_iter() + .map(|h| { + h.join().unwrap_or_else(|_| { + Err(CompileError::Io(std::io::Error::other( + "compile_batch worker thread panicked", + ))) + }) + }) + .collect() }) } diff --git a/src/detector.rs b/src/detector.rs index 305e293..45cb3ce 100644 --- a/src/detector.rs +++ b/src/detector.rs @@ -304,7 +304,15 @@ pub fn scan_directory(dir: &Path) -> Vec<(std::path::PathBuf, Language)> { if let Ok(entries) = std::fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); - if path.is_dir() { + let file_type = match entry.file_type() { + Ok(ft) => ft, + Err(_) => continue, + }; + // Do not follow directory symlinks (avoids cycles and unexpected trees). + if file_type.is_symlink() { + continue; + } + if file_type.is_dir() { // Skip common non-source directories let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); if !matches!( @@ -494,4 +502,18 @@ mod tests { assert_eq!(results.len(), 1); assert_eq!(results[0].1, Language::Zig); } + + #[cfg(unix)] + #[test] + fn test_scan_directory_skips_symlinked_dirs() { + let dir = tempdir().unwrap(); + let real = dir.path().join("real"); + std::fs::create_dir(&real).unwrap(); + std::fs::write(real.join("lib.c"), "").unwrap(); + std::os::unix::fs::symlink(&real, dir.path().join("link")).unwrap(); + + let results = scan_directory(dir.path()); + assert_eq!(results.len(), 1); + assert!(results[0].0.ends_with("lib.c")); + } } diff --git a/src/scanner.rs b/src/scanner.rs index 1454fe2..1922b8c 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -99,13 +99,33 @@ impl LibraryScanner { let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; let path = entry.path(); - if path.is_file() { + let file_type = match entry.file_type() { + Ok(ft) => ft, + Err(_) => continue, + }; + if file_type.is_symlink() { + continue; + } + if file_type.is_file() { if let Some(ext) = path.extension() { if ext == "h" && !self.should_exclude(&path) { headers.push(path); } } - } else if path.is_dir() && self.options.recursive { + } else if file_type.is_dir() && self.options.recursive { + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if matches!( + name, + "target" + | "node_modules" + | ".git" + | "build" + | "dist" + | "zig-cache" + | "nimcache" + ) { + continue; + } self.scan_directory(&path, discoveries)?; } } @@ -354,6 +374,21 @@ mod tests { assert!(headers[0].ends_with("api.h")); } + #[test] + fn test_scan_skips_target_and_git_dirs() { + let dir = tempdir().unwrap(); + let target = dir.path().join("target"); + fs::create_dir(&target).unwrap(); + fs::write(target.join("generated.h"), "int skip(void);").unwrap(); + fs::write(dir.path().join("api.h"), "int add(int a, int b);").unwrap(); + + let scanner = LibraryScanner::new(dir.path()); + let discoveries = scanner.scan().unwrap(); + assert_eq!(discoveries.len(), 1); + assert_eq!(discoveries[0].headers.len(), 1); + assert!(discoveries[0].headers[0].ends_with("api.h")); + } + #[test] fn test_generate_all_creates_mod_rs() { let dir = tempdir().unwrap();