From 99169d5404eeaab7193d2a8810ca137c17c6650c Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 5 Nov 2026 04:50:08 -0800 Subject: [PATCH] Reject malformed Batch Trace Folder options --- crates/engine/src/cmd/addons/document.rs | 2 +- .../src/cmd/addons/document/batch_trace.rs | 31 +++++++++++++++---- .../engine/src/cmd/addons/document/tests.rs | 29 +++++++++++++++++ docs/mcp.md | 6 ++-- docs/target-app-parity.md | 2 +- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/cmd/addons/document.rs b/crates/engine/src/cmd/addons/document.rs index 52f7f3168..fafdfc07d 100644 --- a/crates/engine/src/cmd/addons/document.rs +++ b/crates/engine/src/cmd/addons/document.rs @@ -47,7 +47,7 @@ pub fn specs() -> Vec { "Batch Trace Folder", ["Object", "Plug-ins", "Document Tools"], None, - "{folder, destinationFolder, preset?: name (imageTrace.presets; default: Default), expand?: false (true: trace and expand, as Make and Expand), format?: vectorcraft (default)|svg|pdf|ai|eps, includeSubdirs?: false} open each image in folder (png, jpg, tif, bmp, gif, webp, psd), select it, trace it with the preset, save it into destinationFolder as . (-2, -3… added when that name is taken, so no file is replaced) and close it again, whether it traced or not; the document that was active stays active. Caps: 500 images, 8 folder levels; a folder with more than 1,000 entries is refused. Not on the web (no file system) → {traced: [{path, output}], failed: [{path, error}], format} (#4921)", + "{folder, destinationFolder, preset?: name (imageTrace.presets; default: Default), expand?: false (true: trace and expand, as Make and Expand), format?: vectorcraft (default)|svg|pdf|ai|eps, includeSubdirs?: false} open each image in folder (png, jpg, tif, bmp, gif, webp, psd), select it, trace it with the preset, save it into destinationFolder as . (-2, -3… added when that name is taken, so no file is replaced) and close it again, whether it traced or not; the document that was active stays active. Optional settings use their defaults only when omitted; null and values of the wrong type return an error before any image is opened. Caps: 500 images, 8 folder levels; a folder with more than 1,000 entries is refused. Not on the web (no file system) → {traced: [{path, output}], failed: [{path, error}], format} (#4921)", always, batch_trace::batch_trace_folder ), diff --git a/crates/engine/src/cmd/addons/document/batch_trace.rs b/crates/engine/src/cmd/addons/document/batch_trace.rs index 0340fca18..61e59fc54 100644 --- a/crates/engine/src/cmd/addons/document/batch_trace.rs +++ b/crates/engine/src/cmd/addons/document/batch_trace.rs @@ -8,9 +8,9 @@ use serde_json::json; #[cfg(not(target_arch = "wasm32"))] use super::super::super::fileio; -use super::super::super::{Result, Session, bad}; #[cfg(not(target_arch = "wasm32"))] -use super::super::super::{bool_or, str_param}; +use super::super::super::str_param; +use super::super::super::{Result, Session, bad}; /// The command id. pub(super) const ID: &str = "addon.document.batchTraceFolder"; @@ -69,20 +69,39 @@ fn images(folder: &str, deep: bool, depth: usize, out: &mut Vec) -> Resu pub(super) fn batch_trace_folder(s: &mut Session, p: &Value) -> Result { let folder = str_param(p, "folder").map(fileio::absolute_path).ok_or_else(|| bad(ID, "missing `folder`"))?; let dest = str_param(p, "destinationFolder").map(fileio::absolute_path).ok_or_else(|| bad(ID, "missing `destinationFolder`"))?; - let format = str_param(p, "format").unwrap_or("vectorcraft"); + let format = match p.get("format") { + None => "vectorcraft", + Some(Value::String(value)) => value.as_str(), + Some(_) => return Err(bad(ID, "`format` must be a string")), + }; + let preset = match p.get("preset") { + None => "Default", + Some(Value::String(value)) => value.as_str(), + Some(_) => return Err(bad(ID, "`preset` must be a string")), + }; + let expand = match p.get("expand") { + None => false, + Some(Value::Bool(value)) => *value, + Some(_) => return Err(bad(ID, "`expand` must be true or false")), + }; + let include_subdirs = match p.get("includeSubdirs") { + None => false, + Some(Value::Bool(value)) => *value, + Some(_) => return Err(bad(ID, "`includeSubdirs` must be true or false")), + }; if !FORMATS.contains(&format) { return Err(bad(ID, format!("`format` must be one of {}", FORMATS.join(", ")))); } - let preset = str_param(p, "preset").unwrap_or("Default").to_string(); + let preset = preset.to_string(); let presets = s.execute("imageTrace.presets", &json!({}))?; if !presets["presets"].as_array().into_iter().flatten().any(|q| q["name"].as_str().is_some_and(|n| n.eq_ignore_ascii_case(&preset))) { return Err(bad(ID, format!("no tracing preset `{preset}` (see imageTrace.presets)"))); } - let trace = if bool_or(p, "expand", false) { "imageTrace.makeAndExpand" } else { "imageTrace.make" }; + let trace = if expand { "imageTrace.makeAndExpand" } else { "imageTrace.make" }; fileio::may_read(&folder)?; fileio::may_write(&dest)?; let mut files = vec![]; - images(&folder, bool_or(p, "includeSubdirs", false), 0, &mut files)?; + images(&folder, include_subdirs, 0, &mut files)?; files.sort(); if files.is_empty() { return Err(bad(ID, "no images in the folder")); diff --git a/crates/engine/src/cmd/addons/document/tests.rs b/crates/engine/src/cmd/addons/document/tests.rs index a1bc5afff..b0fbb4142 100644 --- a/crates/engine/src/cmd/addons/document/tests.rs +++ b/crates/engine/src/cmd/addons/document/tests.rs @@ -333,6 +333,35 @@ fn batch_trace_folder_traces_and_saves_each_image() { let _ = std::fs::remove_dir_all(&dir); } +/// Batch Trace Folder rejects optional values with the wrong type before opening images. +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn batch_trace_folder_rejects_wrong_typed_options() { + const C: &str = "addon.document.batchTraceFolder"; + let dir = folder("batch-trace-bad-options"); + let src = dir.join("in"); + let out = dir.join("out"); + std::fs::create_dir_all(&src).unwrap(); + let mut s = Session::new(); + for (key, value) in [ + ("format", json!(42)), + ("format", Value::Null), + ("preset", json!(false)), + ("preset", Value::Null), + ("expand", json!("true")), + ("expand", Value::Null), + ("includeSubdirs", json!(1)), + ("includeSubdirs", Value::Null), + ] { + let mut params = json!({"folder": src, "destinationFolder": out}); + params[key] = value; + let error = s.execute(C, ¶ms).unwrap_err(); + assert!(error.to_string().contains(&format!("`{key}`")), "{key}: {error}"); + assert!(!out.exists(), "invalid options must not create a destination folder"); + } + let _ = std::fs::remove_dir_all(&dir); +} + /// A folder too big to list in full is refused rather than traced in part. #[cfg(not(target_arch = "wasm32"))] #[test] diff --git a/docs/mcp.md b/docs/mcp.md index 4dc3a0e6c..ab64cfbff 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -7078,8 +7078,10 @@ image in `folder` (PNG, JPEG, TIFF, BMP, GIF, WebP, PSD; with `includeSubdirs`, `preset` (an `imageTrace.presets` name; 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. Images that can't be traced are listed in `failed` and the others are still traced. Up to 500 -images and 8 folder levels (a folder with more than 1,000 entries is refused); not on the web. The desktop app's Batch +document stays active. Images that can't be traced are listed in `failed` and the others are still traced. Optional +`format` and `preset` accept strings; `expand` and `includeSubdirs` accept true or false. Null and values of another +type return an error before any image is opened. Up to 500 images and 8 folder levels (a folder with more than 1,000 +entries is refused); not on the web. The desktop app's Batch Trace Folder asks for both folders (#4921). → `{traced: [{path, output}], failed: [{path, error}], format}` ```json diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index dfdeee287..01a33eef3 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1639,7 +1639,7 @@ A VectorCraft addition, not a feature of the reference app, so no area score mov ## Document Tools add-on: Batch Trace Folder (2026-11-04) -A VectorCraft addition (it replaces a script some users ran), so no area score moves. Object › Plug-ins › Document Tools › Batch Trace Folder (`addon.document.batchTraceFolder`) asks for a folder of images and a destination folder, traces each image with a preset (optionally expanded) and saves it there under its own name as `.vectorcraft`, `.svg`, `.pdf`, `.ai` or `.eps`, without replacing files, closing each image's document again and reporting the images it couldn't trace (#4921). A folder with more than 1,000 entries is refused rather than traced in part. Tests: `cmd::addons::document::tests::batch_trace_folder_traces_and_saves_each_image` and `batch_trace_folder_refuses_a_folder_it_cannot_list_in_full` (engine), `addons::document::tests::batch_trace_folder_asks_for_both_folders` (ui-egui). +A VectorCraft addition (it replaces a script some users ran), so no area score moves. Object › Plug-ins › Document Tools › Batch Trace Folder (`addon.document.batchTraceFolder`) asks for a folder of images and a destination folder, traces each image with a preset (optionally expanded) and saves it there under its own name as `.vectorcraft`, `.svg`, `.pdf`, `.ai` or `.eps`, without replacing files, closing each image's document again and reporting the images it couldn't trace (#4921). A folder with more than 1,000 entries is refused rather than traced in part. Optional settings reject null and values of the wrong type before opening images. Tests: `cmd::addons::document::tests::batch_trace_folder_traces_and_saves_each_image`, `batch_trace_folder_refuses_a_folder_it_cannot_list_in_full`, and `batch_trace_folder_rejects_wrong_typed_options` (engine), `addons::document::tests::batch_trace_folder_asks_for_both_folders` (ui-egui). ## Color Effects add-on: Find Nearest Swatch (2026-11-01)