From 64988aaa49612e2509a0280579372c9235d4f843 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 17:27:49 -0800 Subject: [PATCH] HP-GL: a .plt file opens when its first instruction is SI, DI or another known one A file read as HP-GL by its extension no longer has to start with an instruction plot files usually start with. Any instruction the reader knows, or one of another set that a semicolon ends, may come first; the rest of the check still refuses files that are not HP-GL. Telling HP-GL apart by content alone keeps the stricter check. --- crates/cad/src/hpgl.rs | 14 +++++++++++--- crates/cad/src/hpgl/tests.rs | 13 +++++++++++++ crates/engine/src/cmd/fileio/tests_hpgl.rs | 10 ++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/crates/cad/src/hpgl.rs b/crates/cad/src/hpgl.rs index 5b47aa445e..de2e6480b6 100644 --- a/crates/cad/src/hpgl.rs +++ b/crates/cad/src/hpgl.rs @@ -66,6 +66,13 @@ const FIRST: &[[u8; 2]] = &[ /// WD, DT, SM and PE. An instruction the reader doesn't know counts when its numbers end with a /// semicolon. pub fn is_hpgl(bytes: &[u8]) -> bool { + holds_instructions(bytes, true) +} + +/// Does `bytes` hold HP-GL instructions as [`is_hpgl`] checks? When `strict` is false, any of them +/// may come first: a file picked by its extension may start with one a plot file seldom starts +/// with (`SI`, `DI`). +fn holds_instructions(bytes: &[u8], strict: bool) -> bool { let mut lex = Lexer::new(bytes); let mut terminator = lex::ETX; let mut first = 0; @@ -77,7 +84,7 @@ pub fn is_hpgl(bytes: &[u8]) -> bool { if n == 0 { first = lex.pos(); } - if n == 0 && !FIRST.contains(&op) { + if strict && n == 0 && !FIRST.contains(&op) { return false; } let known = PLAYED.contains(&op) || IGNORED.contains(&op); @@ -110,9 +117,10 @@ pub fn is_hpgl(bytes: &[u8]) -> bool { true } -/// Read an HP-GL file. +/// Read an HP-GL file. It may start with any instruction the reader knows, or one of another set +/// that a semicolon ends; the rest of [`is_hpgl`]'s check applies. pub fn import(bytes: &[u8]) -> Result { - if !is_hpgl(bytes) { + if !holds_instructions(bytes, false) { return Err("not an HP-GL file".into()); } let mut lex = Lexer::new(bytes); diff --git a/crates/cad/src/hpgl/tests.rs b/crates/cad/src/hpgl/tests.rs index d3be0c87eb..873e2d8fb2 100644 --- a/crates/cad/src/hpgl/tests.rs +++ b/crates/cad/src/hpgl/tests.rs @@ -440,6 +440,19 @@ fn only_plot_files_are_taken_for_hpgl() { assert!(is_hpgl(b"IN;SP1;PU;PD;PU;PD;PU;PD;hello world")); } +/// A file read as HP-GL (picked by its extension) may start with any instruction the reader knows, +/// or one of another set that a semicolon ends; its content alone doesn't make it HP-GL. +#[test] +fn a_plot_file_may_start_with_any_instruction() { + for f in [&b"SI0.2,0.3;SP1;PA0,0;PD1016,0;"[..], b"DI1,0;SP1;PA0,0;PD1016,0;", b"GC5;SP1;PA0,0;PD1016,0;"] { + assert!(!is_hpgl(f), "{:?}", String::from_utf8_lossy(f)); + assert_eq!(leaves(&plot_bytes(f).document).len(), 1, "{:?}", String::from_utf8_lossy(f)); + } + for f in [&b"GC5"[..], b"SIhello world", b"set terminal png\n"] { + assert_eq!(import(f).err().as_deref(), Some("not an HP-GL file"), "{:?}", String::from_utf8_lossy(f)); + } +} + #[test] fn escapes_that_carry_values_or_data_are_skipped_whole() { // A reset leaves HP-GL until a language switch; symbol-set escapes carry a value and a letter. diff --git a/crates/engine/src/cmd/fileio/tests_hpgl.rs b/crates/engine/src/cmd/fileio/tests_hpgl.rs index d453d9178d..271f7b86e2 100644 --- a/crates/engine/src/cmd/fileio/tests_hpgl.rs +++ b/crates/engine/src/cmd/fileio/tests_hpgl.rs @@ -79,6 +79,16 @@ fn files_that_are_not_hpgl_are_refused() { assert_eq!(text["format"], "txt", "{text}"); } +/// A plot file named as one opens when its first instruction is one a plot file seldom starts with; +/// under another name it isn't taken for HP-GL. +#[test] +fn a_plot_file_by_its_name_may_start_with_any_instruction() { + let plot = b"SI0.2,0.3;SP1;PA0,0;PD1016,0;"; + let r = Session::new().execute("document.open", &json!({"name": "part.plt", "dataBase64": b64(plot)})).unwrap(); + assert_eq!(r["format"].as_str(), Some("hpgl"), "{r}"); + assert!(detect("drawing.prn", plot).is_none_or(|f| f.id != "hpgl")); +} + #[test] fn the_dialogs_and_the_format_list_name_hpgl() { let exts = ["plt", "hpgl", "hgl", "hpg"];