1use std::collections::HashMap;
2
3use crate::error::{Result, XfaError};
4use crate::javascript_policy::{self, JavaScriptEntryPoint};
5use crate::js_runtime::{
6 activity_allowed_for_sandbox_with_gate, presave_during_flatten_enabled, NullRuntime,
7 RuntimeMetadata, SandboxError, XfaJsRuntime,
8};
9use formcalc_interpreter::{
10 interpreter::Interpreter, lexer::tokenize, parser, som_bridge::SomResolver,
11 value::Value as FormCalcValue,
12};
13use xfa_dom_resolver::som::{parse_som, SomExpression, SomIndex, SomRoot, SomSelector};
14use xfa_layout_engine::form::{
15 EventScript, FormNodeId, FormNodeType, FormTree, GroupKind, Presence, ScriptLanguage,
16};
17
18const MAX_SCRIPT_PASSES: usize = 3;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum JsExecutionMode {
28 Strict,
31 #[default]
35 BestEffortStatic,
36 SandboxedRuntime,
47}
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
50pub enum OutputQuality {
51 #[default]
53 Exact,
54 BestEffort,
57 Sandboxed,
60}
61
62impl OutputQuality {
63 pub fn as_str(self) -> &'static str {
65 match self {
66 Self::Exact => "exact",
67 Self::BestEffort => "best_effort",
68 Self::Sandboxed => "sandboxed",
69 }
70 }
71}
72#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct DynamicScriptOutcome {
78 pub changes: usize,
80 pub js_present: bool,
82 pub js_skipped: usize,
84 pub other_skipped: usize,
86 pub formcalc_run: usize,
88 pub formcalc_errors: usize,
90 pub output_quality: OutputQuality,
92 pub js_executed: usize,
96 pub js_runtime_errors: usize,
101 pub js_timeouts: usize,
103 pub js_oom: usize,
105 pub js_host_calls: usize,
107 pub js_mutations: usize,
109 pub js_instance_writes: usize,
111 pub js_list_writes: usize,
113 pub js_binding_errors: usize,
115 pub js_resolve_failures: usize,
117 pub js_data_reads: usize,
119 pub js_unsupported_host_calls: usize,
126 pub js_probe_skips: usize,
131 pub variables_scripts_collected: usize,
133 pub variables_data_items_collected: usize,
135 pub script_objects_registered: usize,
138 pub script_objects_register_failed: usize,
141 pub script_objects_subform_scoped: usize,
144 pub som_lookups_total: usize,
146 pub som_lookup_successes: usize,
148 pub som_lookup_failures: usize,
150 pub som_lookup_ambiguous: usize,
152 pub som_subform_scripts_exposed: usize,
154 pub som_occur_path_refs: usize,
156 pub occur_lookups_total: usize,
158 pub occur_lookup_successes: usize,
160 pub occur_lookup_failures: usize,
162 pub occur_property_reads: usize,
164 pub occur_property_writes: usize,
166 pub occur_min_writes: usize,
168 pub occur_max_writes: usize,
170 pub occur_mutations_captured: usize,
172 pub occur_mutations_applied: usize,
174 pub occur_mutations_skipped: usize,
176 pub occur_application_ambiguous: usize,
179 pub occur_application_targets: usize,
181 pub presence_retry_enabled: bool,
183 pub presence_retry_candidates: usize,
185 pub presence_retry_admitted: usize,
187 pub presence_retry_skipped: usize,
189 pub presence_retry_nodes_under_admitted: usize,
191 pub presence_retry_text_nodes_admitted: usize,
193
194 pub script_lifecycle: Vec<ScriptLifecycleEntry>,
199
200 pub skipped_activities: SkippedActivities,
203
204 pub som_fail_log: Vec<SomFailEntry>,
207
208 pub instance_write_log: Vec<InstanceWriteEntry>,
210
211 pub presence_mutation_log: Vec<PresenceMutationEntry>,
216
217 pub form_dom_match_failures: usize,
220
221 pub form_dom_match_log: Vec<FormDomMatchEntry>,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct ScriptLifecycleEntry {
231 pub script_idx: usize,
233 pub node_id: usize,
235 pub node_name: String,
237 pub activity: String,
239 pub lang: &'static str,
241 pub outcome: &'static str,
243}
244
245#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
247pub struct SkippedActivities {
248 pub initialize: usize,
250 pub calculate: usize,
252 pub click: usize,
254 pub doc_ready: usize,
256 pub layout_ready: usize,
258 pub other: usize,
260}
261
262impl SkippedActivities {
263 fn bump(&mut self, activity: Option<&str>) {
264 match activity {
265 Some("initialize") => self.initialize += 1,
266 Some("calculate") => self.calculate += 1,
267 Some("click") => self.click += 1,
268 Some("docReady") => self.doc_ready += 1,
269 Some("layoutReady") => self.layout_ready += 1,
270 _ => self.other += 1,
271 }
272 }
273}
274
275#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct SomFailEntry {
278 pub path: String,
280 pub kind: String,
282 pub script_idx: usize,
284 pub activity: String,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
290pub struct InstanceWriteEntry {
291 pub script_idx: usize,
293 pub activity: String,
295 pub parent_node_id: usize,
297 pub parent_node_name: String,
299 pub prototype_node_name: String,
301 pub old_count: usize,
303 pub new_count: usize,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct PresenceMutationEntry {
310 pub script_idx: usize,
312 pub activity: String,
314 pub node_id: usize,
316 pub node_name: String,
318 pub old_presence: String,
320 pub new_presence: String,
322}
323
324#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct FormDomMatchEntry {
327 pub template_node_id: usize,
329 pub template_node_name: String,
331 pub reason: String,
333}
334
335impl Default for DynamicScriptOutcome {
336 fn default() -> Self {
337 Self {
338 changes: 0,
339 js_present: false,
340 js_skipped: 0,
341 other_skipped: 0,
342 formcalc_run: 0,
343 formcalc_errors: 0,
344 output_quality: OutputQuality::Exact,
345 js_executed: 0,
346 js_runtime_errors: 0,
347 js_timeouts: 0,
348 js_oom: 0,
349 js_host_calls: 0,
350 js_mutations: 0,
351 js_instance_writes: 0,
352 js_list_writes: 0,
353 js_binding_errors: 0,
354 js_resolve_failures: 0,
355 js_data_reads: 0,
356 js_unsupported_host_calls: 0,
357 js_probe_skips: 0,
358 variables_scripts_collected: 0,
359 variables_data_items_collected: 0,
360 script_objects_registered: 0,
361 script_objects_register_failed: 0,
362 script_objects_subform_scoped: 0,
363 som_lookups_total: 0,
364 som_lookup_successes: 0,
365 som_lookup_failures: 0,
366 som_lookup_ambiguous: 0,
367 som_subform_scripts_exposed: 0,
368 som_occur_path_refs: 0,
369 occur_lookups_total: 0,
370 occur_lookup_successes: 0,
371 occur_lookup_failures: 0,
372 occur_property_reads: 0,
373 occur_property_writes: 0,
374 occur_min_writes: 0,
375 occur_max_writes: 0,
376 occur_mutations_captured: 0,
377 occur_mutations_applied: 0,
378 occur_mutations_skipped: 0,
379 occur_application_ambiguous: 0,
380 occur_application_targets: 0,
381 presence_retry_enabled: false,
382 presence_retry_candidates: 0,
383 presence_retry_admitted: 0,
384 presence_retry_skipped: 0,
385 presence_retry_nodes_under_admitted: 0,
386 presence_retry_text_nodes_admitted: 0,
387 script_lifecycle: Vec::new(),
388 skipped_activities: SkippedActivities::default(),
389 som_fail_log: Vec::new(),
390 instance_write_log: Vec::new(),
391 presence_mutation_log: Vec::new(),
392 form_dom_match_failures: 0,
393 form_dom_match_log: Vec::new(),
394 }
395 }
396}
397
398struct FormSnapshot {
414 field_values: Vec<(usize, String)>,
415 presences: Vec<(usize, Presence)>,
416 children: Vec<Vec<FormNodeId>>,
419 node_count: usize,
423 populated_count: usize,
424}
425
426fn snapshot_form(form: &FormTree) -> FormSnapshot {
427 let mut field_values = Vec::new();
428 let mut presences = Vec::new();
429 let mut children = Vec::with_capacity(form.nodes.len());
430 let mut populated_count = 0usize;
431 for (idx, node) in form.nodes.iter().enumerate() {
432 if let FormNodeType::Field { value } = &node.node_type {
433 field_values.push((idx, value.clone()));
434 if !value.trim().is_empty() {
435 populated_count += 1;
436 }
437 }
438 presences.push((idx, form.metadata[idx].presence));
439 children.push(node.children.clone());
440 }
441 FormSnapshot {
442 field_values,
443 presences,
444 children,
445 node_count: form.nodes.len(),
446 populated_count,
447 }
448}
449
450fn restore_snapshot(form: &mut FormTree, snapshot: &FormSnapshot) {
451 for (idx, value) in &snapshot.field_values {
452 if let FormNodeType::Field { value: fv } = &mut form.nodes[*idx].node_type {
453 *fv = value.clone();
454 }
455 }
456 for (idx, presence) in &snapshot.presences {
457 form.metadata[*idx].presence = *presence;
458 }
459 if form.nodes.len() > snapshot.node_count {
465 form.nodes.truncate(snapshot.node_count);
466 form.metadata.truncate(snapshot.node_count);
467 }
468 for (idx, saved_children) in snapshot.children.iter().enumerate() {
469 if let Some(node) = form.nodes.get_mut(idx) {
470 node.children = saved_children.clone();
471 }
472 }
473}
474
475fn should_rollback(
476 form: &FormTree,
477 snapshot: &FormSnapshot,
478 errors: usize,
479 successes: usize,
480) -> bool {
481 if errors > 0 && errors > successes {
482 return true;
483 }
484 if snapshot.populated_count >= 2 {
485 let mut now_empty = 0usize;
486 for (idx, old_value) in &snapshot.field_values {
487 if old_value.trim().is_empty() {
488 continue;
489 }
490 if let FormNodeType::Field { value } = &form.nodes[*idx].node_type {
491 if value.trim().is_empty() {
492 now_empty += 1;
493 }
494 }
495 }
496 if now_empty * 2 > snapshot.populated_count {
497 return true;
498 }
499 }
500 false
501}
502#[doc(hidden)]
525pub fn apply_dynamic_scripts(
526 form: &mut FormTree,
527 root_id: FormNodeId,
528) -> Result<DynamicScriptOutcome> {
529 apply_dynamic_scripts_with_mode(form, root_id, JsExecutionMode::default())
530}
531
532#[doc(hidden)]
540pub fn apply_dynamic_scripts_with_mode(
541 form: &mut FormTree,
542 root_id: FormNodeId,
543 mode: JsExecutionMode,
544) -> Result<DynamicScriptOutcome> {
545 #[cfg(feature = "xfa-js-sandboxed")]
551 {
552 if mode == JsExecutionMode::SandboxedRuntime {
553 match crate::js_runtime::QuickJsRuntime::new() {
554 Ok(mut rt) => {
555 return apply_dynamic_scripts_with_runtime(form, root_id, mode, &mut rt);
556 }
557 Err(_e) => {
558 }
562 }
563 }
564 }
565 apply_dynamic_scripts_with_runtime(form, root_id, mode, &mut NullRuntime::new())
566}
567
568fn occur_apply_enabled() -> bool {
573 std::env::var("XFA_OCCUR_APPLY").ok().as_deref() == Some("1")
574}
575
576fn presence_retry_enabled() -> bool {
580 std::env::var("XFA_PRESENCE_RETRY").ok().as_deref() == Some("1")
581}
582
583pub(crate) fn runtime_diag_enabled() -> bool {
587 std::env::var("XFA_RUNTIME_DIAG").ok().as_deref() == Some("1")
588}
589
590std::thread_local! {
595 static PRESENCE_MUT_LOG: std::cell::RefCell<Option<Vec<PresenceMutationEntry>>> =
596 const { std::cell::RefCell::new(None) };
597}
598
599fn push_presence_mutation(
602 node_id: usize,
603 node_name: &str,
604 old_presence: &'static str,
605 new_presence: &'static str,
606) {
607 PRESENCE_MUT_LOG.with(|cell| {
608 if let Some(ref mut log) = *cell.borrow_mut() {
609 if log.len() < 200 {
610 log.push(PresenceMutationEntry {
611 script_idx: 0,
614 activity: String::new(),
615 node_id,
616 node_name: node_name.to_string(),
617 old_presence: old_presence.to_string(),
618 new_presence: new_presence.to_string(),
619 });
620 }
621 }
622 });
623}
624
625pub fn apply_dynamic_scripts_with_runtime(
637 form: &mut FormTree,
638 root_id: FormNodeId,
639 mode: JsExecutionMode,
640 runtime: &mut dyn XfaJsRuntime,
641) -> Result<DynamicScriptOutcome> {
642 let parents = build_parent_map(form, root_id);
643 let all_scripts: Vec<(FormNodeId, Vec<EventScript>)> = form
644 .nodes
645 .iter()
646 .enumerate()
647 .filter_map(|(idx, _)| {
648 let node_id = FormNodeId(idx);
649 let scripts = form.meta(node_id).event_scripts.clone();
650 (!scripts.is_empty()).then_some((node_id, scripts))
651 })
652 .collect();
653
654 let has_unsupported_script = all_scripts.iter().any(|(_, node_scripts)| {
655 node_scripts
656 .iter()
657 .any(|script| script.language != ScriptLanguage::FormCalc)
658 });
659
660 if mode == JsExecutionMode::Strict && has_unsupported_script {
661 return Err(javascript_policy::reject_execution(
662 JavaScriptEntryPoint::XfaEventHook,
663 ));
664 }
665
666 let mut js_skipped = 0usize;
667 let mut other_skipped = 0usize;
668 let mut sandbox_metadata = RuntimeMetadata::default();
669 let mut scripts = Vec::new();
670 let sandbox_active = mode == JsExecutionMode::SandboxedRuntime;
671 let snapshot = snapshot_form(form);
672
673 let presave_gate = sandbox_active && presave_during_flatten_enabled();
680
681 let trace_enabled = crate::flatten_trace::enabled();
683 let diag_enabled = runtime_diag_enabled();
684
685 let mut script_lifecycle: Vec<ScriptLifecycleEntry> = Vec::new();
687 let mut skipped_activities = SkippedActivities::default();
689 let mut dispatch_script_idx: usize = 0;
692
693 if sandbox_active {
694 let _ = runtime.init();
697 let _ = runtime.reset_for_new_document();
698 let _ = runtime.set_form_handle(form as *mut FormTree, root_id);
699 runtime.set_presave_gate(presave_gate);
700 }
701
702 for (node_id, node_scripts) in all_scripts {
703 let node = form.get(node_id);
704 let node_name = if trace_enabled || diag_enabled {
705 node.name.clone()
706 } else {
707 String::new()
708 };
709 let mut formcalc_scripts = Vec::new();
710 for script in node_scripts {
711 match script.language {
712 ScriptLanguage::FormCalc => formcalc_scripts.push(script),
713 ScriptLanguage::JavaScript => {
714 let activity_str = script.activity.as_deref().unwrap_or("");
715 if sandbox_active
716 && activity_allowed_for_sandbox_with_gate(
717 script.activity.as_deref(),
718 presave_gate,
719 )
720 {
721 let this_idx = dispatch_script_idx;
722 dispatch_script_idx += 1;
723 let _ = runtime.reset_per_script(node_id, script.activity.as_deref());
724 let outcome_str = match runtime
725 .execute_script(script.activity.as_deref(), &script.script)
726 {
727 Ok(_) => {
728 "executed"
730 }
731 Err(SandboxError::Timeout) => {
732 js_skipped += 1;
733 "timeout"
734 }
735 Err(SandboxError::OutOfMemory) => {
736 js_skipped += 1;
737 "error"
738 }
739 Err(e) => {
740 log::debug!(
746 "sandbox script error on activity={:?}: {}",
747 script.activity.as_deref(),
748 e
749 );
750 if std::env::var("XFA_JS_DEBUG").ok().as_deref() == Some("1") {
751 eprintln!(
752 "XFA_JS_DEBUG sandbox script error on activity={:?}: {}",
753 script.activity.as_deref(),
754 e
755 );
756 }
757 js_skipped += 1;
758 "error"
759 }
760 };
761 if trace_enabled && script_lifecycle.len() < 500 {
763 script_lifecycle.push(ScriptLifecycleEntry {
764 script_idx: this_idx,
765 node_id: node_id.0,
766 node_name: node_name.clone(),
767 activity: activity_str.to_string(),
768 lang: "javascript",
769 outcome: outcome_str,
770 });
771 }
772 } else {
773 js_skipped += 1;
774 if trace_enabled && script_lifecycle.len() < 500 {
776 let skip_reason = if sandbox_active {
777 "skipped_activity"
778 } else {
779 "skipped_mode"
780 };
781 script_lifecycle.push(ScriptLifecycleEntry {
782 script_idx: dispatch_script_idx,
783 node_id: node_id.0,
784 node_name: node_name.clone(),
785 activity: activity_str.to_string(),
786 lang: "javascript",
787 outcome: skip_reason,
788 });
789 }
790 if trace_enabled {
792 skipped_activities.bump(script.activity.as_deref());
793 }
794 dispatch_script_idx += 1;
795 }
796 }
797 ScriptLanguage::Other => {
798 other_skipped += 1;
799 if trace_enabled && script_lifecycle.len() < 500 {
801 script_lifecycle.push(ScriptLifecycleEntry {
802 script_idx: dispatch_script_idx,
803 node_id: node_id.0,
804 node_name: node_name.clone(),
805 activity: script.activity.as_deref().unwrap_or("").to_string(),
806 lang: "other",
807 outcome: "skipped_mode",
808 });
809 }
810 dispatch_script_idx += 1;
811 }
812 }
813 }
814 if !formcalc_scripts.is_empty() {
815 scripts.push((node_id, formcalc_scripts));
816 }
817 }
818
819 let mut captured_occur: Vec<(usize, String, i64)> = Vec::new();
820 let mut sandbox_diag = crate::js_runtime::RuntimeDiagLogs::default();
822 if sandbox_active {
823 let _ = runtime.set_form_handle(std::ptr::null_mut(), root_id);
824 sandbox_metadata = runtime.take_metadata();
825 captured_occur = runtime.take_occur_mutations();
826 if diag_enabled {
827 sandbox_diag = runtime.take_diag_logs();
828 }
829 }
830
831 let mut stats = ScriptStats::default();
832
833 if diag_enabled {
835 PRESENCE_MUT_LOG.with(|cell| {
836 *cell.borrow_mut() = Some(Vec::new());
837 });
838 }
839
840 let mut changes = sandbox_metadata
841 .mutations
842 .saturating_add(sandbox_metadata.instance_writes)
843 + run_script_phase(
844 form,
845 root_id,
846 &parents,
847 &scripts,
848 ScriptPhase::Initialize,
849 1,
850 &mut stats,
851 )?
852 + run_script_phase(
853 form,
854 root_id,
855 &parents,
856 &scripts,
857 ScriptPhase::Calculate,
858 MAX_SCRIPT_PASSES,
859 &mut stats,
860 )?;
861
862 let presence_mutation_log = if diag_enabled {
864 PRESENCE_MUT_LOG.with(|cell| cell.borrow_mut().take().unwrap_or_default())
865 } else {
866 Vec::new()
867 };
868
869 let sandbox_rollback_errors = sandbox_metadata
870 .runtime_errors
871 .saturating_add(sandbox_metadata.timeouts)
872 .saturating_add(sandbox_metadata.oom)
873 .saturating_add(sandbox_metadata.binding_errors);
874 let rollback_errors = stats.errors.saturating_add(sandbox_rollback_errors);
875 let rollback_successes = stats.successes.saturating_add(sandbox_metadata.executed);
876
877 let rolled_back = should_rollback(form, &snapshot, rollback_errors, rollback_successes);
878 if rolled_back {
879 restore_snapshot(form, &snapshot);
880 changes = 0;
881 }
882
883 let mut occur_applied_targets: std::collections::HashSet<usize> =
891 std::collections::HashSet::new();
892 if sandbox_active && !rolled_back && occur_apply_enabled() && !captured_occur.is_empty() {
893 for (idx, prop, value) in &captured_occur {
894 if prop != "min" {
895 sandbox_metadata.occur_mutations_skipped =
897 sandbox_metadata.occur_mutations_skipped.saturating_add(1);
898 continue;
899 }
900 if *value < 0 || *idx >= form.nodes.len() {
901 sandbox_metadata.occur_mutations_skipped =
902 sandbox_metadata.occur_mutations_skipped.saturating_add(1);
903 continue;
904 }
905 let nid = FormNodeId(*idx);
906 let is_repeatable = matches!(
907 form.get(nid).node_type,
908 FormNodeType::Subform | FormNodeType::Area | FormNodeType::ExclGroup
909 );
910 if !is_repeatable {
911 sandbox_metadata.occur_application_ambiguous = sandbox_metadata
913 .occur_application_ambiguous
914 .saturating_add(1);
915 sandbox_metadata.occur_mutations_skipped =
916 sandbox_metadata.occur_mutations_skipped.saturating_add(1);
917 continue;
918 }
919 let v = *value as u32;
920 let node = form.get_mut(nid);
921 node.occur.min = v;
922 if node.occur.initial < v {
923 node.occur.initial = v;
924 }
925 if let Some(m) = node.occur.max {
926 if m < v {
927 node.occur.max = Some(v);
928 }
929 }
930 sandbox_metadata.occur_mutations_applied =
931 sandbox_metadata.occur_mutations_applied.saturating_add(1);
932 occur_applied_targets.insert(*idx);
933 }
934 sandbox_metadata.occur_application_targets = sandbox_metadata
935 .occur_application_targets
936 .saturating_add(occur_applied_targets.len());
937 } else if !captured_occur.is_empty() {
938 sandbox_metadata.occur_mutations_skipped = sandbox_metadata
941 .occur_mutations_skipped
942 .saturating_add(captured_occur.len());
943 }
944
945 let mut pr_candidates = 0usize;
952 let mut pr_admitted = 0usize;
953 let mut pr_skipped = 0usize;
954 let mut pr_nodes_under_admitted = 0usize;
955 let mut pr_text_nodes_admitted = 0usize;
956 let presence_retry_active = sandbox_active
957 && !rolled_back
958 && occur_apply_enabled()
959 && presence_retry_enabled()
960 && !occur_applied_targets.is_empty();
961 if presence_retry_active {
962 let mut visited: std::collections::HashSet<usize> = std::collections::HashSet::new();
963 let mut stack: Vec<FormNodeId> = occur_applied_targets
964 .iter()
965 .map(|&i| FormNodeId(i))
966 .collect();
967 let mut admitted_ids: std::collections::HashSet<usize> = std::collections::HashSet::new();
968 while let Some(nid) = stack.pop() {
969 if nid.0 >= form.nodes.len() || !visited.insert(nid.0) {
970 continue;
971 }
972 let presence = form.meta(nid).presence;
973 if presence == Presence::Hidden || presence == Presence::Invisible {
974 pr_candidates += 1;
975 form.meta_mut(nid).presence = Presence::Visible;
978 pr_admitted += 1;
979 admitted_ids.insert(nid.0);
980 } else if presence == Presence::Inactive {
981 pr_candidates += 1;
982 pr_skipped += 1;
983 }
984 for &c in &form.get(nid).children {
985 stack.push(c);
986 }
987 }
988 for &aid in &admitted_ids {
990 let mut s = vec![FormNodeId(aid)];
991 let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
992 while let Some(n) = s.pop() {
993 if n.0 >= form.nodes.len() || !seen.insert(n.0) {
994 continue;
995 }
996 pr_nodes_under_admitted += 1;
997 if let FormNodeType::Draw(_) | FormNodeType::Field { .. } = form.get(n).node_type {
998 pr_text_nodes_admitted += 1;
999 }
1000 for &c in &form.get(n).children {
1001 s.push(c);
1002 }
1003 }
1004 }
1005 }
1006
1007 let js_seen_count = js_skipped + sandbox_metadata.executed;
1008 let js_present = js_seen_count > 0;
1009 let output_quality = if sandbox_active && js_present && sandbox_metadata.is_clean() {
1010 OutputQuality::Sandboxed
1011 } else if (sandbox_active && js_present) || js_skipped > 0 || other_skipped > 0 {
1012 OutputQuality::BestEffort
1013 } else {
1014 OutputQuality::Exact
1015 };
1016
1017 Ok(DynamicScriptOutcome {
1018 changes,
1019 js_present,
1020 js_skipped,
1021 other_skipped,
1022 formcalc_run: stats.formcalc_run,
1023 formcalc_errors: stats.formcalc_errors,
1024 output_quality,
1025 js_executed: sandbox_metadata.executed,
1026 js_runtime_errors: sandbox_metadata.runtime_errors,
1027 js_timeouts: sandbox_metadata.timeouts,
1028 js_oom: sandbox_metadata.oom,
1029 js_host_calls: sandbox_metadata.host_calls,
1030 js_mutations: sandbox_metadata.mutations,
1031 js_instance_writes: sandbox_metadata.instance_writes,
1032 js_list_writes: sandbox_metadata.list_writes,
1033 js_binding_errors: sandbox_metadata.binding_errors,
1034 js_resolve_failures: sandbox_metadata.resolve_failures,
1035 js_data_reads: sandbox_metadata.data_reads,
1036 js_unsupported_host_calls: sandbox_metadata.unsupported_host_calls,
1037 js_probe_skips: sandbox_metadata.probe_skips,
1038 variables_scripts_collected: sandbox_metadata.variables_scripts_collected,
1039 variables_data_items_collected: sandbox_metadata.variables_data_items_collected,
1040 script_objects_registered: sandbox_metadata.script_objects_registered,
1041 script_objects_register_failed: sandbox_metadata.script_objects_register_failed,
1042 script_objects_subform_scoped: sandbox_metadata.script_objects_subform_scoped,
1043 som_lookups_total: sandbox_metadata.som_lookups_total,
1044 som_lookup_successes: sandbox_metadata.som_lookup_successes,
1045 som_lookup_failures: sandbox_metadata.som_lookup_failures,
1046 som_lookup_ambiguous: sandbox_metadata.som_lookup_ambiguous,
1047 som_subform_scripts_exposed: sandbox_metadata.som_subform_scripts_exposed,
1048 som_occur_path_refs: sandbox_metadata.som_occur_path_refs,
1049 occur_lookups_total: sandbox_metadata.occur_lookups_total,
1050 occur_lookup_successes: sandbox_metadata.occur_lookup_successes,
1051 occur_lookup_failures: sandbox_metadata.occur_lookup_failures,
1052 occur_property_reads: sandbox_metadata.occur_property_reads,
1053 occur_property_writes: sandbox_metadata.occur_property_writes,
1054 occur_min_writes: sandbox_metadata.occur_min_writes,
1055 occur_max_writes: sandbox_metadata.occur_max_writes,
1056 occur_mutations_captured: sandbox_metadata.occur_mutations_captured,
1057 occur_mutations_applied: sandbox_metadata.occur_mutations_applied,
1058 occur_mutations_skipped: sandbox_metadata.occur_mutations_skipped,
1059 occur_application_ambiguous: sandbox_metadata.occur_application_ambiguous,
1060 occur_application_targets: sandbox_metadata.occur_application_targets,
1061 presence_retry_enabled: presence_retry_active,
1062 presence_retry_candidates: pr_candidates,
1063 presence_retry_admitted: pr_admitted,
1064 presence_retry_skipped: pr_skipped,
1065 presence_retry_nodes_under_admitted: pr_nodes_under_admitted,
1066 presence_retry_text_nodes_admitted: pr_text_nodes_admitted,
1067 script_lifecycle,
1069 skipped_activities,
1070 som_fail_log: sandbox_diag.som_fail_log,
1071 instance_write_log: sandbox_diag.instance_write_log,
1072 presence_mutation_log,
1073 form_dom_match_failures: 0,
1074 form_dom_match_log: Vec::new(),
1075 })
1076}
1077
1078fn has_hidden_ancestor(
1079 form: &FormTree,
1080 parents: &HashMap<FormNodeId, FormNodeId>,
1081 node_id: FormNodeId,
1082) -> bool {
1083 let mut cursor = parents.get(&node_id).copied();
1084 while let Some(ancestor) = cursor {
1085 if form.meta(ancestor).presence.is_not_visible() {
1086 return true;
1087 }
1088 cursor = parents.get(&ancestor).copied();
1089 }
1090 false
1091}
1092
1093#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1094enum ScriptPhase {
1095 Initialize,
1096 Calculate,
1097}
1098
1099#[derive(Default)]
1100struct ScriptStats {
1101 errors: usize,
1102 successes: usize,
1103 formcalc_run: usize,
1104 formcalc_errors: usize,
1105}
1106
1107#[derive(Debug)]
1108struct ScriptResult {
1109 changes: usize,
1110 error: bool,
1111}
1112
1113fn run_script_phase(
1114 form: &mut FormTree,
1115 root_id: FormNodeId,
1116 parents: &HashMap<FormNodeId, FormNodeId>,
1117 scripts: &[(FormNodeId, Vec<EventScript>)],
1118 phase: ScriptPhase,
1119 max_passes: usize,
1120 stats: &mut ScriptStats,
1121) -> Result<usize> {
1122 let mut total_changes = 0;
1123
1124 for _ in 0..max_passes {
1125 let mut pass_changes = 0;
1126
1127 for (node_id, node_scripts) in scripts {
1128 if has_hidden_ancestor(form, parents, *node_id) {
1129 continue;
1130 }
1131
1132 for script in node_scripts
1133 .iter()
1134 .filter(|script| should_run_script(script, phase))
1135 {
1136 let result = execute_event_script(form, root_id, parents, *node_id, script, phase)?;
1137 stats.formcalc_run += 1;
1138 if result.error {
1139 stats.errors += 1;
1140 stats.formcalc_errors += 1;
1141 } else {
1142 stats.successes += 1;
1143 }
1144 pass_changes += result.changes;
1145 }
1146 }
1147
1148 total_changes += pass_changes;
1149 if pass_changes == 0 {
1150 break;
1151 }
1152 }
1153
1154 Ok(total_changes)
1155}
1156
1157fn should_run_script(script: &EventScript, phase: ScriptPhase) -> bool {
1158 match phase {
1159 ScriptPhase::Initialize => script.activity.as_deref() == Some("initialize"),
1160 ScriptPhase::Calculate => script.activity.as_deref() == Some("calculate"),
1161 }
1162}
1163
1164fn execute_event_script(
1165 form: &mut FormTree,
1166 root_id: FormNodeId,
1167 parents: &HashMap<FormNodeId, FormNodeId>,
1168 current_id: FormNodeId,
1169 script: &EventScript,
1170 phase: ScriptPhase,
1171) -> Result<ScriptResult> {
1172 match script.language {
1173 ScriptLanguage::FormCalc => Ok(execute_formcalc_script(
1174 form, root_id, parents, current_id, script, phase,
1175 )),
1176 ScriptLanguage::JavaScript => Err(javascript_policy::reject_execution(
1177 JavaScriptEntryPoint::XfaEventHook,
1178 )),
1179 ScriptLanguage::Other => Err(XfaError::UnsupportedFeature("script language".to_string())),
1180 }
1181}
1182
1183fn formcalc_debug_emit(stage: &str, message: &str, script: &EventScript) {
1189 if std::env::var("XFA_FORMCALC_DEBUG").ok().as_deref() != Some("1") {
1190 return;
1191 }
1192 let activity = script.activity.as_deref().unwrap_or("?");
1193 let one_line = message.replace(['\n', '\r'], " ");
1196 eprintln!("XFA_FORMCALC_DEBUG stage={stage} activity=\"{activity}\" message=\"{one_line}\"");
1197}
1198
1199fn execute_formcalc_script(
1200 form: &mut FormTree,
1201 root_id: FormNodeId,
1202 parents: &HashMap<FormNodeId, FormNodeId>,
1203 current_id: FormNodeId,
1204 script: &EventScript,
1205 phase: ScriptPhase,
1206) -> ScriptResult {
1207 let tokens = match tokenize(&script.script) {
1208 Ok(t) => t,
1209 Err(err) => {
1210 formcalc_debug_emit("lexer", &format!("{err}"), script);
1211 return ScriptResult {
1212 changes: 0,
1213 error: true,
1214 };
1215 }
1216 };
1217 let ast = match parser::parse(tokens) {
1218 Ok(a) => a,
1219 Err(err) => {
1220 formcalc_debug_emit("parser", &format!("{err}"), script);
1221 return ScriptResult {
1222 changes: 0,
1223 error: true,
1224 };
1225 }
1226 };
1227
1228 let mut interpreter = Interpreter::new();
1229 let mut resolver = FormTreeSomResolver::new(form, root_id, parents, current_id);
1230 let result = match interpreter.exec_with_resolver(&ast, &mut resolver) {
1231 Ok(r) => r,
1232 Err(err) => {
1233 formcalc_debug_emit("interpreter", &format!("{err}"), script);
1234 return ScriptResult {
1235 changes: resolver.changes,
1236 error: true,
1237 };
1238 }
1239 };
1240
1241 if matches!(phase, ScriptPhase::Calculate) {
1242 resolver.changes += write_formcalc_value(
1243 resolver.form,
1244 current_id,
1245 ResolvedProperty::RawValue,
1246 result,
1247 );
1248 }
1249
1250 ScriptResult {
1251 changes: resolver.changes,
1252 error: false,
1253 }
1254}
1255
1256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1257enum ResolvedProperty {
1258 RawValue,
1259 Presence,
1260 SomExpression,
1261}
1262
1263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1264struct ResolvedTarget {
1265 node_id: FormNodeId,
1266 property: ResolvedProperty,
1267}
1268
1269struct FormTreeSomResolver<'a> {
1270 form: &'a mut FormTree,
1271 root_id: FormNodeId,
1272 parents: &'a HashMap<FormNodeId, FormNodeId>,
1273 current_id: FormNodeId,
1274 changes: usize,
1275}
1276
1277impl<'a> FormTreeSomResolver<'a> {
1278 fn new(
1279 form: &'a mut FormTree,
1280 root_id: FormNodeId,
1281 parents: &'a HashMap<FormNodeId, FormNodeId>,
1282 current_id: FormNodeId,
1283 ) -> Self {
1284 Self {
1285 form,
1286 root_id,
1287 parents,
1288 current_id,
1289 changes: 0,
1290 }
1291 }
1292
1293 fn resolve_target(&self, path: &str) -> Option<ResolvedTarget> {
1294 let trimmed = path.trim();
1295 if trimmed.is_empty() {
1296 return None;
1297 }
1298
1299 if matches!(trimmed, "rawValue" | "presence" | "somExpression") {
1300 return Some(ResolvedTarget {
1301 node_id: self.current_id,
1302 property: parse_property_name(trimmed)?,
1303 });
1304 }
1305
1306 let (expr, property) = split_property_path(trimmed)?;
1307 let node_id = self.resolve_expression(&expr)?.into_iter().next()?;
1308 Some(ResolvedTarget { node_id, property })
1309 }
1310
1311 fn count_targets(&self, path: &str) -> usize {
1312 let trimmed = path.trim();
1313 if trimmed.is_empty() {
1314 return 0;
1315 }
1316 if matches!(trimmed, "rawValue" | "presence" | "somExpression") {
1317 return 1;
1318 }
1319 let Some((expr, _property)) = split_property_path(trimmed) else {
1320 return 0;
1321 };
1322 self.resolve_expression(&expr)
1323 .map_or(0, |nodes| nodes.len())
1324 }
1325
1326 fn resolve_expression(&self, expr: &SomExpression) -> Option<Vec<FormNodeId>> {
1327 match expr.root {
1328 SomRoot::Data | SomRoot::Record | SomRoot::Template => None,
1329 SomRoot::CurrentContainer => {
1330 if expr.segments.is_empty() {
1331 Some(vec![self.current_id])
1332 } else {
1333 Some(self.follow_absolute(vec![self.current_id], &expr.segments))
1334 }
1335 }
1336 SomRoot::Form => {
1337 if expr.segments.is_empty() {
1338 Some(vec![self.root_id])
1339 } else {
1340 Some(self.follow_absolute(vec![self.root_id], &expr.segments))
1341 }
1342 }
1343 SomRoot::Xfa => {
1344 let segments = strip_xfa_form_prefix(&expr.segments);
1345 if segments.is_empty() {
1346 Some(vec![self.root_id])
1347 } else {
1348 Some(self.follow_absolute(vec![self.root_id], segments))
1349 }
1350 }
1351 SomRoot::Unqualified => {
1352 if expr.segments.is_empty() {
1353 Some(vec![self.current_id])
1354 } else {
1355 Some(self.follow_unqualified(&expr.segments))
1356 }
1357 }
1358 }
1359 }
1360
1361 fn follow_absolute(
1362 &self,
1363 mut current: Vec<FormNodeId>,
1364 segments: &[xfa_dom_resolver::som::SomSegment],
1365 ) -> Vec<FormNodeId> {
1366 for (idx, segment) in segments.iter().enumerate() {
1367 let allow_self = idx == 0;
1368 current = current
1369 .into_iter()
1370 .flat_map(|node_id| self.step_from_node(node_id, segment, allow_self))
1371 .collect();
1372 if current.is_empty() {
1373 break;
1374 }
1375 }
1376 current
1377 }
1378
1379 fn follow_unqualified(
1380 &self,
1381 segments: &[xfa_dom_resolver::som::SomSegment],
1382 ) -> Vec<FormNodeId> {
1383 let Some((first, rest)) = segments.split_first() else {
1384 return vec![self.current_id];
1385 };
1386
1387 let mut scope = Some(self.current_id);
1388 while let Some(scope_id) = scope {
1389 let anchors: Vec<_> = descendants_inclusive(self.form, scope_id)
1390 .into_iter()
1391 .filter(|node_id| self.node_matches_segment(*node_id, first))
1392 .collect();
1393 let matched = self.follow_remaining(anchors, rest);
1394 if !matched.is_empty() {
1395 return matched;
1396 }
1397 scope = self.parents.get(&scope_id).copied();
1398 }
1399
1400 let anchors: Vec<_> = descendants_inclusive(self.form, self.root_id)
1401 .into_iter()
1402 .filter(|node_id| self.node_matches_segment(*node_id, first))
1403 .collect();
1404 self.follow_remaining(anchors, rest)
1405 }
1406
1407 fn follow_remaining(
1408 &self,
1409 mut current: Vec<FormNodeId>,
1410 segments: &[xfa_dom_resolver::som::SomSegment],
1411 ) -> Vec<FormNodeId> {
1412 for segment in segments {
1413 current = current
1414 .into_iter()
1415 .flat_map(|node_id| self.step_from_node(node_id, segment, false))
1416 .collect();
1417 if current.is_empty() {
1418 break;
1419 }
1420 }
1421 current
1422 }
1423
1424 fn step_from_node(
1425 &self,
1426 node_id: FormNodeId,
1427 segment: &xfa_dom_resolver::som::SomSegment,
1428 allow_self: bool,
1429 ) -> Vec<FormNodeId> {
1430 if let SomSelector::Name(name) = &segment.selector {
1434 if name == ".." {
1435 if let Some(&parent_id) = self.parents.get(&node_id) {
1437 return apply_index_to_single(parent_id, segment.index);
1438 }
1439 return Vec::new();
1440 }
1441 }
1442
1443 if allow_self && self.node_matches_selector(node_id, &segment.selector) {
1444 return apply_index_to_single(node_id, segment.index);
1445 }
1446
1447 let matches: Vec<_> = self
1448 .form
1449 .get(node_id)
1450 .children
1451 .iter()
1452 .copied()
1453 .filter(|child_id| self.node_matches_selector(*child_id, &segment.selector))
1454 .collect();
1455
1456 apply_index(matches, segment.index)
1457 }
1458
1459 fn node_matches_segment(
1460 &self,
1461 node_id: FormNodeId,
1462 segment: &xfa_dom_resolver::som::SomSegment,
1463 ) -> bool {
1464 if !self.node_matches_selector(node_id, &segment.selector) {
1465 return false;
1466 }
1467
1468 match segment.index {
1469 SomIndex::All => true,
1470 SomIndex::None => self.sibling_position(node_id, &segment.selector) == Some(0),
1471 SomIndex::Specific(idx) => {
1472 self.sibling_position(node_id, &segment.selector) == Some(idx)
1473 }
1474 }
1475 }
1476
1477 fn sibling_position(&self, node_id: FormNodeId, selector: &SomSelector) -> Option<usize> {
1478 let Some(parent_id) = self.parents.get(&node_id).copied() else {
1479 return self.node_matches_selector(node_id, selector).then_some(0);
1480 };
1481
1482 self.form
1483 .get(parent_id)
1484 .children
1485 .iter()
1486 .copied()
1487 .filter(|candidate| self.node_matches_selector(*candidate, selector))
1488 .position(|candidate| candidate == node_id)
1489 }
1490
1491 fn node_matches_selector(&self, node_id: FormNodeId, selector: &SomSelector) -> bool {
1492 match selector {
1493 SomSelector::Name(name) => self.form.get(node_id).name == *name,
1494 SomSelector::Class(class_name) => self.node_matches_class(node_id, class_name),
1495 SomSelector::AllChildren => true,
1496 }
1497 }
1498
1499 fn node_matches_class(&self, node_id: FormNodeId, class_name: &str) -> bool {
1500 let class_name = class_name.to_ascii_lowercase();
1501 match class_name.as_str() {
1502 "subform" => matches!(
1503 self.form.get(node_id).node_type,
1504 FormNodeType::Root | FormNodeType::Subform
1505 ),
1506 "pageset" => {
1507 matches!(self.form.get(node_id).node_type, FormNodeType::PageSet)
1508 }
1509 "pagearea" => matches!(
1510 self.form.get(node_id).node_type,
1511 FormNodeType::PageArea { .. }
1512 ),
1513 "field" => matches!(self.form.get(node_id).node_type, FormNodeType::Field { .. }),
1514 "draw" => matches!(
1515 self.form.get(node_id).node_type,
1516 FormNodeType::Draw(_) | FormNodeType::Image { .. }
1517 ),
1518 "exclgroup" => self.form.meta(node_id).group_kind == GroupKind::ExclusiveChoice,
1519 _ => false,
1520 }
1521 }
1522}
1523
1524impl SomResolver for FormTreeSomResolver<'_> {
1525 fn resolve_path(
1526 &mut self,
1527 path: &str,
1528 ) -> formcalc_interpreter::error::Result<Option<FormCalcValue>> {
1529 let Some(target) = self.resolve_target(path) else {
1530 if !path.trim().is_empty() {
1533 log::warn!("SOM bridge: path not resolved: {:?}", path.trim());
1534 }
1535 return Ok(None);
1536 };
1537 Ok(Some(read_formcalc_value(
1538 self.form,
1539 self.root_id,
1540 self.parents,
1541 target,
1542 )))
1543 }
1544
1545 fn assign_path(
1546 &mut self,
1547 path: &str,
1548 value: FormCalcValue,
1549 ) -> formcalc_interpreter::error::Result<bool> {
1550 let Some(target) = self.resolve_target(path) else {
1551 if !path.trim().is_empty() {
1553 log::warn!("SOM bridge: assignment target not found: {:?}", path.trim());
1554 }
1555 return Ok(false);
1556 };
1557 self.changes += write_formcalc_value(self.form, target.node_id, target.property, value);
1558 Ok(true)
1559 }
1560
1561 fn count_path_matches(&mut self, path: &str) -> formcalc_interpreter::error::Result<usize> {
1562 Ok(self.count_targets(path))
1563 }
1564}
1565
1566fn split_property_path(path: &str) -> Option<(SomExpression, ResolvedProperty)> {
1567 let normalized = if let Some(rest) = path.strip_prefix("this.") {
1568 format!("$.{rest}")
1569 } else if path == "this" {
1570 "$".to_string()
1571 } else {
1572 path.to_string()
1573 };
1574
1575 let mut expr = parse_som(&normalized).ok()?;
1576 let property = if let Some(last) = expr.segments.last() {
1577 match &last.selector {
1578 SomSelector::Name(name) => {
1579 parse_property_name(name).unwrap_or(ResolvedProperty::RawValue)
1580 }
1581 _ => ResolvedProperty::RawValue,
1582 }
1583 } else {
1584 ResolvedProperty::RawValue
1585 };
1586
1587 if matches!(
1588 expr.segments.last().map(|segment| &segment.selector),
1589 Some(SomSelector::Name(name)) if parse_property_name(name).is_some()
1590 ) {
1591 expr.segments.pop();
1592 }
1593
1594 Some((expr, property))
1595}
1596
1597fn parse_property_name(name: &str) -> Option<ResolvedProperty> {
1598 match name {
1599 "rawValue" => Some(ResolvedProperty::RawValue),
1600 "presence" => Some(ResolvedProperty::Presence),
1601 "somExpression" => Some(ResolvedProperty::SomExpression),
1602 _ => None,
1603 }
1604}
1605
1606fn strip_xfa_form_prefix(
1607 segments: &[xfa_dom_resolver::som::SomSegment],
1608) -> &[xfa_dom_resolver::som::SomSegment] {
1609 match segments.first() {
1610 Some(segment)
1611 if matches!(&segment.selector, SomSelector::Name(name) if name == "form")
1612 && matches!(segment.index, SomIndex::None) =>
1613 {
1614 &segments[1..]
1615 }
1616 _ => segments,
1617 }
1618}
1619
1620fn apply_index(matches: Vec<FormNodeId>, index: SomIndex) -> Vec<FormNodeId> {
1621 match index {
1622 SomIndex::None => matches.into_iter().take(1).collect(),
1623 SomIndex::Specific(idx) => matches.get(idx).copied().into_iter().collect(),
1624 SomIndex::All => matches,
1625 }
1626}
1627
1628fn apply_index_to_single(node_id: FormNodeId, index: SomIndex) -> Vec<FormNodeId> {
1629 match index {
1630 SomIndex::None | SomIndex::Specific(0) | SomIndex::All => vec![node_id],
1631 SomIndex::Specific(_) => Vec::new(),
1632 }
1633}
1634
1635fn read_formcalc_value(
1636 form: &FormTree,
1637 root_id: FormNodeId,
1638 parents: &HashMap<FormNodeId, FormNodeId>,
1639 target: ResolvedTarget,
1640) -> FormCalcValue {
1641 match target.property {
1642 ResolvedProperty::RawValue => get_formcalc_raw_value(form, target.node_id),
1643 ResolvedProperty::Presence => FormCalcValue::String(
1644 match form.meta(target.node_id).presence {
1645 Presence::Visible => "visible",
1646 Presence::Hidden => "hidden",
1647 Presence::Invisible => "invisible",
1648 Presence::Inactive => "inactive",
1649 }
1650 .to_string(),
1651 ),
1652 ResolvedProperty::SomExpression => {
1653 FormCalcValue::String(build_som_expression(form, root_id, parents, target.node_id))
1654 }
1655 }
1656}
1657
1658fn get_formcalc_raw_value(form: &FormTree, node_id: FormNodeId) -> FormCalcValue {
1659 match &form.get(node_id).node_type {
1660 FormNodeType::Field { value } => string_to_formcalc_value(value),
1661 _ if form.meta(node_id).group_kind == GroupKind::ExclusiveChoice => {
1662 for &child_id in &form.get(node_id).children {
1663 if let FormNodeType::Field { value } = &form.get(child_id).node_type {
1664 if !value.is_empty() {
1665 let selected = form.meta(child_id).item_value.as_deref().unwrap_or(value);
1666 return string_to_formcalc_value(selected);
1667 }
1668 }
1669 }
1670 FormCalcValue::Null
1671 }
1672 _ => FormCalcValue::Null,
1673 }
1674}
1675
1676fn write_formcalc_value(
1677 form: &mut FormTree,
1678 node_id: FormNodeId,
1679 property: ResolvedProperty,
1680 value: FormCalcValue,
1681) -> usize {
1682 match property {
1683 ResolvedProperty::RawValue => set_raw_value(form, node_id, formcalc_to_script_value(value)),
1684 ResolvedProperty::Presence => {
1685 set_presence(form, node_id, ScriptValue::String(value.to_string_val()))
1686 }
1687 ResolvedProperty::SomExpression => 0,
1688 }
1689}
1690
1691fn string_to_formcalc_value(value: &str) -> FormCalcValue {
1692 let trimmed = value.trim();
1693 if trimmed.is_empty() {
1694 FormCalcValue::Null
1695 } else if let Ok(number) = trimmed.parse::<f64>() {
1696 FormCalcValue::Number(number)
1697 } else {
1698 FormCalcValue::String(value.to_string())
1699 }
1700}
1701
1702fn formcalc_to_script_value(value: FormCalcValue) -> ScriptValue {
1703 match value {
1704 FormCalcValue::Null => ScriptValue::Null,
1705 FormCalcValue::Number(number) => ScriptValue::String(normalize_number(number)),
1706 FormCalcValue::String(value) => ScriptValue::String(value),
1707 }
1708}
1709
1710fn build_som_expression(
1711 form: &FormTree,
1712 root_id: FormNodeId,
1713 parents: &HashMap<FormNodeId, FormNodeId>,
1714 node_id: FormNodeId,
1715) -> String {
1716 let mut parts = Vec::new();
1717 let mut cursor = Some(node_id);
1718 while let Some(current) = cursor {
1719 let node = form.get(current);
1720 if !node.name.is_empty() {
1721 let index = if let Some(parent_id) = parents.get(¤t).copied() {
1722 form.get(parent_id)
1723 .children
1724 .iter()
1725 .copied()
1726 .filter(|sibling_id| form.get(*sibling_id).name == node.name)
1727 .position(|sibling_id| sibling_id == current)
1728 .unwrap_or(0)
1729 } else {
1730 0
1731 };
1732 parts.push(format!("{}[{index}]", node.name));
1733 }
1734 if current == root_id {
1735 break;
1736 }
1737 cursor = parents.get(¤t).copied();
1738 }
1739 parts.reverse();
1740
1741 if parts.is_empty() {
1742 "$form".to_string()
1743 } else {
1744 format!("$form.{}", parts.join("."))
1745 }
1746}
1747
1748fn descendants_inclusive(form: &FormTree, root_id: FormNodeId) -> Vec<FormNodeId> {
1749 let mut out = Vec::new();
1750 collect_descendants(form, root_id, &mut out);
1751 out
1752}
1753
1754fn collect_descendants(form: &FormTree, node_id: FormNodeId, out: &mut Vec<FormNodeId>) {
1755 out.push(node_id);
1756 for &child_id in &form.get(node_id).children {
1757 collect_descendants(form, child_id, out);
1758 }
1759}
1760
1761fn build_parent_map(form: &FormTree, root_id: FormNodeId) -> HashMap<FormNodeId, FormNodeId> {
1762 let mut parents = HashMap::new();
1763 populate_parent_map(form, root_id, &mut parents);
1764 parents
1765}
1766
1767fn populate_parent_map(
1768 form: &FormTree,
1769 node_id: FormNodeId,
1770 parents: &mut HashMap<FormNodeId, FormNodeId>,
1771) {
1772 for &child_id in &form.get(node_id).children {
1773 parents.insert(child_id, node_id);
1774 populate_parent_map(form, child_id, parents);
1775 }
1776}
1777
1778fn set_raw_value(form: &mut FormTree, node_id: FormNodeId, value: ScriptValue) -> usize {
1779 let value = match value {
1780 ScriptValue::Null => String::new(),
1781 ScriptValue::String(value) => value,
1782 };
1783
1784 if form.meta(node_id).group_kind == GroupKind::ExclusiveChoice {
1785 let mut changes = 0;
1786 for &child_id in &form.get(node_id).children.clone() {
1787 let item_value = form.meta(child_id).item_value.clone();
1788 let next = if item_value.as_deref() == Some(value.as_str()) {
1789 value.clone()
1790 } else {
1791 String::new()
1792 };
1793 if let FormNodeType::Field { value: field_value } =
1794 &mut form.get_mut(child_id).node_type
1795 {
1796 if *field_value != next {
1797 *field_value = next;
1798 changes += 1;
1799 }
1800 }
1801 }
1802 return changes;
1803 }
1804
1805 if let FormNodeType::Field { value: field_value } = &mut form.get_mut(node_id).node_type {
1806 if *field_value != value {
1807 *field_value = value;
1808 return 1;
1809 }
1810 }
1811
1812 0
1813}
1814
1815fn set_presence(form: &mut FormTree, node_id: FormNodeId, value: ScriptValue) -> usize {
1816 set_presence_inner(form, node_id, value).0
1817}
1818
1819fn set_presence_inner(
1821 form: &mut FormTree,
1822 node_id: FormNodeId,
1823 value: ScriptValue,
1824) -> (usize, Option<(&'static str, &'static str)>) {
1825 let value = match value {
1826 ScriptValue::Null => return (0, None),
1827 ScriptValue::String(value) => value,
1828 };
1829 let normalized = value.trim().to_ascii_lowercase();
1830 let new_presence = match normalized.as_str() {
1831 "visible" | "open" => Presence::Visible,
1832 "hidden" => Presence::Hidden,
1833 "invisible" => Presence::Invisible,
1834 "inactive" => Presence::Inactive,
1835 _ => return (0, None),
1836 };
1837
1838 let meta = form.meta_mut(node_id);
1839 let old_presence = meta.presence;
1840 if old_presence == new_presence {
1841 return (0, None);
1842 }
1843 meta.presence = new_presence;
1844
1845 fn pres_str(p: Presence) -> &'static str {
1846 match p {
1847 Presence::Visible => "visible",
1848 Presence::Hidden => "hidden",
1849 Presence::Invisible => "invisible",
1850 Presence::Inactive => "inactive",
1851 }
1852 }
1853 let mutation = (pres_str(old_presence), pres_str(new_presence));
1854 if runtime_diag_enabled() {
1855 let node_name = form.get(node_id).name.clone();
1856 push_presence_mutation(node_id.0, &node_name, mutation.0, mutation.1);
1857 }
1858 (1, Some(mutation))
1859}
1860
1861fn normalize_number(number: f64) -> String {
1862 if number.fract().abs() < f64::EPSILON {
1863 (number as i64).to_string()
1864 } else {
1865 number.to_string()
1866 }
1867}
1868
1869#[derive(Debug, Clone, PartialEq, Eq)]
1870enum ScriptValue {
1871 Null,
1872 String(String),
1873}
1874
1875#[cfg(test)]
1876mod tests {
1877 use super::*;
1878 use xfa_layout_engine::form::{
1879 FieldKind, FormNode, FormNodeMeta, FormNodeStyle, GroupKind, Occur,
1880 };
1881 use xfa_layout_engine::text::FontMetrics;
1882 use xfa_layout_engine::types::{BoxModel, LayoutStrategy};
1883
1884 fn add_node(tree: &mut FormTree, name: &str, node_type: FormNodeType) -> FormNodeId {
1885 tree.add_node(FormNode {
1886 name: name.to_string(),
1887 node_type,
1888 box_model: BoxModel::default(),
1889 layout: LayoutStrategy::TopToBottom,
1890 children: Vec::new(),
1891 occur: Occur::once(),
1892 font: FontMetrics::default(),
1893 calculate: None,
1894 validate: None,
1895 column_widths: Vec::new(),
1896 col_span: 1,
1897 })
1898 }
1899
1900 fn empty_meta() -> FormNodeMeta {
1901 FormNodeMeta {
1902 field_kind: FieldKind::Text,
1903 group_kind: GroupKind::None,
1904 style: FormNodeStyle::default(),
1905 ..Default::default()
1906 }
1907 }
1908
1909 fn formcalc_script(script: &str, activity: &str) -> EventScript {
1910 EventScript::formcalc(script, Some(activity))
1911 }
1912
1913 fn javascript_script(script: &str, activity: &str) -> EventScript {
1914 EventScript::javascript(script, Some(activity))
1915 }
1916
1917 fn other_script(script: &str, activity: &str) -> EventScript {
1918 EventScript::new(
1919 script.to_string(),
1920 ScriptLanguage::Other,
1921 Some(activity.to_string()),
1922 None,
1923 None,
1924 )
1925 }
1926
1927 fn script_policy_fixture(include_js: bool) -> (FormTree, FormNodeId, FormNodeId) {
1928 let mut tree = FormTree::new();
1929 let root = add_node(&mut tree, "root", FormNodeType::Root);
1930 let js_hook = add_node(
1931 &mut tree,
1932 "JsHook",
1933 FormNodeType::Field {
1934 value: String::new(),
1935 },
1936 );
1937 let runner = add_node(
1938 &mut tree,
1939 "Runner",
1940 FormNodeType::Field {
1941 value: String::new(),
1942 },
1943 );
1944 let target = add_node(
1945 &mut tree,
1946 "Target",
1947 FormNodeType::Field {
1948 value: String::new(),
1949 },
1950 );
1951
1952 tree.get_mut(root).children = vec![js_hook, runner, target];
1953 if include_js {
1954 tree.meta_mut(js_hook).event_scripts = vec![javascript_script(
1955 "xfa.host.messageBox('skip');",
1956 "initialize",
1957 )];
1958 }
1959 tree.meta_mut(runner).event_scripts =
1960 vec![formcalc_script(r#"Target.rawValue = "ran""#, "initialize")];
1961
1962 (tree, root, target)
1963 }
1964
1965 fn other_language_policy_fixture() -> (FormTree, FormNodeId, FormNodeId) {
1966 let mut tree = FormTree::new();
1967 let root = add_node(&mut tree, "root", FormNodeType::Root);
1968 let other = add_node(&mut tree, "OtherHook", FormNodeType::Subform);
1969 let runner = add_node(&mut tree, "Runner", FormNodeType::Subform);
1970 let target = add_node(
1971 &mut tree,
1972 "Target",
1973 FormNodeType::Field {
1974 value: String::new(),
1975 },
1976 );
1977
1978 tree.get_mut(root).children = vec![other, runner, target];
1979 tree.meta_mut(other).event_scripts = vec![other_script("MsgBox \"skip\"", "initialize")];
1980 tree.meta_mut(runner).event_scripts =
1981 vec![formcalc_script(r#"Target.rawValue = "ran""#, "initialize")];
1982
1983 (tree, root, target)
1984 }
1985
1986 fn field_value(tree: &FormTree, node_id: FormNodeId) -> &str {
1987 match &tree.get(node_id).node_type {
1988 FormNodeType::Field { value } => value,
1989 _ => panic!("expected field"),
1990 }
1991 }
1992
1993 #[test]
1994 fn change_event_toggles_relative_hidden_subform() {
1995 let mut tree = FormTree::new();
1996 let root = add_node(&mut tree, "root", FormNodeType::Root);
1997 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
1998 let group = add_node(&mut tree, "Choice", FormNodeType::Subform);
1999 let option1 = add_node(
2000 &mut tree,
2001 "Option1",
2002 FormNodeType::Field {
2003 value: "1".to_string(),
2004 },
2005 );
2006 let option2 = add_node(
2007 &mut tree,
2008 "Option2",
2009 FormNodeType::Field {
2010 value: String::new(),
2011 },
2012 );
2013 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
2014
2015 tree.get_mut(root).children = vec![section];
2016 tree.get_mut(section).children = vec![group, details];
2017 tree.get_mut(group).children = vec![option1, option2];
2018
2019 tree.meta_mut(group).group_kind = GroupKind::ExclusiveChoice;
2020 tree.meta_mut(group).event_scripts = vec![formcalc_script(
2021 r#"
2022Details.presence = "hidden"
2023if (this.rawValue == 1) then
2024 Details.presence = "visible"
2025endif
2026"#,
2027 "initialize",
2028 )];
2029 tree.meta_mut(option1).item_value = Some("1".into());
2030 tree.meta_mut(option2).item_value = Some("2".into());
2031 tree.meta_mut(details).presence = Presence::Hidden;
2032
2033 apply_dynamic_scripts(&mut tree, root).unwrap();
2034
2035 assert_eq!(tree.meta(details).presence, Presence::Visible);
2036 }
2037
2038 #[test]
2039 fn calculate_script_on_hidden_block_uses_sibling_values() {
2040 let mut tree = FormTree::new();
2041 let root = add_node(&mut tree, "root", FormNodeType::Root);
2042 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
2043 let option1 = add_node(
2044 &mut tree,
2045 "Opt1",
2046 FormNodeType::Field {
2047 value: "1".to_string(),
2048 },
2049 );
2050 let option2 = add_node(
2051 &mut tree,
2052 "Opt2",
2053 FormNodeType::Field {
2054 value: String::new(),
2055 },
2056 );
2057 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
2058
2059 tree.get_mut(root).children = vec![section];
2060 tree.get_mut(section).children = vec![option1, option2, details];
2061 tree.meta_mut(details).presence = Presence::Hidden;
2062 tree.meta_mut(details).event_scripts = vec![formcalc_script(
2063 r#"
2064this.presence = "hidden"
2065if ((Opt1.rawValue == 1) or (Opt2.rawValue == 1)) then
2066 this.presence = "visible"
2067endif
2068"#,
2069 "calculate",
2070 )];
2071
2072 apply_dynamic_scripts(&mut tree, root).unwrap();
2073
2074 assert_eq!(tree.meta(details).presence, Presence::Visible);
2075 }
2076
2077 #[test]
2078 fn multi_pass_scripts_propagate_raw_values() {
2079 let mut tree = FormTree::new();
2080 let root = add_node(&mut tree, "root", FormNodeType::Root);
2081 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
2082 let controller = add_node(
2083 &mut tree,
2084 "Controller",
2085 FormNodeType::Field {
2086 value: "1".to_string(),
2087 },
2088 );
2089 let target = add_node(
2090 &mut tree,
2091 "Target",
2092 FormNodeType::Field {
2093 value: String::new(),
2094 },
2095 );
2096 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
2097
2098 tree.get_mut(root).children = vec![section];
2099 tree.get_mut(section).children = vec![controller, target, details];
2100
2101 tree.meta_mut(controller).event_scripts = vec![formcalc_script(
2102 r#"
2103if (this.rawValue == 1) then
2104 Target.rawValue = 1
2105endif
2106"#,
2107 "calculate",
2108 )];
2109 tree.meta_mut(details).presence = Presence::Hidden;
2110 tree.meta_mut(details).event_scripts = vec![formcalc_script(
2111 r#"
2112this.presence = "hidden"
2113if (Target.rawValue == 1) then
2114 this.presence = "visible"
2115endif
2116"#,
2117 "calculate",
2118 )];
2119
2120 apply_dynamic_scripts(&mut tree, root).unwrap();
2121
2122 if let FormNodeType::Field { value } = &tree.get(target).node_type {
2123 assert_eq!(value, "1");
2124 } else {
2125 panic!("expected field");
2126 }
2127 assert_eq!(tree.meta(details).presence, Presence::Visible);
2128 }
2129
2130 #[test]
2135 fn som_path_resolves_on_simple_form_tree() {
2136 let mut tree = FormTree::new();
2137 let root = add_node(&mut tree, "root", FormNodeType::Root);
2138 let form1 = add_node(&mut tree, "form1", FormNodeType::Subform);
2139 let subform = add_node(&mut tree, "subform1", FormNodeType::Subform);
2140 let field1 = add_node(
2141 &mut tree,
2142 "field1",
2143 FormNodeType::Field {
2144 value: "hello".to_string(),
2145 },
2146 );
2147
2148 tree.get_mut(root).children = vec![form1];
2149 tree.get_mut(form1).children = vec![subform];
2150 tree.get_mut(subform).children = vec![field1];
2151
2152 tree.meta_mut(root).event_scripts = vec![formcalc_script(
2154 "form1.subform1.field1.rawValue",
2155 "calculate",
2156 )];
2157
2158 let parents = super::build_parent_map(&tree, root);
2159 let resolver = FormTreeSomResolver::new(&mut tree, root, &parents, root);
2160 let target = resolver.resolve_target("form1.subform1.field1.rawValue");
2161 assert!(target.is_some(), "SOM path must resolve to a node");
2162 let target = target.unwrap();
2163 let val = super::read_formcalc_value(&tree, root, &parents, target);
2164 match val {
2165 formcalc_interpreter::value::Value::String(s) => assert_eq!(s, "hello"),
2166 formcalc_interpreter::value::Value::Number(n) => {
2167 panic!("expected string, got number {n}")
2169 }
2170 _ => panic!("expected string value"),
2171 }
2172 }
2173
2174 #[test]
2176 fn invalid_som_path_returns_none_not_panic() {
2177 let mut tree = FormTree::new();
2178 let root = add_node(&mut tree, "root", FormNodeType::Root);
2179 let parents = super::build_parent_map(&tree, root);
2180 let resolver = FormTreeSomResolver::new(&mut tree, root, &parents, root);
2181
2182 let result = resolver.resolve_target("nonexistent.deep.path.rawValue");
2184 assert!(
2185 result.is_none(),
2186 "invalid SOM path must return None, not panic"
2187 );
2188 }
2189
2190 #[test]
2191 fn best_effort_skips_javascript_and_runs_formcalc() {
2192 let (mut tree, root, target) = script_policy_fixture(true);
2193
2194 let outcome = apply_dynamic_scripts(&mut tree, root).unwrap();
2195
2196 assert_eq!(field_value(&tree, target), "ran");
2197 assert!(outcome.js_present);
2198 assert_eq!(outcome.js_skipped, 1);
2199 assert_eq!(outcome.other_skipped, 0);
2200 assert_eq!(outcome.formcalc_run, 1);
2201 assert_eq!(outcome.formcalc_errors, 0);
2202 assert_eq!(outcome.output_quality, OutputQuality::BestEffort);
2203 }
2204
2205 #[test]
2206 fn strict_mode_preserves_javascript_reject() {
2207 let (mut tree, root, target) = script_policy_fixture(true);
2208
2209 let err =
2210 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
2211
2212 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2213 assert_eq!(field_value(&tree, target), "");
2214 }
2215
2216 #[test]
2217 fn formcalc_only_reports_exact_quality() {
2218 let (mut tree, root, target) = script_policy_fixture(false);
2219
2220 let outcome = apply_dynamic_scripts(&mut tree, root).unwrap();
2221
2222 assert_eq!(field_value(&tree, target), "ran");
2223 assert!(!outcome.js_present);
2224 assert_eq!(outcome.js_skipped, 0);
2225 assert_eq!(outcome.other_skipped, 0);
2226 assert_eq!(outcome.formcalc_run, 1);
2227 assert_eq!(outcome.formcalc_errors, 0);
2228 assert_eq!(outcome.output_quality, OutputQuality::Exact);
2229 }
2230
2231 #[test]
2232 fn other_language_scripts_skip_in_best_effort_and_reject_in_strict() {
2233 let (mut tree, root, target) = other_language_policy_fixture();
2234 let (mut strict_tree, strict_root, _) = other_language_policy_fixture();
2235
2236 let outcome = apply_dynamic_scripts(&mut tree, root).unwrap();
2237 assert_eq!(field_value(&tree, target), "ran");
2238 assert_eq!(outcome.js_skipped, 0);
2239 assert_eq!(outcome.other_skipped, 1);
2240 assert_eq!(outcome.formcalc_run, 1);
2241 assert_eq!(outcome.output_quality, OutputQuality::BestEffort);
2242
2243 let err =
2244 apply_dynamic_scripts_with_mode(&mut strict_tree, strict_root, JsExecutionMode::Strict)
2245 .unwrap_err();
2246 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2247 }
2248
2249 #[test]
2250 fn javascript_direct_executor_call_is_still_denied() {
2251 let mut tree = FormTree::new();
2252 let root = add_node(&mut tree, "root", FormNodeType::Root);
2253 let trigger = add_node(
2254 &mut tree,
2255 "Trigger",
2256 FormNodeType::Field {
2257 value: String::new(),
2258 },
2259 );
2260 tree.get_mut(root).children = vec![trigger];
2261
2262 let parents = build_parent_map(&tree, root);
2263 let script = javascript_script("xfa.host.messageBox('deny');", "initialize");
2264 let err = execute_event_script(
2265 &mut tree,
2266 root,
2267 &parents,
2268 trigger,
2269 &script,
2270 ScriptPhase::Initialize,
2271 )
2272 .unwrap_err();
2273
2274 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2275 }
2276
2277 #[test]
2278 fn javascript_resolve_node_call_is_explicitly_denied() {
2279 let mut tree = FormTree::new();
2280 let root = add_node(&mut tree, "root", FormNodeType::Root);
2281 let form = add_node(&mut tree, "formulier1", FormNodeType::Subform);
2282 let admin = add_node(&mut tree, "ADMIN", FormNodeType::Subform);
2283 let lock = add_node(
2284 &mut tree,
2285 "LockForm_AD",
2286 FormNodeType::Field {
2287 value: "1".to_string(),
2288 },
2289 );
2290 let reset = add_node(
2291 &mut tree,
2292 "Reset",
2293 FormNodeType::Field {
2294 value: "1".to_string(),
2295 },
2296 );
2297
2298 tree.get_mut(root).children = vec![form];
2299 tree.get_mut(form).children = vec![admin, reset];
2300 tree.get_mut(admin).children = vec![lock];
2301 tree.meta_mut(reset).event_scripts = vec![javascript_script(
2302 r#"xfa.resolveNode("formulier1.ADMIN.LockForm_AD").rawValue = 0;"#,
2303 "initialize",
2304 )];
2305
2306 let err =
2307 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
2308 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2309
2310 if let FormNodeType::Field { value } = &tree.get(lock).node_type {
2311 assert_eq!(value, "1");
2312 } else {
2313 panic!("expected field");
2314 }
2315 }
2316
2317 #[test]
2318 fn javascript_utils_hide_if_empty_is_explicitly_denied() {
2319 let mut tree = FormTree::new();
2320 let root = add_node(&mut tree, "root", FormNodeType::Root);
2321 let empty = add_node(
2322 &mut tree,
2323 "EmptyField",
2324 FormNodeType::Field {
2325 value: String::new(),
2326 },
2327 );
2328 tree.get_mut(root).children = vec![empty];
2329 tree.meta_mut(empty).event_scripts =
2330 vec![javascript_script("Utils.hideIfEmpty(this);", "initialize")];
2331
2332 let err =
2333 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
2334 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2335
2336 assert!(!tree.meta(empty).presence.is_not_visible());
2337 }
2338
2339 #[test]
2340 fn malformed_javascript_payload_is_explicitly_denied_without_panic() {
2341 let mut tree = FormTree::new();
2342 let root = add_node(&mut tree, "root", FormNodeType::Root);
2343 let container = add_node(&mut tree, "Container", FormNodeType::Subform);
2344 let empty = add_node(
2345 &mut tree,
2346 "EmptyField",
2347 FormNodeType::Field {
2348 value: String::new(),
2349 },
2350 );
2351
2352 tree.get_mut(root).children = vec![container];
2353 tree.get_mut(container).children = vec![empty];
2354 tree.meta_mut(empty).event_scripts = vec![javascript_script(
2355 "\0}{{not.valid.javascript(",
2356 "initialize",
2357 )];
2358
2359 let err =
2360 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
2361 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
2362
2363 assert!(!tree.meta(container).presence.is_not_visible());
2364 }
2365
2366 #[test]
2367 fn default_meta_helper_is_constructible() {
2368 let meta = empty_meta();
2369 assert_eq!(meta.group_kind, GroupKind::None);
2370 }
2371
2372 #[test]
2373 fn calculate_event_applies_formcalc_return_value() {
2374 let mut tree = FormTree::new();
2375 let root = add_node(&mut tree, "root", FormNodeType::Root);
2376 let total = add_node(
2377 &mut tree,
2378 "Total",
2379 FormNodeType::Field {
2380 value: String::new(),
2381 },
2382 );
2383
2384 tree.get_mut(root).children = vec![total];
2385 tree.meta_mut(total).event_scripts = vec![formcalc_script("40 + 2", "calculate")];
2386
2387 apply_dynamic_scripts(&mut tree, root).unwrap();
2388
2389 match &tree.get(total).node_type {
2390 FormNodeType::Field { value } => assert_eq!(value, "42"),
2391 _ => panic!("expected field"),
2392 }
2393 }
2394
2395 #[test]
2396 fn calculate_event_resolves_bare_field_names_as_raw_values() {
2397 let mut tree = FormTree::new();
2398 let root = add_node(&mut tree, "root", FormNodeType::Root);
2399 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
2400 let number1 = add_node(
2401 &mut tree,
2402 "Number1",
2403 FormNodeType::Field {
2404 value: "40".to_string(),
2405 },
2406 );
2407 let number2 = add_node(
2408 &mut tree,
2409 "Number2",
2410 FormNodeType::Field {
2411 value: "2".to_string(),
2412 },
2413 );
2414 let total = add_node(
2415 &mut tree,
2416 "Total",
2417 FormNodeType::Field {
2418 value: String::new(),
2419 },
2420 );
2421
2422 tree.get_mut(root).children = vec![section];
2423 tree.get_mut(section).children = vec![number1, number2, total];
2424 tree.meta_mut(total).event_scripts =
2425 vec![formcalc_script("Number1 + Number2", "calculate")];
2426
2427 apply_dynamic_scripts(&mut tree, root).unwrap();
2428
2429 match &tree.get(total).node_type {
2430 FormNodeType::Field { value } => assert_eq!(value, "42"),
2431 _ => panic!("expected field"),
2432 }
2433 }
2434
2435 #[test]
2436 fn click_events_are_skipped_during_flatten() {
2437 let mut tree = FormTree::new();
2438 let root = add_node(&mut tree, "root", FormNodeType::Root);
2439 let trigger = add_node(
2440 &mut tree,
2441 "Trigger",
2442 FormNodeType::Field {
2443 value: "1".to_string(),
2444 },
2445 );
2446 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
2447
2448 tree.get_mut(root).children = vec![trigger, details];
2449 tree.meta_mut(details).presence = Presence::Hidden;
2450 tree.meta_mut(trigger).event_scripts = vec![formcalc_script(
2451 r#"
2452Details.presence = "visible"
2453"#,
2454 "click",
2455 )];
2456
2457 apply_dynamic_scripts(&mut tree, root).unwrap();
2458
2459 assert_eq!(tree.meta(details).presence, Presence::Hidden);
2460 }
2461
2462 #[test]
2463 fn rollback_when_scripts_mostly_error() {
2464 let mut tree = FormTree::new();
2468 let root = add_node(&mut tree, "root", FormNodeType::Root);
2469 let field_a = add_node(
2470 &mut tree,
2471 "FieldA",
2472 FormNodeType::Field {
2473 value: "hello".to_string(),
2474 },
2475 );
2476 let field_b = add_node(
2477 &mut tree,
2478 "FieldB",
2479 FormNodeType::Field {
2480 value: "world".to_string(),
2481 },
2482 );
2483
2484 tree.get_mut(root).children = vec![field_a, field_b];
2485
2486 tree.meta_mut(field_a).event_scripts = vec![formcalc_script("@@INVALID@@", "initialize")];
2488 tree.meta_mut(field_b).event_scripts =
2489 vec![formcalc_script("@@ALSO_BROKEN@@", "initialize")];
2490
2491 apply_dynamic_scripts(&mut tree, root).unwrap();
2492
2493 match &tree.get(field_a).node_type {
2495 FormNodeType::Field { value } => assert_eq!(value, "hello"),
2496 _ => panic!("expected field"),
2497 }
2498 match &tree.get(field_b).node_type {
2499 FormNodeType::Field { value } => assert_eq!(value, "world"),
2500 _ => panic!("expected field"),
2501 }
2502 }
2503
2504 #[test]
2505 fn rollback_when_populated_fields_go_empty() {
2506 let mut tree = FormTree::new();
2509 let root = add_node(&mut tree, "root", FormNodeType::Root);
2510 let field_a = add_node(
2511 &mut tree,
2512 "FieldA",
2513 FormNodeType::Field {
2514 value: "keep".to_string(),
2515 },
2516 );
2517 let field_b = add_node(
2518 &mut tree,
2519 "FieldB",
2520 FormNodeType::Field {
2521 value: "also_keep".to_string(),
2522 },
2523 );
2524
2525 tree.get_mut(root).children = vec![field_a, field_b];
2526
2527 let snapshot = super::snapshot_form(&tree);
2533
2534 if let FormNodeType::Field { value } = &mut tree.get_mut(field_a).node_type {
2536 *value = String::new();
2537 }
2538 if let FormNodeType::Field { value } = &mut tree.get_mut(field_b).node_type {
2539 *value = String::new();
2540 }
2541
2542 assert!(super::should_rollback(&tree, &snapshot, 0, 2));
2543
2544 super::restore_snapshot(&mut tree, &snapshot);
2545
2546 match &tree.get(field_a).node_type {
2547 FormNodeType::Field { value } => assert_eq!(value, "keep"),
2548 _ => panic!("expected field"),
2549 }
2550 match &tree.get(field_b).node_type {
2551 FormNodeType::Field { value } => assert_eq!(value, "also_keep"),
2552 _ => panic!("expected field"),
2553 }
2554 }
2555
2556 #[test]
2557 fn no_rollback_when_scripts_succeed() {
2558 let mut tree = FormTree::new();
2560 let root = add_node(&mut tree, "root", FormNodeType::Root);
2561 let total = add_node(
2562 &mut tree,
2563 "Total",
2564 FormNodeType::Field {
2565 value: String::new(),
2566 },
2567 );
2568
2569 tree.get_mut(root).children = vec![total];
2570 tree.meta_mut(total).event_scripts = vec![formcalc_script("40 + 2", "calculate")];
2571
2572 apply_dynamic_scripts(&mut tree, root).unwrap();
2573
2574 match &tree.get(total).node_type {
2575 FormNodeType::Field { value } => assert_eq!(value, "42"),
2576 _ => panic!("expected field"),
2577 }
2578 }
2579
2580 #[test]
2585 fn formcalc_unknown_function_increments_error_counter() {
2586 let mut tree = FormTree::new();
2587 let root = add_node(&mut tree, "root", FormNodeType::Root);
2588 let total = add_node(
2589 &mut tree,
2590 "Total",
2591 FormNodeType::Field {
2592 value: String::new(),
2593 },
2594 );
2595
2596 tree.get_mut(root).children = vec![total];
2597 tree.meta_mut(total).event_scripts = vec![formcalc_script(
2600 "definitelyNotAFormCalcBuiltin(1)",
2601 "calculate",
2602 )];
2603
2604 let outcome = apply_dynamic_scripts(&mut tree, root).unwrap();
2605 assert_eq!(outcome.formcalc_run, 1, "the script must be attempted");
2606 assert_eq!(
2607 outcome.formcalc_errors, 1,
2608 "unknown-function failure must increment formcalc_errors"
2609 );
2610 }
2611
2612 static ENV_LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
2617
2618 fn env_lock() -> std::sync::MutexGuard<'static, ()> {
2619 ENV_LOCK
2620 .get_or_init(|| std::sync::Mutex::new(()))
2621 .lock()
2622 .unwrap_or_else(|e| e.into_inner())
2623 }
2624
2625 #[test]
2631 fn script_lifecycle_populated_when_trace_enabled() {
2632 let _guard = env_lock();
2633 std::env::set_var("XFA_FLATTEN_TRACE", "1");
2634
2635 let mut tree = FormTree::new();
2636 let root = add_node(&mut tree, "root", FormNodeType::Root);
2637 let field = add_node(
2638 &mut tree,
2639 "TraceField",
2640 FormNodeType::Field {
2641 value: String::new(),
2642 },
2643 );
2644 tree.get_mut(root).children = vec![field];
2645 tree.meta_mut(field).event_scripts = vec![javascript_script(
2646 "xfa.host.messageBox('trace');",
2647 "initialize",
2648 )];
2649
2650 let outcome =
2651 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::BestEffortStatic)
2652 .unwrap();
2653
2654 std::env::remove_var("XFA_FLATTEN_TRACE");
2655
2656 assert!(
2657 !outcome.script_lifecycle.is_empty(),
2658 "script_lifecycle must have at least one entry when XFA_FLATTEN_TRACE=1"
2659 );
2660 let entry = &outcome.script_lifecycle[0];
2661 assert_eq!(entry.node_name, "TraceField");
2662 assert_eq!(entry.activity, "initialize");
2663 assert_eq!(entry.lang, "javascript");
2664 assert_eq!(
2666 entry.outcome, "skipped_mode",
2667 "BestEffortStatic JS must appear as skipped_mode in lifecycle"
2668 );
2669 }
2670
2671 #[test]
2678 fn skipped_activities_tallies_click_correctly() {
2679 let _guard = env_lock();
2680 std::env::set_var("XFA_FLATTEN_TRACE", "1");
2681
2682 let mut tree = FormTree::new();
2683 let root = add_node(&mut tree, "root", FormNodeType::Root);
2684 let btn = add_node(
2685 &mut tree,
2686 "ClickBtn",
2687 FormNodeType::Field {
2688 value: String::new(),
2689 },
2690 );
2691 tree.get_mut(root).children = vec![btn];
2692 tree.meta_mut(btn).event_scripts = vec![javascript_script(
2694 "xfa.host.messageBox('clicked');",
2695 "click",
2696 )];
2697
2698 let outcome =
2699 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::BestEffortStatic)
2700 .unwrap();
2701
2702 std::env::remove_var("XFA_FLATTEN_TRACE");
2703
2704 assert_eq!(
2705 outcome.skipped_activities.click, 1,
2706 "exactly one click-activity JS script must be tallied in skipped_activities.click"
2707 );
2708 assert_eq!(outcome.skipped_activities.initialize, 0);
2710 assert_eq!(outcome.skipped_activities.calculate, 0);
2711 assert_eq!(outcome.skipped_activities.other, 0);
2712 }
2713
2714 #[test]
2722 fn form_dom_match_failures_increments_for_unmatched_subform() {
2723 use crate::flatten::{apply_form_dom_presence, XfaRenderingPolicy};
2724
2725 let _guard = env_lock();
2726 std::env::set_var("XFA_FLATTEN_TRACE", "1");
2728
2729 let mut tree = FormTree::new();
2730 let root = add_node(&mut tree, "root", FormNodeType::Root);
2731 let form1 = add_node(&mut tree, "form1", FormNodeType::Subform);
2733 let present = add_node(&mut tree, "Present", FormNodeType::Subform);
2735 let ghost = add_node(&mut tree, "Ghost", FormNodeType::Subform);
2737
2738 tree.get_mut(root).children = vec![form1];
2739 tree.get_mut(form1).children = vec![present, ghost];
2740
2741 let form_xml = r#"<form>
2745 <subform name="form1">
2746 <subform name="Present"/>
2747 </subform>
2748</form>"#;
2749
2750 let (_admitted, match_failures, match_log) = apply_form_dom_presence(
2751 &mut tree,
2752 root,
2753 form_xml,
2754 XfaRenderingPolicy::SavedStateFaithful,
2755 false,
2756 );
2757
2758 std::env::remove_var("XFA_FLATTEN_TRACE");
2759
2760 assert_eq!(
2761 match_failures, 1,
2762 "exactly one unmatched named subform (Ghost) must be counted"
2763 );
2764 assert_eq!(match_log.len(), 1, "match_log must contain the Ghost entry");
2765 assert_eq!(match_log[0].template_node_name, "Ghost");
2766 assert_eq!(match_log[0].reason, "formdom_unmatched_suppressed");
2767 assert!(
2769 tree.meta(ghost).presence.is_not_visible(),
2770 "Ghost subform must have been hidden by apply_form_dom_presence"
2771 );
2772 }
2773}