diff --git a/crates/engine/src/script/mod.rs b/crates/engine/src/script/mod.rs index 35e93c1ad..97b5f1889 100644 --- a/crates/engine/src/script/mod.rs +++ b/crates/engine/src/script/mod.rs @@ -502,6 +502,8 @@ struct Job { /// The documents open when the run began, by [`DocState::uid`]. start_uids: Vec, name: String, + /// The file-access scope `script.run` came in, which every slice runs in. + scope: crate::file_access::ScopeHandle, } /// The `Data` for a JSON value, within `limits`. When it does not fit, the error is the place in @@ -986,7 +988,8 @@ impl Session { self.script_report = Some(report); return Err(EngineError::Other(message)); } - let job = Job { realm, host, world: World::working(self), start_uids: open, name: request.name }; + let scope = crate::file_access::ScopeHandle::capture(); + let job = Job { realm, host, world: World::working(self), start_uids: open, name: request.name, scope }; let id = NEXT_JOB.fetch_add(1, std::sync::atomic::Ordering::Relaxed); JOBS.with(|jobs| jobs.borrow_mut().insert(id, job)); self.script_key = Some(JobKey(id)); @@ -1170,20 +1173,24 @@ fn failure_text(report: &Value) -> String { } /// A run's slice: the working copy in, the real session lent to the host functions, one measured -/// slice run, and everything back. A panic in the interpreter is the outer `Err`. +/// slice run in the file-access scope the run began in, and everything back. A panic in the +/// interpreter is the outer `Err`. fn run_slice(s: &mut Session, job: &mut Job) -> std::result::Result, String> { let depth = std::mem::replace(&mut s.depth, 0); job.world.swap(s); s.script_in_run = true; let cell = job.host.session.clone(); std::mem::swap(s, &mut *cell.borrow_mut()); + let scope = job.scope.clone(); let realm = &mut job.realm; - let r = guard::catch_panic(|| { - #[cfg(test)] - if PANIC_IN_SLICE.with(Cell::get) { - panic!("a slice failed"); - } - realm.advance(SLICE_FUEL) + let r = scope.run(|| { + guard::catch_panic(|| { + #[cfg(test)] + if PANIC_IN_SLICE.with(Cell::get) { + panic!("a slice failed"); + } + realm.advance(SLICE_FUEL) + }) }); std::mem::swap(s, &mut *cell.borrow_mut()); s.script_in_run = false; diff --git a/crates/engine/src/tests/script_run.rs b/crates/engine/src/tests/script_run.rs index 5480ba499..eba254116 100644 --- a/crates/engine/src/tests/script_run.rs +++ b/crates/engine/src/tests/script_run.rs @@ -862,6 +862,44 @@ fn a_file_saved_before_a_failure_stays_written_and_is_named() { let _ = std::fs::remove_file(&path); } +/// A run in the app's frames keeps the file-access scope its `script.run` came in, whatever scope +/// the frames that advance it run in. +#[test] +fn a_framed_run_keeps_the_file_access_scope_it_started_in() { + let dir = std::env::temp_dir().join(format!("vc-script-scope-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let (allowed, outside) = (dir.join("allowed"), dir.join("outside")); + std::fs::create_dir_all(&allowed).unwrap(); + std::fs::create_dir_all(&outside).unwrap(); + let roots = crate::file_access::AutomationRoots::new(&[&allowed], &[&allowed]).unwrap().unwrap(); + let export = |target: &std::path::Path| { + let p = target.to_string_lossy().into_owned(); + format!("try {{ vc.run('document.export', {{path: {p:?}, format: 'svg'}}); vc.log('ok'); }} catch (e) {{ vc.log(e.name); }}") + }; + let finish = |s: &mut Session| { + loop { + if let Some(r) = s.script_pump() { + return r; + } + } + }; + let mut s = two_docs(); + s.script_frames = true; + let confined_target = outside.join("confined.svg"); + let started = crate::file_access::confine(Some(&roots), || run(&mut s, &export(&confined_target))).unwrap(); + assert_eq!(started["running"], true); + let r = crate::file_access::unconfined(|| finish(&mut s)).unwrap(); + assert_eq!(r["log"], json!(["Error"]), "a confined caller's run stays confined: {r}"); + assert!(!confined_target.exists()); + let user_target = outside.join("user.svg"); + let started = crate::file_access::unconfined(|| run(&mut s, &export(&user_target))).unwrap(); + assert_eq!(started["running"], true); + let r = crate::file_access::confine(Some(&roots), || finish(&mut s)).unwrap(); + assert_eq!(r["log"], json!(["ok"]), "a run the person started is not confined by a later caller: {r}"); + assert!(user_target.exists()); + let _ = std::fs::remove_dir_all(&dir); +} + #[test] fn a_committed_run_exports_what_it_drew() { let path = std::env::temp_dir().join(format!("vc-script-export-{}.svg", std::process::id()));