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();