From aef50c2470b6a6ee8822922891620b9073685bac Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 22:06:08 -0800 Subject: [PATCH] Package: a package started after its save keeps the automation roots With Background Save and Background Export on, Package's OK on a dirty document starts the package once the save's result arrives. That start ran in background::after's callback, on the frame that applied the save's result, so the worker took the frame's file access scope. The desktop frame has no automation roots, so a package asked for by an automation request confined to its roots could write outside them. OK now captures the scope in force and starts the deferred package in it, so the package is checked against the same roots as the save. --- crates/ui-egui/src/dialogs/package.rs | 5 ++-- crates/ui-egui/src/dialogs/tests_package.rs | 29 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/dialogs/package.rs b/crates/ui-egui/src/dialogs/package.rs index 2ff2097975..25d17d3902 100644 --- a/crates/ui-egui/src/dialogs/package.rs +++ b/crates/ui-egui/src/dialogs/package.rs @@ -151,13 +151,14 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let start = move |app: &mut VectorcraftApp| background::run(app, enabled, label, None, work, move |app, r| packaged(app, r, web, in_background.get())); // Both in the background: the package, collected from the document as it is now, starts once - // the save is written. + // the save is written, with the automation roots in force at OK. if let Some(id) = saving.filter(|_| enabled) { later.set(true); app.ui.dialog = None; + let scope = vectorcraft_engine::file_access::ScopeHandle::capture(); background::after(app, id, move |app, r| { if saved(r) - && let Err(e) = start(app) + && let Err(e) = scope.run(|| start(app)) { app.status(e); } diff --git a/crates/ui-egui/src/dialogs/tests_package.rs b/crates/ui-egui/src/dialogs/tests_package.rs index 5c9e899354..acd1368ed4 100644 --- a/crates/ui-egui/src/dialogs/tests_package.rs +++ b/crates/ui-egui/src/dialogs/tests_package.rs @@ -248,6 +248,35 @@ fn a_failed_save_before_packaging_writes_no_package() { } } +/// A package that an automation request confined to its roots asks for, started once the +/// document's background save is written, is still confined to those roots, though the save's +/// result arrives outside the request. +#[test] +fn a_package_started_after_its_save_keeps_the_automation_roots() { + let (dir, elsewhere) = (Folder::new("scope"), Folder::new("scope-elsewhere")); + let (doc, out) = (dir.path("card.vectorcraft"), elsewhere.path("out")); + let root = std::path::Path::new(&out).join("card Folder"); + let mut app = saved(&dir); + let files = Files::default(); + let recorded = files.clone(); + app.services.write_shared = Some(Arc::new(move |p: &str, b: &[u8]| { + vectorcraft_engine::file_access::check_write(p)?; + recorded.lock().unwrap().push((p.to_string(), b.to_vec())); + Ok(()) + })); + let roots = AutomationRoots::new(&[dir.0.as_path()], &[dir.0.as_path()]).unwrap(); + app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); + package_dialog(&mut app, &out); + let r = vectorcraft_engine::file_access::confine(roots.as_ref(), || confirm(&mut app)).unwrap(); + assert_eq!(r["background"], true, "{r}"); + // The save's result is applied outside the request, as the next frame does. + background::wait_all(&mut app); + assert_eq!(written(&files), [doc], "only the save, inside the roots"); + assert!(!root.exists(), "no package folder outside the roots"); + assert!(!app.session.active().unwrap().is_dirty(), "the save was written"); + assert!(app.ui.status.contains("failed"), "the package was refused: {}", app.ui.status); +} + #[test] fn a_package_made_at_once_waits_for_one_in_the_background() { let dir = Folder::new("twice");