diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ce5b221834..ed53ad2a50 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -6576,6 +6576,57 @@ fn xfa_initialize_scripts_that_hide_subforms_lay_the_form_out_once() { assert_eq!(names(&s, again), opened); } +/// A script that resets several subforms one after another (`xfa.host.resetData` once for +/// each, as forms with optional pages do) resets every one of them, and nothing else; resets +/// in a row are applied together. +#[test] +fn xfa_resets_in_a_row_reset_every_named_subform_and_nothing_else() { + let sections = 5; + let mut body = String::from( + r#"100"#, + ); + let resets: String = (0..sections).map(|n| format!("xfa.host.resetData('form.extra{n}');")).collect(); + body.push_str(&format!( + r#""# + )); + body.push_str(r#""#); + for n in 0..sections { + body.push_str(&format!( + r#"default"# + )); + } + let notes: String = (0..sections).map(|n| format!("typed {n}")).collect(); + let tpl = format!( + r#" + + +
typed{notes}
+
+"# + ); + let mut s = Session::new().with_clock(|| 1_700_000_000).with_xfa_time_scale(100); + // Without the resets the sections show what was typed. + let untouched = Arc::new(pdfcraft_xfa::fixtures::shell(&tpl.replace(&resets, ""))); + let plain = s.open("plain.pdf", None, untouched, None).expect("opens"); + let typed: Vec<_> = s.get(plain).unwrap().form.iter().filter(|f| f.name.contains("note")).map(|f| f.value.clone()).collect(); + assert_eq!(typed, (0..sections).map(|n| vec![format!("typed {n}")]).collect::>()); + let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&tpl)); + let id = s.open("resets.pdf", None, bytes, None).expect("opens"); + let out = s.take_js_output(id); + assert!(out.errors.is_empty(), "{:?}", out.errors); + let form = &s.get(id).unwrap().form; + let notes: Vec<_> = form.iter().filter(|f| f.name.contains("note")).map(|f| f.value.clone()).collect(); + assert_eq!(notes.len(), sections, "{form:?}"); + assert!(notes.iter().all(|v| v == &vec!["default".to_string()]), "every section is reset: {notes:?}"); + let keep = form.iter().find(|f| f.name.contains("keep")).map(|f| f.value.clone()); + assert_eq!(keep, Some(vec!["kept".to_string()]), "the script's later write stands"); +} + /// The scripts of one event share the form as the engine holds it: a failed script's /// half-done writes reach nobody, and a script reads the saved value a choice list was given /// by its shown text. diff --git a/crates/engine/src/xfa.rs b/crates/engine/src/xfa.rs index 55e31e187f..ae56646986 100644 --- a/crates/engine/src/xfa.rs +++ b/crates/engine/src/xfa.rs @@ -159,6 +159,23 @@ fn to_js(n: &FormNode) -> XfaNode { } } +/// Add the names of a `resetData` call to the run of resets before it. No names means the +/// whole form, which takes in every other. +fn merge_reset(merged: &mut Vec, names: &[String]) { + if merged.is_empty() { + return; + } + if names.is_empty() { + merged.clear(); + return; + } + for n in names { + if !merged.contains(n) { + merged.push(n.clone()); + } + } +} + fn find_container<'a>(n: &'a FormNode, som: &str, depth: usize) -> Option<&'a FormNode> { if depth > 64 { return None; @@ -275,6 +292,9 @@ struct Runner<'a, 'b> { fields: Option>, /// The live form as last built; dropped when a script changed the form. cached: Option, + /// `cached` as the scripts get it, made once and sent to each script until the live form + /// changes in any way (a value too): the script engine reuses what it built from it. + js_root: Option>, /// The script engine for this event: one thread for all its scripts, each run in a fresh /// context on the form as it is then (`cached`, kept up to date in place for values and /// rebuilt when the structure changed). @@ -309,6 +329,7 @@ impl<'a, 'b> Runner<'a, 'b> { by_path: None, fields: None, cached: None, + js_root: None, session: None, changes: Changes::default(), } @@ -318,6 +339,7 @@ impl<'a, 'b> Runner<'a, 'b> { /// script. fn invalidate(&mut self) { self.cached = None; + self.js_root = None; } /// The script engine for this event. @@ -334,6 +356,8 @@ impl<'a, 'b> Runner<'a, 'b> { let op = pdfcraft_xfa::DataOp::Value { path, text }; self.data.get_or_insert_with(Default::default).apply(&op); self.pending.push(op); + // The value changes in the live form below: the scripts' copy is made again. + self.js_root = None; let refreshed = match self.cached.as_mut() { Some(live) => pdfcraft_xfa::refresh_value(&mut live.root, self.tpl, self.data.as_ref(), som).is_some(), None => true, @@ -591,7 +615,9 @@ impl<'a, 'b> Runner<'a, 'b> { } } } - for e in effects { + // Resets in a row, applied together when the run of them ends. + let mut pending_reset: Option> = None; + for (at, e) in effects.iter().enumerate() { // Only a click script may print, save, open links or move the focus (not the // calculations a click sets off, nor anything run on open or on a change). if let Some(what) = click_only(e) @@ -640,7 +666,19 @@ impl<'a, 'b> Runner<'a, 'b> { relayout = true; } XfaEffect::MessageBox(m) => self.alert(m.clone()), - XfaEffect::ResetData(names) => self.reset(names)?, + XfaEffect::ResetData(names) => { + // A script resetting several subforms in a row (`resetData` once for each) + // resets them together: the same fields end up at their defaults. + if let Some(merged) = pending_reset.as_mut() { + merge_reset(merged, names); + } else { + pending_reset = Some(names.clone()); + } + let next_is_reset = effects.get(at.saturating_add(1)).is_some_and(|e| matches!(e, XfaEffect::ResetData(_))); + if !next_is_reset && let Some(names) = pending_reset.take() { + self.reset(&names)?; + } + } XfaEffect::Print => self.run.out.requests.push(Request::Print), XfaEffect::SaveAs => self.run.out.requests.push(Request::SaveAs), XfaEffect::LaunchUrl(u) => self.run.out.requests.push(Request::LaunchUrl(u.clone())), @@ -669,7 +707,14 @@ impl<'a, 'b> Runner<'a, 'b> { #[cfg(test)] self.check_live(); let event = XfaEvent { activity: ev.activity.clone(), target: ev.som.clone(), new_text: new_text.to_string(), prev_text: String::new() }; - let root = to_js(&self.live().root); + let root = match &self.js_root { + Some(r) => Arc::clone(r), + None => { + let r = Arc::new(to_js(&self.live().root)); + self.js_root = Some(Arc::clone(&r)); + r + } + }; let script = Script { source: ev.script.clone(), formcalc: ev.formcalc, diff --git a/crates/js/src/formcalc.rs b/crates/js/src/formcalc.rs index 3e68ce081d..ff43bf0837 100644 --- a/crates/js/src/formcalc.rs +++ b/crates/js/src/formcalc.rs @@ -109,7 +109,7 @@ type R = Result; /// Run a FormCalc `script` for `event` on the form `root`, as [`crate::xfa::run_xfa`] runs a /// JavaScript one. pub fn run_formcalc(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, limits: Limits) -> XfaOutcome { - run_formcalc_at(script, event, doc, root.clone(), limits, None) + run_formcalc_at(script, event, doc, std::sync::Arc::new(root.clone()), false, limits, None) } /// [`run_formcalc`] on an owned form, giving up at `deadline` (the clock is read every @@ -118,7 +118,8 @@ pub(crate) fn run_formcalc_at( script: &str, event: &XfaEvent, doc: &XfaDoc, - root: XfaNode, + root: std::sync::Arc, + reuse: bool, limits: Limits, deadline: Option, ) -> XfaOutcome { @@ -133,7 +134,7 @@ pub(crate) fn run_formcalc_at( Ok(b) => b, Err(e) => return XfaOutcome { error: Some(e), ..Default::default() }, }; - let mut host = XHost::new(root, &event.target, doc); + let mut host = XHost::new(root, reuse, &event.target, doc); #[cfg(target_arch = "wasm32")] let _ = deadline; let mut interp = Interp { @@ -177,7 +178,7 @@ pub fn run_formcalc_within(script: &str, event: &XfaEvent, doc: &XfaDoc, root: X // FormCalc has no engine to start: its time (and its own deadline) begins at once. run_within("pdfcraft-formcalc", timeout, move |started| { started(); - run_formcalc_at(&script, &event, &doc, root, limits, Some(timeout)) + run_formcalc_at(&script, &event, &doc, std::sync::Arc::new(root), false, limits, Some(timeout)) }) } diff --git a/crates/js/src/formcalc/access.rs b/crates/js/src/formcalc/access.rs index bea902c752..5f279ea4f6 100644 --- a/crates/js/src/formcalc/access.rs +++ b/crates/js/src/formcalc/access.rs @@ -313,16 +313,16 @@ impl Interp<'_> { let Some(n) = self.node(i) else { return Ok(Val::Null) }; Ok(match name { "formattedValue" => match &n.formatted { - Some(f) => Val::Str(f.clone()), + Some(f) => Val::Str(f.to_string()), None => self.node_value(i), }, "rawValue" | "value" => self.node_value(i), - "name" => Val::Str(n.name.clone()), - "somExpression" => Val::Str(n.som.clone()), + "name" => Val::Str(n.name.to_string()), + "somExpression" => Val::Str(n.som.to_string()), "className" => Val::Str(n.kind.class_name().into()), "index" | "instanceIndex" => Val::Num(n.index as f64), - "presence" => Val::Str(n.presence.clone()), - "access" => Val::Str(n.access.clone()), + "presence" => Val::Str(n.presence.to_string()), + "access" => Val::Str(n.access.to_string()), "isNull" => Val::Num(if n.value.is_empty() { 1.0 } else { 0.0 }), "mandatory" => Val::Str("disabled".into()), "parent" => n.parent.map_or(Val::Null, Val::Node), @@ -332,7 +332,7 @@ impl Interp<'_> { None => Val::Node(i), }, "instanceManager" => match n.parent { - Some(p) => Val::Im(p, n.name.clone()), + Some(p) => Val::Im(p, n.name.to_string()), None => Val::Null, }, "border" | "ui" | "font" | "caption" | "fillColor" | "borderColor" | "fontColor" | "margin" | "para" | "assist" | "validate" diff --git a/crates/js/src/formcalc/eval.rs b/crates/js/src/formcalc/eval.rs index 0b2dfe4b94..216e26cba5 100644 --- a/crates/js/src/formcalc/eval.rs +++ b/crates/js/src/formcalc/eval.rs @@ -277,11 +277,11 @@ impl Interp<'_> { Some(e) => { let raw = self.eval(e)?; match self.nodes_of(&raw).first() { - Some(i) => self.node(*i).map(|n| n.som.clone()).unwrap_or_default(), + Some(i) => self.node(*i).map(|n| n.som.to_string()).unwrap_or_default(), None => { let s = self.to_str(&raw); let found = resolve_som(self.h, self.h.current, &s); - found.first().and_then(|i| self.node(*i)).map(|n| n.som.clone()).unwrap_or(s) + found.first().and_then(|i| self.node(*i)).map(|n| n.som.to_string()).unwrap_or(s) } } } @@ -396,7 +396,7 @@ impl Interp<'_> { match target { Val::Node(i) => { let Some(n) = self.node(i) else { return Ok(()) }; - let som = n.som.clone(); + let som = n.som.to_string(); match prop { "rawValue" | "value" | "formattedValue" => { let text = self.to_str(&v); diff --git a/crates/js/src/tests.rs b/crates/js/src/tests.rs index 2d3877efe3..c933097330 100644 --- a/crates/js/src/tests.rs +++ b/crates/js/src/tests.rs @@ -1542,7 +1542,8 @@ $host.gotoURL("https://example.org")"#, "for i = 1 upto 100000 do Eval(\"1\") endfor", &ev, &doc, - form(), + std::sync::Arc::new(form()), + false, Limits { loop_iterations: u64::MAX, recursion: 32 }, Some(std::time::Duration::ZERO), ); @@ -1660,12 +1661,53 @@ mod session { source: source.into(), formcalc, event: XfaEvent { activity: activity.into(), target: target.into(), ..Default::default() }, - root: form(), + root: std::sync::Arc::new(form()), limits: Limits::default(), timeout: std::time::Duration::from_secs(3), } } + /// The scripts of an event given the same form start from one flattened copy of it: the + /// second host shares the first one's text, and each host changes only its own copy. + #[test] + fn hosts_given_the_same_form_reuse_its_flattened_copy_and_change_only_their_own() { + let root = std::sync::Arc::new(form()); + let doc = XfaDoc::default(); + let mut first = crate::xfa::XHost::new(std::sync::Arc::clone(&root), true, "form1[0].page1[0].qty[0]", &doc); + let second = crate::xfa::XHost::new(std::sync::Arc::clone(&root), true, "form1[0].page1[0].qty[0]", &doc); + let shared = std::sync::Arc::clone(&first.nodes.get(1).expect("form1").som); + assert!(std::sync::Arc::ptr_eq(&shared, &second.nodes.get(1).expect("form1").som), "the second host starts from the first one's copy"); + let qty = first.current; + let before = second.nodes.get(qty).map(|n| n.value.clone()); + first.set_state(qty, XfaEffect::SetPresence { som: "form1[0].page1[0].qty[0]".into(), presence: "hidden".into() }); + if let Some(n) = first.nodes.get_mut(qty) { + n.value = "99".into(); + } + let third = crate::xfa::XHost::new(std::sync::Arc::clone(&root), true, "form1[0].page1[0].qty[0]", &doc); + for h in [&second, &third] { + assert_eq!(h.nodes.get(qty).map(|n| n.value.clone()), before); + assert_eq!(h.nodes.get(qty).map(|n| n.presence.to_string()).as_deref(), Some("visible")); + } + // Another form (a new `Arc`, even with the same content) is flattened again. + let other = crate::xfa::XHost::new(std::sync::Arc::new(form()), true, "form1[0].page1[0].qty[0]", &doc); + assert!(!std::sync::Arc::ptr_eq(&other.nodes.get(1).expect("form1").som, &shared)); + } + + /// One session given the same form for two scripts: what the first changed on its copy is + /// not in the second's. + #[test] + fn a_form_sent_again_does_not_carry_the_last_scripts_changes() { + let mut s = XfaSession::new(XfaDoc::default()); + let root = std::sync::Arc::new(form()); + let mut a = script("qty.rawValue = 10; details.presence = 'visible'; qty.rawValue", false, "form1[0].page1[0].total[0]", "calculate"); + a.root = std::sync::Arc::clone(&root); + assert_eq!(s.run(a).result.as_deref(), Some("10")); + let mut b = script("qty.rawValue + ':' + details.presence", false, "form1[0].page1[0].total[0]", "calculate"); + b.root = std::sync::Arc::clone(&root); + let o = s.run(b); + assert_eq!(o.result.as_deref(), Some("4:hidden"), "{o:?}"); + } + /// JavaScript and FormCalc scripts run in turn in one session, each on the form the caller /// sends: nothing an earlier script changed or defined leaks into the next, a failing script /// doesn't end the session, and hundreds of scripts run on its one thread. @@ -1683,7 +1725,7 @@ mod session { let o = s.run(script("qty * price", true, "form1[0].page1[0].total[0]", "calculate")); assert_eq!(o.result.as_deref(), Some("10"), "{o:?}"); let mut with = script("qty * price", true, "form1[0].page1[0].total[0]", "calculate"); - with.root.children[0].children[0].value = "10".into(); + std::sync::Arc::make_mut(&mut with.root).children[0].children[0].value = "10".into(); assert_eq!(s.run(with).result.as_deref(), Some("25")); // Nor do variables, globals, prototype changes or kept objects. s.run(script("leak = 5; String.prototype.trim = function () { return 'P' }; kept = qty; 1", false, "form1[0].page1[0].qty[0]", "calculate")); diff --git a/crates/js/src/xfa.rs b/crates/js/src/xfa.rs index eb275d1822..c623d0766f 100644 --- a/crates/js/src/xfa.rs +++ b/crates/js/src/xfa.rs @@ -24,6 +24,8 @@ mod model; mod nodes; mod run; +#[cfg(target_arch = "wasm32")] +pub(crate) use host::forget_base; pub(crate) use host::{HNode, XHost}; pub use model::{MAX_ALERTS, MAX_CONSOLE, MAX_EFFECTS, XfaDoc, XfaEffect, XfaEvent, XfaKind, XfaNode, XfaOutcome}; pub(crate) use model::{MAX_LINE, MAX_PAYLOAD, clip, clip_result, reset_names}; diff --git a/crates/js/src/xfa/globals.rs b/crates/js/src/xfa/globals.rs index 971cf12fc1..d166b7147b 100644 --- a/crates/js/src/xfa/globals.rs +++ b/crates/js/src/xfa/globals.rs @@ -69,13 +69,13 @@ fn set_focus(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { let i = node_index(&target, ctx)?; - host(ctx)?.borrow().nodes.get(i).map(|n| n.som.clone()).unwrap_or_default() + host(ctx)?.borrow().nodes.get(i).map(|n| n.som.to_string()).unwrap_or_default() } _ => { let expr = text(&target, ctx)?; let h = host(ctx)?.borrow(); let found = resolve_som(&h, h.current, &expr); - found.first().and_then(|i| h.nodes.get(*i)).map(|n| n.som.clone()).unwrap_or(expr) + found.first().and_then(|i| h.nodes.get(*i)).map(|n| n.som.to_string()).unwrap_or(expr) } }; effect(ctx, XfaEffect::SetFocus(som))?; @@ -329,7 +329,7 @@ pub(super) fn install(ctx: &mut Context, event: &XfaEvent, doc: &XfaDoc, current let h = host(ctx)?.borrow(); h.nodes .get(scope) - .map(|n| n.children.iter().filter_map(|&c| h.nodes.get(c).map(|k| (c, k.name.clone(), k.repeatable && !k.gone))).collect()) + .map(|n| n.children.iter().filter_map(|&c| h.nodes.get(c).map(|k| (c, k.name.to_string(), k.repeatable && !k.gone))).collect()) .unwrap_or_default() }; for (c, name, repeatable) in kids { diff --git a/crates/js/src/xfa/host.rs b/crates/js/src/xfa/host.rs index ec222a4ca8..ffc8352005 100644 --- a/crates/js/src/xfa/host.rs +++ b/crates/js/src/xfa/host.rs @@ -2,27 +2,71 @@ use super::model::{ MAX_ALERTS, MAX_CONSOLE, MAX_DEPTH, MAX_EFFECTS, MAX_NODES, MAX_PAYLOAD, MAX_VALUE, XfaDoc, XfaEffect, XfaKind, XfaNode, clip, clip_value, effect_key, payload, }; +use std::sync::Arc; + use super::{Context, HashMap, JsResult, Message, RefCell, error}; -/// One node of the flattened form, shared by the JavaScript and FormCalc engines. +/// One node of the flattened form, shared by the JavaScript and FormCalc engines. Its text is +/// shared (`Arc`), so the copy each script starts from costs little: the scripts of one +/// event start from the same flattened form until it changes (see [`XHost::new`]). +#[derive(Clone)] pub(crate) struct HNode { - pub(crate) name: String, - pub(crate) som: String, + pub(crate) name: Arc, + pub(crate) som: Arc, pub(crate) kind: XfaKind, pub(crate) value: String, pub(crate) numeric: bool, - pub(crate) presence: String, - pub(crate) access: String, + pub(crate) presence: Arc, + pub(crate) access: Arc, pub(crate) repeatable: bool, pub(crate) occur_min: usize, pub(crate) occur_max: Option, pub(crate) index: usize, pub(crate) parent: Option, - pub(crate) children: Vec, + pub(crate) children: Arc<[usize]>, /// Removed by `removeInstance`: skipped everywhere. pub(crate) gone: bool, /// See [`XfaNode::formatted`]; dropped when a script sets the value, which it no longer shows. - pub(crate) formatted: Option, + pub(crate) formatted: Option>, +} + +thread_local! { + /// The form the last script on this thread started from, flattened: the next script of the + /// event starts from a copy of it while the engine sends the same form. + static BASE: std::cell::RefCell, Vec)>> = const { std::cell::RefCell::new(None) }; +} + +/// Let go of the form the scripts on this thread started from. +#[cfg(target_arch = "wasm32")] +pub(crate) fn forget_base() { + BASE.with(|b| b.borrow_mut().take()); +} + +/// `root` flattened under a synthetic `form` node (index 0). +fn flattened(root: &XfaNode) -> Vec { + let mut nodes = vec![HNode { + name: "form".into(), + som: "".into(), + kind: XfaKind::Form, + value: String::new(), + numeric: false, + presence: "visible".into(), + access: "open".into(), + repeatable: false, + occur_min: 0, + occur_max: None, + index: 0, + parent: None, + children: Arc::from([]), + gone: false, + formatted: None, + }]; + if let Some(ci) = flatten(root, Some(0), &mut nodes, 1) + && let Some(f) = nodes.get_mut(0) + { + f.children = Arc::from([ci]); + } + nodes } /// The form as a script sees and changes it, with the effects it records. @@ -53,11 +97,25 @@ pub(crate) struct XHost { impl XHost { /// The host for a script on `root` (the template's root subform), with `this` at the node /// named by `target`. `xfa.form` is a synthetic root whose child is the root subform. - pub(crate) fn new(root: XfaNode, target: &str, doc: &XfaDoc) -> XHost { - let form = XfaNode { name: "form".into(), kind: Some(XfaKind::Form), children: vec![root], ..Default::default() }; - let mut nodes = Vec::new(); - flatten(&form, None, &mut nodes, 0); - let current = nodes.iter().position(|n| n.som == target && n.kind != XfaKind::Form).unwrap_or(1.min(nodes.len().saturating_sub(1))); + /// With `reuse`, the flattened form is kept on this thread for the next script given the + /// same `root` (the scripts of one event, [`crate::xfa_session`]). + pub(crate) fn new(root: Arc, reuse: bool, target: &str, doc: &XfaDoc) -> XHost { + let nodes = if !reuse { + flattened(&root) + } else { + BASE.with(|b| { + let mut b = b.borrow_mut(); + match b.as_ref() { + Some((r, nodes)) if Arc::ptr_eq(r, &root) => nodes.clone(), + _ => { + let nodes = flattened(&root); + *b = Some((root, nodes.clone())); + nodes + } + } + }) + }; + let current = nodes.iter().position(|n| &*n.som == target && n.kind != XfaKind::Form).unwrap_or(1.min(nodes.len().saturating_sub(1))); XHost { nodes, effects: Vec::new(), @@ -122,8 +180,8 @@ impl XHost { } if let Some(n) = self.nodes.get_mut(i) { match &e { - XfaEffect::SetPresence { presence, .. } => n.presence = presence.clone(), - XfaEffect::SetAccess { access, .. } => n.access = access.clone(), + XfaEffect::SetPresence { presence, .. } => n.presence = presence.as_str().into(), + XfaEffect::SetAccess { access, .. } => n.access = access.as_str().into(), _ => {} } } @@ -220,7 +278,7 @@ impl XHost { if n.gone || !matches!(n.kind, XfaKind::Field | XfaKind::ExclGroup) { return false; } - let som = n.som.clone(); + let som = n.som.to_string(); // Cut here, not only in the effect: later reads in the script see what is saved, and // the snapshot stays bounded however often a long value is written. let (text, cut) = clip_value(text); @@ -257,21 +315,21 @@ fn flatten(n: &XfaNode, parent: Option, out: &mut Vec, depth: usiz } let idx = out.len(); out.push(HNode { - name: n.name.clone(), - som: n.som.clone(), + name: n.name.as_str().into(), + som: n.som.as_str().into(), kind: n.kind.unwrap_or(XfaKind::Subform), value: n.value.clone(), numeric: n.numeric, - presence: if n.presence.is_empty() { "visible".into() } else { n.presence.clone() }, - access: if n.access.is_empty() { "open".into() } else { n.access.clone() }, + presence: if n.presence.is_empty() { "visible".into() } else { n.presence.as_str().into() }, + access: if n.access.is_empty() { "open".into() } else { n.access.as_str().into() }, repeatable: n.repeatable, occur_min: n.occur_min, occur_max: n.occur_max, index: n.index, parent, - children: Vec::new(), + children: Arc::from([]), gone: false, - formatted: n.formatted.clone(), + formatted: n.formatted.as_deref().map(Arc::from), }); let mut kids = Vec::new(); for c in &n.children { @@ -280,7 +338,7 @@ fn flatten(n: &XfaNode, parent: Option, out: &mut Vec, depth: usiz } } if let Some(h) = out.get_mut(idx) { - h.children = kids; + h.children = kids.into(); } Some(idx) } diff --git a/crates/js/src/xfa/instances.rs b/crates/js/src/xfa/instances.rs index c099c5abe8..b7e338057e 100644 --- a/crates/js/src/xfa/instances.rs +++ b/crates/js/src/xfa/instances.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use super::host::{HNode, XHost, host}; use super::model::{MAX_ADDED, MAX_DEPTH, MAX_NODES, XfaEffect}; use super::nodes::{instances, node_object}; @@ -112,14 +114,14 @@ fn renumber(h: &mut XHost, idx: usize, depth: usize) { } let (parent_som, name, index, kids) = { let Some(n) = h.nodes.get(idx) else { return }; - let parent_som = n.parent.and_then(|p| h.nodes.get(p)).map(|p| p.som.clone()).unwrap_or_default(); + let parent_som = n.parent.and_then(|p| h.nodes.get(p)).map(|p| p.som.to_string()).unwrap_or_default(); (parent_som, n.name.clone(), n.index, n.children.clone()) }; let som = child_som(parent_som, &name, index); if let Some(n) = h.nodes.get_mut(idx) { - n.som = som; + n.som = som.into(); } - for k in kids { + for k in kids.iter().copied() { renumber(h, k, depth + 1); } } @@ -145,7 +147,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option, depth: usize) ( HNode { name: n.name.clone(), - som: String::new(), + som: "".into(), kind: n.kind, value: String::new(), numeric: n.numeric, @@ -156,7 +158,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option, depth: usize) occur_max: n.occur_max, index: n.index, parent, - children: Vec::new(), + children: Arc::from([]), gone: false, // The copy holds no value, so nothing is shown in its picture either. formatted: None, @@ -167,7 +169,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option, depth: usize) let idx = h.nodes.len(); h.nodes.push(template); let mut copied = Vec::new(); - for k in kids { + for k in kids.iter().copied() { // Nested repeating subforms copy their first instance only. let skip = h.nodes.get(k).is_some_and(|c| c.gone || c.repeatable && c.index > 0); if skip { @@ -178,7 +180,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option, depth: usize) } } if let Some(n) = h.nodes.get_mut(idx) { - n.children = copied; + n.children = copied.into(); } Some(idx) } @@ -206,8 +208,8 @@ impl XHost { return None; } // The copy's SOM, as `renumber` will give it, so its effect is reserved before the copy. - let name = h.nodes.get(first).map(|n| n.name.clone()).unwrap_or_default(); - let parent_som = h.nodes.get(parent).map(|p| p.som.clone()).unwrap_or_default(); + let name = h.nodes.get(first).map(|n| n.name.to_string()).unwrap_or_default(); + let parent_som = h.nodes.get(parent).map(|p| p.som.to_string()).unwrap_or_default(); let som = child_som(parent_som, &name, live.len()); if !h.reserve(&XfaEffect::AddInstance { som: String::new() }, som.len()) { return None; @@ -219,8 +221,10 @@ impl XHost { } // Keep instances together: right after the last one. if let Some(p) = h.nodes.get_mut(parent) { - let pos = p.children.iter().position(|&c| c == last).map_or(p.children.len(), |i| i + 1); - p.children.insert(pos, new); + let mut kids = p.children.to_vec(); + let pos = kids.iter().position(|&c| c == last).map_or(kids.len(), |i| i + 1); + kids.insert(pos, new); + p.children = kids.into(); } renumber(h, new, 0); h.added += 1; @@ -260,7 +264,7 @@ impl XHost { if live.len() <= min { return; } - let som = h.nodes.get(gone).map(|n| n.som.clone()).unwrap_or_default(); + let som = h.nodes.get(gone).map(|n| n.som.to_string()).unwrap_or_default(); if !h.reserve(&XfaEffect::RemoveInstance { som: String::new() }, som.len()) { return; } diff --git a/crates/js/src/xfa/nodes.rs b/crates/js/src/xfa/nodes.rs index 75943352e6..1f5ae50c17 100644 --- a/crates/js/src/xfa/nodes.rs +++ b/crates/js/src/xfa/nodes.rs @@ -70,14 +70,14 @@ fn value_js(h: &HNode) -> JsValue { /// `(idx, name)` of the child of `parent` named `name`, first instance. pub(crate) fn child_named(h: &XHost, parent: usize, name: &str) -> Option { let p = h.nodes.get(parent)?; - p.children.iter().copied().find(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && n.name == name)) + p.children.iter().copied().find(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && &*n.name == name)) } /// Every live instance named `name` under `parent`, in order. pub(crate) fn instances(h: &XHost, parent: usize, name: &str) -> Vec { h.nodes .get(parent) - .map(|p| p.children.iter().copied().filter(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && n.name == name)).collect()) + .map(|p| p.children.iter().copied().filter(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && &*n.name == name)).collect()) .unwrap_or_default() } @@ -179,11 +179,11 @@ pub(crate) fn descendants_named(h: &XHost, from: usize, name: &str, depth: usize return out; } let Some(n) = h.nodes.get(from) else { return out }; - for &c in &n.children { + for &c in n.children.iter() { if let Some(k) = h.nodes.get(c) && !k.gone { - if k.name == name { + if &*k.name == name { out.push(c); } else { out.extend(descendants_named(h, c, name, depth + 1)); @@ -211,7 +211,7 @@ fn node_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult JsValue::from(idx as f64), "rawValue" | "value" => host(ctx)?.borrow().nodes.get(idx).map_or(JsValue::null(), value_js), - "formattedValue" => s(&host(ctx)?.borrow().nodes.get(idx).and_then(|n| n.formatted.clone()).unwrap_or(value)), + "formattedValue" => s(&host(ctx)?.borrow().nodes.get(idx).and_then(|n| n.formatted.as_deref().map(str::to_string)).unwrap_or(value)), "name" => s(&name), "somExpression" => s(&som), "className" => s(kind.class_name()), @@ -289,7 +289,7 @@ fn node_set(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { // Not a field: the property doesn't exist, as in Acrobat, and nothing is recorded. diff --git a/crates/js/src/xfa/run.rs b/crates/js/src/xfa/run.rs index f951e02a54..6044c37c05 100644 --- a/crates/js/src/xfa/run.rs +++ b/crates/js/src/xfa/run.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use super::globals::install; use super::host::{Shared, XHost}; use super::model::{XfaDoc, XfaEvent, XfaNode, XfaOutcome, clip_result}; @@ -15,7 +17,7 @@ pub fn run_xfa(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, lim let spawned = std::thread::Builder::new() .name("pdfcraft-xfa-js".into()) .stack_size(crate::SCRIPT_STACK) - .spawn_scoped(scope, || run_here(script, event, doc, root.clone(), limits, &|| {})); + .spawn_scoped(scope, || run_here(script, event, doc, Arc::new(root.clone()), false, limits, &|| {})); match spawned { Ok(thread) => thread.join().unwrap_or_else(|_| failed("the script stopped with an internal error".into())), Err(e) => failed(format!("the script engine could not start: {e}")), @@ -23,7 +25,7 @@ pub fn run_xfa(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, lim }) } #[cfg(target_arch = "wasm32")] - run_here(script, event, doc, root.clone(), limits, &|| {}) + run_here(script, event, doc, Arc::new(root.clone()), false, limits, &|| {}) } /// [`run_xfa`] with a time limit: a script still running `timeout` after the engine is ready @@ -36,7 +38,7 @@ pub fn run_xfa_within(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNod return XfaOutcome { error: Some(why), ..Default::default() }; } let (script, event, doc) = (script.to_string(), event.clone(), doc.clone()); - run_within("pdfcraft-xfa-js", timeout, move |started| run_here(&script, &event, &doc, root, limits, started)) + run_within("pdfcraft-xfa-js", timeout, move |started| run_here(&script, &event, &doc, Arc::new(root), false, limits, started)) } /// Longest the script engine may take to be ready for a script (its thread and context, and @@ -125,7 +127,15 @@ pub(crate) fn warm_up() { } /// `started` is called once the engine is ready: a time limit on the script runs from then. -pub(crate) fn run_here(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNode, limits: Limits, started: &dyn Fn()) -> XfaOutcome { +pub(crate) fn run_here( + script: &str, + event: &XfaEvent, + doc: &XfaDoc, + root: Arc, + reuse: bool, + limits: Limits, + started: &dyn Fn(), +) -> XfaOutcome { warm_up(); let mut ctx = match Context::builder().host_hooks(std::rc::Rc::new(crate::CheckedCodeFromStrings)).build() { Ok(ctx) => ctx, @@ -133,7 +143,7 @@ pub(crate) fn run_here(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNo }; ctx.runtime_limits_mut().set_loop_iteration_limit(limits.loop_iterations); ctx.runtime_limits_mut().set_recursion_limit(limits.recursion); - let host_state = XHost::new(root, &event.target, doc); + let host_state = XHost::new(root, reuse, &event.target, doc); let current = host_state.current; ctx.insert_data(RefCell::new(host_state)); let mut out = XfaOutcome::default(); diff --git a/crates/js/src/xfa_session.rs b/crates/js/src/xfa_session.rs index 28ce53994d..797f813332 100644 --- a/crates/js/src/xfa_session.rs +++ b/crates/js/src/xfa_session.rs @@ -36,8 +36,10 @@ pub struct Script { pub source: String, pub formcalc: bool, pub event: XfaEvent, - /// The form as the caller holds it now. - pub root: XfaNode, + /// The form as the caller holds it now. Send the same `Arc` while the form is unchanged: + /// the next script then starts from the copy the last one was given, without flattening the + /// form again. + pub root: std::sync::Arc, pub limits: Limits, pub timeout: std::time::Duration, } @@ -58,13 +60,13 @@ fn run_script(s: Script, doc: &XfaDoc, started: &dyn Fn()) -> XfaOutcome { if s.formcalc { // FormCalc has no engine to start: its time (and its own deadline) begins at once. started(); - return crate::formcalc::run_formcalc_at(&s.source, &s.event, doc, s.root, s.limits, Some(s.timeout)); + return crate::formcalc::run_formcalc_at(&s.source, &s.event, doc, s.root, true, s.limits, Some(s.timeout)); } if let Some(why) = crate::refuse(&s.source) { started(); return XfaOutcome { error: Some(why), ..Default::default() }; } - crate::xfa::run_here(&s.source, &s.event, doc, s.root, s.limits, started) + crate::xfa::run_here(&s.source, &s.event, doc, s.root, true, s.limits, started) } #[cfg(not(target_arch = "wasm32"))] @@ -236,6 +238,14 @@ mod native { run_script(script, &self.doc, &|| {}) } } + + impl Drop for XfaSession { + /// The scripts ran on this thread: the form they started from is let go of with the + /// event (on other targets it goes with the session's thread). + fn drop(&mut self) { + crate::xfa::forget_base(); + } + } } pub use native::XfaSession;