1use std::collections::HashMap;
8
9use crate::error::{Result, XfaError};
10use crate::javascript_policy::{self, JavaScriptEntryPoint};
11use crate::js_runtime::{
12 activity_allowed_for_sandbox_with_gate, presave_during_flatten_enabled, NullRuntime,
13 RuntimeMetadata, SandboxError, XfaJsRuntime,
14};
15use formcalc_interpreter::{
16 interpreter::Interpreter, lexer::tokenize, parser, som_bridge::SomResolver,
17 value::Value as FormCalcValue,
18};
19use xfa_dom_resolver::som::{parse_som, SomExpression, SomIndex, SomRoot, SomSelector};
20use xfa_layout_engine::form::{
21 EventScript, FormNodeId, FormNodeType, FormTree, GroupKind, Presence, ScriptLanguage,
22};
23
24const MAX_SCRIPT_PASSES: usize = 3;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum JsExecutionMode {
34 Strict,
37 #[default]
41 BestEffortStatic,
42 SandboxedRuntime,
53}
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum OutputQuality {
57 #[default]
59 Exact,
60 BestEffort,
63 Sandboxed,
66}
67
68impl OutputQuality {
69 pub fn as_str(self) -> &'static str {
71 match self {
72 Self::Exact => "exact",
73 Self::BestEffort => "best_effort",
74 Self::Sandboxed => "sandboxed",
75 }
76 }
77}
78#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct DynamicScriptOutcome {
84 pub changes: usize,
86 pub js_present: bool,
88 pub js_skipped: usize,
90 pub other_skipped: usize,
92 pub formcalc_run: usize,
94 pub formcalc_errors: usize,
96 pub output_quality: OutputQuality,
98 pub js_executed: usize,
102 pub js_runtime_errors: usize,
107 pub js_timeouts: usize,
109 pub js_oom: usize,
111 pub js_host_calls: usize,
113 pub js_mutations: usize,
115 pub js_instance_writes: usize,
117 pub js_list_writes: usize,
119 pub js_binding_errors: usize,
121 pub js_resolve_failures: usize,
123 pub js_data_reads: usize,
125 pub js_unsupported_host_calls: usize,
132 pub js_probe_skips: usize,
137 pub variables_scripts_collected: usize,
139 pub variables_data_items_collected: usize,
141 pub script_objects_registered: usize,
144 pub script_objects_register_failed: usize,
147 pub script_objects_subform_scoped: usize,
150 pub som_lookups_total: usize,
152 pub som_lookup_successes: usize,
154 pub som_lookup_failures: usize,
156 pub som_lookup_ambiguous: usize,
158 pub som_subform_scripts_exposed: usize,
160 pub som_occur_path_refs: usize,
162 pub occur_lookups_total: usize,
164 pub occur_lookup_successes: usize,
166 pub occur_lookup_failures: usize,
168 pub occur_property_reads: usize,
170 pub occur_property_writes: usize,
172 pub occur_min_writes: usize,
174 pub occur_max_writes: usize,
176 pub occur_mutations_captured: usize,
178 pub occur_mutations_applied: usize,
180 pub occur_mutations_skipped: usize,
182 pub occur_application_ambiguous: usize,
185 pub occur_application_targets: usize,
187 pub presence_retry_enabled: bool,
189 pub presence_retry_candidates: usize,
191 pub presence_retry_admitted: usize,
193 pub presence_retry_skipped: usize,
195 pub presence_retry_nodes_under_admitted: usize,
197 pub presence_retry_text_nodes_admitted: usize,
199
200 pub script_lifecycle: Vec<ScriptLifecycleEntry>,
205
206 pub skipped_activities: SkippedActivities,
209
210 pub som_fail_log: Vec<SomFailEntry>,
213
214 pub instance_write_log: Vec<InstanceWriteEntry>,
216
217 pub presence_mutation_log: Vec<PresenceMutationEntry>,
222
223 pub form_dom_match_failures: usize,
226
227 pub form_dom_match_log: Vec<FormDomMatchEntry>,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct ScriptLifecycleEntry {
237 pub script_idx: usize,
239 pub node_id: usize,
241 pub node_name: String,
243 pub activity: String,
245 pub lang: &'static str,
247 pub outcome: &'static str,
249}
250
251#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
253pub struct SkippedActivities {
254 pub initialize: usize,
256 pub calculate: usize,
258 pub click: usize,
260 pub doc_ready: usize,
262 pub layout_ready: usize,
264 pub other: usize,
266}
267
268impl SkippedActivities {
269 fn bump(&mut self, activity: Option<&str>) {
270 match activity {
271 Some("initialize") => self.initialize += 1,
272 Some("calculate") => self.calculate += 1,
273 Some("click") => self.click += 1,
274 Some("docReady") => self.doc_ready += 1,
275 Some("layoutReady") => self.layout_ready += 1,
276 _ => self.other += 1,
277 }
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct SomFailEntry {
284 pub path: String,
286 pub kind: String,
288 pub script_idx: usize,
290 pub activity: String,
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct InstanceWriteEntry {
297 pub script_idx: usize,
299 pub activity: String,
301 pub parent_node_id: usize,
303 pub parent_node_name: String,
305 pub prototype_node_name: String,
307 pub old_count: usize,
309 pub new_count: usize,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct PresenceMutationEntry {
316 pub script_idx: usize,
318 pub activity: String,
320 pub node_id: usize,
322 pub node_name: String,
324 pub old_presence: String,
326 pub new_presence: String,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct FormDomMatchEntry {
333 pub template_node_id: usize,
335 pub template_node_name: String,
337 pub reason: String,
339}
340
341impl Default for DynamicScriptOutcome {
342 fn default() -> Self {
343 Self {
344 changes: 0,
345 js_present: false,
346 js_skipped: 0,
347 other_skipped: 0,
348 formcalc_run: 0,
349 formcalc_errors: 0,
350 output_quality: OutputQuality::Exact,
351 js_executed: 0,
352 js_runtime_errors: 0,
353 js_timeouts: 0,
354 js_oom: 0,
355 js_host_calls: 0,
356 js_mutations: 0,
357 js_instance_writes: 0,
358 js_list_writes: 0,
359 js_binding_errors: 0,
360 js_resolve_failures: 0,
361 js_data_reads: 0,
362 js_unsupported_host_calls: 0,
363 js_probe_skips: 0,
364 variables_scripts_collected: 0,
365 variables_data_items_collected: 0,
366 script_objects_registered: 0,
367 script_objects_register_failed: 0,
368 script_objects_subform_scoped: 0,
369 som_lookups_total: 0,
370 som_lookup_successes: 0,
371 som_lookup_failures: 0,
372 som_lookup_ambiguous: 0,
373 som_subform_scripts_exposed: 0,
374 som_occur_path_refs: 0,
375 occur_lookups_total: 0,
376 occur_lookup_successes: 0,
377 occur_lookup_failures: 0,
378 occur_property_reads: 0,
379 occur_property_writes: 0,
380 occur_min_writes: 0,
381 occur_max_writes: 0,
382 occur_mutations_captured: 0,
383 occur_mutations_applied: 0,
384 occur_mutations_skipped: 0,
385 occur_application_ambiguous: 0,
386 occur_application_targets: 0,
387 presence_retry_enabled: false,
388 presence_retry_candidates: 0,
389 presence_retry_admitted: 0,
390 presence_retry_skipped: 0,
391 presence_retry_nodes_under_admitted: 0,
392 presence_retry_text_nodes_admitted: 0,
393 script_lifecycle: Vec::new(),
394 skipped_activities: SkippedActivities::default(),
395 som_fail_log: Vec::new(),
396 instance_write_log: Vec::new(),
397 presence_mutation_log: Vec::new(),
398 form_dom_match_failures: 0,
399 form_dom_match_log: Vec::new(),
400 }
401 }
402}
403
404struct FormSnapshot {
420 field_values: Vec<(usize, String)>,
421 presences: Vec<(usize, Presence)>,
422 children: Vec<Vec<FormNodeId>>,
425 node_count: usize,
429 populated_count: usize,
430}
431
432fn snapshot_form(form: &FormTree) -> FormSnapshot {
433 let mut field_values = Vec::new();
434 let mut presences = Vec::new();
435 let mut children = Vec::with_capacity(form.nodes.len());
436 let mut populated_count = 0usize;
437 for (idx, node) in form.nodes.iter().enumerate() {
438 if let FormNodeType::Field { value } = &node.node_type {
439 field_values.push((idx, value.clone()));
440 if !value.trim().is_empty() {
441 populated_count += 1;
442 }
443 }
444 presences.push((idx, form.metadata[idx].presence));
445 children.push(node.children.clone());
446 }
447 FormSnapshot {
448 field_values,
449 presences,
450 children,
451 node_count: form.nodes.len(),
452 populated_count,
453 }
454}
455
456fn restore_snapshot(form: &mut FormTree, snapshot: &FormSnapshot) {
457 for (idx, value) in &snapshot.field_values {
458 if let FormNodeType::Field { value: fv } = &mut form.nodes[*idx].node_type {
459 *fv = value.clone();
460 }
461 }
462 for (idx, presence) in &snapshot.presences {
463 form.metadata[*idx].presence = *presence;
464 }
465 if form.nodes.len() > snapshot.node_count {
471 form.nodes.truncate(snapshot.node_count);
472 form.metadata.truncate(snapshot.node_count);
473 }
474 for (idx, saved_children) in snapshot.children.iter().enumerate() {
475 if let Some(node) = form.nodes.get_mut(idx) {
476 node.children = saved_children.clone();
477 }
478 }
479}
480
481fn should_rollback(
482 form: &FormTree,
483 snapshot: &FormSnapshot,
484 errors: usize,
485 successes: usize,
486) -> bool {
487 if errors > 0 && errors > successes {
488 return true;
489 }
490 if snapshot.populated_count >= 2 {
491 let mut now_empty = 0usize;
492 for (idx, old_value) in &snapshot.field_values {
493 if old_value.trim().is_empty() {
494 continue;
495 }
496 if let FormNodeType::Field { value } = &form.nodes[*idx].node_type {
497 if value.trim().is_empty() {
498 now_empty += 1;
499 }
500 }
501 }
502 if now_empty * 2 > snapshot.populated_count {
503 return true;
504 }
505 }
506 false
507}
508#[doc(hidden)]
531pub fn apply_dynamic_scripts(
532 form: &mut FormTree,
533 root_id: FormNodeId,
534) -> Result<DynamicScriptOutcome> {
535 apply_dynamic_scripts_with_mode(form, root_id, JsExecutionMode::default())
536}
537
538#[doc(hidden)]
546pub fn apply_dynamic_scripts_with_mode(
547 form: &mut FormTree,
548 root_id: FormNodeId,
549 mode: JsExecutionMode,
550) -> Result<DynamicScriptOutcome> {
551 #[cfg(feature = "xfa-js-sandboxed")]
557 {
558 if mode == JsExecutionMode::SandboxedRuntime {
559 match crate::js_runtime::SandboxedRuntime::new() {
560 Ok(mut rt) => {
561 return apply_dynamic_scripts_with_runtime(form, root_id, mode, &mut rt);
562 }
563 Err(_e) => {
564 }
568 }
569 }
570 }
571 apply_dynamic_scripts_with_runtime(form, root_id, mode, &mut NullRuntime::new())
572}
573
574fn occur_apply_enabled() -> bool {
579 std::env::var("XFA_OCCUR_APPLY").ok().as_deref() == Some("1")
580}
581
582fn presence_retry_enabled() -> bool {
586 std::env::var("XFA_PRESENCE_RETRY").ok().as_deref() == Some("1")
587}
588
589pub(crate) fn runtime_diag_enabled() -> bool {
593 std::env::var("XFA_RUNTIME_DIAG").ok().as_deref() == Some("1")
594}
595
596std::thread_local! {
601 static PRESENCE_MUT_LOG: std::cell::RefCell<Option<Vec<PresenceMutationEntry>>> =
602 const { std::cell::RefCell::new(None) };
603}
604
605fn push_presence_mutation(
608 node_id: usize,
609 node_name: &str,
610 old_presence: &'static str,
611 new_presence: &'static str,
612) {
613 PRESENCE_MUT_LOG.with(|cell| {
614 if let Some(ref mut log) = *cell.borrow_mut() {
615 if log.len() < 200 {
616 log.push(PresenceMutationEntry {
617 script_idx: 0,
620 activity: String::new(),
621 node_id,
622 node_name: node_name.to_string(),
623 old_presence: old_presence.to_string(),
624 new_presence: new_presence.to_string(),
625 });
626 }
627 }
628 });
629}
630
631pub fn apply_dynamic_scripts_with_runtime(
643 form: &mut FormTree,
644 root_id: FormNodeId,
645 mode: JsExecutionMode,
646 runtime: &mut dyn XfaJsRuntime,
647) -> Result<DynamicScriptOutcome> {
648 let parents = build_parent_map(form, root_id);
649 let all_scripts: Vec<(FormNodeId, Vec<EventScript>)> = form
650 .nodes
651 .iter()
652 .enumerate()
653 .filter_map(|(idx, _)| {
654 let node_id = FormNodeId(idx);
655 let scripts = form.meta(node_id).event_scripts.clone();
656 (!scripts.is_empty()).then_some((node_id, scripts))
657 })
658 .collect();
659
660 let has_unsupported_script = all_scripts.iter().any(|(_, node_scripts)| {
661 node_scripts
662 .iter()
663 .any(|script| script.language != ScriptLanguage::FormCalc)
664 });
665
666 if mode == JsExecutionMode::Strict && has_unsupported_script {
667 return Err(javascript_policy::reject_execution(
668 JavaScriptEntryPoint::XfaEventHook,
669 ));
670 }
671
672 let mut js_skipped = 0usize;
673 let mut other_skipped = 0usize;
674 let mut sandbox_metadata = RuntimeMetadata::default();
675 let mut scripts = Vec::new();
676 let sandbox_active = mode == JsExecutionMode::SandboxedRuntime;
677 let snapshot = snapshot_form(form);
678
679 let presave_gate = sandbox_active && presave_during_flatten_enabled();
686
687 let trace_enabled = crate::flatten_trace::enabled();
689 let diag_enabled = runtime_diag_enabled();
690
691 let mut script_lifecycle: Vec<ScriptLifecycleEntry> = Vec::new();
693 let mut skipped_activities = SkippedActivities::default();
695 let mut dispatch_script_idx: usize = 0;
698
699 if sandbox_active {
700 let _ = runtime.init();
703 let _ = runtime.reset_for_new_document();
704 let _ = runtime.set_form_handle(form as *mut FormTree, root_id);
705 runtime.set_presave_gate(presave_gate);
706 }
707
708 for (node_id, node_scripts) in all_scripts {
709 let node = form.get(node_id);
710 let node_name = if trace_enabled || diag_enabled {
711 node.name.clone()
712 } else {
713 String::new()
714 };
715 let mut formcalc_scripts = Vec::new();
716 for script in node_scripts {
717 match script.language {
718 ScriptLanguage::FormCalc => formcalc_scripts.push(script),
719 ScriptLanguage::JavaScript => {
720 let activity_str = script.activity.as_deref().unwrap_or("");
721 if sandbox_active
722 && activity_allowed_for_sandbox_with_gate(
723 script.activity.as_deref(),
724 presave_gate,
725 )
726 {
727 let this_idx = dispatch_script_idx;
728 dispatch_script_idx += 1;
729 let _ = runtime.reset_per_script(node_id, script.activity.as_deref());
730 let outcome_str = match runtime
731 .execute_script(script.activity.as_deref(), &script.script)
732 {
733 Ok(_) => {
734 "executed"
736 }
737 Err(SandboxError::Timeout) => {
738 js_skipped += 1;
739 "timeout"
740 }
741 Err(SandboxError::OutOfMemory) => {
742 js_skipped += 1;
743 "error"
744 }
745 Err(e) => {
746 log::debug!(
752 "sandbox script error on activity={:?}: {}",
753 script.activity.as_deref(),
754 e
755 );
756 if std::env::var("XFA_JS_DEBUG").ok().as_deref() == Some("1") {
757 eprintln!(
758 "XFA_JS_DEBUG sandbox script error on activity={:?}: {}",
759 script.activity.as_deref(),
760 e
761 );
762 }
763 js_skipped += 1;
764 "error"
765 }
766 };
767 if trace_enabled && script_lifecycle.len() < 500 {
769 script_lifecycle.push(ScriptLifecycleEntry {
770 script_idx: this_idx,
771 node_id: node_id.0,
772 node_name: node_name.clone(),
773 activity: activity_str.to_string(),
774 lang: "javascript",
775 outcome: outcome_str,
776 });
777 }
778 } else {
779 js_skipped += 1;
780 if trace_enabled && script_lifecycle.len() < 500 {
782 let skip_reason = if sandbox_active {
783 "skipped_activity"
784 } else {
785 "skipped_mode"
786 };
787 script_lifecycle.push(ScriptLifecycleEntry {
788 script_idx: dispatch_script_idx,
789 node_id: node_id.0,
790 node_name: node_name.clone(),
791 activity: activity_str.to_string(),
792 lang: "javascript",
793 outcome: skip_reason,
794 });
795 }
796 if trace_enabled {
798 skipped_activities.bump(script.activity.as_deref());
799 }
800 dispatch_script_idx += 1;
801 }
802 }
803 ScriptLanguage::Other => {
804 other_skipped += 1;
805 if trace_enabled && script_lifecycle.len() < 500 {
807 script_lifecycle.push(ScriptLifecycleEntry {
808 script_idx: dispatch_script_idx,
809 node_id: node_id.0,
810 node_name: node_name.clone(),
811 activity: script.activity.as_deref().unwrap_or("").to_string(),
812 lang: "other",
813 outcome: "skipped_mode",
814 });
815 }
816 dispatch_script_idx += 1;
817 }
818 }
819 }
820 if !formcalc_scripts.is_empty() {
821 scripts.push((node_id, formcalc_scripts));
822 }
823 }
824
825 let mut captured_occur: Vec<(usize, String, i64)> = Vec::new();
826 let mut sandbox_diag = crate::js_runtime::RuntimeDiagLogs::default();
828 if sandbox_active {
829 let _ = runtime.set_form_handle(std::ptr::null_mut(), root_id);
830 sandbox_metadata = runtime.take_metadata();
831 captured_occur = runtime.take_occur_mutations();
832 if diag_enabled {
833 sandbox_diag = runtime.take_diag_logs();
834 }
835 }
836
837 let mut stats = ScriptStats::default();
838
839 if diag_enabled {
841 PRESENCE_MUT_LOG.with(|cell| {
842 *cell.borrow_mut() = Some(Vec::new());
843 });
844 }
845
846 let mut changes = sandbox_metadata
847 .mutations
848 .saturating_add(sandbox_metadata.instance_writes)
849 + run_script_phase(
850 form,
851 root_id,
852 &parents,
853 &scripts,
854 ScriptPhase::Initialize,
855 1,
856 &mut stats,
857 )?
858 + run_script_phase(
859 form,
860 root_id,
861 &parents,
862 &scripts,
863 ScriptPhase::Calculate,
864 MAX_SCRIPT_PASSES,
865 &mut stats,
866 )?;
867
868 let presence_mutation_log = if diag_enabled {
870 PRESENCE_MUT_LOG.with(|cell| cell.borrow_mut().take().unwrap_or_default())
871 } else {
872 Vec::new()
873 };
874
875 let sandbox_rollback_errors = sandbox_metadata
876 .runtime_errors
877 .saturating_add(sandbox_metadata.timeouts)
878 .saturating_add(sandbox_metadata.oom)
879 .saturating_add(sandbox_metadata.binding_errors);
880 let rollback_errors = stats.errors.saturating_add(sandbox_rollback_errors);
881 let rollback_successes = stats.successes.saturating_add(sandbox_metadata.executed);
882
883 let rolled_back = should_rollback(form, &snapshot, rollback_errors, rollback_successes);
884 if rolled_back {
885 restore_snapshot(form, &snapshot);
886 changes = 0;
887 }
888
889 let mut occur_applied_targets: std::collections::HashSet<usize> =
897 std::collections::HashSet::new();
898 if sandbox_active && !rolled_back && occur_apply_enabled() && !captured_occur.is_empty() {
899 for (idx, prop, value) in &captured_occur {
900 if prop != "min" {
901 sandbox_metadata.occur_mutations_skipped =
903 sandbox_metadata.occur_mutations_skipped.saturating_add(1);
904 continue;
905 }
906 if *value < 0 || *idx >= form.nodes.len() {
907 sandbox_metadata.occur_mutations_skipped =
908 sandbox_metadata.occur_mutations_skipped.saturating_add(1);
909 continue;
910 }
911 let nid = FormNodeId(*idx);
912 let is_repeatable = matches!(
913 form.get(nid).node_type,
914 FormNodeType::Subform | FormNodeType::Area | FormNodeType::ExclGroup
915 );
916 if !is_repeatable {
917 sandbox_metadata.occur_application_ambiguous = sandbox_metadata
919 .occur_application_ambiguous
920 .saturating_add(1);
921 sandbox_metadata.occur_mutations_skipped =
922 sandbox_metadata.occur_mutations_skipped.saturating_add(1);
923 continue;
924 }
925 let v = *value as u32;
926 let node = form.get_mut(nid);
927 node.occur.min = v;
928 if node.occur.initial < v {
929 node.occur.initial = v;
930 }
931 if let Some(m) = node.occur.max {
932 if m < v {
933 node.occur.max = Some(v);
934 }
935 }
936 sandbox_metadata.occur_mutations_applied =
937 sandbox_metadata.occur_mutations_applied.saturating_add(1);
938 occur_applied_targets.insert(*idx);
939 }
940 sandbox_metadata.occur_application_targets = sandbox_metadata
941 .occur_application_targets
942 .saturating_add(occur_applied_targets.len());
943 } else if !captured_occur.is_empty() {
944 sandbox_metadata.occur_mutations_skipped = sandbox_metadata
947 .occur_mutations_skipped
948 .saturating_add(captured_occur.len());
949 }
950
951 let mut pr_candidates = 0usize;
958 let mut pr_admitted = 0usize;
959 let mut pr_skipped = 0usize;
960 let mut pr_nodes_under_admitted = 0usize;
961 let mut pr_text_nodes_admitted = 0usize;
962 let presence_retry_active = sandbox_active
963 && !rolled_back
964 && occur_apply_enabled()
965 && presence_retry_enabled()
966 && !occur_applied_targets.is_empty();
967 if presence_retry_active {
968 let mut visited: std::collections::HashSet<usize> = std::collections::HashSet::new();
969 let mut stack: Vec<FormNodeId> = occur_applied_targets
970 .iter()
971 .map(|&i| FormNodeId(i))
972 .collect();
973 let mut admitted_ids: std::collections::HashSet<usize> = std::collections::HashSet::new();
974 while let Some(nid) = stack.pop() {
975 if nid.0 >= form.nodes.len() || !visited.insert(nid.0) {
976 continue;
977 }
978 let presence = form.meta(nid).presence;
979 if presence == Presence::Hidden || presence == Presence::Invisible {
980 pr_candidates += 1;
981 form.meta_mut(nid).presence = Presence::Visible;
984 pr_admitted += 1;
985 admitted_ids.insert(nid.0);
986 } else if presence == Presence::Inactive {
987 pr_candidates += 1;
988 pr_skipped += 1;
989 }
990 for &c in &form.get(nid).children {
991 stack.push(c);
992 }
993 }
994 for &aid in &admitted_ids {
996 let mut s = vec![FormNodeId(aid)];
997 let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
998 while let Some(n) = s.pop() {
999 if n.0 >= form.nodes.len() || !seen.insert(n.0) {
1000 continue;
1001 }
1002 pr_nodes_under_admitted += 1;
1003 if let FormNodeType::Draw(_) | FormNodeType::Field { .. } = form.get(n).node_type {
1004 pr_text_nodes_admitted += 1;
1005 }
1006 for &c in &form.get(n).children {
1007 s.push(c);
1008 }
1009 }
1010 }
1011 }
1012
1013 let js_seen_count = js_skipped + sandbox_metadata.executed;
1014 let js_present = js_seen_count > 0;
1015 let output_quality = if sandbox_active && js_present && sandbox_metadata.is_clean() {
1016 OutputQuality::Sandboxed
1017 } else if (sandbox_active && js_present) || js_skipped > 0 || other_skipped > 0 {
1018 OutputQuality::BestEffort
1019 } else {
1020 OutputQuality::Exact
1021 };
1022
1023 Ok(DynamicScriptOutcome {
1024 changes,
1025 js_present,
1026 js_skipped,
1027 other_skipped,
1028 formcalc_run: stats.formcalc_run,
1029 formcalc_errors: stats.formcalc_errors,
1030 output_quality,
1031 js_executed: sandbox_metadata.executed,
1032 js_runtime_errors: sandbox_metadata.runtime_errors,
1033 js_timeouts: sandbox_metadata.timeouts,
1034 js_oom: sandbox_metadata.oom,
1035 js_host_calls: sandbox_metadata.host_calls,
1036 js_mutations: sandbox_metadata.mutations,
1037 js_instance_writes: sandbox_metadata.instance_writes,
1038 js_list_writes: sandbox_metadata.list_writes,
1039 js_binding_errors: sandbox_metadata.binding_errors,
1040 js_resolve_failures: sandbox_metadata.resolve_failures,
1041 js_data_reads: sandbox_metadata.data_reads,
1042 js_unsupported_host_calls: sandbox_metadata.unsupported_host_calls,
1043 js_probe_skips: sandbox_metadata.probe_skips,
1044 variables_scripts_collected: sandbox_metadata.variables_scripts_collected,
1045 variables_data_items_collected: sandbox_metadata.variables_data_items_collected,
1046 script_objects_registered: sandbox_metadata.script_objects_registered,
1047 script_objects_register_failed: sandbox_metadata.script_objects_register_failed,
1048 script_objects_subform_scoped: sandbox_metadata.script_objects_subform_scoped,
1049 som_lookups_total: sandbox_metadata.som_lookups_total,
1050 som_lookup_successes: sandbox_metadata.som_lookup_successes,
1051 som_lookup_failures: sandbox_metadata.som_lookup_failures,
1052 som_lookup_ambiguous: sandbox_metadata.som_lookup_ambiguous,
1053 som_subform_scripts_exposed: sandbox_metadata.som_subform_scripts_exposed,
1054 som_occur_path_refs: sandbox_metadata.som_occur_path_refs,
1055 occur_lookups_total: sandbox_metadata.occur_lookups_total,
1056 occur_lookup_successes: sandbox_metadata.occur_lookup_successes,
1057 occur_lookup_failures: sandbox_metadata.occur_lookup_failures,
1058 occur_property_reads: sandbox_metadata.occur_property_reads,
1059 occur_property_writes: sandbox_metadata.occur_property_writes,
1060 occur_min_writes: sandbox_metadata.occur_min_writes,
1061 occur_max_writes: sandbox_metadata.occur_max_writes,
1062 occur_mutations_captured: sandbox_metadata.occur_mutations_captured,
1063 occur_mutations_applied: sandbox_metadata.occur_mutations_applied,
1064 occur_mutations_skipped: sandbox_metadata.occur_mutations_skipped,
1065 occur_application_ambiguous: sandbox_metadata.occur_application_ambiguous,
1066 occur_application_targets: sandbox_metadata.occur_application_targets,
1067 presence_retry_enabled: presence_retry_active,
1068 presence_retry_candidates: pr_candidates,
1069 presence_retry_admitted: pr_admitted,
1070 presence_retry_skipped: pr_skipped,
1071 presence_retry_nodes_under_admitted: pr_nodes_under_admitted,
1072 presence_retry_text_nodes_admitted: pr_text_nodes_admitted,
1073 script_lifecycle,
1075 skipped_activities,
1076 som_fail_log: sandbox_diag.som_fail_log,
1077 instance_write_log: sandbox_diag.instance_write_log,
1078 presence_mutation_log,
1079 form_dom_match_failures: 0,
1080 form_dom_match_log: Vec::new(),
1081 })
1082}
1083
1084#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1099#[non_exhaustive]
1100pub enum InteractiveTrigger {
1101 Change,
1103 Click,
1105}
1106
1107impl InteractiveTrigger {
1108 fn activity(self) -> &'static str {
1109 match self {
1110 InteractiveTrigger::Change => "change",
1111 InteractiveTrigger::Click => "click",
1112 }
1113 }
1114}
1115
1116#[derive(Debug, Clone)]
1118pub struct PresenceChange {
1119 pub node: FormNodeId,
1121 pub name: String,
1123 pub before: Presence,
1125 pub after: Presence,
1127}
1128
1129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1131#[non_exhaustive]
1132pub enum XfaScriptErrorKind {
1133 Threw,
1135 Timeout,
1137 Refused,
1140}
1141
1142impl XfaScriptErrorKind {
1143 fn of(error: &SandboxError) -> Self {
1144 match error {
1145 SandboxError::Timeout | SandboxError::WallTimeExceeded(_) => Self::Timeout,
1146 SandboxError::ScriptError(_) | SandboxError::PanicCaptured(_) => Self::Threw,
1147 _ => Self::Refused,
1148 }
1149 }
1150}
1151
1152#[derive(Debug, Clone, PartialEq, Eq)]
1154#[non_exhaustive]
1155pub struct XfaScriptError {
1156 pub script_id: String,
1159 pub kind: XfaScriptErrorKind,
1161}
1162
1163#[derive(Debug, Clone, Default, PartialEq, Eq)]
1165#[non_exhaustive]
1166pub struct XfaScriptsSkipped {
1167 pub javascript: usize,
1169 pub formcalc: usize,
1172 pub other: usize,
1174}
1175
1176#[derive(Debug, Clone, Default, PartialEq, Eq)]
1179#[non_exhaustive]
1180pub struct XfaScriptReport {
1181 pub skipped: XfaScriptsSkipped,
1183 pub runtime_errors: Vec<XfaScriptError>,
1185 counted: std::collections::BTreeMap<String, ScriptLanguage>,
1187}
1188
1189impl XfaScriptReport {
1190 pub fn merge(&mut self, other: XfaScriptReport) {
1193 for (id, language) in &other.counted {
1194 self.skip(id, language);
1195 }
1196 for error in other.runtime_errors {
1197 if !self
1198 .runtime_errors
1199 .iter()
1200 .any(|e| e.script_id == error.script_id)
1201 {
1202 self.runtime_errors.push(error);
1203 }
1204 }
1205 }
1206
1207 pub fn is_clean(&self) -> bool {
1209 self.runtime_errors.is_empty() && self.counted.is_empty()
1210 }
1211
1212 fn skip(&mut self, id: &str, language: &ScriptLanguage) -> bool {
1213 if self.counted.contains_key(id) {
1214 return false;
1215 }
1216 self.counted.insert(id.to_string(), *language);
1217 match language {
1218 ScriptLanguage::JavaScript => self.skipped.javascript += 1,
1219 ScriptLanguage::FormCalc => self.skipped.formcalc += 1,
1220 _ => self.skipped.other += 1,
1221 }
1222 true
1223 }
1224
1225 fn fail(&mut self, id: &str, error: &SandboxError) {
1226 if self.skip(id, &ScriptLanguage::JavaScript) {
1227 self.runtime_errors.push(XfaScriptError {
1228 script_id: id.to_string(),
1229 kind: XfaScriptErrorKind::of(error),
1230 });
1231 }
1232 }
1233}
1234
1235#[derive(Debug, Clone, Default)]
1237pub struct InteractiveOutcome {
1238 pub scripts_executed: usize,
1240 pub scripts_skipped: usize,
1242 pub host_mutations: usize,
1244 pub instance_writes: usize,
1246 pub runtime_errors: usize,
1248 pub presence_changes: Vec<PresenceChange>,
1250}
1251
1252pub fn apply_interactive_open(
1257 form: &mut FormTree,
1258 root_id: FormNodeId,
1259 runtime: &mut dyn XfaJsRuntime,
1260) -> Result<InteractiveOutcome> {
1261 apply_interactive_open_reported(form, root_id, runtime).map(|(outcome, _)| outcome)
1262}
1263
1264pub fn apply_interactive_open_reported(
1266 form: &mut FormTree,
1267 root_id: FormNodeId,
1268 runtime: &mut dyn XfaJsRuntime,
1269) -> Result<(InteractiveOutcome, XfaScriptReport)> {
1270 run_interactive_episode(form, root_id, runtime, None, &["initialize", "docReady"])
1271}
1272
1273pub fn apply_interactive_commit(
1282 form: &mut FormTree,
1283 root_id: FormNodeId,
1284 changed: FormNodeId,
1285 triggers: &[InteractiveTrigger],
1286 runtime: &mut dyn XfaJsRuntime,
1287) -> Result<InteractiveOutcome> {
1288 apply_interactive_commit_reported(form, root_id, changed, triggers, runtime)
1289 .map(|(outcome, _)| outcome)
1290}
1291
1292pub fn apply_interactive_commit_reported(
1294 form: &mut FormTree,
1295 root_id: FormNodeId,
1296 changed: FormNodeId,
1297 triggers: &[InteractiveTrigger],
1298 runtime: &mut dyn XfaJsRuntime,
1299) -> Result<(InteractiveOutcome, XfaScriptReport)> {
1300 run_interactive_episode(form, root_id, runtime, Some((changed, triggers)), &[])
1301}
1302
1303fn run_interactive_episode(
1308 form: &mut FormTree,
1309 root_id: FormNodeId,
1310 runtime: &mut dyn XfaJsRuntime,
1311 focused: Option<(FormNodeId, &[InteractiveTrigger])>,
1312 pre_calculate_phases: &[&str],
1313) -> Result<(InteractiveOutcome, XfaScriptReport)> {
1314 let before_presence: Vec<Presence> = (0..form.nodes.len())
1315 .map(|idx| form.meta(FormNodeId(idx)).presence)
1316 .collect();
1317
1318 let all_scripts = collect_node_scripts(form);
1319 let names: HashMap<FormNodeId, String> = all_scripts
1321 .iter()
1322 .map(|(id, _)| (*id, form.get(*id).name.clone()))
1323 .collect();
1324 let mut report = XfaScriptReport::default();
1325
1326 let _ = runtime.init();
1329 let _ = runtime.reset_for_new_document();
1330 let _ = runtime.set_form_handle(form as *mut FormTree, root_id);
1331 runtime.set_interactive(true);
1332
1333 let mut outcome = InteractiveOutcome::default();
1334
1335 if let Some((node, triggers)) = focused {
1337 if let Some((_, scripts)) = all_scripts.iter().find(|(id, _)| *id == node) {
1338 for trigger in triggers {
1339 dispatch_node_activity(
1340 runtime,
1341 scripts,
1342 node,
1343 &names[&node],
1344 trigger.activity(),
1345 &mut outcome,
1346 &mut report,
1347 );
1348 }
1349 }
1350 }
1351
1352 for phase in pre_calculate_phases {
1354 for (node_id, scripts) in &all_scripts {
1355 dispatch_node_activity(
1356 runtime,
1357 scripts,
1358 *node_id,
1359 &names[node_id],
1360 phase,
1361 &mut outcome,
1362 &mut report,
1363 );
1364 }
1365 }
1366
1367 let pre_meta = runtime.take_metadata();
1370 let mut cumulative_mutations = pre_meta.mutations;
1371 let mut cumulative_instances = pre_meta.instance_writes;
1372 outcome.runtime_errors += pre_meta.runtime_errors + pre_meta.timeouts + pre_meta.oom;
1373
1374 for _ in 0..MAX_SCRIPT_PASSES {
1377 for (node_id, scripts) in &all_scripts {
1378 dispatch_node_activity(
1379 runtime,
1380 scripts,
1381 *node_id,
1382 &names[node_id],
1383 "calculate",
1384 &mut outcome,
1385 &mut report,
1386 );
1387 }
1388 let pass_meta = runtime.take_metadata();
1389 cumulative_mutations += pass_meta.mutations;
1390 cumulative_instances += pass_meta.instance_writes;
1391 outcome.runtime_errors += pass_meta.runtime_errors + pass_meta.timeouts + pass_meta.oom;
1392 if pass_meta.mutations == 0 && pass_meta.instance_writes == 0 {
1393 break;
1394 }
1395 }
1396
1397 let _ = runtime.set_form_handle(std::ptr::null_mut(), root_id);
1399 outcome.host_mutations = cumulative_mutations;
1400 outcome.instance_writes = cumulative_instances;
1401
1402 for (idx, &before) in before_presence.iter().enumerate() {
1404 let node = FormNodeId(idx);
1405 let after = form.meta(node).presence;
1406 if before != after {
1407 outcome.presence_changes.push(PresenceChange {
1408 node,
1409 name: form.get(node).name.clone(),
1410 before,
1411 after,
1412 });
1413 }
1414 }
1415
1416 Ok((outcome, report))
1417}
1418
1419fn collect_node_scripts(form: &FormTree) -> Vec<(FormNodeId, Vec<EventScript>)> {
1421 form.nodes
1422 .iter()
1423 .enumerate()
1424 .filter_map(|(idx, _)| {
1425 let node_id = FormNodeId(idx);
1426 let scripts = form.meta(node_id).event_scripts.clone();
1427 (!scripts.is_empty()).then_some((node_id, scripts))
1428 })
1429 .collect()
1430}
1431
1432fn dispatch_node_activity(
1434 runtime: &mut dyn XfaJsRuntime,
1435 scripts: &[EventScript],
1436 node_id: FormNodeId,
1437 node_name: &str,
1438 activity: &str,
1439 outcome: &mut InteractiveOutcome,
1440 report: &mut XfaScriptReport,
1441) {
1442 for (position, script) in scripts.iter().enumerate() {
1443 if script.activity.as_deref() != Some(activity) {
1444 continue;
1445 }
1446 let id = format!("{node_name}:{activity}#{position}");
1447 if script.language != ScriptLanguage::JavaScript {
1448 report.skip(&id, &script.language);
1449 continue;
1450 }
1451 let _ = runtime.reset_per_script(node_id, Some(activity));
1452 match runtime.execute_script(Some(activity), &script.script) {
1453 Ok(_) => outcome.scripts_executed += 1,
1454 Err(error) => {
1455 outcome.scripts_skipped += 1;
1456 report.fail(&id, &error);
1457 }
1458 }
1459 }
1460}
1461
1462fn has_hidden_ancestor(
1463 form: &FormTree,
1464 parents: &HashMap<FormNodeId, FormNodeId>,
1465 node_id: FormNodeId,
1466) -> bool {
1467 let mut cursor = parents.get(&node_id).copied();
1468 while let Some(ancestor) = cursor {
1469 if form.meta(ancestor).presence.is_not_visible() {
1470 return true;
1471 }
1472 cursor = parents.get(&ancestor).copied();
1473 }
1474 false
1475}
1476
1477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1478enum ScriptPhase {
1479 Initialize,
1480 Calculate,
1481}
1482
1483#[derive(Default)]
1484struct ScriptStats {
1485 errors: usize,
1486 successes: usize,
1487 formcalc_run: usize,
1488 formcalc_errors: usize,
1489}
1490
1491#[derive(Debug)]
1492struct ScriptResult {
1493 changes: usize,
1494 error: bool,
1495}
1496
1497fn run_script_phase(
1498 form: &mut FormTree,
1499 root_id: FormNodeId,
1500 parents: &HashMap<FormNodeId, FormNodeId>,
1501 scripts: &[(FormNodeId, Vec<EventScript>)],
1502 phase: ScriptPhase,
1503 max_passes: usize,
1504 stats: &mut ScriptStats,
1505) -> Result<usize> {
1506 let mut total_changes = 0;
1507
1508 for _ in 0..max_passes {
1509 let mut pass_changes = 0;
1510
1511 for (node_id, node_scripts) in scripts {
1512 if has_hidden_ancestor(form, parents, *node_id) {
1513 continue;
1514 }
1515
1516 for script in node_scripts
1517 .iter()
1518 .filter(|script| should_run_script(script, phase))
1519 {
1520 let result = execute_event_script(form, root_id, parents, *node_id, script, phase)?;
1521 stats.formcalc_run += 1;
1522 if result.error {
1523 stats.errors += 1;
1524 stats.formcalc_errors += 1;
1525 } else {
1526 stats.successes += 1;
1527 }
1528 pass_changes += result.changes;
1529 }
1530 }
1531
1532 total_changes += pass_changes;
1533 if pass_changes == 0 {
1534 break;
1535 }
1536 }
1537
1538 Ok(total_changes)
1539}
1540
1541fn should_run_script(script: &EventScript, phase: ScriptPhase) -> bool {
1542 match phase {
1543 ScriptPhase::Initialize => script.activity.as_deref() == Some("initialize"),
1544 ScriptPhase::Calculate => script.activity.as_deref() == Some("calculate"),
1545 }
1546}
1547
1548fn execute_event_script(
1549 form: &mut FormTree,
1550 root_id: FormNodeId,
1551 parents: &HashMap<FormNodeId, FormNodeId>,
1552 current_id: FormNodeId,
1553 script: &EventScript,
1554 phase: ScriptPhase,
1555) -> Result<ScriptResult> {
1556 match script.language {
1557 ScriptLanguage::FormCalc => Ok(execute_formcalc_script(
1558 form, root_id, parents, current_id, script, phase,
1559 )),
1560 ScriptLanguage::JavaScript => Err(javascript_policy::reject_execution(
1561 JavaScriptEntryPoint::XfaEventHook,
1562 )),
1563 ScriptLanguage::Other => Err(XfaError::UnsupportedFeature("script language".to_string())),
1564 }
1565}
1566
1567fn formcalc_debug_emit(stage: &str, message: &str, script: &EventScript) {
1573 if std::env::var("XFA_FORMCALC_DEBUG").ok().as_deref() != Some("1") {
1574 return;
1575 }
1576 let activity = script.activity.as_deref().unwrap_or("?");
1577 let one_line = message.replace(['\n', '\r'], " ");
1580 eprintln!("XFA_FORMCALC_DEBUG stage={stage} activity=\"{activity}\" message=\"{one_line}\"");
1581}
1582
1583fn execute_formcalc_script(
1584 form: &mut FormTree,
1585 root_id: FormNodeId,
1586 parents: &HashMap<FormNodeId, FormNodeId>,
1587 current_id: FormNodeId,
1588 script: &EventScript,
1589 phase: ScriptPhase,
1590) -> ScriptResult {
1591 let tokens = match tokenize(&script.script) {
1592 Ok(t) => t,
1593 Err(err) => {
1594 formcalc_debug_emit("lexer", &format!("{err}"), script);
1595 return ScriptResult {
1596 changes: 0,
1597 error: true,
1598 };
1599 }
1600 };
1601 let ast = match parser::parse(tokens) {
1602 Ok(a) => a,
1603 Err(err) => {
1604 formcalc_debug_emit("parser", &format!("{err}"), script);
1605 return ScriptResult {
1606 changes: 0,
1607 error: true,
1608 };
1609 }
1610 };
1611
1612 let mut interpreter = Interpreter::new();
1613 let mut resolver = FormTreeSomResolver::new(form, root_id, parents, current_id);
1614 let result = match interpreter.exec_with_resolver(&ast, &mut resolver) {
1615 Ok(r) => r,
1616 Err(err) => {
1617 formcalc_debug_emit("interpreter", &format!("{err}"), script);
1618 return ScriptResult {
1619 changes: resolver.changes,
1620 error: true,
1621 };
1622 }
1623 };
1624
1625 if matches!(phase, ScriptPhase::Calculate) {
1626 resolver.changes += write_formcalc_value(
1627 resolver.form,
1628 current_id,
1629 ResolvedProperty::RawValue,
1630 result,
1631 );
1632 }
1633
1634 ScriptResult {
1635 changes: resolver.changes,
1636 error: false,
1637 }
1638}
1639
1640#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1641enum ResolvedProperty {
1642 RawValue,
1643 Presence,
1644 SomExpression,
1645}
1646
1647#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1648struct ResolvedTarget {
1649 node_id: FormNodeId,
1650 property: ResolvedProperty,
1651}
1652
1653struct FormTreeSomResolver<'a> {
1654 form: &'a mut FormTree,
1655 root_id: FormNodeId,
1656 parents: &'a HashMap<FormNodeId, FormNodeId>,
1657 current_id: FormNodeId,
1658 changes: usize,
1659}
1660
1661impl<'a> FormTreeSomResolver<'a> {
1662 fn new(
1663 form: &'a mut FormTree,
1664 root_id: FormNodeId,
1665 parents: &'a HashMap<FormNodeId, FormNodeId>,
1666 current_id: FormNodeId,
1667 ) -> Self {
1668 Self {
1669 form,
1670 root_id,
1671 parents,
1672 current_id,
1673 changes: 0,
1674 }
1675 }
1676
1677 fn resolve_target(&self, path: &str) -> Option<ResolvedTarget> {
1678 let trimmed = path.trim();
1679 if trimmed.is_empty() {
1680 return None;
1681 }
1682
1683 if matches!(trimmed, "rawValue" | "presence" | "somExpression") {
1684 return Some(ResolvedTarget {
1685 node_id: self.current_id,
1686 property: parse_property_name(trimmed)?,
1687 });
1688 }
1689
1690 let (expr, property) = split_property_path(trimmed)?;
1691 let node_id = self.resolve_expression(&expr)?.into_iter().next()?;
1692 Some(ResolvedTarget { node_id, property })
1693 }
1694
1695 fn count_targets(&self, path: &str) -> usize {
1696 let trimmed = path.trim();
1697 if trimmed.is_empty() {
1698 return 0;
1699 }
1700 if matches!(trimmed, "rawValue" | "presence" | "somExpression") {
1701 return 1;
1702 }
1703 let Some((expr, _property)) = split_property_path(trimmed) else {
1704 return 0;
1705 };
1706 self.resolve_expression(&expr)
1707 .map_or(0, |nodes| nodes.len())
1708 }
1709
1710 fn resolve_expression(&self, expr: &SomExpression) -> Option<Vec<FormNodeId>> {
1711 match expr.root {
1712 SomRoot::Data | SomRoot::Record | SomRoot::Template => None,
1713 SomRoot::CurrentContainer => {
1714 if expr.segments.is_empty() {
1715 Some(vec![self.current_id])
1716 } else {
1717 Some(self.follow_absolute(vec![self.current_id], &expr.segments))
1718 }
1719 }
1720 SomRoot::Form => {
1721 if expr.segments.is_empty() {
1722 Some(vec![self.root_id])
1723 } else {
1724 Some(self.follow_absolute(vec![self.root_id], &expr.segments))
1725 }
1726 }
1727 SomRoot::Xfa => {
1728 let segments = strip_xfa_form_prefix(&expr.segments);
1729 if segments.is_empty() {
1730 Some(vec![self.root_id])
1731 } else {
1732 Some(self.follow_absolute(vec![self.root_id], segments))
1733 }
1734 }
1735 SomRoot::Unqualified => {
1736 if expr.segments.is_empty() {
1737 Some(vec![self.current_id])
1738 } else {
1739 Some(self.follow_unqualified(&expr.segments))
1740 }
1741 }
1742 }
1743 }
1744
1745 fn follow_absolute(
1746 &self,
1747 mut current: Vec<FormNodeId>,
1748 segments: &[xfa_dom_resolver::som::SomSegment],
1749 ) -> Vec<FormNodeId> {
1750 for (idx, segment) in segments.iter().enumerate() {
1751 let allow_self = idx == 0;
1752 current = current
1753 .into_iter()
1754 .flat_map(|node_id| self.step_from_node(node_id, segment, allow_self))
1755 .collect();
1756 if current.is_empty() {
1757 break;
1758 }
1759 }
1760 current
1761 }
1762
1763 fn follow_unqualified(
1764 &self,
1765 segments: &[xfa_dom_resolver::som::SomSegment],
1766 ) -> Vec<FormNodeId> {
1767 let Some((first, rest)) = segments.split_first() else {
1768 return vec![self.current_id];
1769 };
1770
1771 let mut scope = Some(self.current_id);
1772 while let Some(scope_id) = scope {
1773 let anchors: Vec<_> = descendants_inclusive(self.form, scope_id)
1774 .into_iter()
1775 .filter(|node_id| self.node_matches_segment(*node_id, first))
1776 .collect();
1777 let matched = self.follow_remaining(anchors, rest);
1778 if !matched.is_empty() {
1779 return matched;
1780 }
1781 scope = self.parents.get(&scope_id).copied();
1782 }
1783
1784 let anchors: Vec<_> = descendants_inclusive(self.form, self.root_id)
1785 .into_iter()
1786 .filter(|node_id| self.node_matches_segment(*node_id, first))
1787 .collect();
1788 self.follow_remaining(anchors, rest)
1789 }
1790
1791 fn follow_remaining(
1792 &self,
1793 mut current: Vec<FormNodeId>,
1794 segments: &[xfa_dom_resolver::som::SomSegment],
1795 ) -> Vec<FormNodeId> {
1796 for segment in segments {
1797 current = current
1798 .into_iter()
1799 .flat_map(|node_id| self.step_from_node(node_id, segment, false))
1800 .collect();
1801 if current.is_empty() {
1802 break;
1803 }
1804 }
1805 current
1806 }
1807
1808 fn step_from_node(
1809 &self,
1810 node_id: FormNodeId,
1811 segment: &xfa_dom_resolver::som::SomSegment,
1812 allow_self: bool,
1813 ) -> Vec<FormNodeId> {
1814 if let SomSelector::Name(name) = &segment.selector {
1818 if name == ".." {
1819 if let Some(&parent_id) = self.parents.get(&node_id) {
1821 return apply_index_to_single(parent_id, segment.index);
1822 }
1823 return Vec::new();
1824 }
1825 }
1826
1827 if allow_self && self.node_matches_selector(node_id, &segment.selector) {
1828 return apply_index_to_single(node_id, segment.index);
1829 }
1830
1831 let matches: Vec<_> = self
1832 .form
1833 .get(node_id)
1834 .children
1835 .iter()
1836 .copied()
1837 .filter(|child_id| self.node_matches_selector(*child_id, &segment.selector))
1838 .collect();
1839
1840 apply_index(matches, segment.index)
1841 }
1842
1843 fn node_matches_segment(
1844 &self,
1845 node_id: FormNodeId,
1846 segment: &xfa_dom_resolver::som::SomSegment,
1847 ) -> bool {
1848 if !self.node_matches_selector(node_id, &segment.selector) {
1849 return false;
1850 }
1851
1852 match segment.index {
1853 SomIndex::All => true,
1854 SomIndex::None => self.sibling_position(node_id, &segment.selector) == Some(0),
1855 SomIndex::Specific(idx) => {
1856 self.sibling_position(node_id, &segment.selector) == Some(idx)
1857 }
1858 }
1859 }
1860
1861 fn sibling_position(&self, node_id: FormNodeId, selector: &SomSelector) -> Option<usize> {
1862 let Some(parent_id) = self.parents.get(&node_id).copied() else {
1863 return self.node_matches_selector(node_id, selector).then_some(0);
1864 };
1865
1866 self.form
1867 .get(parent_id)
1868 .children
1869 .iter()
1870 .copied()
1871 .filter(|candidate| self.node_matches_selector(*candidate, selector))
1872 .position(|candidate| candidate == node_id)
1873 }
1874
1875 fn node_matches_selector(&self, node_id: FormNodeId, selector: &SomSelector) -> bool {
1876 match selector {
1877 SomSelector::Name(name) => self.form.get(node_id).name == *name,
1878 SomSelector::Class(class_name) => self.node_matches_class(node_id, class_name),
1879 SomSelector::AllChildren => true,
1880 }
1881 }
1882
1883 fn node_matches_class(&self, node_id: FormNodeId, class_name: &str) -> bool {
1884 let class_name = class_name.to_ascii_lowercase();
1885 match class_name.as_str() {
1886 "subform" => matches!(
1887 self.form.get(node_id).node_type,
1888 FormNodeType::Root | FormNodeType::Subform
1889 ),
1890 "pageset" => {
1891 matches!(self.form.get(node_id).node_type, FormNodeType::PageSet)
1892 }
1893 "pagearea" => matches!(
1894 self.form.get(node_id).node_type,
1895 FormNodeType::PageArea { .. }
1896 ),
1897 "field" => matches!(self.form.get(node_id).node_type, FormNodeType::Field { .. }),
1898 "draw" => matches!(
1899 self.form.get(node_id).node_type,
1900 FormNodeType::Draw(_) | FormNodeType::Image { .. }
1901 ),
1902 "exclgroup" => self.form.meta(node_id).group_kind == GroupKind::ExclusiveChoice,
1903 _ => false,
1904 }
1905 }
1906}
1907
1908impl SomResolver for FormTreeSomResolver<'_> {
1909 fn resolve_path(
1910 &mut self,
1911 path: &str,
1912 ) -> formcalc_interpreter::error::Result<Option<FormCalcValue>> {
1913 let Some(target) = self.resolve_target(path) else {
1914 if !path.trim().is_empty() {
1917 log::warn!("SOM bridge: path not resolved: {:?}", path.trim());
1918 }
1919 return Ok(None);
1920 };
1921 Ok(Some(read_formcalc_value(
1922 self.form,
1923 self.root_id,
1924 self.parents,
1925 target,
1926 )))
1927 }
1928
1929 fn assign_path(
1930 &mut self,
1931 path: &str,
1932 value: FormCalcValue,
1933 ) -> formcalc_interpreter::error::Result<bool> {
1934 let Some(target) = self.resolve_target(path) else {
1935 if !path.trim().is_empty() {
1937 log::warn!("SOM bridge: assignment target not found: {:?}", path.trim());
1938 }
1939 return Ok(false);
1940 };
1941 self.changes += write_formcalc_value(self.form, target.node_id, target.property, value);
1942 Ok(true)
1943 }
1944
1945 fn count_path_matches(&mut self, path: &str) -> formcalc_interpreter::error::Result<usize> {
1946 Ok(self.count_targets(path))
1947 }
1948}
1949
1950fn split_property_path(path: &str) -> Option<(SomExpression, ResolvedProperty)> {
1951 let normalized = if let Some(rest) = path.strip_prefix("this.") {
1952 format!("$.{rest}")
1953 } else if path == "this" {
1954 "$".to_string()
1955 } else {
1956 path.to_string()
1957 };
1958
1959 let mut expr = parse_som(&normalized).ok()?;
1960 let property = if let Some(last) = expr.segments.last() {
1961 match &last.selector {
1962 SomSelector::Name(name) => {
1963 parse_property_name(name).unwrap_or(ResolvedProperty::RawValue)
1964 }
1965 _ => ResolvedProperty::RawValue,
1966 }
1967 } else {
1968 ResolvedProperty::RawValue
1969 };
1970
1971 if matches!(
1972 expr.segments.last().map(|segment| &segment.selector),
1973 Some(SomSelector::Name(name)) if parse_property_name(name).is_some()
1974 ) {
1975 expr.segments.pop();
1976 }
1977
1978 Some((expr, property))
1979}
1980
1981fn parse_property_name(name: &str) -> Option<ResolvedProperty> {
1982 match name {
1983 "rawValue" => Some(ResolvedProperty::RawValue),
1984 "presence" => Some(ResolvedProperty::Presence),
1985 "somExpression" => Some(ResolvedProperty::SomExpression),
1986 _ => None,
1987 }
1988}
1989
1990fn strip_xfa_form_prefix(
1991 segments: &[xfa_dom_resolver::som::SomSegment],
1992) -> &[xfa_dom_resolver::som::SomSegment] {
1993 match segments.first() {
1994 Some(segment)
1995 if matches!(&segment.selector, SomSelector::Name(name) if name == "form")
1996 && matches!(segment.index, SomIndex::None) =>
1997 {
1998 &segments[1..]
1999 }
2000 _ => segments,
2001 }
2002}
2003
2004fn apply_index(matches: Vec<FormNodeId>, index: SomIndex) -> Vec<FormNodeId> {
2005 match index {
2006 SomIndex::None => matches.into_iter().take(1).collect(),
2007 SomIndex::Specific(idx) => matches.get(idx).copied().into_iter().collect(),
2008 SomIndex::All => matches,
2009 }
2010}
2011
2012fn apply_index_to_single(node_id: FormNodeId, index: SomIndex) -> Vec<FormNodeId> {
2013 match index {
2014 SomIndex::None | SomIndex::Specific(0) | SomIndex::All => vec![node_id],
2015 SomIndex::Specific(_) => Vec::new(),
2016 }
2017}
2018
2019fn read_formcalc_value(
2020 form: &FormTree,
2021 root_id: FormNodeId,
2022 parents: &HashMap<FormNodeId, FormNodeId>,
2023 target: ResolvedTarget,
2024) -> FormCalcValue {
2025 match target.property {
2026 ResolvedProperty::RawValue => get_formcalc_raw_value(form, target.node_id),
2027 ResolvedProperty::Presence => FormCalcValue::String(
2028 match form.meta(target.node_id).presence {
2029 Presence::Visible => "visible",
2030 Presence::Hidden => "hidden",
2031 Presence::Invisible => "invisible",
2032 Presence::Inactive => "inactive",
2033 }
2034 .to_string(),
2035 ),
2036 ResolvedProperty::SomExpression => {
2037 FormCalcValue::String(build_som_expression(form, root_id, parents, target.node_id))
2038 }
2039 }
2040}
2041
2042fn get_formcalc_raw_value(form: &FormTree, node_id: FormNodeId) -> FormCalcValue {
2043 match &form.get(node_id).node_type {
2044 FormNodeType::Field { value } => string_to_formcalc_value(value),
2045 _ if form.meta(node_id).group_kind == GroupKind::ExclusiveChoice => {
2046 for &child_id in &form.get(node_id).children {
2047 if let FormNodeType::Field { value } = &form.get(child_id).node_type {
2048 if !value.is_empty() {
2049 let selected = form.meta(child_id).item_value.as_deref().unwrap_or(value);
2050 return string_to_formcalc_value(selected);
2051 }
2052 }
2053 }
2054 FormCalcValue::Null
2055 }
2056 _ => FormCalcValue::Null,
2057 }
2058}
2059
2060fn write_formcalc_value(
2061 form: &mut FormTree,
2062 node_id: FormNodeId,
2063 property: ResolvedProperty,
2064 value: FormCalcValue,
2065) -> usize {
2066 match property {
2067 ResolvedProperty::RawValue => set_raw_value(form, node_id, formcalc_to_script_value(value)),
2068 ResolvedProperty::Presence => {
2069 set_presence(form, node_id, ScriptValue::String(value.to_string_val()))
2070 }
2071 ResolvedProperty::SomExpression => 0,
2072 }
2073}
2074
2075fn string_to_formcalc_value(value: &str) -> FormCalcValue {
2076 let trimmed = value.trim();
2077 if trimmed.is_empty() {
2078 FormCalcValue::Null
2079 } else if let Ok(number) = trimmed.parse::<f64>() {
2080 FormCalcValue::Number(number)
2081 } else {
2082 FormCalcValue::String(value.to_string())
2083 }
2084}
2085
2086fn formcalc_to_script_value(value: FormCalcValue) -> ScriptValue {
2087 match value {
2088 FormCalcValue::Null => ScriptValue::Null,
2089 FormCalcValue::Number(number) => ScriptValue::String(normalize_number(number)),
2090 FormCalcValue::String(value) => ScriptValue::String(value),
2091 }
2092}
2093
2094fn build_som_expression(
2095 form: &FormTree,
2096 root_id: FormNodeId,
2097 parents: &HashMap<FormNodeId, FormNodeId>,
2098 node_id: FormNodeId,
2099) -> String {
2100 let mut parts = Vec::new();
2101 let mut cursor = Some(node_id);
2102 while let Some(current) = cursor {
2103 let node = form.get(current);
2104 if !node.name.is_empty() {
2105 let index = if let Some(parent_id) = parents.get(¤t).copied() {
2106 form.get(parent_id)
2107 .children
2108 .iter()
2109 .copied()
2110 .filter(|sibling_id| form.get(*sibling_id).name == node.name)
2111 .position(|sibling_id| sibling_id == current)
2112 .unwrap_or(0)
2113 } else {
2114 0
2115 };
2116 parts.push(format!("{}[{index}]", node.name));
2117 }
2118 if current == root_id {
2119 break;
2120 }
2121 cursor = parents.get(¤t).copied();
2122 }
2123 parts.reverse();
2124
2125 if parts.is_empty() {
2126 "$form".to_string()
2127 } else {
2128 format!("$form.{}", parts.join("."))
2129 }
2130}
2131
2132fn descendants_inclusive(form: &FormTree, root_id: FormNodeId) -> Vec<FormNodeId> {
2133 let mut out = Vec::new();
2134 collect_descendants(form, root_id, &mut out);
2135 out
2136}
2137
2138fn collect_descendants(form: &FormTree, node_id: FormNodeId, out: &mut Vec<FormNodeId>) {
2139 out.push(node_id);
2140 for &child_id in &form.get(node_id).children {
2141 collect_descendants(form, child_id, out);
2142 }
2143}
2144
2145fn build_parent_map(form: &FormTree, root_id: FormNodeId) -> HashMap<FormNodeId, FormNodeId> {
2146 let mut parents = HashMap::new();
2147 populate_parent_map(form, root_id, &mut parents);
2148 parents
2149}
2150
2151fn populate_parent_map(
2152 form: &FormTree,
2153 node_id: FormNodeId,
2154 parents: &mut HashMap<FormNodeId, FormNodeId>,
2155) {
2156 for &child_id in &form.get(node_id).children {
2157 parents.insert(child_id, node_id);
2158 populate_parent_map(form, child_id, parents);
2159 }
2160}
2161
2162fn set_raw_value(form: &mut FormTree, node_id: FormNodeId, value: ScriptValue) -> usize {
2163 let value = match value {
2164 ScriptValue::Null => String::new(),
2165 ScriptValue::String(value) => value,
2166 };
2167
2168 if form.meta(node_id).group_kind == GroupKind::ExclusiveChoice {
2169 let mut changes = 0;
2170 for &child_id in &form.get(node_id).children.clone() {
2171 let item_value = form.meta(child_id).item_value.clone();
2172 let next = if item_value.as_deref() == Some(value.as_str()) {
2173 value.clone()
2174 } else {
2175 String::new()
2176 };
2177 if let FormNodeType::Field { value: field_value } =
2178 &mut form.get_mut(child_id).node_type
2179 {
2180 if *field_value != next {
2181 *field_value = next;
2182 changes += 1;
2183 }
2184 }
2185 }
2186 return changes;
2187 }
2188
2189 if let FormNodeType::Field { value: field_value } = &mut form.get_mut(node_id).node_type {
2190 if *field_value != value {
2191 *field_value = value;
2192 return 1;
2193 }
2194 }
2195
2196 0
2197}
2198
2199fn set_presence(form: &mut FormTree, node_id: FormNodeId, value: ScriptValue) -> usize {
2200 set_presence_inner(form, node_id, value).0
2201}
2202
2203fn set_presence_inner(
2205 form: &mut FormTree,
2206 node_id: FormNodeId,
2207 value: ScriptValue,
2208) -> (usize, Option<(&'static str, &'static str)>) {
2209 let value = match value {
2210 ScriptValue::Null => return (0, None),
2211 ScriptValue::String(value) => value,
2212 };
2213 let normalized = value.trim().to_ascii_lowercase();
2214 let new_presence = match normalized.as_str() {
2215 "visible" | "open" => Presence::Visible,
2216 "hidden" => Presence::Hidden,
2217 "invisible" => Presence::Invisible,
2218 "inactive" => Presence::Inactive,
2219 _ => return (0, None),
2220 };
2221
2222 let meta = form.meta_mut(node_id);
2223 let old_presence = meta.presence;
2224 if old_presence == new_presence {
2225 return (0, None);
2226 }
2227 meta.presence = new_presence;
2228
2229 fn pres_str(p: Presence) -> &'static str {
2230 match p {
2231 Presence::Visible => "visible",
2232 Presence::Hidden => "hidden",
2233 Presence::Invisible => "invisible",
2234 Presence::Inactive => "inactive",
2235 }
2236 }
2237 let mutation = (pres_str(old_presence), pres_str(new_presence));
2238 if runtime_diag_enabled() {
2239 let node_name = form.get(node_id).name.clone();
2240 push_presence_mutation(node_id.0, &node_name, mutation.0, mutation.1);
2241 }
2242 (1, Some(mutation))
2243}
2244
2245fn normalize_number(number: f64) -> String {
2246 if number.fract().abs() < f64::EPSILON {
2247 (number as i64).to_string()
2248 } else {
2249 number.to_string()
2250 }
2251}
2252
2253#[derive(Debug, Clone, PartialEq, Eq)]
2254enum ScriptValue {
2255 Null,
2256 String(String),
2257}
2258
2259#[cfg(test)]
2260mod tests {
2261 use super::*;
2262 use xfa_layout_engine::form::{
2263 FieldKind, FormNode, FormNodeMeta, FormNodeStyle, GroupKind, Occur,
2264 };
2265 use xfa_layout_engine::text::FontMetrics;
2266 use xfa_layout_engine::types::{BoxModel, LayoutStrategy};
2267
2268 fn add_node(tree: &mut FormTree, name: &str, node_type: FormNodeType) -> FormNodeId {
2269 tree.add_node(FormNode {
2270 name: name.to_string(),
2271 node_type,
2272 box_model: BoxModel::default(),
2273 layout: LayoutStrategy::TopToBottom,
2274 children: Vec::new(),
2275 occur: Occur::once(),
2276 font: FontMetrics::default(),
2277 calculate: None,
2278 validate: None,
2279 column_widths: Vec::new(),
2280 col_span: 1,
2281 })
2282 }
2283
2284 fn empty_meta() -> FormNodeMeta {
2285 FormNodeMeta {
2286 field_kind: FieldKind::Text,
2287 group_kind: GroupKind::None,
2288 style: FormNodeStyle::default(),
2289 ..Default::default()
2290 }
2291 }
2292
2293 fn formcalc_script(script: &str, activity: &str) -> EventScript {
2294 EventScript::formcalc(script, Some(activity))
2295 }
2296
2297 fn javascript_script(script: &str, activity: &str) -> EventScript {
2298 EventScript::javascript(script, Some(activity))
2299 }
2300
2301 fn other_script(script: &str, activity: &str) -> EventScript {
2302 EventScript::new(
2303 script.to_string(),
2304 ScriptLanguage::Other,
2305 Some(activity.to_string()),
2306 None,
2307 None,
2308 )
2309 }
2310
2311 fn script_policy_fixture(include_js: bool) -> (FormTree, FormNodeId, FormNodeId) {
2312 let mut tree = FormTree::new();
2313 let root = add_node(&mut tree, "root", FormNodeType::Root);
2314 let js_hook = add_node(
2315 &mut tree,
2316 "JsHook",
2317 FormNodeType::Field {
2318 value: String::new(),
2319 },
2320 );
2321 let runner = add_node(
2322 &mut tree,
2323 "Runner",
2324 FormNodeType::Field {
2325 value: String::new(),
2326 },
2327 );
2328 let target = add_node(
2329 &mut tree,
2330 "Target",
2331 FormNodeType::Field {
2332 value: String::new(),
2333 },
2334 );
2335
2336 tree.get_mut(root).children = vec![js_hook, runner, target];
2337 if include_js {
2338 tree.meta_mut(js_hook).event_scripts = vec![javascript_script(
2339 "xfa.host.messageBox('skip');",
2340 "initialize",
2341 )];
2342 }
2343 tree.meta_mut(runner).event_scripts =
2344 vec![formcalc_script(r#"Target.rawValue = "ran""#, "initialize")];
2345
2346 (tree, root, target)
2347 }
2348
2349 fn other_language_policy_fixture() -> (FormTree, FormNodeId, FormNodeId) {
2350 let mut tree = FormTree::new();
2351 let root = add_node(&mut tree, "root", FormNodeType::Root);
2352 let other = add_node(&mut tree, "OtherHook", FormNodeType::Subform);
2353 let runner = add_node(&mut tree, "Runner", FormNodeType::Subform);
2354 let target = add_node(
2355 &mut tree,
2356 "Target",
2357 FormNodeType::Field {
2358 value: String::new(),
2359 },
2360 );
2361
2362 tree.get_mut(root).children = vec![other, runner, target];
2363 tree.meta_mut(other).event_scripts = vec![other_script("MsgBox \"skip\"", "initialize")];
2364 tree.meta_mut(runner).event_scripts =
2365 vec![formcalc_script(r#"Target.rawValue = "ran""#, "initialize")];
2366
2367 (tree, root, target)
2368 }
2369
2370 fn field_value(tree: &FormTree, node_id: FormNodeId) -> &str {
2371 match &tree.get(node_id).node_type {
2372 FormNodeType::Field { value } => value,
2373 _ => panic!("expected field"),
2374 }
2375 }
2376
2377 #[test]
2378 fn change_event_toggles_relative_hidden_subform() {
2379 let mut tree = FormTree::new();
2380 let root = add_node(&mut tree, "root", FormNodeType::Root);
2381 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
2382 let group = add_node(&mut tree, "Choice", FormNodeType::Subform);
2383 let option1 = add_node(
2384 &mut tree,
2385 "Option1",
2386 FormNodeType::Field {
2387 value: "1".to_string(),
2388 },
2389 );
2390 let option2 = add_node(
2391 &mut tree,
2392 "Option2",
2393 FormNodeType::Field {
2394 value: String::new(),
2395 },
2396 );
2397 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
2398
2399 tree.get_mut(root).children = vec![section];
2400 tree.get_mut(section).children = vec![group, details];
2401 tree.get_mut(group).children = vec![option1, option2];
2402
2403 tree.meta_mut(group).group_kind = GroupKind::ExclusiveChoice;
2404 tree.meta_mut(group).event_scripts = vec![formcalc_script(
2405 r#"
2406Details.presence = "hidden"
2407if (this.rawValue == 1) then
2408 Details.presence = "visible"
2409endif
2410"#,
2411 "initialize",
2412 )];
2413 tree.meta_mut(option1).item_value = Some("1".into());
2414 tree.meta_mut(option2).item_value = Some("2".into());
2415 tree.meta_mut(details).presence = Presence::Hidden;
2416
2417 apply_dynamic_scripts(&mut tree, root).unwrap();
2418
2419 assert_eq!(tree.meta(details).presence, Presence::Visible);
2420 }
2421
2422 #[test]
2423 fn calculate_script_on_hidden_block_uses_sibling_values() {
2424 let mut tree = FormTree::new();
2425 let root = add_node(&mut tree, "root", FormNodeType::Root);
2426 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
2427 let option1 = add_node(
2428 &mut tree,
2429 "Opt1",
2430 FormNodeType::Field {
2431 value: "1".to_string(),
2432 },
2433 );
2434 let option2 = add_node(
2435 &mut tree,
2436 "Opt2",
2437 FormNodeType::Field {
2438 value: String::new(),
2439 },
2440 );
2441 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
2442
2443 tree.get_mut(root).children = vec![section];
2444 tree.get_mut(section).children = vec![option1, option2, details];
2445 tree.meta_mut(details).presence = Presence::Hidden;
2446 tree.meta_mut(details).event_scripts = vec![formcalc_script(
2447 r#"
2448this.presence = "hidden"
2449if ((Opt1.rawValue == 1) or (Opt2.rawValue == 1)) then
2450 this.presence = "visible"
2451endif
2452"#,
2453 "calculate",
2454 )];
2455
2456 apply_dynamic_scripts(&mut tree, root).unwrap();
2457
2458 assert_eq!(tree.meta(details).presence, Presence::Visible);
2459 }
2460
2461 #[test]
2462 fn multi_pass_scripts_propagate_raw_values() {
2463 let mut tree = FormTree::new();
2464 let root = add_node(&mut tree, "root", FormNodeType::Root);
2465 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
2466 let controller = add_node(
2467 &mut tree,
2468 "Controller",
2469 FormNodeType::Field {
2470 value: "1".to_string(),
2471 },
2472 );
2473 let target = add_node(
2474 &mut tree,
2475 "Target",
2476 FormNodeType::Field {
2477 value: String::new(),
2478 },
2479 );
2480 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
2481
2482 tree.get_mut(root).children = vec![section];
2483 tree.get_mut(section).children = vec![controller, target, details];
2484
2485 tree.meta_mut(controller).event_scripts = vec![formcalc_script(
2486 r#"
2487if (this.rawValue == 1) then
2488 Target.rawValue = 1
2489endif
2490"#,
2491 "calculate",
2492 )];
2493 tree.meta_mut(details).presence = Presence::Hidden;
2494 tree.meta_mut(details).event_scripts = vec![formcalc_script(
2495 r#"
2496this.presence = "hidden"
2497if (Target.rawValue == 1) then
2498 this.presence = "visible"
2499endif
2500"#,
2501 "calculate",
2502 )];
2503
2504 apply_dynamic_scripts(&mut tree, root).unwrap();
2505
2506 if let FormNodeType::Field { value } = &tree.get(target).node_type {
2507 assert_eq!(value, "1");
2508 } else {
2509 panic!("expected field");
2510 }
2511 assert_eq!(tree.meta(details).presence, Presence::Visible);
2512 }
2513
2514 #[test]
2519 fn som_path_resolves_on_simple_form_tree() {
2520 let mut tree = FormTree::new();
2521 let root = add_node(&mut tree, "root", FormNodeType::Root);
2522 let form1 = add_node(&mut tree, "form1", FormNodeType::Subform);
2523 let subform = add_node(&mut tree, "subform1", FormNodeType::Subform);
2524 let field1 = add_node(
2525 &mut tree,
2526 "field1",
2527 FormNodeType::Field {
2528 value: "hello".to_string(),
2529 },
2530 );
2531
2532 tree.get_mut(root).children = vec![form1];
2533 tree.get_mut(form1).children = vec![subform];
2534 tree.get_mut(subform).children = vec![field1];
2535
2536 tree.meta_mut(root).event_scripts = vec![formcalc_script(
2538 "form1.subform1.field1.rawValue",
2539 "calculate",
2540 )];
2541
2542 let parents = super::build_parent_map(&tree, root);
2543 let resolver = FormTreeSomResolver::new(&mut tree, root, &parents, root);
2544 let target = resolver.resolve_target("form1.subform1.field1.rawValue");
2545 assert!(target.is_some(), "SOM path must resolve to a node");
2546 let target = target.unwrap();
2547 let val = super::read_formcalc_value(&tree, root, &parents, target);
2548 match val {
2549 formcalc_interpreter::value::Value::String(s) => assert_eq!(s, "hello"),
2550 formcalc_interpreter::value::Value::Number(n) => {
2551 panic!("expected string, got number {n}")
2553 }
2554 _ => panic!("expected string value"),
2555 }
2556 }
2557
2558 #[test]
2560 fn invalid_som_path_returns_none_not_panic() {
2561 let mut tree = FormTree::new();
2562 let root = add_node(&mut tree, "root", FormNodeType::Root);
2563 let parents = super::build_parent_map(&tree, root);
2564 let resolver = FormTreeSomResolver::new(&mut tree, root, &parents, root);
2565
2566 let result = resolver.resolve_target("nonexistent.deep.path.rawValue");
2568 assert!(
2569 result.is_none(),
2570 "invalid SOM path must return None, not panic"
2571 );
2572 }
2573
2574 #[test]
2575 fn best_effort_skips_javascript_and_runs_formcalc() {
2576 let (mut tree, root, target) = script_policy_fixture(true);
2577
2578 let outcome = apply_dynamic_scripts(&mut tree, root).unwrap();
2579
2580 assert_eq!(field_value(&tree, target), "ran");
2581 assert!(outcome.js_present);
2582 assert_eq!(outcome.js_skipped, 1);
2583 assert_eq!(outcome.other_skipped, 0);
2584 assert_eq!(outcome.formcalc_run, 1);
2585 assert_eq!(outcome.formcalc_errors, 0);
2586 assert_eq!(outcome.output_quality, OutputQuality::BestEffort);
2587 }
2588
2589 #[test]
2590 fn strict_mode_preserves_javascript_reject() {
2591 let (mut tree, root, target) = script_policy_fixture(true);
2592
2593 let err =
2594 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
2595
2596 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2597 assert_eq!(field_value(&tree, target), "");
2598 }
2599
2600 #[test]
2601 fn formcalc_only_reports_exact_quality() {
2602 let (mut tree, root, target) = script_policy_fixture(false);
2603
2604 let outcome = apply_dynamic_scripts(&mut tree, root).unwrap();
2605
2606 assert_eq!(field_value(&tree, target), "ran");
2607 assert!(!outcome.js_present);
2608 assert_eq!(outcome.js_skipped, 0);
2609 assert_eq!(outcome.other_skipped, 0);
2610 assert_eq!(outcome.formcalc_run, 1);
2611 assert_eq!(outcome.formcalc_errors, 0);
2612 assert_eq!(outcome.output_quality, OutputQuality::Exact);
2613 }
2614
2615 #[test]
2616 fn other_language_scripts_skip_in_best_effort_and_reject_in_strict() {
2617 let (mut tree, root, target) = other_language_policy_fixture();
2618 let (mut strict_tree, strict_root, _) = other_language_policy_fixture();
2619
2620 let outcome = apply_dynamic_scripts(&mut tree, root).unwrap();
2621 assert_eq!(field_value(&tree, target), "ran");
2622 assert_eq!(outcome.js_skipped, 0);
2623 assert_eq!(outcome.other_skipped, 1);
2624 assert_eq!(outcome.formcalc_run, 1);
2625 assert_eq!(outcome.output_quality, OutputQuality::BestEffort);
2626
2627 let err =
2628 apply_dynamic_scripts_with_mode(&mut strict_tree, strict_root, JsExecutionMode::Strict)
2629 .unwrap_err();
2630 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2631 }
2632
2633 #[test]
2634 fn javascript_direct_executor_call_is_still_denied() {
2635 let mut tree = FormTree::new();
2636 let root = add_node(&mut tree, "root", FormNodeType::Root);
2637 let trigger = add_node(
2638 &mut tree,
2639 "Trigger",
2640 FormNodeType::Field {
2641 value: String::new(),
2642 },
2643 );
2644 tree.get_mut(root).children = vec![trigger];
2645
2646 let parents = build_parent_map(&tree, root);
2647 let script = javascript_script("xfa.host.messageBox('deny');", "initialize");
2648 let err = execute_event_script(
2649 &mut tree,
2650 root,
2651 &parents,
2652 trigger,
2653 &script,
2654 ScriptPhase::Initialize,
2655 )
2656 .unwrap_err();
2657
2658 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2659 }
2660
2661 #[test]
2662 fn javascript_resolve_node_call_is_explicitly_denied() {
2663 let mut tree = FormTree::new();
2664 let root = add_node(&mut tree, "root", FormNodeType::Root);
2665 let form = add_node(&mut tree, "formulier1", FormNodeType::Subform);
2666 let admin = add_node(&mut tree, "ADMIN", FormNodeType::Subform);
2667 let lock = add_node(
2668 &mut tree,
2669 "LockForm_AD",
2670 FormNodeType::Field {
2671 value: "1".to_string(),
2672 },
2673 );
2674 let reset = add_node(
2675 &mut tree,
2676 "Reset",
2677 FormNodeType::Field {
2678 value: "1".to_string(),
2679 },
2680 );
2681
2682 tree.get_mut(root).children = vec![form];
2683 tree.get_mut(form).children = vec![admin, reset];
2684 tree.get_mut(admin).children = vec![lock];
2685 tree.meta_mut(reset).event_scripts = vec![javascript_script(
2686 r#"xfa.resolveNode("formulier1.ADMIN.LockForm_AD").rawValue = 0;"#,
2687 "initialize",
2688 )];
2689
2690 let err =
2691 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
2692 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2693
2694 if let FormNodeType::Field { value } = &tree.get(lock).node_type {
2695 assert_eq!(value, "1");
2696 } else {
2697 panic!("expected field");
2698 }
2699 }
2700
2701 #[test]
2702 fn javascript_utils_hide_if_empty_is_explicitly_denied() {
2703 let mut tree = FormTree::new();
2704 let root = add_node(&mut tree, "root", FormNodeType::Root);
2705 let empty = add_node(
2706 &mut tree,
2707 "EmptyField",
2708 FormNodeType::Field {
2709 value: String::new(),
2710 },
2711 );
2712 tree.get_mut(root).children = vec![empty];
2713 tree.meta_mut(empty).event_scripts =
2714 vec![javascript_script("Utils.hideIfEmpty(this);", "initialize")];
2715
2716 let err =
2717 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
2718 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2719
2720 assert!(!tree.meta(empty).presence.is_not_visible());
2721 }
2722
2723 #[test]
2724 fn malformed_javascript_payload_is_explicitly_denied_without_panic() {
2725 let mut tree = FormTree::new();
2726 let root = add_node(&mut tree, "root", FormNodeType::Root);
2727 let container = add_node(&mut tree, "Container", FormNodeType::Subform);
2728 let empty = add_node(
2729 &mut tree,
2730 "EmptyField",
2731 FormNodeType::Field {
2732 value: String::new(),
2733 },
2734 );
2735
2736 tree.get_mut(root).children = vec![container];
2737 tree.get_mut(container).children = vec![empty];
2738 tree.meta_mut(empty).event_scripts = vec![javascript_script(
2739 "\0}{{not.valid.javascript(",
2740 "initialize",
2741 )];
2742
2743 let err =
2744 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
2745 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2746
2747 assert!(!tree.meta(container).presence.is_not_visible());
2748 }
2749
2750 #[test]
2751 fn default_meta_helper_is_constructible() {
2752 let meta = empty_meta();
2753 assert_eq!(meta.group_kind, GroupKind::None);
2754 }
2755
2756 #[test]
2757 fn calculate_event_applies_formcalc_return_value() {
2758 let mut tree = FormTree::new();
2759 let root = add_node(&mut tree, "root", FormNodeType::Root);
2760 let total = add_node(
2761 &mut tree,
2762 "Total",
2763 FormNodeType::Field {
2764 value: String::new(),
2765 },
2766 );
2767
2768 tree.get_mut(root).children = vec![total];
2769 tree.meta_mut(total).event_scripts = vec![formcalc_script("40 + 2", "calculate")];
2770
2771 apply_dynamic_scripts(&mut tree, root).unwrap();
2772
2773 match &tree.get(total).node_type {
2774 FormNodeType::Field { value } => assert_eq!(value, "42"),
2775 _ => panic!("expected field"),
2776 }
2777 }
2778
2779 #[test]
2780 fn calculate_event_resolves_bare_field_names_as_raw_values() {
2781 let mut tree = FormTree::new();
2782 let root = add_node(&mut tree, "root", FormNodeType::Root);
2783 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
2784 let number1 = add_node(
2785 &mut tree,
2786 "Number1",
2787 FormNodeType::Field {
2788 value: "40".to_string(),
2789 },
2790 );
2791 let number2 = add_node(
2792 &mut tree,
2793 "Number2",
2794 FormNodeType::Field {
2795 value: "2".to_string(),
2796 },
2797 );
2798 let total = add_node(
2799 &mut tree,
2800 "Total",
2801 FormNodeType::Field {
2802 value: String::new(),
2803 },
2804 );
2805
2806 tree.get_mut(root).children = vec![section];
2807 tree.get_mut(section).children = vec![number1, number2, total];
2808 tree.meta_mut(total).event_scripts =
2809 vec![formcalc_script("Number1 + Number2", "calculate")];
2810
2811 apply_dynamic_scripts(&mut tree, root).unwrap();
2812
2813 match &tree.get(total).node_type {
2814 FormNodeType::Field { value } => assert_eq!(value, "42"),
2815 _ => panic!("expected field"),
2816 }
2817 }
2818
2819 #[test]
2820 fn click_events_are_skipped_during_flatten() {
2821 let mut tree = FormTree::new();
2822 let root = add_node(&mut tree, "root", FormNodeType::Root);
2823 let trigger = add_node(
2824 &mut tree,
2825 "Trigger",
2826 FormNodeType::Field {
2827 value: "1".to_string(),
2828 },
2829 );
2830 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
2831
2832 tree.get_mut(root).children = vec![trigger, details];
2833 tree.meta_mut(details).presence = Presence::Hidden;
2834 tree.meta_mut(trigger).event_scripts = vec![formcalc_script(
2835 r#"
2836Details.presence = "visible"
2837"#,
2838 "click",
2839 )];
2840
2841 apply_dynamic_scripts(&mut tree, root).unwrap();
2842
2843 assert_eq!(tree.meta(details).presence, Presence::Hidden);
2844 }
2845
2846 #[test]
2847 fn rollback_when_scripts_mostly_error() {
2848 let mut tree = FormTree::new();
2852 let root = add_node(&mut tree, "root", FormNodeType::Root);
2853 let field_a = add_node(
2854 &mut tree,
2855 "FieldA",
2856 FormNodeType::Field {
2857 value: "hello".to_string(),
2858 },
2859 );
2860 let field_b = add_node(
2861 &mut tree,
2862 "FieldB",
2863 FormNodeType::Field {
2864 value: "world".to_string(),
2865 },
2866 );
2867
2868 tree.get_mut(root).children = vec![field_a, field_b];
2869
2870 tree.meta_mut(field_a).event_scripts = vec![formcalc_script("@@INVALID@@", "initialize")];
2872 tree.meta_mut(field_b).event_scripts =
2873 vec![formcalc_script("@@ALSO_BROKEN@@", "initialize")];
2874
2875 apply_dynamic_scripts(&mut tree, root).unwrap();
2876
2877 match &tree.get(field_a).node_type {
2879 FormNodeType::Field { value } => assert_eq!(value, "hello"),
2880 _ => panic!("expected field"),
2881 }
2882 match &tree.get(field_b).node_type {
2883 FormNodeType::Field { value } => assert_eq!(value, "world"),
2884 _ => panic!("expected field"),
2885 }
2886 }
2887
2888 #[test]
2889 fn rollback_when_populated_fields_go_empty() {
2890 let mut tree = FormTree::new();
2893 let root = add_node(&mut tree, "root", FormNodeType::Root);
2894 let field_a = add_node(
2895 &mut tree,
2896 "FieldA",
2897 FormNodeType::Field {
2898 value: "keep".to_string(),
2899 },
2900 );
2901 let field_b = add_node(
2902 &mut tree,
2903 "FieldB",
2904 FormNodeType::Field {
2905 value: "also_keep".to_string(),
2906 },
2907 );
2908
2909 tree.get_mut(root).children = vec![field_a, field_b];
2910
2911 let snapshot = super::snapshot_form(&tree);
2917
2918 if let FormNodeType::Field { value } = &mut tree.get_mut(field_a).node_type {
2920 *value = String::new();
2921 }
2922 if let FormNodeType::Field { value } = &mut tree.get_mut(field_b).node_type {
2923 *value = String::new();
2924 }
2925
2926 assert!(super::should_rollback(&tree, &snapshot, 0, 2));
2927
2928 super::restore_snapshot(&mut tree, &snapshot);
2929
2930 match &tree.get(field_a).node_type {
2931 FormNodeType::Field { value } => assert_eq!(value, "keep"),
2932 _ => panic!("expected field"),
2933 }
2934 match &tree.get(field_b).node_type {
2935 FormNodeType::Field { value } => assert_eq!(value, "also_keep"),
2936 _ => panic!("expected field"),
2937 }
2938 }
2939
2940 #[test]
2941 fn no_rollback_when_scripts_succeed() {
2942 let mut tree = FormTree::new();
2944 let root = add_node(&mut tree, "root", FormNodeType::Root);
2945 let total = add_node(
2946 &mut tree,
2947 "Total",
2948 FormNodeType::Field {
2949 value: String::new(),
2950 },
2951 );
2952
2953 tree.get_mut(root).children = vec![total];
2954 tree.meta_mut(total).event_scripts = vec![formcalc_script("40 + 2", "calculate")];
2955
2956 apply_dynamic_scripts(&mut tree, root).unwrap();
2957
2958 match &tree.get(total).node_type {
2959 FormNodeType::Field { value } => assert_eq!(value, "42"),
2960 _ => panic!("expected field"),
2961 }
2962 }
2963
2964 #[test]
2969 fn formcalc_unknown_function_increments_error_counter() {
2970 let mut tree = FormTree::new();
2971 let root = add_node(&mut tree, "root", FormNodeType::Root);
2972 let total = add_node(
2973 &mut tree,
2974 "Total",
2975 FormNodeType::Field {
2976 value: String::new(),
2977 },
2978 );
2979
2980 tree.get_mut(root).children = vec![total];
2981 tree.meta_mut(total).event_scripts = vec![formcalc_script(
2984 "definitelyNotAFormCalcBuiltin(1)",
2985 "calculate",
2986 )];
2987
2988 let outcome = apply_dynamic_scripts(&mut tree, root).unwrap();
2989 assert_eq!(outcome.formcalc_run, 1, "the script must be attempted");
2990 assert_eq!(
2991 outcome.formcalc_errors, 1,
2992 "unknown-function failure must increment formcalc_errors"
2993 );
2994 }
2995
2996 static ENV_LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
3001
3002 fn env_lock() -> std::sync::MutexGuard<'static, ()> {
3003 ENV_LOCK
3004 .get_or_init(|| std::sync::Mutex::new(()))
3005 .lock()
3006 .unwrap_or_else(|e| e.into_inner())
3007 }
3008
3009 #[test]
3015 fn script_lifecycle_populated_when_trace_enabled() {
3016 let _guard = env_lock();
3017 std::env::set_var("XFA_FLATTEN_TRACE", "1");
3018
3019 let mut tree = FormTree::new();
3020 let root = add_node(&mut tree, "root", FormNodeType::Root);
3021 let field = add_node(
3022 &mut tree,
3023 "TraceField",
3024 FormNodeType::Field {
3025 value: String::new(),
3026 },
3027 );
3028 tree.get_mut(root).children = vec![field];
3029 tree.meta_mut(field).event_scripts = vec![javascript_script(
3030 "xfa.host.messageBox('trace');",
3031 "initialize",
3032 )];
3033
3034 let outcome =
3035 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::BestEffortStatic)
3036 .unwrap();
3037
3038 std::env::remove_var("XFA_FLATTEN_TRACE");
3039
3040 assert!(
3041 !outcome.script_lifecycle.is_empty(),
3042 "script_lifecycle must have at least one entry when XFA_FLATTEN_TRACE=1"
3043 );
3044 let entry = &outcome.script_lifecycle[0];
3045 assert_eq!(entry.node_name, "TraceField");
3046 assert_eq!(entry.activity, "initialize");
3047 assert_eq!(entry.lang, "javascript");
3048 assert_eq!(
3050 entry.outcome, "skipped_mode",
3051 "BestEffortStatic JS must appear as skipped_mode in lifecycle"
3052 );
3053 }
3054
3055 #[test]
3062 fn skipped_activities_tallies_click_correctly() {
3063 let _guard = env_lock();
3064 std::env::set_var("XFA_FLATTEN_TRACE", "1");
3065
3066 let mut tree = FormTree::new();
3067 let root = add_node(&mut tree, "root", FormNodeType::Root);
3068 let btn = add_node(
3069 &mut tree,
3070 "ClickBtn",
3071 FormNodeType::Field {
3072 value: String::new(),
3073 },
3074 );
3075 tree.get_mut(root).children = vec![btn];
3076 tree.meta_mut(btn).event_scripts = vec![javascript_script(
3078 "xfa.host.messageBox('clicked');",
3079 "click",
3080 )];
3081
3082 let outcome =
3083 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::BestEffortStatic)
3084 .unwrap();
3085
3086 std::env::remove_var("XFA_FLATTEN_TRACE");
3087
3088 assert_eq!(
3089 outcome.skipped_activities.click, 1,
3090 "exactly one click-activity JS script must be tallied in skipped_activities.click"
3091 );
3092 assert_eq!(outcome.skipped_activities.initialize, 0);
3094 assert_eq!(outcome.skipped_activities.calculate, 0);
3095 assert_eq!(outcome.skipped_activities.other, 0);
3096 }
3097
3098 #[test]
3106 fn form_dom_match_failures_increments_for_unmatched_subform() {
3107 use crate::flatten::{apply_form_dom_presence, XfaRenderingPolicy};
3108
3109 let _guard = env_lock();
3110 std::env::set_var("XFA_FLATTEN_TRACE", "1");
3112
3113 let mut tree = FormTree::new();
3114 let root = add_node(&mut tree, "root", FormNodeType::Root);
3115 let form1 = add_node(&mut tree, "form1", FormNodeType::Subform);
3117 let present = add_node(&mut tree, "Present", FormNodeType::Subform);
3119 let ghost = add_node(&mut tree, "Ghost", FormNodeType::Subform);
3121
3122 tree.get_mut(root).children = vec![form1];
3123 tree.get_mut(form1).children = vec![present, ghost];
3124
3125 let form_xml = r#"<form>
3129 <subform name="form1">
3130 <subform name="Present"/>
3131 </subform>
3132</form>"#;
3133
3134 let (_admitted, match_failures, match_log) = apply_form_dom_presence(
3135 &mut tree,
3136 root,
3137 form_xml,
3138 XfaRenderingPolicy::SavedStateFaithful,
3139 false,
3140 );
3141
3142 std::env::remove_var("XFA_FLATTEN_TRACE");
3143
3144 assert_eq!(
3145 match_failures, 1,
3146 "exactly one unmatched named subform (Ghost) must be counted"
3147 );
3148 assert_eq!(match_log.len(), 1, "match_log must contain the Ghost entry");
3149 assert_eq!(match_log[0].template_node_name, "Ghost");
3150 assert_eq!(match_log[0].reason, "formdom_unmatched_suppressed");
3151 assert!(
3153 tree.meta(ghost).presence.is_not_visible(),
3154 "Ghost subform must have been hidden by apply_form_dom_presence"
3155 );
3156 }
3157}