Plot: refuse a path whose extension names no format (review on #2585)

A path like x.txt (or one with no extension) wrote a PDF under that name. Like EXPORT, PLOT now refuses it unless output or a device says what to write. Test: plot_path_extension_picks_the_format_like_export.
This commit is contained in:
SBaksa
2026-11-01 19:13:53 +11:00
parent 12ce66889e
commit 09b5fb9328
3 changed files with 50 additions and 3 deletions
+33
View File
@@ -303,3 +303,36 @@ fn mspace_edits_model_through_viewport() {
s.execute("layout.set", &json!({"name": "Model"})).unwrap();
assert!(s.state().unwrap().mspace.is_none());
}
/// The path's extension picks the plot format as EXPORT does, in any case; one that names no
/// format (or none) is refused instead of writing a PDF under that name, and a device that writes
/// another format is refused.
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn plot_path_extension_picks_the_format_like_export() {
super::super::file::set_io(super::super::file::IoHooks {
read: |_, _| Err("no reader in tests".into()),
write: |_, _| Err("no writer in tests".into()),
plot: Some(fake_plot),
});
if io().and_then(|h| h.plot).is_none() {
return; // another test installed hooks without a plotter first
}
let mut s = session_with_model();
let dir = std::env::temp_dir().join(format!("cadcraft-plot-ext-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = |n: &str| dir.join(n).to_string_lossy().to_string();
let upper = s.execute("plot", &json!({"path": path("X.PNG")})).unwrap();
assert_eq!(upper["format"], "png", "an upper-case extension still picks png");
let e = s.execute("plot", &json!({"path": path("x.txt")})).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");
let e = s.execute("plot", &json!({"path": path("noext")})).unwrap_err().to_string();
assert!(e.contains("png") && e.contains("svg") && e.contains("pdf"), "{e}");
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}");
// 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();
}
+13
View File
@@ -296,6 +296,19 @@ pub(crate) fn plot_options(s: &Session, p: &Value, cmd: &str) -> Result<Value> {
// The path's extension names the format, like EXPORT, unless a device is given; a given
// device that writes another format is refused. EXPORTPDF always writes PDF.
let by_path = if cmd == "exportpdf" { None } else { str_param(p, "path").and_then(path_output) };
// A path whose extension names no format (or has none) would hold a PDF under that name:
// 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())
&& let Some(path) = str_param(p, "path")
{
return Err(bad(
cmd,
format!("can't tell the format of `{path}`: give a path ending in .png, .svg or .pdf, or set `output` to png, svg or pdf"),
));
}
if let (Some(want), Some(dev), true) = (by_path, &dev, given.is_some_and(|n| !n.is_empty()))
&& dev.output != want
{
+4 -3
View File
@@ -36,11 +36,12 @@ fn the_extension_picks_png_svg_or_pdf() {
assert_eq!(r["format"], format, "{name}: the reply names the format");
assert_eq!(magic(&out), head, "{name}: the file holds that format");
}
// No path, or an extension that names no format: PDF as before.
// No path: PDF data as before. An extension that names no format is refused, like EXPORT.
assert_eq!(s.execute("plot", &json!({ "layout": "Model" })).unwrap()["format"], "pdf");
let other = dir.join("e.plt");
assert_eq!(s.execute("plot", &json!({ "path": other.to_string_lossy(), "layout": "Model" })).unwrap()["format"], "pdf");
assert_eq!(magic(&other), b"%PDF-");
let e = s.execute("plot", &json!({ "path": other.to_string_lossy(), "layout": "Model" })).unwrap_err().to_string();
assert!(e.contains("png") && e.contains("svg") && e.contains("pdf"), "{e}");
assert!(!other.exists(), "nothing is written under a name that is not a format");
// EXPORTPDF always writes PDF.
let pdf = dir.join("f.png");
s.execute("exportpdf", &json!({ "path": pdf.to_string_lossy(), "layout": "Model" })).unwrap();