From 0864db94945b3074c659bca012e406c73a713b70 Mon Sep 17 00:00:00 2001 From: Adithya Shankaran <72570811+adikshan11@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:21:54 +0530 Subject: [PATCH] Keep the other library when two names share a file name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A library called COM¹ is saved as COM¹-, which can be the file of a library really called COM¹-, and the numbered file one of them then gets can be asked for by a third name. Saving one replaced the other. Each now gets a free file name, and saving it again goes back to that file. --- crates/engine/src/cmd/swatchlib.rs | 23 ++++++++++++++++++++++- crates/engine/src/tests/swatchlib.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/swatchlib.rs b/crates/engine/src/cmd/swatchlib.rs index 8e929f14d..9ecec6c67 100644 --- a/crates/engine/src/cmd/swatchlib.rs +++ b/crates/engine/src/cmd/swatchlib.rs @@ -211,7 +211,28 @@ impl Libraries { .user_dir() .ok_or_else(|| bad(cmd, "no user library folder here (save with a path, or without one to get the data)"))? .to_string(); - let file = library_file_name(name, ext); + let mut file = library_file_name(name, ext); + // `COM¹` is saved as `COM¹-`, which can be another library's own name, and a library put + // under a numbered name holds a file another name can ask for: such a file is kept. + self.rescan(); + let device = crate::file_access::is_device_name; + let id = format!("user/{file}").to_lowercase(); + let fallback = |other: &str| library_file_name(other, ext).to_lowercase() != file.to_lowercase(); + if self + .extra + .iter() + .any(|e| e.info.id.to_lowercase() == id && e.lib.name() != name && (device(name) || device(e.lib.name()) || fallback(e.lib.name()))) + { + // Saved again, it goes back to the file it was given the first time. + let own = self.extra.iter().find(|e| e.info.category == "user" && e.lib.name() == name && extension(&e.info.id) == extension(&file)); + file = match own.and_then(|e| e.info.id.strip_prefix("user/")) { + Some(own) => own.to_string(), + None => { + let mut taken: HashSet = library_files(&dir, L::EXTS).iter().map(|p| file_name(p).to_lowercase()).collect(); + free_name(&file, &mut taken) + } + }; + } let path = std::path::Path::new(&dir).join(&file).to_string_lossy().to_string(); // The app's own folder, under a name made safe: no root applies. crate::file_access::unconfined(|| create_dir(&dir).and_then(|()| write_file(&path, bytes)))?; diff --git a/crates/engine/src/tests/swatchlib.rs b/crates/engine/src/tests/swatchlib.rs index 18d2ecd33..23980c5d5 100644 --- a/crates/engine/src/tests/swatchlib.rs +++ b/crates/engine/src/tests/swatchlib.rs @@ -765,6 +765,33 @@ fn a_pattern_library_adds_only_the_picked_pattern_and_it_undoes_and_saves() { assert_eq!(back.swatch("Checkerboard").map(|w| &w.paint), doc(&s).swatch("Checkerboard").map(|w| &w.paint)); } +/// A device name is saved with a `-` after it, which can be another library's own name: neither +/// library replaces the other, and saving either again replaces only itself. +#[test] +fn a_library_named_like_a_device_keeps_the_library_with_its_file_name() { + for (first, second) in [("COM¹", "COM¹-"), ("COM¹-", "COM¹"), ("com¹", "COM¹-")] { + let dir = temp_dir("device-collision"); + let mut s = session(); + s.swatch_libraries.set_user_dir(Some(dir.to_string_lossy().to_string())); + let save = + |s: &mut Session, name: &str| run(s, "swatch.library.save", json!({"user": true, "name": name}))["library"].as_str().unwrap().to_string(); + let a = save(&mut s, first); + let b = save(&mut s, second); + assert_ne!(a, b); + assert_eq!(save(&mut s, first), a, "saving {first} again replaces {first}"); + assert_eq!(save(&mut s, second), b, "saving {second} again replaces {second}"); + assert_eq!(s.swatch_libraries.get(&a).unwrap().1.name, first); + assert_eq!(s.swatch_libraries.get(&b).unwrap().1.name, second); + // The name of the numbered file the second one got is a third library's own name. + let third = b.strip_prefix("user/").unwrap().strip_suffix(".vcswatches").unwrap().to_string(); + let c = save(&mut s, &third); + assert!(c != a && c != b, "{third}: {c}"); + assert_eq!(save(&mut s, &third), c, "saving {third} again replaces {third}"); + assert_eq!(s.swatch_libraries.get(&b).unwrap().1.name, second); + let _ = std::fs::remove_dir_all(&dir); + } +} + /// #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() {