From 80363c7e6011ae158dcec58efda8f277e5097b46 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:52:29 +0100 Subject: [PATCH] Protect Windows device names followed by a space in safe_file_name Found while testing crates/engine/src/cmd/fileio/screens.rs:168-177 (safe_file_name) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/fileio/screens.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/fileio/screens.rs b/crates/engine/src/cmd/fileio/screens.rs index 1d9c0303c..e4c886e98 100644 --- a/crates/engine/src/cmd/fileio/screens.rs +++ b/crates/engine/src/cmd/fileio/screens.rs @@ -164,11 +164,14 @@ pub(super) fn file_part(s: &str) -> String { /// trailing periods and spaces, and with `-` after a Windows device name that is the whole name or /// comes before its first period: `CON`, `PRN`, `AUX`, `NUL`, `COM1` to `COM9` and `LPT1` to `LPT9`, /// also with `¹`, `²` or `³` for the digit, in any case (`CON.png` becomes `CON-.png`). The same on -/// every platform. +/// every platform. Spaces between the device name and the period are dropped (`CON .png` becomes +/// `CON-.png`). pub(crate) fn safe_file_name(s: &str) -> String { let name = file_part(s); let name = name.trim_end_matches(['.', ' ']); let (base, rest) = name.split_at(name.find('.').unwrap_or(name.len())); + // Windows ignores spaces before the extension, so `CON .png` is the CON device too. + let base = base.trim_end_matches(' '); let upper = base.to_ascii_uppercase(); let numbered = |n: &str| n.chars().count() == 1 && n.chars().all(|c| matches!(c, '1'..='9' | '¹' | '²' | '³')); let device = matches!(upper.as_str(), "CON" | "PRN" | "AUX" | "NUL") @@ -995,3 +998,18 @@ pub(crate) fn store_settings(s: &mut Session, settings: Map) -> R pub(super) fn export_settings(s: &mut Session, _: &Value) -> Result { Ok(json!({ "settings": s.doc()?.doc.export_settings })) } + +#[cfg(test)] +mod tests { + use super::safe_file_name; + + #[test] + fn device_name_with_space_before_extension_is_protected() { + assert_eq!(safe_file_name("CON.png"), "CON-.png"); + assert_eq!(safe_file_name("CON .png"), "CON-.png"); + assert_eq!(safe_file_name("NUL .png"), "NUL-.png"); + assert_eq!(safe_file_name("COM1 .png"), "COM1-.png"); + assert_eq!(safe_file_name("lpt9 .png"), "lpt9-.png"); + assert_eq!(safe_file_name("My CON .png"), "My CON .png"); + } +}