From 42ba91fb6d27fc1936fbe21c0adb8f8de806bcbf Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 12:49:41 -0800 Subject: [PATCH] Plug-ins: the failed delete of grants.json is tested on every platform A test-only switch makes removing grants.json fail on the test's thread while the file still reads. The new test replaces an approved plug-in with one that asks for no permissions while the switch is on, and checks that the store gets the approved module back and keeps the grant unchanged. The Windows test that holds the file open without delete sharing stays. --- crates/engine/src/store.rs | 13 ++++++++++++- crates/engine/src/tests/plugins.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/store.rs b/crates/engine/src/store.rs index 01ebc4652..3bc092d06 100644 --- a/crates/engine/src/store.rs +++ b/crates/engine/src/store.rs @@ -190,12 +190,23 @@ fn lock_disabled(_: &Path) -> Option { thread_local! { /// Tests: [`Session::put_back_plugin`] fails on this thread, as on a full volume. pub(crate) static PUT_BACK_FAILS: std::cell::Cell = const { std::cell::Cell::new(false) }; + /// Tests: removing [`GRANTS_FILE`] fails on this thread while the file still reads, as when + /// another program holds it open. + pub(crate) static GRANTS_DELETE_FAILS: std::cell::Cell = const { std::cell::Cell::new(false) }; } /// Remove `dir/file` → whether it is gone; a file that isn't there is what was wanted. fn take(dir: &Path, file: &str) -> bool { let path = dir.join(file); - match std::fs::remove_file(&path) { + #[cfg(test)] + let removed = if file == GRANTS_FILE && GRANTS_DELETE_FAILS.get() { + Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) + } else { + std::fs::remove_file(&path) + }; + #[cfg(not(test))] + let removed = std::fs::remove_file(&path); + match removed { Err(e) if e.kind() != std::io::ErrorKind::NotFound => { log::warn!("can't remove {}: {e}", path.display()); false diff --git a/crates/engine/src/tests/plugins.rs b/crates/engine/src/tests/plugins.rs index a4be6e554..cfaf554df 100644 --- a/crates/engine/src/tests/plugins.rs +++ b/crates/engine/src/tests/plugins.rs @@ -2000,6 +2000,35 @@ fn a_grants_file_that_cannot_be_deleted_keeps_the_stored_module() { run(&mut s, "plugin.remove", json!({"id": id})); } +/// When the last grant can't be removed because `grants.json` can't be deleted, a replacing module +/// that asks for no permissions doesn't stay in the store: the store gets back the module the grant +/// still covers, and the grant is left as it was. +#[test] +fn a_grants_file_whose_deletion_fails_keeps_the_stored_module() { + let folder = crate::tests_links::Folder::new("plugin-store-undeletable-grants"); + let id = "test.engine.permissions.undeletablegrants"; + let empty = json!({"objects": []}); + let approved = asking(id, r#"["doc.read"]"#, &empty); + let approved_data = vectorcraft_format::base64_encode(&approved); + let plain = asking(id, "null", &empty); + let grants = folder.0.join("grants.json"); + let mut s = session(); + s.attach_plugin_store(folder.0.clone()); + run(&mut s, "plugin.install", json!({"dataBase64": approved_data, "grant": ["doc.read"]})); + let file = s.stored_plugin_file(id).unwrap(); + let recorded = std::fs::read(&grants).unwrap(); + + crate::store::GRANTS_DELETE_FAILS.set(true); + run(&mut s, "plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&plain), "replace": true})); + crate::store::GRANTS_DELETE_FAILS.set(false); + assert_eq!(std::fs::read(&file).unwrap(), approved, "the store gets the module the grant covers back"); + assert_eq!(std::fs::read(&grants).unwrap(), recorded, "the grant stays as it was"); + assert_eq!(run(&mut s, "plugin.inspect", json!({"dataBase64": approved_data}))["needsApproval"], false, "the grant still covers the module"); + + run(&mut s, "plugin.remove", json!({"id": id})); + assert!(!grants.exists(), "a removal that can delete the file forgets the grant"); +} + /// A plug-in folder that leaves a module listing no permissions under an id forgets the process's /// grant for it, unless a later file in the folder leaves a granted module there. A grant kept in /// the plug-in store stays, with the stored module it covers.