diff --git a/crates/engine/src/cmd/fileio/trace_batch.rs b/crates/engine/src/cmd/fileio/trace_batch.rs index 354af7c02..d2483440b 100644 --- a/crates/engine/src/cmd/fileio/trace_batch.rs +++ b/crates/engine/src/cmd/fileio/trace_batch.rs @@ -32,7 +32,12 @@ fn images(folder: &str, deep: bool, depth: usize, out: &mut Vec) -> Resu if depth > MAX_DEPTH { return Err(bad("imageTrace.batchFolder", format!("folders deeper than {MAX_DEPTH} levels"))); } - let (entries, _) = super::list_dir(folder, MAX_FILES.saturating_mul(2))?; + let max = MAX_FILES.saturating_mul(2); + let (entries, truncated) = super::list_dir(folder, max)?; + // The listing is cut before it is sorted, so a cut one could skip any image: refuse it. + if truncated { + return Err(bad("imageTrace.batchFolder", format!("{folder}: more than {max} files and folders"))); + } let mut subdirs = vec![]; for e in entries { let name = e.get("name").and_then(Value::as_str).unwrap_or(""); @@ -177,4 +182,22 @@ mod tests { } let _ = std::fs::remove_dir_all(&dir); } + + /// A folder too big to list in full is refused rather than traced in part. + #[test] + fn batch_trace_folder_refuses_a_folder_it_cannot_list_in_full() { + let dir = std::env::temp_dir().join(format!("vc-batch-trace-big-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let src = dir.join("in"); + std::fs::create_dir_all(&src).unwrap(); + for i in 0..=super::MAX_FILES * 2 { + std::fs::write(src.join(format!("note-{i}.txt")), "").unwrap(); + } + png(&src.join("logo.png"), true); + let mut s = Session::new(); + let e = s.execute("imageTrace.batchFolder", &json!({"folder": src, "destinationFolder": dir.join("out")})).unwrap_err(); + assert!(e.to_string().contains("more than"), "{e}"); + assert!(!dir.join("out").exists(), "nothing traced"); + let _ = std::fs::remove_dir_all(&dir); + } } diff --git a/docs/mcp.md b/docs/mcp.md index 75721fb75..75ede6e30 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1885,7 +1885,7 @@ already walks Image Trace. Short agent entry points: `preset` (Default by default; with `expand`, as Make and Expand), saves it into `destinationFolder` as `.` in `format` (`vectorcraft` by default; `svg`, `pdf`, `ai`, `eps`), with `-2`, `-3`… added when the name is taken so no file is replaced, and closes it again whether it traced or not; the active document stays - active. Up to 500 images and 8 folder levels; not on the web. The app asks for both folders (#4921). + active. Up to 500 images and 8 folder levels (a folder with more than 1,000 entries is refused); not on the web. The app asks for both folders (#4921). → `{traced: [{path, output}], failed: [{path, error}], format}` - Live Paint: `livePaint.make`, `livePaint.merge`, `livePaint.fill {group?, point}`, `livePaint.strokeEdge`, `livePaint.info {group}`, `livePaint.release`, `livePaint.expand`, and `livePaint.gapOptions {ids?, detect?, size?,