Skip to main content

pdf_xfa/js_runtime/
host.rs

1//! Phase C host bindings for the sandboxed JavaScript runtime.
2//!
3//! The adapter exposes a narrow, in-process view of the merged Form DOM:
4//! bounded SOM resolution plus `field.rawValue` reads/writes. It never changes
5//! tree structure or layout metadata.
6
7use std::collections::HashMap;
8
9use xfa_dom_resolver::data_dom::{DataDom, DataNodeId};
10use xfa_dom_resolver::som::{
11    parse_som, resolve_data_path, SomExpression, SomIndex, SomRoot, SomSelector,
12};
13use xfa_layout_engine::form::{FormNodeId, FormNodeType, FormTree, GroupKind};
14
15use super::RuntimeMetadata;
16
17/// Maximum successful `rawValue` writes recorded for one document.
18pub const MAX_MUTATIONS_PER_DOC: usize = 4096;
19/// Maximum live instances allowed for one script-managed subform run.
20pub const MAX_INSTANCES_PER_SUBFORM: u32 = 256;
21/// Maximum items allowed in a single runtime-populated listbox.
22pub const MAX_ITEMS_PER_LISTBOX: u32 = 4096;
23/// Maximum SOM resolution calls a single script may perform.
24pub const MAX_RESOLVE_CALLS_PER_SCRIPT: u32 = 1024;
25/// Maximum handles returned from one `xfa.resolveNodes` call.
26pub const MAX_RESOLVE_RESULTS: usize = 256;
27/// Maximum SOM segment depth accepted by the sandbox binding.
28pub const MAX_SOM_DEPTH: usize = 16;
29
30/// One successful `field.rawValue` write.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct MutationLogEntry {
33    /// Mutated form node.
34    pub node_id: FormNodeId,
35    /// Zero-based script invocation index within the current document.
36    pub script_idx: usize,
37    /// Value before the write.
38    pub before: String,
39    /// Value after the write.
40    pub after: String,
41}
42
43/// Host-side state shared by QuickJS binding closures.
44#[derive(Debug)]
45pub struct HostBindings {
46    form: *mut FormTree,
47    root_id: FormNodeId,
48    current_id: Option<FormNodeId>,
49    current_activity: Option<String>,
50    current_script_idx: usize,
51    next_script_idx: usize,
52    generation: u64,
53    mutation_log: Vec<MutationLogEntry>,
54    mutation_count_this_doc: usize,
55    resolve_count_this_script: u32,
56    metadata: RuntimeMetadata,
57    static_page_count: u32,
58    zero_instance_runs: HashMap<(FormNodeId, String), u64>,
59    /// Phase D-γ: read-only pointer to the DataDom for the current document.
60    /// Set from a stack reference in `flatten.rs` that outlives script execution.
61    /// `None` when no data packet is present or the feature is inactive.
62    data_dom: Option<*const DataDom>,
63}
64
65impl Default for HostBindings {
66    fn default() -> Self {
67        Self {
68            form: std::ptr::null_mut(),
69            root_id: FormNodeId(0),
70            current_id: None,
71            current_activity: None,
72            current_script_idx: 0,
73            next_script_idx: 0,
74            generation: 0,
75            mutation_log: Vec::new(),
76            mutation_count_this_doc: 0,
77            resolve_count_this_script: 0,
78            metadata: RuntimeMetadata::default(),
79            static_page_count: 0,
80            zero_instance_runs: HashMap::new(),
81            data_dom: None,
82        }
83    }
84}
85
86impl HostBindings {
87    /// Create empty host-binding state.
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    /// Install or clear the form pointer used by host bindings.
93    pub fn set_form_handle(&mut self, form: *mut FormTree, root_id: FormNodeId) {
94        self.form = form;
95        self.root_id = root_id;
96        if form.is_null() {
97            self.current_id = None;
98            self.current_activity = None;
99            self.current_script_idx = 0;
100            self.zero_instance_runs.clear();
101        }
102    }
103
104    /// Reset counters and invalidate all existing handles for a new document.
105    ///
106    /// Note: `data_dom` is intentionally NOT cleared here. The caller sets it
107    /// explicitly via `set_data_handle` before calling
108    /// `apply_dynamic_scripts_with_runtime`, and `reset_for_new_document` (which
109    /// calls this) runs inside the dispatch function — after `set_data_handle`.
110    /// Clearing it here would wipe the pointer before any scripts execute.
111    /// The caller is responsible for managing DataDom lifetime.
112    pub fn reset_per_document(&mut self) {
113        self.form = std::ptr::null_mut();
114        self.root_id = FormNodeId(0);
115        self.current_id = None;
116        self.current_activity = None;
117        self.current_script_idx = 0;
118        self.next_script_idx = 0;
119        self.generation = self.generation.wrapping_add(1);
120        self.mutation_log.clear();
121        self.mutation_count_this_doc = 0;
122        self.resolve_count_this_script = 0;
123        self.metadata = RuntimeMetadata::default();
124        self.static_page_count = 0;
125        self.zero_instance_runs.clear();
126        // data_dom is NOT reset here — see doc comment above.
127    }
128
129    /// Phase D-γ: install the DataDom pointer for the current document.
130    /// # Safety
131    /// `dom` must outlive all script execution for this document.
132    pub fn set_data_handle(&mut self, dom: *const DataDom) {
133        self.data_dom = Some(dom);
134    }
135
136    /// Reset per-script state and install the current event context.
137    pub fn reset_per_script(&mut self, current_id: FormNodeId, activity: Option<&str>) {
138        self.current_id = Some(current_id);
139        self.current_activity = activity.map(str::to_string);
140        self.current_script_idx = self.next_script_idx;
141        self.next_script_idx = self.next_script_idx.saturating_add(1);
142        self.resolve_count_this_script = 0;
143    }
144
145    /// Cache the page count visible to read-only page-count bindings.
146    pub fn set_static_page_count(&mut self, page_count: u32) {
147        self.static_page_count = page_count;
148    }
149
150    /// Current handle generation. Handles capture this and are invalid after a
151    /// document reset.
152    pub fn generation(&self) -> u64 {
153        self.generation
154    }
155
156    /// Current script node for `this`.
157    pub fn current_node(&self) -> Option<FormNodeId> {
158        self.current_id
159    }
160
161    /// Root form node for `xfa.form`.
162    pub fn root_node(&mut self, generation: u64) -> Option<FormNodeId> {
163        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
164        if generation != self.generation {
165            return None;
166        }
167        let Some(form) = self.form_ref() else {
168            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
169            return None;
170        };
171        (self.root_id.0 < form.nodes.len()).then_some(self.root_id)
172    }
173
174    /// Return the names of subform ancestors of `node_id` from innermost to
175    /// outermost.  Used by the implicit-globals proxy to resolve bare names
176    /// that belong to a `<variables>` block in an ancestor subform (D-ι.2
177    /// scope chain).  Iterative DFS from root; O(n) worst case, acceptable for
178    /// the small form trees XFA templates produce.
179    pub fn subform_scope_chain(&mut self, node_id: FormNodeId, generation: u64) -> Vec<String> {
180        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
181        if !self.handle_is_live(node_id, generation) {
182            return vec![];
183        }
184        let Some(form) = self.form_ref() else {
185            return vec![];
186        };
187        let root = self.root_id;
188        // Iterative DFS with backtracking.
189        // Stack: (node_id, next_child_index, did_push_name_to_result)
190        let mut names: Vec<String> = vec![];
191        let mut stack: Vec<(FormNodeId, usize, bool)> = vec![(root, 0, false)];
192        while let Some(&(current, child_idx, _pushed)) = stack.last() {
193            if current == node_id {
194                return names;
195            }
196            let children_len = form.get(current).children.len();
197            if child_idx >= children_len {
198                let (_, _, pushed) = stack.pop().unwrap();
199                if pushed {
200                    names.pop();
201                }
202                continue;
203            }
204            let child_id = form.get(current).children[child_idx];
205            stack.last_mut().unwrap().1 += 1;
206            let child = form.get(child_id);
207            let is_scope = matches!(
208                child.node_type,
209                FormNodeType::Subform | FormNodeType::Area | FormNodeType::ExclGroup
210            );
211            let pushed = is_scope && !child.name.is_empty();
212            if pushed {
213                names.push(child.name.clone());
214            }
215            stack.push((child_id, 0, pushed));
216        }
217        vec![]
218    }
219
220    /// Read and clear host metadata counters.
221    pub fn take_metadata(&mut self) -> RuntimeMetadata {
222        std::mem::take(&mut self.metadata)
223    }
224
225    /// Mutation log for tests and debug reporting.
226    pub fn mutation_log(&self) -> &[MutationLogEntry] {
227        &self.mutation_log
228    }
229
230    /// Read `field.rawValue`; returns `None` for stale handles, missing nodes,
231    /// and non-field nodes.
232    pub fn get_raw_value(&mut self, node_id: FormNodeId, generation: u64) -> Option<String> {
233        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
234        if !self.handle_is_live(node_id, generation) {
235            return None;
236        }
237        let Some(form) = self.form_ref() else {
238            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
239            return None;
240        };
241        match &form.get(node_id).node_type {
242            FormNodeType::Field { value } => Some(value.clone()),
243            _ => None,
244        }
245    }
246
247    /// Return the `name` attribute of any live node. Used by D-ι.2 to expose
248    /// `subformHandle.variables` as the subform's own variables namespace.
249    pub fn node_name(&self, node_id: FormNodeId, generation: u64) -> Option<String> {
250        if !self.handle_is_live(node_id, generation) {
251            return None;
252        }
253        let form = self.form_ref()?;
254        Some(form.get(node_id).name.clone())
255    }
256
257    /// Read a property from a node's XFA `occur` object.
258    pub fn get_occur_property(
259        &mut self,
260        node_id: FormNodeId,
261        generation: u64,
262        property: &str,
263    ) -> Option<i32> {
264        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
265        if !self.handle_is_live(node_id, generation) {
266            return None;
267        }
268        let Some(form) = self.form_ref() else {
269            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
270            return None;
271        };
272        let occur = &form.get(node_id).occur;
273        match property {
274            "min" => Some(clamp_occur_to_i32(occur.min)),
275            "max" => Some(occur.max.map(clamp_occur_to_i32).unwrap_or(-1)),
276            "initial" => Some(clamp_occur_to_i32(occur.initial)),
277            _ => {
278                self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
279                None
280            }
281        }
282    }
283
284    /// Write a property on a node's XFA `occur` object when mutation is allowed.
285    pub fn set_occur_property(
286        &mut self,
287        node_id: FormNodeId,
288        generation: u64,
289        property: &str,
290        value: &str,
291    ) -> bool {
292        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
293        if !self.write_activity_allowed()
294            || !self.handle_is_live(node_id, generation)
295            || self.mutation_count_this_doc >= MAX_MUTATIONS_PER_DOC
296        {
297            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
298            return false;
299        }
300
301        enum OccurValue {
302            Nonnegative(u32),
303            Max(Option<u32>),
304        }
305
306        let parsed = match property {
307            "min" | "initial" => parse_nonnegative_occur_value(value).map(OccurValue::Nonnegative),
308            "max" => parse_max_occur_value(value).map(OccurValue::Max),
309            _ => None,
310        };
311        let Some(parsed) = parsed else {
312            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
313            return false;
314        };
315
316        let Some(form) = self.form_mut() else {
317            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
318            return false;
319        };
320        let Some(node) = form.nodes.get_mut(node_id.0) else {
321            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
322            return false;
323        };
324        match (property, parsed) {
325            ("min", OccurValue::Nonnegative(n)) => node.occur.min = n,
326            ("initial", OccurValue::Nonnegative(n)) => node.occur.initial = n,
327            ("max", OccurValue::Max(n)) => node.occur.max = n,
328            _ => {
329                self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
330                return false;
331            }
332        }
333        self.mutation_count_this_doc = self.mutation_count_this_doc.saturating_add(1);
334        true
335    }
336
337    /// Write `field.rawValue` when the activity and target are permitted.
338    pub fn set_raw_value(&mut self, node_id: FormNodeId, value: String, generation: u64) -> bool {
339        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
340        if !self.write_activity_allowed()
341            || !self.handle_is_live(node_id, generation)
342            || self.mutation_count_this_doc >= MAX_MUTATIONS_PER_DOC
343        {
344            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
345            return false;
346        }
347
348        let Some((before, after)) = self.write_field_value(node_id, value) else {
349            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
350            return false;
351        };
352
353        self.mutation_log.push(MutationLogEntry {
354            node_id,
355            script_idx: self.current_script_idx,
356            before,
357            after,
358        });
359        self.metadata.mutations = self.metadata.mutations.saturating_add(1);
360        self.mutation_count_this_doc = self.mutation_count_this_doc.saturating_add(1);
361        true
362    }
363
364    /// Resolve a SOM path to the first field node.
365    pub fn resolve_node(&mut self, path: &str) -> Option<FormNodeId> {
366        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
367        let nodes = match self.resolve_path(path) {
368            ResolveOutcome::Ok(nodes) => nodes,
369            ResolveOutcome::NoMatch => {
370                self.metadata.resolve_failures = self.metadata.resolve_failures.saturating_add(1);
371                return None;
372            }
373            ResolveOutcome::BindingError => {
374                self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
375                return None;
376            }
377        };
378
379        let Some(form) = self.form_ref() else {
380            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
381            return None;
382        };
383        let found = nodes
384            .into_iter()
385            .find(|node_id| matches!(form.get(*node_id).node_type, FormNodeType::Field { .. }));
386        if found.is_none() {
387            self.metadata.resolve_failures = self.metadata.resolve_failures.saturating_add(1);
388        }
389        found
390    }
391
392    /// Resolve a SOM path to field handles, capped at
393    /// [`MAX_RESOLVE_RESULTS`].
394    pub fn resolve_nodes(&mut self, path: &str) -> Vec<FormNodeId> {
395        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
396        let nodes = match self.resolve_path(path) {
397            ResolveOutcome::Ok(nodes) => nodes,
398            ResolveOutcome::NoMatch => return Vec::new(),
399            ResolveOutcome::BindingError => {
400                self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
401                self.metadata.resolve_failures = self.metadata.resolve_failures.saturating_add(1);
402                return Vec::new();
403            }
404        };
405
406        let Some(form) = self.form_ref() else {
407            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
408            return Vec::new();
409        };
410        nodes
411            .into_iter()
412            .filter(|node_id| matches!(form.get(*node_id).node_type, FormNodeType::Field { .. }))
413            .take(MAX_RESOLVE_RESULTS)
414            .collect()
415    }
416
417    /// Resolve an implicit JavaScript identifier from the current XFA scope.
418    ///
419    /// Adobe's XFA JavaScript environment makes sibling and ancestor-scoped
420    /// SOM nodes visible as bare identifiers. This method searches from the
421    /// supplied current node upward, returning the first descendant with the
422    /// requested name at each scope.
423    pub fn resolve_implicit(&mut self, current_id: FormNodeId, name: &str) -> Option<FormNodeId> {
424        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
425        match self.resolve_implicit_inner(current_id, name) {
426            ResolveOutcome::Ok(nodes) => nodes.into_iter().next(),
427            ResolveOutcome::NoMatch => {
428                self.metadata.resolve_failures = self.metadata.resolve_failures.saturating_add(1);
429                None
430            }
431            ResolveOutcome::BindingError => {
432                self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
433                None
434            }
435        }
436    }
437
438    /// Resolve all viable implicit JavaScript identifier candidates from the
439    /// current XFA scope. The first candidate is identical to
440    /// [`resolve_implicit`]; later candidates preserve same-name alternatives
441    /// so the JS proxy can filter them when a chained property supplies the
442    /// next SOM segment.
443    pub fn resolve_implicit_candidates(
444        &mut self,
445        current_id: FormNodeId,
446        name: &str,
447    ) -> Vec<FormNodeId> {
448        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
449        match self.resolve_implicit_inner(current_id, name) {
450            ResolveOutcome::Ok(nodes) => nodes,
451            ResolveOutcome::NoMatch => {
452                self.metadata.resolve_failures = self.metadata.resolve_failures.saturating_add(1);
453                Vec::new()
454            }
455            ResolveOutcome::BindingError => {
456                self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
457                Vec::new()
458            }
459        }
460    }
461
462    /// Resolve a direct child node for chained dotted JavaScript access.
463    pub fn resolve_child(&mut self, parent_id: FormNodeId, name: &str) -> Option<FormNodeId> {
464        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
465        match self.resolve_child_inner(parent_id, name) {
466            ResolveOutcome::Ok(nodes) => nodes.into_iter().next(),
467            ResolveOutcome::NoMatch => {
468                self.metadata.resolve_failures = self.metadata.resolve_failures.saturating_add(1);
469                None
470            }
471            ResolveOutcome::BindingError => {
472                self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
473                None
474            }
475        }
476    }
477
478    /// Resolve chained child candidates from an ordered parent candidate set.
479    ///
480    /// Direct children are preferred. If none match, this uses the same
481    /// bounded descendant heuristic as the implicit resolver inside each
482    /// parent, which matches the historical permissiveness of XFA SOM dotted
483    /// access without inventing handles when the form structure is absent.
484    pub fn resolve_child_candidates(
485        &mut self,
486        parent_ids: &[FormNodeId],
487        name: &str,
488    ) -> Vec<FormNodeId> {
489        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
490        match self.resolve_child_candidates_inner(parent_ids, name) {
491            ResolveOutcome::Ok(nodes) => nodes,
492            ResolveOutcome::NoMatch => {
493                self.metadata.resolve_failures = self.metadata.resolve_failures.saturating_add(1);
494                Vec::new()
495            }
496            ResolveOutcome::BindingError => {
497                self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
498                Vec::new()
499            }
500        }
501    }
502
503    /// Resolve a property name from each candidate's own implicit scope.
504    ///
505    /// This is the last fallback used by JS-side candidate filtering, covering
506    /// forms that author a later segment as an ancestor-scoped implicit name
507    /// rather than as a direct child of the previous segment.
508    pub fn resolve_scoped_candidates(
509        &mut self,
510        scope_ids: &[FormNodeId],
511        name: &str,
512    ) -> Vec<FormNodeId> {
513        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
514        match self.resolve_scoped_candidates_inner(scope_ids, name) {
515            ResolveOutcome::Ok(nodes) => nodes,
516            ResolveOutcome::NoMatch => {
517                self.metadata.resolve_failures = self.metadata.resolve_failures.saturating_add(1);
518                Vec::new()
519            }
520            ResolveOutcome::BindingError => {
521                self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
522                Vec::new()
523            }
524        }
525    }
526
527    /// Count live sibling instances with the same name as `parent_id`.
528    pub fn instance_count(&mut self, parent_id: FormNodeId) -> u32 {
529        self.instance_count_inner(parent_id, None)
530    }
531
532    /// Count live sibling instances for a JS handle with generation checking.
533    pub fn instance_count_for_handle(&mut self, parent_id: FormNodeId, generation: u64) -> u32 {
534        self.instance_count_inner(parent_id, Some(generation))
535    }
536
537    /// Return the zero-based sibling index among instances with the same name.
538    pub fn instance_index(&mut self, node_id: FormNodeId) -> u32 {
539        self.instance_index_inner(node_id, None)
540    }
541
542    /// Return the zero-based sibling index for a JS handle.
543    pub fn instance_index_for_handle(&mut self, node_id: FormNodeId, generation: u64) -> u32 {
544        self.instance_index_inner(node_id, Some(generation))
545    }
546
547    /// Whether `parent_id._name` refers to an instance run that was explicitly
548    /// set to zero during this document. This lets the JS shorthand return a
549    /// read-only empty manager only for a real prior instance run, while
550    /// keeping unrelated private-looking `_id` properties hidden.
551    pub fn has_zero_instance_run(
552        &mut self,
553        parent_id: FormNodeId,
554        generation: u64,
555        name: &str,
556    ) -> bool {
557        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
558        let name = name.trim();
559        if name.is_empty() || !self.consume_resolve_call() {
560            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
561            return false;
562        }
563        let Some(form) = self.form_ref() else {
564            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
565            return false;
566        };
567        if generation != self.generation || parent_id.0 >= form.nodes.len() {
568            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
569            return false;
570        }
571        self.zero_instance_runs
572            .contains_key(&(parent_id, name.to_string()))
573    }
574
575    /// Replace the live same-name sibling run with exactly `n` instances,
576    /// clamped to the prototype's occur limits and the sandbox safety cap.
577    #[allow(clippy::result_unit_err)]
578    pub fn instance_set(&mut self, parent_id: FormNodeId, n: u32) -> Result<u32, ()> {
579        self.instance_set_inner(parent_id, None, n)
580    }
581
582    /// Generation-checked variant used by the QuickJS bridge.
583    #[allow(clippy::result_unit_err)]
584    pub fn instance_set_for_handle(
585        &mut self,
586        parent_id: FormNodeId,
587        generation: u64,
588        n: u32,
589    ) -> Result<u32, ()> {
590        self.instance_set_inner(parent_id, Some(generation), n)
591    }
592
593    /// Append one cloned instance to the end of the live same-name sibling run.
594    #[allow(clippy::result_unit_err)]
595    pub fn instance_add(&mut self, parent_id: FormNodeId) -> Result<FormNodeId, ()> {
596        self.instance_add_inner(parent_id, None)
597    }
598
599    /// Generation-checked variant used by the QuickJS bridge.
600    #[allow(clippy::result_unit_err)]
601    pub fn instance_add_for_handle(
602        &mut self,
603        parent_id: FormNodeId,
604        generation: u64,
605    ) -> Result<FormNodeId, ()> {
606        self.instance_add_inner(parent_id, Some(generation))
607    }
608
609    /// Remove one live same-name sibling instance by zero-based index.
610    #[allow(clippy::result_unit_err)]
611    pub fn instance_remove(&mut self, parent_id: FormNodeId, index: u32) -> Result<(), ()> {
612        self.instance_remove_inner(parent_id, None, index)
613    }
614
615    /// Generation-checked variant used by the QuickJS bridge.
616    #[allow(clippy::result_unit_err)]
617    pub fn instance_remove_for_handle(
618        &mut self,
619        parent_id: FormNodeId,
620        generation: u64,
621        index: u32,
622    ) -> Result<(), ()> {
623        self.instance_remove_inner(parent_id, Some(generation), index)
624    }
625
626    /// Clear all runtime-populated listbox items on a field.
627    #[allow(clippy::result_unit_err)]
628    pub fn list_clear(&mut self, field_id: FormNodeId) -> Result<(), ()> {
629        self.list_clear_inner(field_id, None)
630    }
631
632    /// Generation-checked variant used by the QuickJS bridge.
633    #[allow(clippy::result_unit_err)]
634    pub fn list_clear_for_handle(
635        &mut self,
636        field_id: FormNodeId,
637        generation: u64,
638    ) -> Result<(), ()> {
639        self.list_clear_inner(field_id, Some(generation))
640    }
641
642    /// Append one item to a field's runtime listbox options.
643    #[allow(clippy::result_unit_err)]
644    pub fn list_add(
645        &mut self,
646        field_id: FormNodeId,
647        display: String,
648        save: Option<String>,
649    ) -> Result<(), ()> {
650        self.list_add_inner(field_id, None, display, save)
651    }
652
653    /// Generation-checked variant used by the QuickJS bridge.
654    #[allow(clippy::result_unit_err)]
655    pub fn list_add_for_handle(
656        &mut self,
657        field_id: FormNodeId,
658        generation: u64,
659        display: String,
660        save: Option<String>,
661    ) -> Result<(), ()> {
662        self.list_add_inner(field_id, Some(generation), display, save)
663    }
664
665    /// XFA 3.3 §App A `boundItem` — listbox display→save lookup.
666    ///
667    /// Returns the save value associated with `display_value` for a listbox
668    /// or dropdown field. Lookup order:
669    /// 1. Runtime listbox items populated via D-β `addItem` (matched first).
670    /// 2. Static `<items>` parsed from the template at merge time.
671    ///
672    /// Adobe's documented behaviour returns the input unchanged when no
673    /// match exists (passthrough). Empty input returns empty string. Stale
674    /// or non-field handles return the input unchanged.
675    pub fn bound_item_for_handle(
676        &mut self,
677        field_id: FormNodeId,
678        generation: u64,
679        display_value: String,
680    ) -> String {
681        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
682        if !self.handle_is_live(field_id, generation) {
683            return display_value;
684        }
685        let Some(form) = self.form_ref() else {
686            return display_value;
687        };
688        if !matches!(form.get(field_id).node_type, FormNodeType::Field { .. }) {
689            return display_value;
690        }
691        let meta = form.meta(field_id);
692        for (display, save) in &meta.runtime_listbox_items {
693            if display == &display_value {
694                return save.clone();
695            }
696        }
697        for (idx, display) in meta.display_items.iter().enumerate() {
698            if display == &display_value {
699                return meta
700                    .save_items
701                    .get(idx)
702                    .cloned()
703                    .unwrap_or_else(|| display_value.clone());
704            }
705        }
706        display_value
707    }
708
709    /// Read-only static page count visible to Phase C scripts.
710    pub fn num_pages(&mut self) -> u32 {
711        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
712        self.static_page_count
713    }
714
715    /// Record a binding-level failure for explicit no-op stubs.
716    pub fn metadata_binding_error(&mut self) {
717        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
718        self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
719    }
720
721    /// Record use of an intentionally approximate read-only stub.
722    pub fn metadata_resolve_failure(&mut self) {
723        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
724        self.metadata.resolve_failures = self.metadata.resolve_failures.saturating_add(1);
725    }
726
727    fn consume_resolve_call(&mut self) -> bool {
728        if self.resolve_count_this_script >= MAX_RESOLVE_CALLS_PER_SCRIPT {
729            return false;
730        }
731        self.resolve_count_this_script = self.resolve_count_this_script.saturating_add(1);
732        true
733    }
734
735    fn resolve_path(&mut self, path: &str) -> ResolveOutcome {
736        if !self.consume_resolve_call() {
737            return ResolveOutcome::BindingError;
738        }
739
740        let Some(form) = self.form_ref() else {
741            return ResolveOutcome::BindingError;
742        };
743        let Some(current_id) = self.current_id else {
744            return ResolveOutcome::BindingError;
745        };
746        if current_id.0 >= form.nodes.len() || self.root_id.0 >= form.nodes.len() {
747            return ResolveOutcome::BindingError;
748        }
749
750        let normalized = normalize_resolve_path(path.trim());
751        let mut expr = match parse_som(&normalized) {
752            Ok(expr) => expr,
753            Err(_) => return ResolveOutcome::BindingError,
754        };
755        if expr.segments.len() > MAX_SOM_DEPTH {
756            return ResolveOutcome::BindingError;
757        }
758        if matches!(
759            expr.segments.last().map(|segment| &segment.selector),
760            Some(SomSelector::Name(name)) if name == "rawValue"
761        ) {
762            expr.segments.pop();
763        }
764
765        let parents = build_parent_map(form, self.root_id);
766        let resolver = HostSomResolver {
767            form,
768            root_id: self.root_id,
769            parents: &parents,
770            current_id,
771        };
772        match resolver.resolve_expression(&expr) {
773            Some(nodes) if !nodes.is_empty() => ResolveOutcome::Ok(nodes),
774            _ => ResolveOutcome::NoMatch,
775        }
776    }
777
778    fn resolve_implicit_inner(&mut self, current_id: FormNodeId, name: &str) -> ResolveOutcome {
779        let name = name.trim();
780        if name.is_empty() || !self.consume_resolve_call() {
781            return ResolveOutcome::BindingError;
782        }
783
784        let Some(form) = self.form_ref() else {
785            return ResolveOutcome::BindingError;
786        };
787        if current_id.0 >= form.nodes.len() || self.root_id.0 >= form.nodes.len() {
788            return ResolveOutcome::BindingError;
789        }
790
791        let parents = build_parent_map(form, self.root_id);
792        resolve_implicit_candidates_in_scope(form, &parents, current_id, name)
793    }
794
795    fn resolve_child_inner(&mut self, parent_id: FormNodeId, name: &str) -> ResolveOutcome {
796        self.resolve_child_candidates_inner(&[parent_id], name)
797    }
798
799    fn resolve_child_candidates_inner(
800        &mut self,
801        parent_ids: &[FormNodeId],
802        name: &str,
803    ) -> ResolveOutcome {
804        let name = name.trim();
805        if name.is_empty() || !self.consume_resolve_call() {
806            return ResolveOutcome::BindingError;
807        }
808
809        let Some(form) = self.form_ref() else {
810            return ResolveOutcome::BindingError;
811        };
812        if parent_ids.is_empty()
813            || parent_ids
814                .iter()
815                .any(|node_id| node_id.0 >= form.nodes.len())
816        {
817            return ResolveOutcome::BindingError;
818        }
819
820        let mut direct = Vec::new();
821        for &parent_id in parent_ids {
822            for &child_id in &form.get(parent_id).children {
823                if form.get(child_id).name == name {
824                    push_unique_candidate(&mut direct, child_id);
825                    if direct.len() >= MAX_RESOLVE_CANDIDATES {
826                        return ResolveOutcome::Ok(direct);
827                    }
828                }
829            }
830        }
831        if !direct.is_empty() {
832            return ResolveOutcome::Ok(direct);
833        }
834
835        let mut descendants = Vec::new();
836        for &parent_id in parent_ids {
837            let mut local =
838                collect_named_descendant_candidates(form, parent_id, name, MAX_SOM_DEPTH);
839            order_candidates(form, &mut local);
840            for node_id in local {
841                push_unique_candidate(&mut descendants, node_id);
842                if descendants.len() >= MAX_RESOLVE_CANDIDATES {
843                    return ResolveOutcome::Ok(descendants);
844                }
845            }
846        }
847        if descendants.is_empty() {
848            ResolveOutcome::NoMatch
849        } else {
850            ResolveOutcome::Ok(descendants)
851        }
852    }
853
854    fn resolve_scoped_candidates_inner(
855        &mut self,
856        scope_ids: &[FormNodeId],
857        name: &str,
858    ) -> ResolveOutcome {
859        let name = name.trim();
860        if name.is_empty() || !self.consume_resolve_call() {
861            return ResolveOutcome::BindingError;
862        }
863
864        let Some(form) = self.form_ref() else {
865            return ResolveOutcome::BindingError;
866        };
867        if scope_ids.is_empty()
868            || self.root_id.0 >= form.nodes.len()
869            || scope_ids
870                .iter()
871                .any(|node_id| node_id.0 >= form.nodes.len())
872        {
873            return ResolveOutcome::BindingError;
874        }
875
876        let parents = build_parent_map(form, self.root_id);
877        let mut out = Vec::new();
878        for &scope_id in scope_ids {
879            if let ResolveOutcome::Ok(nodes) =
880                resolve_implicit_candidates_in_scope(form, &parents, scope_id, name)
881            {
882                for node_id in nodes {
883                    push_unique_candidate(&mut out, node_id);
884                    if out.len() >= MAX_RESOLVE_CANDIDATES {
885                        return ResolveOutcome::Ok(out);
886                    }
887                }
888            }
889        }
890        if out.is_empty() {
891            ResolveOutcome::NoMatch
892        } else {
893            ResolveOutcome::Ok(out)
894        }
895    }
896
897    fn instance_count_inner(&mut self, parent_id: FormNodeId, generation: Option<u64>) -> u32 {
898        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
899        let Some(run) = self.read_instance_run(parent_id, generation) else {
900            return 0;
901        };
902        run.nodes.len() as u32
903    }
904
905    fn instance_index_inner(&mut self, node_id: FormNodeId, generation: Option<u64>) -> u32 {
906        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
907        let Some(run) = self.read_instance_run(node_id, generation) else {
908            return 0;
909        };
910        run.nodes
911            .iter()
912            .position(|candidate| *candidate == node_id)
913            .unwrap_or(0) as u32
914    }
915
916    fn instance_set_inner(
917        &mut self,
918        parent_id: FormNodeId,
919        generation: Option<u64>,
920        n: u32,
921    ) -> Result<u32, ()> {
922        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
923        if !self.write_activity_allowed()
924            || self.mutation_count_this_doc >= MAX_MUTATIONS_PER_DOC
925            || !self.consume_resolve_call()
926        {
927            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
928            return Err(());
929        }
930
931        let Some(run) = self.live_instance_run(parent_id, generation) else {
932            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
933            return Err(());
934        };
935        let Some(target_count) = self.clamped_instance_count(run.prototype_id, n) else {
936            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
937            return Err(());
938        };
939
940        let target_count = target_count as usize;
941        let prototype_id = run.prototype_id;
942        let parent_id = run.parent_id;
943        let first_pos = run.first_position;
944        let Some(prototype_name) = self
945            .form_ref()
946            .and_then(|form| form.nodes.get(prototype_id.0))
947            .map(|node| node.name.clone())
948        else {
949            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
950            return Err(());
951        };
952        let remove_ids = run.nodes;
953
954        let mut new_ids = Vec::with_capacity(target_count);
955        if target_count > 0 {
956            self.normalize_instance_occurrence(prototype_id);
957            new_ids.push(prototype_id);
958            for _ in 1..target_count {
959                let cloned_id = self.clone_subtree(prototype_id)?;
960                new_ids.push(cloned_id);
961            }
962        }
963
964        let Some(form) = self.form_mut() else {
965            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
966            return Err(());
967        };
968        let parent = form.get_mut(parent_id);
969        parent
970            .children
971            .retain(|child_id| !remove_ids.contains(child_id));
972        let insert_pos = first_pos.min(parent.children.len());
973        for (offset, node_id) in new_ids.iter().copied().enumerate() {
974            parent.children.insert(insert_pos + offset, node_id);
975        }
976
977        let key = (parent_id, prototype_name);
978        if target_count == 0 {
979            self.zero_instance_runs.insert(key, self.generation);
980        } else {
981            self.zero_instance_runs.remove(&key);
982        }
983        self.record_instance_write();
984        Ok(target_count as u32)
985    }
986
987    fn instance_add_inner(
988        &mut self,
989        parent_id: FormNodeId,
990        generation: Option<u64>,
991    ) -> Result<FormNodeId, ()> {
992        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
993        if !self.write_activity_allowed()
994            || self.mutation_count_this_doc >= MAX_MUTATIONS_PER_DOC
995            || !self.consume_resolve_call()
996        {
997            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
998            return Err(());
999        }
1000
1001        let Some(run) = self.live_instance_run(parent_id, generation) else {
1002            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1003            return Err(());
1004        };
1005        let zero_key = self
1006            .form_ref()
1007            .and_then(|form| form.nodes.get(run.prototype_id.0))
1008            .map(|node| (run.parent_id, node.name.clone()));
1009        let Some(max_allowed) = self.max_instances_for(run.prototype_id) else {
1010            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1011            return Err(());
1012        };
1013        if run.nodes.len() as u32 >= max_allowed {
1014            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1015            return Err(());
1016        }
1017
1018        let cloned_id = self.clone_subtree(run.prototype_id)?;
1019        let Some(form) = self.form_mut() else {
1020            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1021            return Err(());
1022        };
1023        form.get_mut(run.parent_id)
1024            .children
1025            .insert(run.last_position + 1, cloned_id);
1026
1027        if let Some(key) = zero_key {
1028            self.zero_instance_runs.remove(&key);
1029        }
1030        self.record_instance_write();
1031        Ok(cloned_id)
1032    }
1033
1034    fn instance_remove_inner(
1035        &mut self,
1036        parent_id: FormNodeId,
1037        generation: Option<u64>,
1038        index: u32,
1039    ) -> Result<(), ()> {
1040        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
1041        if !self.write_activity_allowed()
1042            || self.mutation_count_this_doc >= MAX_MUTATIONS_PER_DOC
1043            || !self.consume_resolve_call()
1044        {
1045            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1046            return Err(());
1047        }
1048
1049        let Some(run) = self.live_instance_run(parent_id, generation) else {
1050            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1051            return Err(());
1052        };
1053        let zero_key = self
1054            .form_ref()
1055            .and_then(|form| form.nodes.get(run.prototype_id.0))
1056            .map(|node| (run.parent_id, node.name.clone()));
1057        let min_allowed = self
1058            .form_ref()
1059            .and_then(|form| form.nodes.get(run.prototype_id.0))
1060            .map(|node| node.occur.min)
1061            .unwrap_or(1);
1062        if run.nodes.len() as u32 <= min_allowed {
1063            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1064            return Err(());
1065        }
1066        let Some(remove_position) = run.positions.get(index as usize).copied() else {
1067            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1068            return Err(());
1069        };
1070
1071        let Some(form) = self.form_mut() else {
1072            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1073            return Err(());
1074        };
1075        form.get_mut(run.parent_id).children.remove(remove_position);
1076
1077        if let Some(key) = zero_key {
1078            if run.nodes.len() == 1 {
1079                self.zero_instance_runs.insert(key, self.generation);
1080            } else {
1081                self.zero_instance_runs.remove(&key);
1082            }
1083        }
1084        self.record_instance_write();
1085        Ok(())
1086    }
1087
1088    fn read_instance_run(
1089        &mut self,
1090        node_id: FormNodeId,
1091        generation: Option<u64>,
1092    ) -> Option<InstanceRun> {
1093        if !self.consume_resolve_call() {
1094            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1095            return None;
1096        }
1097        let Some(form) = self.form_ref() else {
1098            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1099            return None;
1100        };
1101        if generation.is_some_and(|value| value != self.generation) {
1102            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1103            return None;
1104        }
1105        if node_id.0 >= form.nodes.len() || self.root_id.0 >= form.nodes.len() {
1106            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1107            return None;
1108        }
1109
1110        let parents = build_parent_map(form, self.root_id);
1111        if !parents.contains_key(&node_id) {
1112            if node_id == self.root_id {
1113                return Some(InstanceRun {
1114                    parent_id: node_id,
1115                    positions: vec![0],
1116                    nodes: vec![node_id],
1117                    prototype_id: node_id,
1118                    first_position: 0,
1119                    last_position: 0,
1120                });
1121            }
1122            return None;
1123        }
1124
1125        build_instance_run(form, &parents, node_id)
1126    }
1127
1128    fn live_instance_run(
1129        &self,
1130        node_id: FormNodeId,
1131        generation: Option<u64>,
1132    ) -> Option<InstanceRun> {
1133        let form = self.form_ref()?;
1134        if generation.is_some_and(|value| value != self.generation)
1135            || node_id.0 >= form.nodes.len()
1136            || self.root_id.0 >= form.nodes.len()
1137            || !is_instance_node(&form.get(node_id).node_type)
1138        {
1139            return None;
1140        }
1141        let parents = build_parent_map(form, self.root_id);
1142        build_instance_run(form, &parents, node_id)
1143    }
1144
1145    fn clamped_instance_count(&self, prototype_id: FormNodeId, requested: u32) -> Option<u32> {
1146        let min_allowed = self.form_ref()?.get(prototype_id).occur.min;
1147        let max_allowed = self.max_instances_for(prototype_id)?;
1148        if min_allowed > max_allowed {
1149            return None;
1150        }
1151        Some(requested.clamp(min_allowed, max_allowed))
1152    }
1153
1154    fn max_instances_for(&self, prototype_id: FormNodeId) -> Option<u32> {
1155        let occur = &self.form_ref()?.get(prototype_id).occur;
1156        let max = occur.max.unwrap_or(u32::MAX);
1157        Some(max.min(MAX_INSTANCES_PER_SUBFORM))
1158    }
1159
1160    fn clone_subtree(&mut self, source_id: FormNodeId) -> Result<FormNodeId, ()> {
1161        let (mut new_node, mut new_meta, child_ids) = {
1162            let Some(form) = self.form_ref() else {
1163                self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1164                return Err(());
1165            };
1166            if source_id.0 >= form.nodes.len() {
1167                self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1168                return Err(());
1169            }
1170            (
1171                form.get(source_id).clone(),
1172                form.meta(source_id).clone(),
1173                form.get(source_id).children.clone(),
1174            )
1175        };
1176
1177        let mut new_children = Vec::with_capacity(child_ids.len());
1178        for child_id in child_ids {
1179            new_children.push(self.clone_subtree(child_id)?);
1180        }
1181        new_node.children = new_children;
1182        // Runtime-created instances are represented as concrete siblings, so
1183        // each physical clone should lay out once while retaining min/max.
1184        new_node.occur.initial = 1;
1185        new_meta.xfa_id = None;
1186
1187        let Some(form) = self.form_mut() else {
1188            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1189            return Err(());
1190        };
1191        Ok(form.add_node_with_meta(new_node, new_meta))
1192    }
1193
1194    fn normalize_instance_occurrence(&mut self, node_id: FormNodeId) {
1195        if let Some(form) = self.form_mut() {
1196            if let Some(node) = form.nodes.get_mut(node_id.0) {
1197                // See clone_subtree: live instance count is encoded by
1198                // sibling multiplicity after an instanceManager write.
1199                node.occur.initial = 1;
1200            }
1201        }
1202    }
1203
1204    fn record_instance_write(&mut self) {
1205        self.metadata.instance_writes = self.metadata.instance_writes.saturating_add(1);
1206        self.mutation_count_this_doc = self.mutation_count_this_doc.saturating_add(1);
1207    }
1208
1209    fn record_list_write(&mut self) {
1210        self.metadata.list_writes = self.metadata.list_writes.saturating_add(1);
1211        self.mutation_count_this_doc = self.mutation_count_this_doc.saturating_add(1);
1212    }
1213
1214    fn list_clear_inner(
1215        &mut self,
1216        field_id: FormNodeId,
1217        generation: Option<u64>,
1218    ) -> Result<(), ()> {
1219        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
1220        if !self.write_activity_allowed()
1221            || self.mutation_count_this_doc >= MAX_MUTATIONS_PER_DOC
1222            || !self.consume_resolve_call()
1223        {
1224            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1225            return Err(());
1226        }
1227        let current_generation = self.generation;
1228        let Some(form) = self.form_mut() else {
1229            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1230            return Err(());
1231        };
1232        if generation.is_some_and(|value| value != current_generation)
1233            || field_id.0 >= form.nodes.len()
1234        {
1235            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1236            return Err(());
1237        }
1238        if !matches!(form.get(field_id).node_type, FormNodeType::Field { .. }) {
1239            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1240            return Err(());
1241        }
1242        form.meta_mut(field_id).runtime_listbox_items.clear();
1243        self.record_list_write();
1244        Ok(())
1245    }
1246
1247    fn list_add_inner(
1248        &mut self,
1249        field_id: FormNodeId,
1250        generation: Option<u64>,
1251        display: String,
1252        save: Option<String>,
1253    ) -> Result<(), ()> {
1254        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
1255        if !self.write_activity_allowed()
1256            || self.mutation_count_this_doc >= MAX_MUTATIONS_PER_DOC
1257            || !self.consume_resolve_call()
1258        {
1259            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1260            return Err(());
1261        }
1262        let current_generation = self.generation;
1263        let Some(form) = self.form_mut() else {
1264            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1265            return Err(());
1266        };
1267        if generation.is_some_and(|value| value != current_generation)
1268            || field_id.0 >= form.nodes.len()
1269        {
1270            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1271            return Err(());
1272        }
1273        if !matches!(form.get(field_id).node_type, FormNodeType::Field { .. }) {
1274            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1275            return Err(());
1276        }
1277        let meta = form.meta_mut(field_id);
1278        if meta.runtime_listbox_items.len() >= MAX_ITEMS_PER_LISTBOX as usize {
1279            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1280            return Err(());
1281        }
1282        let save_value = save.unwrap_or_else(|| display.clone());
1283        meta.runtime_listbox_items.push((display, save_value));
1284        self.record_list_write();
1285        Ok(())
1286    }
1287
1288    fn write_activity_allowed(&self) -> bool {
1289        matches!(
1290            self.current_activity.as_deref(),
1291            Some("initialize")
1292                | Some("calculate")
1293                | Some("validate")
1294                | Some("docReady")
1295                | Some("layoutReady")
1296        )
1297    }
1298
1299    fn handle_is_live(&self, node_id: FormNodeId, generation: u64) -> bool {
1300        generation == self.generation
1301            && self
1302                .form_ref()
1303                .is_some_and(|form| node_id.0 < form.nodes.len())
1304    }
1305
1306    fn write_field_value(
1307        &mut self,
1308        node_id: FormNodeId,
1309        value: String,
1310    ) -> Option<(String, String)> {
1311        let form = self.form_mut()?;
1312        let node = form.nodes.get_mut(node_id.0)?;
1313        let FormNodeType::Field { value: field_value } = &mut node.node_type else {
1314            return None;
1315        };
1316        let before = field_value.clone();
1317        *field_value = value;
1318        Some((before, field_value.clone()))
1319    }
1320
1321    fn form_ref(&self) -> Option<&FormTree> {
1322        if self.form.is_null() {
1323            None
1324        } else {
1325            // SAFETY: `dynamic.rs` installs a pointer derived from its live
1326            // `&mut FormTree` before script dispatch and clears it before
1327            // returning. Host methods never store references derived from it.
1328            unsafe { self.form.as_ref() }
1329        }
1330    }
1331
1332    fn form_mut(&mut self) -> Option<&mut FormTree> {
1333        if self.form.is_null() {
1334            None
1335        } else {
1336            // SAFETY: See `form_ref`; the dispatch path is the sole owner of
1337            // the mutable form borrow while QuickJS closures execute.
1338            unsafe { self.form.as_mut() }
1339        }
1340    }
1341
1342    /// Phase D-γ: obtain a read-only reference to the DataDom.
1343    fn data_dom_ref(&self) -> Option<&DataDom> {
1344        // SAFETY: `data_dom` is set from `&data_dom` in flatten.rs where the
1345        // DataDom lives on the stack and outlives all script execution. We only
1346        // ever read through this pointer, never write.
1347        self.data_dom.map(|ptr| unsafe { &*ptr })
1348    }
1349
1350    /// Phase D-γ: children of a DataDom node, returned as raw indices.
1351    /// Capped at `MAX_RESOLVE_RESULTS`. Returns empty vec on invalid input.
1352    pub fn data_children(&mut self, raw_id: usize) -> Vec<usize> {
1353        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
1354        // Scope the borrow so we can increment metadata after.
1355        let result = {
1356            let Some(dom) = self.data_dom_ref() else {
1357                return Vec::new();
1358            };
1359            let id = DataNodeId::from_raw(raw_id);
1360            if dom.get(id).is_none() {
1361                return Vec::new();
1362            }
1363            dom.children(id)
1364                .iter()
1365                .take(MAX_RESOLVE_RESULTS)
1366                .map(|c| c.as_raw())
1367                .collect::<Vec<_>>()
1368        };
1369        self.metadata.data_reads = self.metadata.data_reads.saturating_add(1);
1370        result
1371    }
1372
1373    /// Phase D-γ: text value of a DataValue node.
1374    /// Returns `None` for DataGroup nodes or out-of-bounds indices.
1375    pub fn data_value(&mut self, raw_id: usize) -> Option<String> {
1376        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
1377        let result = {
1378            let dom = self.data_dom_ref()?;
1379            let id = DataNodeId::from_raw(raw_id);
1380            dom.get(id)?;
1381            dom.value(id).ok().map(|s| s.to_owned())
1382        };
1383        if result.is_some() {
1384            self.metadata.data_reads = self.metadata.data_reads.saturating_add(1);
1385        }
1386        result
1387    }
1388
1389    /// Phase D-γ: first child of `parent_raw` whose name matches `name`.
1390    /// Returns `None` if not found or parent is out-of-bounds.
1391    pub fn data_child_by_name(&mut self, parent_raw: usize, name: &str) -> Option<usize> {
1392        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
1393        let result = {
1394            let dom = self.data_dom_ref()?;
1395            let parent = DataNodeId::from_raw(parent_raw);
1396            dom.get(parent)?;
1397            dom.children_by_name(parent, name)
1398                .into_iter()
1399                .next()
1400                .map(|id| id.as_raw())
1401        };
1402        if result.is_some() {
1403            self.metadata.data_reads = self.metadata.data_reads.saturating_add(1);
1404        }
1405        result
1406    }
1407
1408    /// Phase D-γ: raw DataDom index bound to a FormTree node.
1409    /// Returns `None` when the node is unbound or the handle is stale.
1410    pub fn data_bound_record(
1411        &mut self,
1412        form_node_id: FormNodeId,
1413        generation: u64,
1414    ) -> Option<usize> {
1415        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
1416        if generation != self.generation {
1417            return None;
1418        }
1419        let form = self.form_ref()?;
1420        if form_node_id.0 >= form.nodes.len() {
1421            return None;
1422        }
1423        form.meta(form_node_id).bound_data_node
1424    }
1425
1426    /// Phase D-γ: resolve a data SOM path to the first matching node.
1427    /// Consumes one resolve-call budget slot. Returns `None` on budget
1428    /// exhaustion, parse failure, or no match.
1429    pub fn data_resolve_node(&mut self, path: &str) -> Option<usize> {
1430        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
1431        if !self.consume_resolve_call() {
1432            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1433            return None;
1434        }
1435        // Scope borrow so we can use self after.
1436        {
1437            let dom = self.data_dom_ref()?;
1438            resolve_data_path(dom, path, None)
1439                .ok()?
1440                .into_iter()
1441                .next()
1442                .map(|id| id.as_raw())
1443        }
1444    }
1445
1446    /// Phase D-γ: resolve a data SOM path to all matching nodes (capped).
1447    /// Consumes one resolve-call budget slot.
1448    pub fn data_resolve_nodes(&mut self, path: &str) -> Vec<usize> {
1449        self.metadata.host_calls = self.metadata.host_calls.saturating_add(1);
1450        if !self.consume_resolve_call() {
1451            self.metadata.binding_errors = self.metadata.binding_errors.saturating_add(1);
1452            return Vec::new();
1453        }
1454        {
1455            let Some(dom) = self.data_dom_ref() else {
1456                return Vec::new();
1457            };
1458            resolve_data_path(dom, path, None)
1459                .unwrap_or_default()
1460                .into_iter()
1461                .take(MAX_RESOLVE_RESULTS)
1462                .map(|id| id.as_raw())
1463                .collect()
1464        }
1465    }
1466}
1467
1468enum ResolveOutcome {
1469    Ok(Vec<FormNodeId>),
1470    NoMatch,
1471    BindingError,
1472}
1473
1474#[derive(Debug, Clone)]
1475struct InstanceRun {
1476    parent_id: FormNodeId,
1477    positions: Vec<usize>,
1478    nodes: Vec<FormNodeId>,
1479    prototype_id: FormNodeId,
1480    first_position: usize,
1481    last_position: usize,
1482}
1483
1484fn build_instance_run(
1485    form: &FormTree,
1486    parents: &HashMap<FormNodeId, FormNodeId>,
1487    node_id: FormNodeId,
1488) -> Option<InstanceRun> {
1489    let parent_id = parents.get(&node_id).copied()?;
1490    let name = form.get(node_id).name.clone();
1491    let parent = form.get(parent_id);
1492    let mut positions = Vec::new();
1493    let mut nodes = Vec::new();
1494    for (position, child_id) in parent.children.iter().copied().enumerate() {
1495        if form.get(child_id).name == name {
1496            positions.push(position);
1497            nodes.push(child_id);
1498        }
1499    }
1500    if !nodes.contains(&node_id) {
1501        return None;
1502    }
1503    Some(InstanceRun {
1504        parent_id,
1505        prototype_id: nodes[0],
1506        first_position: positions[0],
1507        last_position: *positions.last()?,
1508        positions,
1509        nodes,
1510    })
1511}
1512
1513fn is_instance_node(node_type: &FormNodeType) -> bool {
1514    matches!(
1515        node_type,
1516        FormNodeType::Root
1517            | FormNodeType::Subform
1518            | FormNodeType::Area
1519            | FormNodeType::ExclGroup
1520            | FormNodeType::SubformSet
1521    )
1522}
1523
1524fn normalize_resolve_path(path: &str) -> String {
1525    if let Some(rest) = path.strip_prefix("this.") {
1526        format!("$.{rest}")
1527    } else if path == "this" {
1528        "$".to_string()
1529    } else {
1530        path.to_string()
1531    }
1532}
1533
1534struct HostSomResolver<'a> {
1535    form: &'a FormTree,
1536    root_id: FormNodeId,
1537    parents: &'a HashMap<FormNodeId, FormNodeId>,
1538    current_id: FormNodeId,
1539}
1540
1541impl HostSomResolver<'_> {
1542    fn resolve_expression(&self, expr: &SomExpression) -> Option<Vec<FormNodeId>> {
1543        match expr.root {
1544            SomRoot::Data | SomRoot::Record | SomRoot::Template => None,
1545            SomRoot::CurrentContainer => {
1546                if expr.segments.is_empty() {
1547                    Some(vec![self.current_id])
1548                } else {
1549                    Some(self.follow_absolute(vec![self.current_id], &expr.segments))
1550                }
1551            }
1552            SomRoot::Form => {
1553                if expr.segments.is_empty() {
1554                    Some(vec![self.root_id])
1555                } else {
1556                    Some(self.follow_absolute(vec![self.root_id], &expr.segments))
1557                }
1558            }
1559            SomRoot::Xfa => {
1560                let segments = strip_xfa_form_prefix(&expr.segments);
1561                if segments.is_empty() {
1562                    Some(vec![self.root_id])
1563                } else {
1564                    Some(self.follow_absolute(vec![self.root_id], segments))
1565                }
1566            }
1567            SomRoot::Unqualified => {
1568                if expr.segments.is_empty() {
1569                    Some(vec![self.current_id])
1570                } else {
1571                    Some(self.follow_unqualified(&expr.segments))
1572                }
1573            }
1574        }
1575    }
1576
1577    fn follow_absolute(
1578        &self,
1579        mut current: Vec<FormNodeId>,
1580        segments: &[xfa_dom_resolver::som::SomSegment],
1581    ) -> Vec<FormNodeId> {
1582        for (idx, segment) in segments.iter().enumerate() {
1583            let allow_self = idx == 0;
1584            current = current
1585                .into_iter()
1586                .flat_map(|node_id| self.step_from_node(node_id, segment, allow_self))
1587                .collect();
1588            if current.is_empty() {
1589                break;
1590            }
1591        }
1592        current
1593    }
1594
1595    fn follow_unqualified(
1596        &self,
1597        segments: &[xfa_dom_resolver::som::SomSegment],
1598    ) -> Vec<FormNodeId> {
1599        let Some((first, rest)) = segments.split_first() else {
1600            return vec![self.current_id];
1601        };
1602
1603        let mut scope = Some(self.current_id);
1604        while let Some(scope_id) = scope {
1605            let anchors: Vec<_> = descendants_inclusive(self.form, scope_id)
1606                .into_iter()
1607                .filter(|node_id| self.node_matches_segment(*node_id, first))
1608                .collect();
1609            let matched = self.follow_remaining(anchors, rest);
1610            if !matched.is_empty() {
1611                return matched;
1612            }
1613            scope = self.parents.get(&scope_id).copied();
1614        }
1615
1616        let anchors: Vec<_> = descendants_inclusive(self.form, self.root_id)
1617            .into_iter()
1618            .filter(|node_id| self.node_matches_segment(*node_id, first))
1619            .collect();
1620        self.follow_remaining(anchors, rest)
1621    }
1622
1623    fn follow_remaining(
1624        &self,
1625        mut current: Vec<FormNodeId>,
1626        segments: &[xfa_dom_resolver::som::SomSegment],
1627    ) -> Vec<FormNodeId> {
1628        for segment in segments {
1629            current = current
1630                .into_iter()
1631                .flat_map(|node_id| self.step_from_node(node_id, segment, false))
1632                .collect();
1633            if current.is_empty() {
1634                break;
1635            }
1636        }
1637        current
1638    }
1639
1640    fn step_from_node(
1641        &self,
1642        node_id: FormNodeId,
1643        segment: &xfa_dom_resolver::som::SomSegment,
1644        allow_self: bool,
1645    ) -> Vec<FormNodeId> {
1646        if let SomSelector::Name(name) = &segment.selector {
1647            if name == ".." {
1648                if let Some(&parent_id) = self.parents.get(&node_id) {
1649                    return apply_index_to_single(parent_id, segment.index);
1650                }
1651                return Vec::new();
1652            }
1653        }
1654
1655        if allow_self && self.node_matches_selector(node_id, &segment.selector) {
1656            return apply_index_to_single(node_id, segment.index);
1657        }
1658
1659        let matches: Vec<_> = self
1660            .form
1661            .get(node_id)
1662            .children
1663            .iter()
1664            .copied()
1665            .filter(|child_id| self.node_matches_selector(*child_id, &segment.selector))
1666            .collect();
1667
1668        apply_index(matches, segment.index)
1669    }
1670
1671    fn node_matches_segment(
1672        &self,
1673        node_id: FormNodeId,
1674        segment: &xfa_dom_resolver::som::SomSegment,
1675    ) -> bool {
1676        if !self.node_matches_selector(node_id, &segment.selector) {
1677            return false;
1678        }
1679
1680        match segment.index {
1681            SomIndex::All => true,
1682            SomIndex::None => self.sibling_position(node_id, &segment.selector) == Some(0),
1683            SomIndex::Specific(idx) => {
1684                self.sibling_position(node_id, &segment.selector) == Some(idx)
1685            }
1686        }
1687    }
1688
1689    fn sibling_position(&self, node_id: FormNodeId, selector: &SomSelector) -> Option<usize> {
1690        let Some(parent_id) = self.parents.get(&node_id).copied() else {
1691            return self.node_matches_selector(node_id, selector).then_some(0);
1692        };
1693
1694        self.form
1695            .get(parent_id)
1696            .children
1697            .iter()
1698            .copied()
1699            .filter(|candidate| self.node_matches_selector(*candidate, selector))
1700            .position(|candidate| candidate == node_id)
1701    }
1702
1703    fn node_matches_selector(&self, node_id: FormNodeId, selector: &SomSelector) -> bool {
1704        match selector {
1705            SomSelector::Name(name) => self.form.get(node_id).name == *name,
1706            SomSelector::Class(class_name) => self.node_matches_class(node_id, class_name),
1707            SomSelector::AllChildren => true,
1708        }
1709    }
1710
1711    fn node_matches_class(&self, node_id: FormNodeId, class_name: &str) -> bool {
1712        let class_name = class_name.to_ascii_lowercase();
1713        match class_name.as_str() {
1714            "subform" => matches!(
1715                self.form.get(node_id).node_type,
1716                FormNodeType::Root | FormNodeType::Subform
1717            ),
1718            "pageset" => matches!(self.form.get(node_id).node_type, FormNodeType::PageSet),
1719            "pagearea" => matches!(
1720                self.form.get(node_id).node_type,
1721                FormNodeType::PageArea { .. }
1722            ),
1723            "field" => matches!(self.form.get(node_id).node_type, FormNodeType::Field { .. }),
1724            "draw" => matches!(
1725                self.form.get(node_id).node_type,
1726                FormNodeType::Draw(_) | FormNodeType::Image { .. }
1727            ),
1728            "exclgroup" => self.form.meta(node_id).group_kind == GroupKind::ExclusiveChoice,
1729            _ => false,
1730        }
1731    }
1732}
1733
1734fn strip_xfa_form_prefix(
1735    segments: &[xfa_dom_resolver::som::SomSegment],
1736) -> &[xfa_dom_resolver::som::SomSegment] {
1737    match segments.first() {
1738        Some(segment)
1739            if matches!(&segment.selector, SomSelector::Name(name) if name == "form")
1740                && matches!(segment.index, SomIndex::None) =>
1741        {
1742            &segments[1..]
1743        }
1744        _ => segments,
1745    }
1746}
1747
1748fn apply_index(matches: Vec<FormNodeId>, index: SomIndex) -> Vec<FormNodeId> {
1749    match index {
1750        SomIndex::None => matches.into_iter().take(1).collect(),
1751        SomIndex::Specific(idx) => matches.get(idx).copied().into_iter().collect(),
1752        SomIndex::All => matches,
1753    }
1754}
1755
1756fn apply_index_to_single(node_id: FormNodeId, index: SomIndex) -> Vec<FormNodeId> {
1757    match index {
1758        SomIndex::None | SomIndex::Specific(0) | SomIndex::All => vec![node_id],
1759        SomIndex::Specific(_) => Vec::new(),
1760    }
1761}
1762
1763fn descendants_inclusive(form: &FormTree, root_id: FormNodeId) -> Vec<FormNodeId> {
1764    let mut out = Vec::new();
1765    collect_descendants(form, root_id, &mut out);
1766    out
1767}
1768
1769fn collect_descendants(form: &FormTree, node_id: FormNodeId, out: &mut Vec<FormNodeId>) {
1770    out.push(node_id);
1771    for &child_id in &form.get(node_id).children {
1772        collect_descendants(form, child_id, out);
1773    }
1774}
1775
1776/// Maximum same-name candidates collected during one bare-identifier
1777/// resolution. XFA forms commonly include multiple subforms with the same
1778/// `name` (e.g. layout-only stubs, signed-data placeholders, real content).
1779/// 32 is far beyond any real corpus we have observed.
1780const MAX_RESOLVE_CANDIDATES: usize = 32;
1781
1782/// Locate a same-name descendant of `scope_id`, biased toward the candidate
1783/// most likely to be the script-meant target.
1784///
1785/// XFA Spec 3.3 §S15 implicit identifier resolution: when a script reads
1786/// `Foo.Bar.Baz`, the leading `Foo` walks the scope chain upward and runs
1787/// a depth-first search for a descendant named `Foo` at each scope. Real
1788/// XFA templates frequently contain multiple subforms named `Foo` —
1789/// typically one empty layout-stub plus one populated content node —
1790/// because authoring tools split layout and data definitions. A naive
1791/// first-hit DFS lands on whichever appears first in the merged form
1792/// tree, which is usually the empty stub.
1793///
1794/// We collect up to [`MAX_RESOLVE_CANDIDATES`] same-name descendants and
1795/// pick the one with the most direct children, breaking ties by encounter
1796/// order (DFS preorder). This keeps single-candidate behaviour identical
1797/// to the prior first-hit semantics while disambiguating the multi-stub
1798/// case to the populated branch.
1799///
1800/// Phase D-η: name-collision tie-break for the implicit resolver.
1801fn collect_named_descendant_candidates(
1802    form: &FormTree,
1803    scope_id: FormNodeId,
1804    name: &str,
1805    max_depth: usize,
1806) -> Vec<FormNodeId> {
1807    let mut candidates: Vec<FormNodeId> = Vec::new();
1808    find_named_descendant_inner(form, scope_id, name, 0, max_depth, &mut candidates);
1809    candidates
1810}
1811
1812fn order_candidates(form: &FormTree, candidates: &mut [FormNodeId]) {
1813    // Phase D-η refinement (Codex review feedback): when same-name
1814    // candidates include a `Field` and a sibling subform/container,
1815    // prefer the Field. Fields have zero children, so naïve children-
1816    // bias would pick the container — but bare `Amount.rawValue` reads
1817    // and writes mean the field, not the container. Picking the
1818    // container would silently return null for reads and refuse writes
1819    // via `set_raw_value`. Preserve first-DFS-hit semantics among
1820    // Field candidates so the pre-D-η field-resolution behaviour is
1821    // unchanged when a field candidate exists at all.
1822    candidates.sort_by(|left, right| {
1823        let left_is_field = matches!(form.get(*left).node_type, FormNodeType::Field { .. });
1824        let right_is_field = matches!(form.get(*right).node_type, FormNodeType::Field { .. });
1825        match (left_is_field, right_is_field) {
1826            (true, false) => std::cmp::Ordering::Less,
1827            (false, true) => std::cmp::Ordering::Greater,
1828            (true, true) => std::cmp::Ordering::Equal,
1829            (false, false) => form
1830                .get(*right)
1831                .children
1832                .len()
1833                .cmp(&form.get(*left).children.len()),
1834        }
1835    });
1836}
1837
1838fn push_unique_candidate(candidates: &mut Vec<FormNodeId>, node_id: FormNodeId) {
1839    if candidates.len() < MAX_RESOLVE_CANDIDATES && !candidates.contains(&node_id) {
1840        candidates.push(node_id);
1841    }
1842}
1843
1844fn resolve_implicit_candidates_in_scope(
1845    form: &FormTree,
1846    parents: &HashMap<FormNodeId, FormNodeId>,
1847    current_id: FormNodeId,
1848    name: &str,
1849) -> ResolveOutcome {
1850    // Phase D-η resolution. The implicit-identifier scope walk has three
1851    // passes; the first hit wins. Phase D-κ keeps that first candidate
1852    // byte-for-byte compatible for one-token reads, but returns same-scope
1853    // alternatives after it so the JS proxy can filter chained SOM access.
1854    let mut scope = Some(current_id);
1855    let mut depth = 0usize;
1856    while let Some(scope_id) = scope {
1857        if depth > MAX_SOM_DEPTH {
1858            return ResolveOutcome::BindingError;
1859        }
1860        if scope_id.0 >= form.nodes.len() {
1861            return ResolveOutcome::BindingError;
1862        }
1863        let mut candidates =
1864            collect_named_descendant_candidates(form, scope_id, name, MAX_SOM_DEPTH);
1865        if !candidates.is_empty() {
1866            order_candidates(form, &mut candidates);
1867            if !form.get(candidates[0]).children.is_empty() {
1868                return ResolveOutcome::Ok(candidates);
1869            }
1870        }
1871        scope = parents.get(&scope_id).copied();
1872        depth += 1;
1873    }
1874
1875    // Pass 2: ancestor self-name walk. Only reached when no scope's
1876    // children-biased descendant DFS produced a populated node.
1877    let mut scope = Some(current_id);
1878    let mut depth = 0usize;
1879    while let Some(scope_id) = scope {
1880        if depth > MAX_SOM_DEPTH {
1881            return ResolveOutcome::BindingError;
1882        }
1883        if scope_id.0 >= form.nodes.len() {
1884            return ResolveOutcome::BindingError;
1885        }
1886        if form.get(scope_id).name == name {
1887            return ResolveOutcome::Ok(vec![scope_id]);
1888        }
1889        scope = parents.get(&scope_id).copied();
1890        depth += 1;
1891    }
1892
1893    // Pass 3: accept a stub descendant as last resort.
1894    let mut scope = Some(current_id);
1895    let mut depth = 0usize;
1896    while let Some(scope_id) = scope {
1897        if depth > MAX_SOM_DEPTH {
1898            return ResolveOutcome::BindingError;
1899        }
1900        if scope_id.0 >= form.nodes.len() {
1901            return ResolveOutcome::BindingError;
1902        }
1903        let mut candidates =
1904            collect_named_descendant_candidates(form, scope_id, name, MAX_SOM_DEPTH);
1905        if !candidates.is_empty() {
1906            order_candidates(form, &mut candidates);
1907            return ResolveOutcome::Ok(candidates);
1908        }
1909        scope = parents.get(&scope_id).copied();
1910        depth += 1;
1911    }
1912
1913    ResolveOutcome::NoMatch
1914}
1915
1916fn find_named_descendant_inner(
1917    form: &FormTree,
1918    node_id: FormNodeId,
1919    name: &str,
1920    depth: usize,
1921    max_depth: usize,
1922    candidates: &mut Vec<FormNodeId>,
1923) {
1924    if depth >= max_depth || candidates.len() >= MAX_RESOLVE_CANDIDATES {
1925        return;
1926    }
1927    for &child_id in &form.get(node_id).children {
1928        if candidates.len() >= MAX_RESOLVE_CANDIDATES {
1929            return;
1930        }
1931        if form.get(child_id).name == name {
1932            candidates.push(child_id);
1933            // Mirror prior semantics: do not recurse INTO a matched node;
1934            // continue scanning siblings so all top-level same-name hits
1935            // at this scope are visible to the bias selection.
1936        } else {
1937            find_named_descendant_inner(form, child_id, name, depth + 1, max_depth, candidates);
1938        }
1939    }
1940}
1941
1942fn build_parent_map(form: &FormTree, root_id: FormNodeId) -> HashMap<FormNodeId, FormNodeId> {
1943    let mut parents = HashMap::new();
1944    populate_parent_map(form, root_id, &mut parents);
1945    parents
1946}
1947
1948fn populate_parent_map(
1949    form: &FormTree,
1950    node_id: FormNodeId,
1951    parents: &mut HashMap<FormNodeId, FormNodeId>,
1952) {
1953    for &child_id in &form.get(node_id).children {
1954        parents.insert(child_id, node_id);
1955        populate_parent_map(form, child_id, parents);
1956    }
1957}
1958
1959fn parse_nonnegative_occur_value(value: &str) -> Option<u32> {
1960    let parsed = value.trim().parse::<u64>().ok()?;
1961    u32::try_from(parsed).ok()
1962}
1963
1964fn parse_max_occur_value(value: &str) -> Option<Option<u32>> {
1965    let trimmed = value.trim();
1966    if trimmed == "-1" {
1967        return Some(None);
1968    }
1969    parse_nonnegative_occur_value(trimmed).map(Some)
1970}
1971
1972fn clamp_occur_to_i32(value: u32) -> i32 {
1973    value.min(i32::MAX as u32) as i32
1974}
1975
1976#[cfg(test)]
1977mod tests {
1978    use super::*;
1979    use xfa_layout_engine::form::{FormNode, Occur};
1980    use xfa_layout_engine::text::FontMetrics;
1981    use xfa_layout_engine::types::{BoxModel, LayoutStrategy};
1982
1983    fn add_node(tree: &mut FormTree, name: &str, node_type: FormNodeType) -> FormNodeId {
1984        tree.add_node(FormNode {
1985            name: name.to_string(),
1986            node_type,
1987            box_model: BoxModel::default(),
1988            layout: LayoutStrategy::TopToBottom,
1989            children: Vec::new(),
1990            occur: Occur::once(),
1991            font: FontMetrics::default(),
1992            calculate: None,
1993            validate: None,
1994            column_widths: Vec::new(),
1995            col_span: 1,
1996        })
1997    }
1998
1999    #[test]
2000    fn raw_value_get_set_and_generation_guard() {
2001        let mut tree = FormTree::new();
2002        let root = add_node(&mut tree, "root", FormNodeType::Root);
2003        let field = add_node(
2004            &mut tree,
2005            "Field1",
2006            FormNodeType::Field {
2007                value: "old".to_string(),
2008            },
2009        );
2010        tree.get_mut(root).children = vec![field];
2011
2012        let mut host = HostBindings::new();
2013        host.reset_per_document();
2014        host.set_form_handle(&mut tree as *mut FormTree, root);
2015        host.reset_per_script(field, Some("calculate"));
2016        let generation = host.generation();
2017
2018        assert_eq!(
2019            host.get_raw_value(field, generation),
2020            Some("old".to_string())
2021        );
2022        assert!(host.set_raw_value(field, "new".to_string(), generation));
2023        assert_eq!(
2024            host.get_raw_value(field, generation),
2025            Some("new".to_string())
2026        );
2027
2028        host.reset_per_document();
2029        assert_eq!(host.get_raw_value(field, generation), None);
2030    }
2031
2032    #[test]
2033    fn resolve_node_rejects_over_depth() {
2034        let mut tree = FormTree::new();
2035        let root = add_node(&mut tree, "root", FormNodeType::Root);
2036        let field = add_node(
2037            &mut tree,
2038            "Field1",
2039            FormNodeType::Field {
2040                value: String::new(),
2041            },
2042        );
2043        tree.get_mut(root).children = vec![field];
2044
2045        let mut host = HostBindings::new();
2046        host.reset_per_document();
2047        host.set_form_handle(&mut tree as *mut FormTree, root);
2048        host.reset_per_script(root, Some("calculate"));
2049        let long_path = (0..=MAX_SOM_DEPTH)
2050            .map(|idx| format!("n{idx}"))
2051            .collect::<Vec<_>>()
2052            .join(".");
2053
2054        assert_eq!(host.resolve_node(&long_path), None);
2055        assert_eq!(host.take_metadata().binding_errors, 1);
2056    }
2057}