From fd7ff70941361bc26850c33cb3ed9fca6d4e7801 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 13:03:38 -0800 Subject: [PATCH 1/2] Plug-ins: an update left in the store by a failed put-back turns a disabled plug-in on When plugin.install writes a module that lists permissions, its grant can't be written, and the old module can't be put back (or, on a first install, the new module can't be removed), the store holds the new module. install now takes put_back_plugin's result, so the plug-in's disabled entries (by id and by stored file) are cleared as for any install the store holds. The old grant, or none, still doesn't cover the new module, so the next start skips it until it is approved. Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md. --- crates/engine/src/cmd/plugin.rs | 9 ++- crates/engine/src/tests/plugins.rs | 103 +++++++++++++++++++++++++++++ docs/mcp.md | 4 +- docs/plugins.md | 4 +- docs/target-app-parity.md | 1 + 5 files changed, 114 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/cmd/plugin.rs b/crates/engine/src/cmd/plugin.rs index a5b71d538..94b8f7f58 100644 --- a/crates/engine/src/cmd/plugin.rs +++ b/crates/engine/src/cmd/plugin.rs @@ -741,17 +741,16 @@ fn install(s: &mut Session, p: &Value) -> Result { } // Installed plug-ins stay installed after a restart (#2541). The module is written first and its // grant only once it is there; when the grant can't be written, the store gets the module it - // held back. A failed write of that module is only logged, and the grant then doesn't cover - // the new module. A stored module that can't be read back isn't replaced by one that needs a - // grant. + // held back. A failed write of that module is only logged: the store then holds the new module, + // which the grant doesn't cover. A stored module that can't be read back isn't replaced by one + // that needs a grant. let previous = s.stored_plugin_bytes(plugin.id()); let mut stored = (previous.is_ok() || !m.asks_permissions()) && s.store_plugin(plugin.id(), &bytes); if m.asks_permissions() { let g = Grant { id: m.id.clone(), hash: plugin.hash().to_string(), permissions: wanted }; registry::grant(g.clone()); if stored && !s.store_grant(&m.id, Some(g)) { - s.put_back_plugin(plugin.id(), previous.ok().flatten().as_deref()); - stored = false; + stored = !s.put_back_plugin(plugin.id(), previous.ok().flatten().as_deref()); } } else { // This module takes the id over, so nothing the user approved for the id applies any more. diff --git a/crates/engine/src/tests/plugins.rs b/crates/engine/src/tests/plugins.rs index a4be6e554..4839a1c45 100644 --- a/crates/engine/src/tests/plugins.rs +++ b/crates/engine/src/tests/plugins.rs @@ -2178,6 +2178,109 @@ fn a_failed_store_write_keeps_the_stored_module_with_its_grant() { vectorcraft_plugins::registry::revoke(id); } +/// Runs the test `name` of this module again in a process of its own → whether this is that +/// process. The list of disabled plug-ins is kept for the whole process, and the other tests here +/// read it again each time they attach a plug-in store. +fn in_own_process(name: &str) -> bool { + const CHILD: &str = "VECTORCRAFT_TEST_PLUGIN_DISABLED_LIST"; + if std::env::var_os(CHILD).is_some() { + return true; + } + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", &format!("tests::plugins::{name}"), "--nocapture"]) + .env(CHILD, "1") + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"), + "{name} failed in its own process:\n{stdout}\n{}", + String::from_utf8_lossy(&output.stderr), + ); + false +} + +/// An update whose grant can't be written, and whose old module can't be put back either, stays in +/// the store: a disabled plug-in is turned on, as for any update the store holds. The old grant +/// doesn't cover the new module, so the next start skips it until it is approved. +#[test] +fn an_update_left_in_the_store_by_a_failed_put_back_turns_a_disabled_plugin_on() { + if !in_own_process("an_update_left_in_the_store_by_a_failed_put_back_turns_a_disabled_plugin_on") { + return; + } + use vectorcraft_plugins::{permissions::sha256_hex, registry, safe_mode}; + let folder = crate::tests_links::Folder::new("plugin-store-failed-put-back-update"); + let id = "test.engine.permissions.putbackupdate"; + let first = asking(id, r#"["doc.read"]"#, &json!({"objects": []})); + let changed = asking(id, r#"["doc.read","doc.write"]"#, &json!({"objects": []})); + let mut s = session(); + s.attach_plugin_store(folder.0.clone()); + run(&mut s, "plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&first), "grant": ["doc.read"]})); + run(&mut s, "plugin.setEnabled", json!({"id": id, "enabled": false})); + let grants = folder.0.join("grants.json"); + let recorded = std::fs::read(&grants).unwrap(); + std::fs::remove_file(&grants).unwrap(); + std::fs::create_dir_all(grants.join("block")).unwrap(); + + crate::store::PUT_BACK_FAILS.set(true); + let update = json!({"dataBase64": vectorcraft_format::base64_encode(&changed), "grant": ["doc.read", "doc.write"], "replace": true}); + let installed = s.execute("plugin.install", &update); + crate::store::PUT_BACK_FAILS.set(false); + assert_eq!(installed.unwrap()["hash"], sha256_hex(&changed), "the new module runs in this session"); + assert_eq!(registry::get(id).map(|p| p.hash().to_string()), Some(sha256_hex(&changed))); + assert_eq!(std::fs::read(s.stored_plugin_file(id).unwrap()).unwrap(), changed, "the store holds the new module"); + assert!(safe_mode::disabled().is_empty(), "the module the store holds is turned on"); + let saved = std::fs::read(folder.0.join(crate::store::DISABLED_FILE)).unwrap(); + assert!(safe_mode::parse_disabled_file(&saved).is_empty(), "nothing disabled is saved"); + + std::fs::remove_dir_all(&grants).unwrap(); + std::fs::write(&grants, &recorded).unwrap(); + registry::remove(id); + registry::revoke(id); + let mut next = session(); + next.attach_plugin_store(folder.0.clone()); + assert!(registry::get(id).is_none(), "the old grant doesn't cover the new module"); + safe_mode::set_disabled(vec![]); +} + +/// A first install whose grant can't be written, and whose module can't be taken out of the store +/// again, stays in the store: the plug-in's file is turned on. No grant covers the module, so the +/// next start skips it until it is approved. +#[test] +fn a_first_install_left_in_the_store_by_a_failed_put_back_turns_its_file_on() { + if !in_own_process("a_first_install_left_in_the_store_by_a_failed_put_back_turns_its_file_on") { + return; + } + use vectorcraft_plugins::{registry, safe_mode}; + let folder = crate::tests_links::Folder::new("plugin-store-failed-put-back-first"); + let id = "test.engine.permissions.putbackfirst"; + let first = asking(id, r#"["doc.read"]"#, &json!({"objects": []})); + let mut s = session(); + s.attach_plugin_store(folder.0.clone()); + let module = s.stored_plugin_file(id).unwrap(); + run(&mut s, "plugin.setEnabled", json!({"file": module, "enabled": false})); + let grants = folder.0.join("grants.json"); + std::fs::create_dir_all(grants.join("block")).unwrap(); + + crate::store::PUT_BACK_FAILS.set(true); + let installed = s.execute("plugin.install", &json!({"dataBase64": vectorcraft_format::base64_encode(&first), "grant": ["doc.read"]})); + crate::store::PUT_BACK_FAILS.set(false); + installed.unwrap(); + assert!(registry::get(id).is_some(), "the module runs in this session"); + assert_eq!(std::fs::read(&module).unwrap(), first, "the store holds the module"); + assert!(safe_mode::disabled().is_empty(), "the file the store holds is turned on"); + let saved = std::fs::read(folder.0.join(crate::store::DISABLED_FILE)).unwrap(); + assert!(safe_mode::parse_disabled_file(&saved).is_empty(), "nothing disabled is saved"); + + std::fs::remove_dir_all(&grants).unwrap(); + registry::remove(id); + registry::revoke(id); + let mut next = session(); + next.attach_plugin_store(folder.0.clone()); + assert!(registry::get(id).is_none(), "no grant covers the module"); + safe_mode::set_disabled(vec![]); +} + /// A plug-in folder reports a changed module as needing approval and still loads the other files; /// granted on its own, the changed module loads from the folder. A session without a store uses the /// grants held by the process. diff --git a/docs/mcp.md b/docs/mcp.md index 5826c5c8c..3d38a2770 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -7593,7 +7593,9 @@ pendingItem}`; a command does not turn Safe Mode on or off. `plugin.setEnabled { off for the next launch, as Help › Safe Mode's Enable on Re-launch does; it does not load, unload or uninstall anything now, and `plugin.install` of a disabled plug-in turns it on. Turning a plug-in on by `id` or by installing it also turns on the file it is stored in, which names it when it stopped a launch while loading; when the new version can't be saved -over that file, the plug-in stays disabled. `plugin.list` reports `safeMode` and `disabled`, and +over that file, the plug-in stays disabled. When the new version is saved but its grant can't be, and the file can't be +returned to what it held before (the old module, or no file), the plug-in is turned on, and the next launch skips it +until it is approved. `plugin.list` reports `safeMode` and `disabled`, and `plugin.reload` skips disabled plug-ins. A change to the disabled list is saved on top of the list the file holds at that moment, so a change another window saved to the same preferences meanwhile is kept. When the list file can't be read (it is too large) or holds another format or version, it is left as it is; a file that isn't JSON is replaced. diff --git a/docs/plugins.md b/docs/plugins.md index 06fd89f28..699de48aa 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -240,7 +240,9 @@ capability model proposed for PhotoCraft plug-ins records the new hash. When the new module or its grant can't be written to the `Plug-ins` folder, the folder keeps the old module and its grant, and the old module loads at the next start. If the write that puts the old module back fails as well, the folder holds the new module with the old grant, the grant does not cover that module, and the - plug-in is skipped at the next start until it is approved again. A grant whose hash is not 64 lower-case + plug-in is skipped at the next start until it is approved again. When the folder held no module for that id, that + write removes the new module; when the removal fails, the folder holds the new module and no grant covers it. In + both cases a disabled plug-in is turned on, as for any install the folder holds. A grant whose hash is not 64 lower-case hexadecimal digits covers nothing. `plugin.remove` forgets the grant, and so does a successful `plugin.install` of a module that lists no permissions under that id, so the earlier bytes need a new approval. When `plugin.remove` can't remove the grant from `grants.json`, it returns an error, the plug-in stays installed, and a second `plugin.remove` diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index b4c949f74..c205dbb22 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1652,6 +1652,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Plug-in data kept with the document (G17):** a filter writes JSON of at most 1 MiB with `documentData` (one undo step with its objects; `null` removes it) and reads it back in `_context.documentData`, which no other filter is given. It is the document's foreign data (`unknown.pluginData`, by plug-in id), as the perspective grid is, so the native file keeps it with no section of its own; an exporter is given the whole document, so it can read every plug-in's. Regressions: `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_export.rs` (the exporter), `crates/doc/src/plugin_data.rs`, `crates/plugins/src/objects/tests.rs`. Scores and hours unchanged. - **Heavy filter plug-ins (G17):** a filter's manifest may ask for `"budget": "heavy"`: 1 G instructions (20 times a light run's) and 300 s a run, for routing, packing or simulation. Install Plug-in… and File › Open ask the user first, `plugin.install` needs `allowHeavy: true` (`plugin.inspect` tells beforehand), the Additional Plug-ins Folder skips such filters, and the plug-in store keeps the answer. Its dialog has no Preview; the app runs it on a thread of its own with a progress window (Cancel, Continue in Background) and applies the result as `plugin.run` with the same parameters, one undo step, unless the document or the plug-in changed meanwhile. Regressions: `crates/plugins/tests/abi.rs`, `crates/plugins/src/manifest.rs`, `crates/engine/src/tests/plugins.rs`, `crates/ui-egui/src/tests/plugin_runs.rs`. Scores and hours unchanged. - **Safe Mode for plug-ins (G17):** `vectorcraft --safe-mode` and `vectorcraft-cli --safe-mode ` start without plug-ins. When a launch stopped while a plug-in was loading or drawing its first live effect, the next launch names that plug-in, without saying it caused the stop, and offers Disable and Continue, Start Normally or Start in Safe Mode. Help › Safe Mode lists the disabled plug-ins with Enable on Re-launch, which takes effect at the next launch, and the app bar and the window title show Safe Mode. The help page (https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf, pages 65 to 69) describes the same choice between diagnosing and starting normally, a Safe Mode with the problem item off and the rest of the app working, and turning the item back on at the next launch. In Safe Mode no plug-in loads or runs, and a live-effect plug-in draws its object's geometry unchanged while the object keeps the effect, which the Appearance panel marks "(Safe Mode)" with the plug-in's id in its tooltip and `effect.list` marks `skipped`; Expand Appearance refuses such an object. The web build keeps no plug-ins between page loads and has no marker. Each desktop launch with preferences keeps a marker in the `Launch` folder next to them; `vectorcraft-cli` and a desktop launch without preferences keep none, so a stop there leads to no choice at the next launch. Disabled plug-ins are listed in `Plug-ins/disabled.json`. `plugin.safeMode` reports Safe Mode and the disabled plug-ins, and `plugin.setEnabled` changes the list. Regressions: `crates/plugins/src/safe_mode.rs`, `crates/plugins/tests/safe_mode.rs`, `crates/engine/tests/plugin_safe_mode.rs`, `crates/ui-egui/tests/safe_mode.rs`. Scores and hours unchanged. +- **A disabled plug-in whose update stays in the store is turned on:** when `plugin.install` writes a module that lists permissions to the `Plug-ins` folder, its grant can't be written, and the folder can't be returned to the old module (or, on a first install, the new module can't be removed), the folder holds the new module, so a disabled plug-in is turned on, by id and by stored file, as for any install the folder holds. The old grant, or none, doesn't cover the new module, so the next start skips it until it is approved. Regressions: `crates/engine/src/tests/plugins.rs`. Scores and hours unchanged. - **Plug-in instruction budget at a bulk memory instruction:** a run that pauses where one slice of its instruction budget ends keeps the instructions the slice still held. A `memory.fill` or `memory.copy` that the slice can't pay for in full runs on the next slice, so a run whose budget covers its work finishes; a run ends with `Limit("instruction budget")` only when the instructions it has left can't pay for the next instruction. Regressions: `crates/plugins/tests/abi.rs`. Scores and hours unchanged. - **Bulk memory fuzz cases use the host's fuel constants:** `crates/engine/tests/plugin_fuzz.rs` samples runs around one slice of the instruction budget and lengths around the bytes one unit of fuel pays for with `vectorcraft_plugins`' own `FUEL_SLICE` and `BYTES_COPIED_PER_FUEL`, so the sampled cases move when either value changes. Regressions: `crates/engine/tests/plugin_fuzz.rs`. Scores and hours unchanged. - **Plug-in permissions (G17):** a manifest may list `permissions` (`doc.read`, `doc.write`, `state`, `effect.render`, `export.write`; the first three follow storytold/photocraft#662) and an `api` level; without the list a plug-in gets what its kind implies and installs as before. Install Plug-in… and File › Open show the name, id, version, file SHA-256 and each permission in plain words, with control and bidirectional characters escaped, and install nothing on Cancel; for a plug-in that lists permissions, `plugin.install` needs `grant` and folders install it only when it is granted. The grant is kept with the module's hash, a module asking for more asks again, and a filter run refuses output its permissions don't allow. Regressions: `crates/plugins/src/permissions.rs`, `crates/plugins/src/manifest.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.rs`, `crates/ui-egui/src/tests/plugins.rs`. Scores and hours unchanged. From 08d4a5b486030a5d78f6873058da01b8f210659d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 14:03:36 -0800 Subject: [PATCH 2/2] Plug-ins: a put-back that fails after writing the old module keeps a disabled plug-in off When plugin.install can't write or remove a grant and the write that puts the old module back reports a failure, install now reads the stored module again and turns the plug-in on only when the store holds the new module. A put-back that wrote the old bytes and then failed (for example at its last flush) leaves a disabled plug-in disabled. This applies to modules that list permissions and to modules that don't. --- crates/engine/src/cmd/plugin.rs | 10 ++-- crates/engine/src/store.rs | 12 ++++- crates/engine/src/tests/plugins.rs | 85 ++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/plugin.rs b/crates/engine/src/cmd/plugin.rs index 94b8f7f58..456d130a9 100644 --- a/crates/engine/src/cmd/plugin.rs +++ b/crates/engine/src/cmd/plugin.rs @@ -742,15 +742,17 @@ fn install(s: &mut Session, p: &Value) -> Result { // Installed plug-ins stay installed after a restart (#2541). The module is written first and its // grant only once it is there; when the grant can't be written, the store gets the module it // held back. A failed write of that module is only logged: the store then holds the new module, - // which the grant doesn't cover. A stored module that can't be read back isn't replaced by one - // that needs a grant. + // which the grant doesn't cover. A write that reports a failure may still have put the old + // module back, so the store is read again to tell which module it holds. A stored module that + // can't be read back isn't replaced by one that needs a grant. let previous = s.stored_plugin_bytes(plugin.id()); let mut stored = (previous.is_ok() || !m.asks_permissions()) && s.store_plugin(plugin.id(), &bytes); + let holds_new = |s: &Session| s.stored_plugin_bytes(plugin.id()).is_ok_and(|held| held.as_deref() == Some(&bytes[..])); if m.asks_permissions() { let g = Grant { id: m.id.clone(), hash: plugin.hash().to_string(), permissions: wanted }; registry::grant(g.clone()); if stored && !s.store_grant(&m.id, Some(g)) { - stored = !s.put_back_plugin(plugin.id(), previous.ok().flatten().as_deref()); + stored = !s.put_back_plugin(plugin.id(), previous.ok().flatten().as_deref()) && holds_new(s); } } else { // This module takes the id over, so nothing the user approved for the id applies any more. @@ -763,7 +765,7 @@ fn install(s: &mut Session, p: &Value) -> Result { // stays; one that can't be opened or read may approve the old bytes once it reads again. if stored && !s.store_grant(&m.id, None) && s.holds_plugin_grant(&m.id) { let back = previous.as_ref().is_ok_and(|held| s.put_back_plugin(plugin.id(), held.as_deref())); - stored = !back; + stored = !back && holds_new(s); if !back || matches!(previous, Ok(None)) { s.ignore_stored_grant(&m.id); } diff --git a/crates/engine/src/store.rs b/crates/engine/src/store.rs index 01ebc4652..a8a474fc9 100644 --- a/crates/engine/src/store.rs +++ b/crates/engine/src/store.rs @@ -190,6 +190,9 @@ 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: [`Session::put_back_plugin`] writes the old module back on this thread and then + /// reports a failure, as when the last flush of a copy fails. + pub(crate) static PUT_BACK_FAILS_AFTER_WRITE: 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. @@ -498,16 +501,21 @@ impl Session { /// Return the store's module for the plug-in `id` to `previous`, the bytes it held before a /// write ([`Session::stored_plugin_bytes`]): with `None`, the file is removed. → whether the - /// store now holds `previous`. + /// write reported success; a write that reports a failure may still have put `previous` back. pub(crate) fn put_back_plugin(&self, id: &str, previous: Option<&[u8]>) -> bool { #[cfg(test)] if PUT_BACK_FAILS.get() { return false; } - match previous { + let back = match previous { Some(bytes) => self.store_plugin(id, bytes), None => self.unstore_plugin(id), + }; + #[cfg(test)] + if PUT_BACK_FAILS_AFTER_WRITE.get() { + return false; } + back } /// The file the plug-in `id` is stored in, spelled as [`Session::load_stored_plugins`] names it diff --git a/crates/engine/src/tests/plugins.rs b/crates/engine/src/tests/plugins.rs index 4839a1c45..51163d3d6 100644 --- a/crates/engine/src/tests/plugins.rs +++ b/crates/engine/src/tests/plugins.rs @@ -2243,6 +2243,91 @@ fn an_update_left_in_the_store_by_a_failed_put_back_turns_a_disabled_plugin_on() safe_mode::set_disabled(vec![]); } +/// An update whose grant can't be written, and whose put-back writes the old module and then +/// reports a failure, leaves the old module in the store: a disabled plug-in stays disabled, and the +/// next start doesn't load it. +#[test] +fn an_update_put_back_that_fails_after_writing_keeps_a_disabled_plugin_off() { + if !in_own_process("an_update_put_back_that_fails_after_writing_keeps_a_disabled_plugin_off") { + return; + } + use vectorcraft_plugins::{registry, safe_mode}; + let folder = crate::tests_links::Folder::new("plugin-store-put-back-fails-after-write"); + let id = "test.engine.permissions.putbacklate"; + let first = asking(id, r#"["doc.read"]"#, &json!({"objects": []})); + let changed = asking(id, r#"["doc.read","doc.write"]"#, &json!({"objects": []})); + let mut s = session(); + s.attach_plugin_store(folder.0.clone()); + run(&mut s, "plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&first), "grant": ["doc.read"]})); + run(&mut s, "plugin.setEnabled", json!({"id": id, "enabled": false})); + let disabled = safe_mode::disabled(); + assert!(!disabled.is_empty()); + let grants = folder.0.join("grants.json"); + let recorded = std::fs::read(&grants).unwrap(); + std::fs::remove_file(&grants).unwrap(); + std::fs::create_dir_all(grants.join("block")).unwrap(); + + crate::store::PUT_BACK_FAILS_AFTER_WRITE.set(true); + let update = json!({"dataBase64": vectorcraft_format::base64_encode(&changed), "grant": ["doc.read", "doc.write"], "replace": true}); + let installed = s.execute("plugin.install", &update); + crate::store::PUT_BACK_FAILS_AFTER_WRITE.set(false); + installed.unwrap(); + assert_eq!(std::fs::read(s.stored_plugin_file(id).unwrap()).unwrap(), first, "the store holds the old module"); + assert_eq!(safe_mode::disabled(), disabled, "the plug-in stays disabled"); + let saved = std::fs::read(folder.0.join(crate::store::DISABLED_FILE)).unwrap(); + assert!(!safe_mode::parse_disabled_file(&saved).is_empty(), "the disabled list on disk is kept"); + + std::fs::remove_dir_all(&grants).unwrap(); + std::fs::write(&grants, &recorded).unwrap(); + registry::remove(id); + registry::revoke(id); + let mut next = session(); + next.attach_plugin_store(folder.0.clone()); + assert!(registry::get(id).is_none(), "the disabled old module doesn't load"); + safe_mode::set_disabled(vec![]); +} + +/// A module without permissions whose install can't remove the old grant, and whose put-back writes +/// the old module and then reports a failure, leaves the old module in the store: a disabled plug-in +/// stays disabled. +#[test] +fn a_plain_update_put_back_that_fails_after_writing_keeps_a_disabled_plugin_off() { + if !in_own_process("a_plain_update_put_back_that_fails_after_writing_keeps_a_disabled_plugin_off") { + return; + } + use vectorcraft_plugins::safe_mode; + let folder = crate::tests_links::Folder::new("plugin-store-plain-put-back-fails-after-write"); + let id = "test.engine.permissions.plainputbacklate"; + let empty = json!({"objects": []}); + let approved = asking(id, r#"["doc.read"]"#, &empty); + 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": vectorcraft_format::base64_encode(&approved), "grant": ["doc.read"]})); + run(&mut s, "plugin.setEnabled", json!({"id": id, "enabled": false})); + let disabled = safe_mode::disabled(); + assert!(!disabled.is_empty()); + let file = s.stored_plugin_file(id).unwrap(); + // `1e9` is written back as `1000000000.0`, which makes the rewritten file larger than the 1 MiB that is read. + let entry = serde_json::from_slice::>(&std::fs::read(&grants).unwrap()).unwrap().remove(0); + let full = format!(r#"[{entry},{{"n":[{}]}}]"#, vec!["1e9"; 200_000].join(",")).into_bytes(); + std::fs::write(&grants, &full).unwrap(); + + crate::store::PUT_BACK_FAILS_AFTER_WRITE.set(true); + let installed = s.execute("plugin.install", &json!({"dataBase64": vectorcraft_format::base64_encode(&plain), "replace": true})); + crate::store::PUT_BACK_FAILS_AFTER_WRITE.set(false); + installed.unwrap(); + assert_eq!(std::fs::read(&file).unwrap(), approved, "the store holds the old module"); + assert_eq!(safe_mode::disabled(), disabled, "the plug-in stays disabled"); + let saved = std::fs::read(folder.0.join(crate::store::DISABLED_FILE)).unwrap(); + assert!(!safe_mode::parse_disabled_file(&saved).is_empty(), "the disabled list on disk is kept"); + + std::fs::remove_file(&grants).unwrap(); + run(&mut s, "plugin.remove", json!({"id": id})); + safe_mode::set_disabled(vec![]); +} + /// A first install whose grant can't be written, and whose module can't be taken out of the store /// again, stays in the store: the plug-in's file is turned on. No grant covers the module, so the /// next start skips it until it is approved.