Swatch libraries: a library titled with a Windows device name is saved under another name (#3602)

library_file_name kept CON, NUL, COM1 and the like, which Windows treats as
devices, not files. It now adds a - after such a name (CON-.json).

Part of #3602
This commit is contained in:
marshiyar
2026-10-31 19:43:16 +07:00
committed by bflatastic
parent f65d28378f
commit 64ffab00a9
3 changed files with 26 additions and 3 deletions
+9 -2
View File
@@ -331,11 +331,18 @@ fn library_tab_name(file: &str, lib_name: &str) -> String {
}
/// The file name of library `name` with extension `ext`: characters file systems reject become
/// `-`, and dots and spaces at either end are dropped. A name with nothing left is `Library`.
/// `-`, and dots and spaces at either end are dropped. A name with nothing left is `Library`, and
/// one that is a Windows device name (`CON`, `NUL`, `COM1`…) gets a `-` after it.
pub fn library_file_name(name: &str, ext: &str) -> String {
let base: String = name.chars().map(|c| if "/\\:*?\"<>|".contains(c) || c.is_control() { '-' } else { c }).collect();
let base = base.trim_matches(['.', ' ']);
format!("{}.{ext}", if base.is_empty() { "Library" } else { base })
if base.is_empty() {
format!("Library.{ext}")
} else if crate::file_access::is_device_name(base) {
format!("{base}-.{ext}")
} else {
format!("{base}.{ext}")
}
}
/// The library files with extensions `exts` in folder `dir`, sorted by name.
+1 -1
View File
@@ -274,7 +274,7 @@ fn same_component(a: Component, b: Component) -> bool {
}
/// A Windows device name (`CON`, `NUL`, `COM1`… also with an extension), which names no file.
fn is_device_name(component: &str) -> bool {
pub(crate) fn is_device_name(component: &str) -> bool {
let stem = component.split('.').next().unwrap_or(component).trim_end_matches(' ').to_ascii_uppercase();
let numbered = |prefix: &str| stem.strip_prefix(prefix).is_some_and(|n| matches!(n, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"));
matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL" | "CLOCK$" | "CONIN$" | "CONOUT$") || numbered("COM") || numbered("LPT")
+16
View File
@@ -637,3 +637,19 @@ fn gradient_libraries_are_listed_and_gradient_swatches_rename_and_take_new_gradi
run(&mut s, "swatch.edit", json!({"name": "Red", "paint": {"color": "#00ff00"}}));
assert_eq!(doc(&s).swatch("Red").unwrap().paint.color().map(|c| c.to_hex()), Some("#00ff00".into()));
}
/// #3602: a library titled with a Windows device name is saved under a name that is not one.
#[test]
fn a_library_file_name_is_never_a_windows_device_name() {
use crate::cmd::swatchlib::library_file_name;
for (title, ext) in [("CON", "json"), ("nul", "json"), ("Com1", "acb"), ("LPT9", "ase"), ("aux.", "json")] {
let file = library_file_name(title, ext);
assert!(!crate::file_access::is_device_name(&file), "{title}: {file}");
assert!(
file.ends_with(&format!(".{ext}")) && file.to_ascii_lowercase().contains(&title.trim_end_matches('.').to_ascii_lowercase()),
"{title}: {file}"
);
}
assert_eq!(library_file_name("Console", "json"), "Console.json");
assert_eq!(library_file_name("COM10", "json"), "COM10.json");
}