From 8712be97b0f2b59bf6c3481d6f210e4b3c4502ab Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 20 Oct 2026 22:45:18 -0700 Subject: [PATCH] Libraries: a User Defined library file whose reading panics is skipped The desktop app reads the User Defined swatch and graphic style libraries while it creates its window: set_user_dir runs Libraries::rescan, which read and parsed every file in the folder outside the panic guard that commands have. A parser bug that panicked on one file stopped the app before its window appeared, at every launch while the file stayed in the folder. rescan now reads and parses each file inside guard::catch_panic and skips a file whose reading panics, as it skips one that can't be read. The other libraries are listed. --- crates/engine/src/cmd/swatchlib.rs | 7 ++++-- crates/engine/src/tests_swatchlib.rs | 32 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/swatchlib.rs b/crates/engine/src/cmd/swatchlib.rs index f5d4b74a1..98e3272d0 100644 --- a/crates/engine/src/cmd/swatchlib.rs +++ b/crates/engine/src/cmd/swatchlib.rs @@ -152,13 +152,16 @@ impl Libraries { } /// Re-read the library files of the user library folder (unreadable ones are skipped). The - /// folder is the app's own: automation roots don't apply to it ([`crate::file_access`]). + /// folder is the app's own: automation roots don't apply to it ([`crate::file_access`]). Each + /// file is read inside [`crate::guard::catch_panic`], and a file whose reading panics is + /// skipped too. pub fn rescan(&mut self) { self.extra.retain(|e| e.info.category != "user"); let Some(dir) = self.user_dir.clone() else { return }; for path in library_files(&dir, L::EXTS) { let file = file_name(&path); - let Some(lib) = crate::file_access::unconfined(|| read_file(&path)).ok().and_then(|b| L::read(&b, stem(&file)).ok()) else { continue }; + let read = || crate::file_access::unconfined(|| read_file(&path)).ok().and_then(|b| L::read(&b, stem(&file)).ok()); + let Some(lib) = crate::guard::catch_panic(read).ok().flatten() else { continue }; let info = LibraryInfo { id: format!("user/{file}"), name: lib.name().to_string(), category: "user" }; self.extra.push(Extra { info, path: Some(path), lib: Arc::new(lib) }); } diff --git a/crates/engine/src/tests_swatchlib.rs b/crates/engine/src/tests_swatchlib.rs index 0f25a04ec..6be4e4f5a 100644 --- a/crates/engine/src/tests_swatchlib.rs +++ b/crates/engine/src/tests_swatchlib.rs @@ -189,6 +189,38 @@ fn libraries_saved_in_the_user_folder_are_user_defined() { let _ = std::fs::remove_dir_all(dir); } +/// A library kind whose reader panics on a file that holds `trap`, as a parser bug would. +#[derive(Default)] +struct Trap(String); + +impl cmd::swatchlib::LibraryFile for Trap { + const EXTS: &'static [&'static str] = &["trap"]; + fn read(bytes: &[u8], stem: &str) -> std::result::Result { + if bytes == b"trap" { + panic!("a parser bug"); + } + Ok(Self(stem.to_string())) + } + fn name(&self) -> &str { + &self.0 + } +} + +/// When reading one file of the user library folder panics, `set_user_dir` (as the desktop app +/// calls it at launch) skips that file and lists the other one. +#[test] +fn a_user_library_file_whose_reading_panics_is_skipped() { + let dir = temp_dir("user-panic"); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("Calm.trap"), "calm").unwrap(); + std::fs::write(dir.join("Loud.trap"), "trap").unwrap(); + let mut libraries = cmd::swatchlib::Libraries::::default(); + libraries.set_user_dir(Some(dir.to_string_lossy().to_string())); + let names: Vec<&str> = libraries.infos().map(|l| l.name.as_str()).collect(); + assert_eq!(names, ["Calm"]); + let _ = std::fs::remove_dir_all(dir); +} + #[test] fn another_documents_swatches_load_as_a_library() { let mut s = session();