From d65c465b1ba227ad36bc7e302c8dc23a02c1ea3e Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:42:13 +0800 Subject: [PATCH] M12: XFA open-time scripts start from one shared copy of the form On a large dynamic XFA form, every script of an event copied the whole form twice before it ran (the engine's live form to the script engine's tree, then that tree to the flattened form the host works on), and every resetData call rewrote and rescanned the whole document. On a 1,300-script form opening spent its 3 s budget on these copies and on twenty resets, and skipped the last 850 initialize and calculate scripts. The engine now sends the scripts of an event one shared tree (Arc) until the live form changes in any way, and the script thread keeps the flattened form it built from that tree; each script starts from a copy whose text is shared (Arc), so it costs a fraction of a flatten. Each script still gets its own copy: nothing one script changes reaches the next except through the engine. Resets in a row in one script's effects are applied as one reset of the names together. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK --- crates/engine/src/tests.rs | 51 ++++++++++++++++ crates/engine/src/xfa.rs | 51 +++++++++++++++- crates/js/src/formcalc.rs | 9 +-- crates/js/src/formcalc/access.rs | 12 ++-- crates/js/src/formcalc/eval.rs | 6 +- crates/js/src/tests.rs | 48 ++++++++++++++- crates/js/src/xfa.rs | 2 + crates/js/src/xfa/globals.rs | 6 +- crates/js/src/xfa/host.rs | 102 ++++++++++++++++++++++++------- crates/js/src/xfa/instances.rs | 28 +++++---- crates/js/src/xfa/nodes.rs | 12 ++-- crates/js/src/xfa/run.rs | 20 ++++-- crates/js/src/xfa_session.rs | 18 ++++-- 13 files changed, 294 insertions(+), 71 deletions(-) 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;