From d6555e5bdf2c92182c65e79c8a935e7b62699adb Mon Sep 17 00:00:00 2001 From: SBaksa Date: Sun, 1 Nov 2026 22:20:10 +1100 Subject: [PATCH] Plot: an unknown extension needs a real format selector (review on #2585) device None and a null or unknown output no longer bypass the refusal and write a PDF under the odd name; only a recognised output or a device that resolves does. Test: plot_path_extension_picks_the_format_like_export. --- crates/engine/src/cmd/layout/tests.rs | 10 ++++++++++ crates/engine/src/cmd/plotters.rs | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/layout/tests.rs b/crates/engine/src/cmd/layout/tests.rs index 75489a8a7..d4c82e17d 100644 --- a/crates/engine/src/cmd/layout/tests.rs +++ b/crates/engine/src/cmd/layout/tests.rs @@ -340,6 +340,16 @@ fn plot_path_extension_picks_the_format_like_export() { assert!(!dir.join("noext").exists()); let e = s.execute("plot", &json!({"path": path("y.png"), "device": "CADCraft PDF"})).unwrap_err().to_string(); assert!(e.contains("writes pdf") && e.contains("png"), "{e}"); + // A device that resolves to none, or a null/unknown `output`, names no format: the refusal stands. + for p in [ + json!({"path": path("x.txt"), "device": "None"}), + json!({"path": path("x.txt"), "output": null}), + json!({"path": path("x.txt"), "output": "bogus"}), + ] { + let e = s.execute("plot", &p).unwrap_err().to_string(); + assert!(e.contains("png") && e.contains("svg") && e.contains("pdf"), "{e}"); + assert!(!dir.join("x.txt").exists(), "nothing written under an unknown extension"); + } // An explicit output still decides for an odd name. assert_eq!(s.execute("plot", &json!({"path": path("z.txt"), "output": "pdf"})).unwrap()["format"], "pdf"); std::fs::remove_dir_all(&dir).ok(); diff --git a/crates/engine/src/cmd/plotters.rs b/crates/engine/src/cmd/plotters.rs index d9aed574f..a8ac31818 100644 --- a/crates/engine/src/cmd/plotters.rs +++ b/crates/engine/src/cmd/plotters.rs @@ -300,8 +300,8 @@ pub(crate) fn plot_options(s: &Session, p: &Value, cmd: &str) -> Result { // refuse it, as EXPORT does, unless `output` or a device says what to write. if cmd == "plot" && by_path.is_none() - && p.get("output").is_none() - && !given.is_some_and(|n| !n.is_empty()) + && !p.get("output").and_then(Value::as_str).is_some_and(|o| PlotOutput::parse(o).is_some()) + && !(dev.is_some() && given.is_some_and(|n| !n.is_empty())) && let Some(path) = str_param(p, "path") { return Err(bad(