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, NullRuntime, RuntimeMetadata, SandboxError, XfaJsRuntime,
7};
8use formcalc_interpreter::{
9 interpreter::Interpreter, lexer::tokenize, parser, som_bridge::SomResolver,
10 value::Value as FormCalcValue,
11};
12use xfa_dom_resolver::som::{parse_som, SomExpression, SomIndex, SomRoot, SomSelector};
13use xfa_layout_engine::form::{
14 EventScript, FormNodeId, FormNodeType, FormTree, GroupKind, Presence, ScriptLanguage,
15};
16
17const MAX_SCRIPT_PASSES: usize = 3;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum JsExecutionMode {
27 Strict,
30 #[default]
34 BestEffortStatic,
35 SandboxedRuntime,
46}
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
50pub enum OutputQuality {
51 #[default]
53 Exact,
54 BestEffort,
56 Sandboxed,
59}
60
61impl OutputQuality {
62 pub fn as_str(self) -> &'static str {
64 match self {
65 Self::Exact => "exact",
66 Self::BestEffort => "best_effort",
67 Self::Sandboxed => "sandboxed",
68 }
69 }
70}
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct DynamicScriptOutcome {
75 pub changes: usize,
77 pub js_present: bool,
79 pub js_skipped: usize,
81 pub other_skipped: usize,
83 pub formcalc_run: usize,
85 pub formcalc_errors: usize,
87 pub output_quality: OutputQuality,
89 pub js_executed: usize,
93 pub js_runtime_errors: usize,
98 pub js_timeouts: usize,
100 pub js_oom: usize,
102 pub js_host_calls: usize,
104 pub js_mutations: usize,
106 pub js_instance_writes: usize,
108 pub js_list_writes: usize,
110 pub js_binding_errors: usize,
112 pub js_resolve_failures: usize,
114 pub js_data_reads: usize,
116}
117
118impl Default for DynamicScriptOutcome {
119 fn default() -> Self {
120 Self {
121 changes: 0,
122 js_present: false,
123 js_skipped: 0,
124 other_skipped: 0,
125 formcalc_run: 0,
126 formcalc_errors: 0,
127 output_quality: OutputQuality::Exact,
128 js_executed: 0,
129 js_runtime_errors: 0,
130 js_timeouts: 0,
131 js_oom: 0,
132 js_host_calls: 0,
133 js_mutations: 0,
134 js_instance_writes: 0,
135 js_list_writes: 0,
136 js_binding_errors: 0,
137 js_resolve_failures: 0,
138 js_data_reads: 0,
139 }
140 }
141}
142
143struct FormSnapshot {
148 field_values: Vec<(usize, String)>,
149 presences: Vec<(usize, Presence)>,
150 populated_count: usize,
151}
152
153fn snapshot_form(form: &FormTree) -> FormSnapshot {
154 let mut field_values = Vec::new();
155 let mut presences = Vec::new();
156 let mut populated_count = 0usize;
157 for (idx, node) in form.nodes.iter().enumerate() {
158 if let FormNodeType::Field { value } = &node.node_type {
159 field_values.push((idx, value.clone()));
160 if !value.trim().is_empty() {
161 populated_count += 1;
162 }
163 }
164 presences.push((idx, form.metadata[idx].presence));
165 }
166 FormSnapshot {
167 field_values,
168 presences,
169 populated_count,
170 }
171}
172
173fn restore_snapshot(form: &mut FormTree, snapshot: &FormSnapshot) {
174 for (idx, value) in &snapshot.field_values {
175 if let FormNodeType::Field { value: fv } = &mut form.nodes[*idx].node_type {
176 *fv = value.clone();
177 }
178 }
179 for (idx, presence) in &snapshot.presences {
180 form.metadata[*idx].presence = *presence;
181 }
182}
183
184fn should_rollback(
185 form: &FormTree,
186 snapshot: &FormSnapshot,
187 errors: usize,
188 successes: usize,
189) -> bool {
190 if errors > 0 && errors > successes {
191 return true;
192 }
193 if snapshot.populated_count >= 2 {
194 let mut now_empty = 0usize;
195 for (idx, old_value) in &snapshot.field_values {
196 if old_value.trim().is_empty() {
197 continue;
198 }
199 if let FormNodeType::Field { value } = &form.nodes[*idx].node_type {
200 if value.trim().is_empty() {
201 now_empty += 1;
202 }
203 }
204 }
205 if now_empty * 2 > snapshot.populated_count {
206 return true;
207 }
208 }
209 false
210}
211pub fn apply_dynamic_scripts(
229 form: &mut FormTree,
230 root_id: FormNodeId,
231) -> Result<DynamicScriptOutcome> {
232 apply_dynamic_scripts_with_mode(form, root_id, JsExecutionMode::default())
233}
234pub fn apply_dynamic_scripts_with_mode(
236 form: &mut FormTree,
237 root_id: FormNodeId,
238 mode: JsExecutionMode,
239) -> Result<DynamicScriptOutcome> {
240 #[cfg(feature = "xfa-js-sandboxed")]
246 {
247 if mode == JsExecutionMode::SandboxedRuntime {
248 match crate::js_runtime::QuickJsRuntime::new() {
249 Ok(mut rt) => {
250 return apply_dynamic_scripts_with_runtime(form, root_id, mode, &mut rt);
251 }
252 Err(_e) => {
253 }
257 }
258 }
259 }
260 apply_dynamic_scripts_with_runtime(form, root_id, mode, &mut NullRuntime::new())
261}
262
263pub fn apply_dynamic_scripts_with_runtime(
275 form: &mut FormTree,
276 root_id: FormNodeId,
277 mode: JsExecutionMode,
278 runtime: &mut dyn XfaJsRuntime,
279) -> Result<DynamicScriptOutcome> {
280 let parents = build_parent_map(form, root_id);
281 let all_scripts: Vec<(FormNodeId, Vec<EventScript>)> = form
282 .nodes
283 .iter()
284 .enumerate()
285 .filter_map(|(idx, _)| {
286 let node_id = FormNodeId(idx);
287 let scripts = form.meta(node_id).event_scripts.clone();
288 (!scripts.is_empty()).then_some((node_id, scripts))
289 })
290 .collect();
291
292 let has_unsupported_script = all_scripts.iter().any(|(_, node_scripts)| {
293 node_scripts
294 .iter()
295 .any(|script| script.language != ScriptLanguage::FormCalc)
296 });
297
298 if mode == JsExecutionMode::Strict && has_unsupported_script {
299 return Err(javascript_policy::reject_execution(
300 JavaScriptEntryPoint::XfaEventHook,
301 ));
302 }
303
304 let mut js_skipped = 0usize;
305 let mut other_skipped = 0usize;
306 let mut sandbox_metadata = RuntimeMetadata::default();
307 let mut scripts = Vec::new();
308 let sandbox_active = mode == JsExecutionMode::SandboxedRuntime;
309 let snapshot = snapshot_form(form);
310
311 if sandbox_active {
312 let _ = runtime.init();
315 let _ = runtime.reset_for_new_document();
316 let _ = runtime.set_form_handle(form as *mut FormTree, root_id);
317 }
318
319 for (node_id, node_scripts) in all_scripts {
320 let mut formcalc_scripts = Vec::new();
321 for script in node_scripts {
322 match script.language {
323 ScriptLanguage::FormCalc => formcalc_scripts.push(script),
324 ScriptLanguage::JavaScript => {
325 if sandbox_active && activity_allowed_for_sandbox(script.activity.as_deref()) {
326 let _ = runtime.reset_per_script(node_id, script.activity.as_deref());
327 match runtime.execute_script(script.activity.as_deref(), &script.script) {
328 Ok(_outcome) => {
329 }
331 Err(SandboxError::Timeout) => js_skipped += 1,
332 Err(SandboxError::OutOfMemory) => js_skipped += 1,
333 Err(e) => {
334 log::debug!(
340 "sandbox script error on activity={:?}: {}",
341 script.activity.as_deref(),
342 e
343 );
344 if std::env::var("XFA_JS_DEBUG").ok().as_deref() == Some("1") {
345 eprintln!(
346 "XFA_JS_DEBUG sandbox script error on activity={:?}: {}",
347 script.activity.as_deref(),
348 e
349 );
350 }
351 js_skipped += 1;
352 }
353 }
354 } else {
355 js_skipped += 1;
356 }
357 }
358 ScriptLanguage::Other => other_skipped += 1,
359 }
360 }
361 if !formcalc_scripts.is_empty() {
362 scripts.push((node_id, formcalc_scripts));
363 }
364 }
365
366 if sandbox_active {
367 let _ = runtime.set_form_handle(std::ptr::null_mut(), root_id);
368 sandbox_metadata = runtime.take_metadata();
369 }
370
371 let mut stats = ScriptStats::default();
372
373 let mut changes = sandbox_metadata
374 .mutations
375 .saturating_add(sandbox_metadata.instance_writes)
376 + run_script_phase(
377 form,
378 root_id,
379 &parents,
380 &scripts,
381 ScriptPhase::Initialize,
382 1,
383 &mut stats,
384 )?
385 + run_script_phase(
386 form,
387 root_id,
388 &parents,
389 &scripts,
390 ScriptPhase::Calculate,
391 MAX_SCRIPT_PASSES,
392 &mut stats,
393 )?;
394
395 let sandbox_rollback_errors = sandbox_metadata
396 .runtime_errors
397 .saturating_add(sandbox_metadata.timeouts)
398 .saturating_add(sandbox_metadata.oom)
399 .saturating_add(sandbox_metadata.binding_errors);
400 let rollback_errors = stats.errors.saturating_add(sandbox_rollback_errors);
401 let rollback_successes = stats.successes.saturating_add(sandbox_metadata.executed);
402
403 if should_rollback(form, &snapshot, rollback_errors, rollback_successes) {
404 restore_snapshot(form, &snapshot);
405 changes = 0;
406 }
407
408 let js_seen_count = js_skipped + sandbox_metadata.executed;
409 let js_present = js_seen_count > 0;
410 let output_quality = if sandbox_active && js_present && sandbox_metadata.is_clean() {
411 OutputQuality::Sandboxed
412 } else if (sandbox_active && js_present) || js_skipped > 0 || other_skipped > 0 {
413 OutputQuality::BestEffort
414 } else {
415 OutputQuality::Exact
416 };
417
418 Ok(DynamicScriptOutcome {
419 changes,
420 js_present,
421 js_skipped,
422 other_skipped,
423 formcalc_run: stats.formcalc_run,
424 formcalc_errors: stats.formcalc_errors,
425 output_quality,
426 js_executed: sandbox_metadata.executed,
427 js_runtime_errors: sandbox_metadata.runtime_errors,
428 js_timeouts: sandbox_metadata.timeouts,
429 js_oom: sandbox_metadata.oom,
430 js_host_calls: sandbox_metadata.host_calls,
431 js_mutations: sandbox_metadata.mutations,
432 js_instance_writes: sandbox_metadata.instance_writes,
433 js_list_writes: sandbox_metadata.list_writes,
434 js_binding_errors: sandbox_metadata.binding_errors,
435 js_resolve_failures: sandbox_metadata.resolve_failures,
436 js_data_reads: sandbox_metadata.data_reads,
437 })
438}
439
440fn has_hidden_ancestor(
441 form: &FormTree,
442 parents: &HashMap<FormNodeId, FormNodeId>,
443 node_id: FormNodeId,
444) -> bool {
445 let mut cursor = parents.get(&node_id).copied();
446 while let Some(ancestor) = cursor {
447 if form.meta(ancestor).presence.is_not_visible() {
448 return true;
449 }
450 cursor = parents.get(&ancestor).copied();
451 }
452 false
453}
454
455#[derive(Debug, Clone, Copy, PartialEq, Eq)]
456enum ScriptPhase {
457 Initialize,
458 Calculate,
459}
460
461#[derive(Default)]
462struct ScriptStats {
463 errors: usize,
464 successes: usize,
465 formcalc_run: usize,
466 formcalc_errors: usize,
467}
468
469#[derive(Debug)]
470struct ScriptResult {
471 changes: usize,
472 error: bool,
473}
474
475fn run_script_phase(
476 form: &mut FormTree,
477 root_id: FormNodeId,
478 parents: &HashMap<FormNodeId, FormNodeId>,
479 scripts: &[(FormNodeId, Vec<EventScript>)],
480 phase: ScriptPhase,
481 max_passes: usize,
482 stats: &mut ScriptStats,
483) -> Result<usize> {
484 let mut total_changes = 0;
485
486 for _ in 0..max_passes {
487 let mut pass_changes = 0;
488
489 for (node_id, node_scripts) in scripts {
490 if has_hidden_ancestor(form, parents, *node_id) {
491 continue;
492 }
493
494 for script in node_scripts
495 .iter()
496 .filter(|script| should_run_script(script, phase))
497 {
498 let result = execute_event_script(form, root_id, parents, *node_id, script, phase)?;
499 stats.formcalc_run += 1;
500 if result.error {
501 stats.errors += 1;
502 stats.formcalc_errors += 1;
503 } else {
504 stats.successes += 1;
505 }
506 pass_changes += result.changes;
507 }
508 }
509
510 total_changes += pass_changes;
511 if pass_changes == 0 {
512 break;
513 }
514 }
515
516 Ok(total_changes)
517}
518
519fn should_run_script(script: &EventScript, phase: ScriptPhase) -> bool {
520 match phase {
521 ScriptPhase::Initialize => script.activity.as_deref() == Some("initialize"),
522 ScriptPhase::Calculate => script.activity.as_deref() == Some("calculate"),
523 }
524}
525
526fn execute_event_script(
527 form: &mut FormTree,
528 root_id: FormNodeId,
529 parents: &HashMap<FormNodeId, FormNodeId>,
530 current_id: FormNodeId,
531 script: &EventScript,
532 phase: ScriptPhase,
533) -> Result<ScriptResult> {
534 match script.language {
535 ScriptLanguage::FormCalc => Ok(execute_formcalc_script(
536 form, root_id, parents, current_id, script, phase,
537 )),
538 ScriptLanguage::JavaScript => Err(javascript_policy::reject_execution(
539 JavaScriptEntryPoint::XfaEventHook,
540 )),
541 ScriptLanguage::Other => Err(XfaError::UnsupportedFeature("script language".to_string())),
542 }
543}
544
545fn execute_formcalc_script(
546 form: &mut FormTree,
547 root_id: FormNodeId,
548 parents: &HashMap<FormNodeId, FormNodeId>,
549 current_id: FormNodeId,
550 script: &EventScript,
551 phase: ScriptPhase,
552) -> ScriptResult {
553 let Ok(tokens) = tokenize(&script.script) else {
554 return ScriptResult {
555 changes: 0,
556 error: true,
557 };
558 };
559 let Ok(ast) = parser::parse(tokens) else {
560 return ScriptResult {
561 changes: 0,
562 error: true,
563 };
564 };
565
566 let mut interpreter = Interpreter::new();
567 let mut resolver = FormTreeSomResolver::new(form, root_id, parents, current_id);
568 let Ok(result) = interpreter.exec_with_resolver(&ast, &mut resolver) else {
569 return ScriptResult {
570 changes: resolver.changes,
571 error: true,
572 };
573 };
574
575 if matches!(phase, ScriptPhase::Calculate) {
576 resolver.changes += write_formcalc_value(
577 resolver.form,
578 current_id,
579 ResolvedProperty::RawValue,
580 result,
581 );
582 }
583
584 ScriptResult {
585 changes: resolver.changes,
586 error: false,
587 }
588}
589
590#[derive(Debug, Clone, Copy, PartialEq, Eq)]
591enum ResolvedProperty {
592 RawValue,
593 Presence,
594 SomExpression,
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Eq)]
598struct ResolvedTarget {
599 node_id: FormNodeId,
600 property: ResolvedProperty,
601}
602
603struct FormTreeSomResolver<'a> {
604 form: &'a mut FormTree,
605 root_id: FormNodeId,
606 parents: &'a HashMap<FormNodeId, FormNodeId>,
607 current_id: FormNodeId,
608 changes: usize,
609}
610
611impl<'a> FormTreeSomResolver<'a> {
612 fn new(
613 form: &'a mut FormTree,
614 root_id: FormNodeId,
615 parents: &'a HashMap<FormNodeId, FormNodeId>,
616 current_id: FormNodeId,
617 ) -> Self {
618 Self {
619 form,
620 root_id,
621 parents,
622 current_id,
623 changes: 0,
624 }
625 }
626
627 fn resolve_target(&self, path: &str) -> Option<ResolvedTarget> {
628 let trimmed = path.trim();
629 if trimmed.is_empty() {
630 return None;
631 }
632
633 if matches!(trimmed, "rawValue" | "presence" | "somExpression") {
634 return Some(ResolvedTarget {
635 node_id: self.current_id,
636 property: parse_property_name(trimmed)?,
637 });
638 }
639
640 let (expr, property) = split_property_path(trimmed)?;
641 let node_id = self.resolve_expression(&expr)?.into_iter().next()?;
642 Some(ResolvedTarget { node_id, property })
643 }
644
645 fn count_targets(&self, path: &str) -> usize {
646 let trimmed = path.trim();
647 if trimmed.is_empty() {
648 return 0;
649 }
650 if matches!(trimmed, "rawValue" | "presence" | "somExpression") {
651 return 1;
652 }
653 let Some((expr, _property)) = split_property_path(trimmed) else {
654 return 0;
655 };
656 self.resolve_expression(&expr)
657 .map_or(0, |nodes| nodes.len())
658 }
659
660 fn resolve_expression(&self, expr: &SomExpression) -> Option<Vec<FormNodeId>> {
661 match expr.root {
662 SomRoot::Data | SomRoot::Record | SomRoot::Template => None,
663 SomRoot::CurrentContainer => {
664 if expr.segments.is_empty() {
665 Some(vec![self.current_id])
666 } else {
667 Some(self.follow_absolute(vec![self.current_id], &expr.segments))
668 }
669 }
670 SomRoot::Form => {
671 if expr.segments.is_empty() {
672 Some(vec![self.root_id])
673 } else {
674 Some(self.follow_absolute(vec![self.root_id], &expr.segments))
675 }
676 }
677 SomRoot::Xfa => {
678 let segments = strip_xfa_form_prefix(&expr.segments);
679 if segments.is_empty() {
680 Some(vec![self.root_id])
681 } else {
682 Some(self.follow_absolute(vec![self.root_id], segments))
683 }
684 }
685 SomRoot::Unqualified => {
686 if expr.segments.is_empty() {
687 Some(vec![self.current_id])
688 } else {
689 Some(self.follow_unqualified(&expr.segments))
690 }
691 }
692 }
693 }
694
695 fn follow_absolute(
696 &self,
697 mut current: Vec<FormNodeId>,
698 segments: &[xfa_dom_resolver::som::SomSegment],
699 ) -> Vec<FormNodeId> {
700 for (idx, segment) in segments.iter().enumerate() {
701 let allow_self = idx == 0;
702 current = current
703 .into_iter()
704 .flat_map(|node_id| self.step_from_node(node_id, segment, allow_self))
705 .collect();
706 if current.is_empty() {
707 break;
708 }
709 }
710 current
711 }
712
713 fn follow_unqualified(
714 &self,
715 segments: &[xfa_dom_resolver::som::SomSegment],
716 ) -> Vec<FormNodeId> {
717 let Some((first, rest)) = segments.split_first() else {
718 return vec![self.current_id];
719 };
720
721 let mut scope = Some(self.current_id);
722 while let Some(scope_id) = scope {
723 let anchors: Vec<_> = descendants_inclusive(self.form, scope_id)
724 .into_iter()
725 .filter(|node_id| self.node_matches_segment(*node_id, first))
726 .collect();
727 let matched = self.follow_remaining(anchors, rest);
728 if !matched.is_empty() {
729 return matched;
730 }
731 scope = self.parents.get(&scope_id).copied();
732 }
733
734 let anchors: Vec<_> = descendants_inclusive(self.form, self.root_id)
735 .into_iter()
736 .filter(|node_id| self.node_matches_segment(*node_id, first))
737 .collect();
738 self.follow_remaining(anchors, rest)
739 }
740
741 fn follow_remaining(
742 &self,
743 mut current: Vec<FormNodeId>,
744 segments: &[xfa_dom_resolver::som::SomSegment],
745 ) -> Vec<FormNodeId> {
746 for segment in segments {
747 current = current
748 .into_iter()
749 .flat_map(|node_id| self.step_from_node(node_id, segment, false))
750 .collect();
751 if current.is_empty() {
752 break;
753 }
754 }
755 current
756 }
757
758 fn step_from_node(
759 &self,
760 node_id: FormNodeId,
761 segment: &xfa_dom_resolver::som::SomSegment,
762 allow_self: bool,
763 ) -> Vec<FormNodeId> {
764 if let SomSelector::Name(name) = &segment.selector {
768 if name == ".." {
769 if let Some(&parent_id) = self.parents.get(&node_id) {
771 return apply_index_to_single(parent_id, segment.index);
772 }
773 return Vec::new();
774 }
775 }
776
777 if allow_self && self.node_matches_selector(node_id, &segment.selector) {
778 return apply_index_to_single(node_id, segment.index);
779 }
780
781 let matches: Vec<_> = self
782 .form
783 .get(node_id)
784 .children
785 .iter()
786 .copied()
787 .filter(|child_id| self.node_matches_selector(*child_id, &segment.selector))
788 .collect();
789
790 apply_index(matches, segment.index)
791 }
792
793 fn node_matches_segment(
794 &self,
795 node_id: FormNodeId,
796 segment: &xfa_dom_resolver::som::SomSegment,
797 ) -> bool {
798 if !self.node_matches_selector(node_id, &segment.selector) {
799 return false;
800 }
801
802 match segment.index {
803 SomIndex::All => true,
804 SomIndex::None => self.sibling_position(node_id, &segment.selector) == Some(0),
805 SomIndex::Specific(idx) => {
806 self.sibling_position(node_id, &segment.selector) == Some(idx)
807 }
808 }
809 }
810
811 fn sibling_position(&self, node_id: FormNodeId, selector: &SomSelector) -> Option<usize> {
812 let Some(parent_id) = self.parents.get(&node_id).copied() else {
813 return self.node_matches_selector(node_id, selector).then_some(0);
814 };
815
816 self.form
817 .get(parent_id)
818 .children
819 .iter()
820 .copied()
821 .filter(|candidate| self.node_matches_selector(*candidate, selector))
822 .position(|candidate| candidate == node_id)
823 }
824
825 fn node_matches_selector(&self, node_id: FormNodeId, selector: &SomSelector) -> bool {
826 match selector {
827 SomSelector::Name(name) => self.form.get(node_id).name == *name,
828 SomSelector::Class(class_name) => self.node_matches_class(node_id, class_name),
829 SomSelector::AllChildren => true,
830 }
831 }
832
833 fn node_matches_class(&self, node_id: FormNodeId, class_name: &str) -> bool {
834 let class_name = class_name.to_ascii_lowercase();
835 match class_name.as_str() {
836 "subform" => matches!(
837 self.form.get(node_id).node_type,
838 FormNodeType::Root | FormNodeType::Subform
839 ),
840 "pageset" => {
841 matches!(self.form.get(node_id).node_type, FormNodeType::PageSet)
842 }
843 "pagearea" => matches!(
844 self.form.get(node_id).node_type,
845 FormNodeType::PageArea { .. }
846 ),
847 "field" => matches!(self.form.get(node_id).node_type, FormNodeType::Field { .. }),
848 "draw" => matches!(
849 self.form.get(node_id).node_type,
850 FormNodeType::Draw(_) | FormNodeType::Image { .. }
851 ),
852 "exclgroup" => self.form.meta(node_id).group_kind == GroupKind::ExclusiveChoice,
853 _ => false,
854 }
855 }
856}
857
858impl SomResolver for FormTreeSomResolver<'_> {
859 fn resolve_path(
860 &mut self,
861 path: &str,
862 ) -> formcalc_interpreter::error::Result<Option<FormCalcValue>> {
863 let Some(target) = self.resolve_target(path) else {
864 if !path.trim().is_empty() {
867 log::warn!("SOM bridge: path not resolved: {:?}", path.trim());
868 }
869 return Ok(None);
870 };
871 Ok(Some(read_formcalc_value(
872 self.form,
873 self.root_id,
874 self.parents,
875 target,
876 )))
877 }
878
879 fn assign_path(
880 &mut self,
881 path: &str,
882 value: FormCalcValue,
883 ) -> formcalc_interpreter::error::Result<bool> {
884 let Some(target) = self.resolve_target(path) else {
885 if !path.trim().is_empty() {
887 log::warn!("SOM bridge: assignment target not found: {:?}", path.trim());
888 }
889 return Ok(false);
890 };
891 self.changes += write_formcalc_value(self.form, target.node_id, target.property, value);
892 Ok(true)
893 }
894
895 fn count_path_matches(&mut self, path: &str) -> formcalc_interpreter::error::Result<usize> {
896 Ok(self.count_targets(path))
897 }
898}
899
900fn split_property_path(path: &str) -> Option<(SomExpression, ResolvedProperty)> {
901 let normalized = if let Some(rest) = path.strip_prefix("this.") {
902 format!("$.{rest}")
903 } else if path == "this" {
904 "$".to_string()
905 } else {
906 path.to_string()
907 };
908
909 let mut expr = parse_som(&normalized).ok()?;
910 let property = if let Some(last) = expr.segments.last() {
911 match &last.selector {
912 SomSelector::Name(name) => {
913 parse_property_name(name).unwrap_or(ResolvedProperty::RawValue)
914 }
915 _ => ResolvedProperty::RawValue,
916 }
917 } else {
918 ResolvedProperty::RawValue
919 };
920
921 if matches!(
922 expr.segments.last().map(|segment| &segment.selector),
923 Some(SomSelector::Name(name)) if parse_property_name(name).is_some()
924 ) {
925 expr.segments.pop();
926 }
927
928 Some((expr, property))
929}
930
931fn parse_property_name(name: &str) -> Option<ResolvedProperty> {
932 match name {
933 "rawValue" => Some(ResolvedProperty::RawValue),
934 "presence" => Some(ResolvedProperty::Presence),
935 "somExpression" => Some(ResolvedProperty::SomExpression),
936 _ => None,
937 }
938}
939
940fn strip_xfa_form_prefix(
941 segments: &[xfa_dom_resolver::som::SomSegment],
942) -> &[xfa_dom_resolver::som::SomSegment] {
943 match segments.first() {
944 Some(segment)
945 if matches!(&segment.selector, SomSelector::Name(name) if name == "form")
946 && matches!(segment.index, SomIndex::None) =>
947 {
948 &segments[1..]
949 }
950 _ => segments,
951 }
952}
953
954fn apply_index(matches: Vec<FormNodeId>, index: SomIndex) -> Vec<FormNodeId> {
955 match index {
956 SomIndex::None => matches.into_iter().take(1).collect(),
957 SomIndex::Specific(idx) => matches.get(idx).copied().into_iter().collect(),
958 SomIndex::All => matches,
959 }
960}
961
962fn apply_index_to_single(node_id: FormNodeId, index: SomIndex) -> Vec<FormNodeId> {
963 match index {
964 SomIndex::None | SomIndex::Specific(0) | SomIndex::All => vec![node_id],
965 SomIndex::Specific(_) => Vec::new(),
966 }
967}
968
969fn read_formcalc_value(
970 form: &FormTree,
971 root_id: FormNodeId,
972 parents: &HashMap<FormNodeId, FormNodeId>,
973 target: ResolvedTarget,
974) -> FormCalcValue {
975 match target.property {
976 ResolvedProperty::RawValue => get_formcalc_raw_value(form, target.node_id),
977 ResolvedProperty::Presence => FormCalcValue::String(
978 match form.meta(target.node_id).presence {
979 Presence::Visible => "visible",
980 Presence::Hidden => "hidden",
981 Presence::Invisible => "invisible",
982 Presence::Inactive => "inactive",
983 }
984 .to_string(),
985 ),
986 ResolvedProperty::SomExpression => {
987 FormCalcValue::String(build_som_expression(form, root_id, parents, target.node_id))
988 }
989 }
990}
991
992fn get_formcalc_raw_value(form: &FormTree, node_id: FormNodeId) -> FormCalcValue {
993 match &form.get(node_id).node_type {
994 FormNodeType::Field { value } => string_to_formcalc_value(value),
995 _ if form.meta(node_id).group_kind == GroupKind::ExclusiveChoice => {
996 for &child_id in &form.get(node_id).children {
997 if let FormNodeType::Field { value } = &form.get(child_id).node_type {
998 if !value.is_empty() {
999 let selected = form.meta(child_id).item_value.as_deref().unwrap_or(value);
1000 return string_to_formcalc_value(selected);
1001 }
1002 }
1003 }
1004 FormCalcValue::Null
1005 }
1006 _ => FormCalcValue::Null,
1007 }
1008}
1009
1010fn write_formcalc_value(
1011 form: &mut FormTree,
1012 node_id: FormNodeId,
1013 property: ResolvedProperty,
1014 value: FormCalcValue,
1015) -> usize {
1016 match property {
1017 ResolvedProperty::RawValue => set_raw_value(form, node_id, formcalc_to_script_value(value)),
1018 ResolvedProperty::Presence => {
1019 set_presence(form, node_id, ScriptValue::String(value.to_string_val()))
1020 }
1021 ResolvedProperty::SomExpression => 0,
1022 }
1023}
1024
1025fn string_to_formcalc_value(value: &str) -> FormCalcValue {
1026 let trimmed = value.trim();
1027 if trimmed.is_empty() {
1028 FormCalcValue::Null
1029 } else if let Ok(number) = trimmed.parse::<f64>() {
1030 FormCalcValue::Number(number)
1031 } else {
1032 FormCalcValue::String(value.to_string())
1033 }
1034}
1035
1036fn formcalc_to_script_value(value: FormCalcValue) -> ScriptValue {
1037 match value {
1038 FormCalcValue::Null => ScriptValue::Null,
1039 FormCalcValue::Number(number) => ScriptValue::String(normalize_number(number)),
1040 FormCalcValue::String(value) => ScriptValue::String(value),
1041 }
1042}
1043
1044fn build_som_expression(
1045 form: &FormTree,
1046 root_id: FormNodeId,
1047 parents: &HashMap<FormNodeId, FormNodeId>,
1048 node_id: FormNodeId,
1049) -> String {
1050 let mut parts = Vec::new();
1051 let mut cursor = Some(node_id);
1052 while let Some(current) = cursor {
1053 let node = form.get(current);
1054 if !node.name.is_empty() {
1055 let index = if let Some(parent_id) = parents.get(¤t).copied() {
1056 form.get(parent_id)
1057 .children
1058 .iter()
1059 .copied()
1060 .filter(|sibling_id| form.get(*sibling_id).name == node.name)
1061 .position(|sibling_id| sibling_id == current)
1062 .unwrap_or(0)
1063 } else {
1064 0
1065 };
1066 parts.push(format!("{}[{index}]", node.name));
1067 }
1068 if current == root_id {
1069 break;
1070 }
1071 cursor = parents.get(¤t).copied();
1072 }
1073 parts.reverse();
1074
1075 if parts.is_empty() {
1076 "$form".to_string()
1077 } else {
1078 format!("$form.{}", parts.join("."))
1079 }
1080}
1081
1082fn descendants_inclusive(form: &FormTree, root_id: FormNodeId) -> Vec<FormNodeId> {
1083 let mut out = Vec::new();
1084 collect_descendants(form, root_id, &mut out);
1085 out
1086}
1087
1088fn collect_descendants(form: &FormTree, node_id: FormNodeId, out: &mut Vec<FormNodeId>) {
1089 out.push(node_id);
1090 for &child_id in &form.get(node_id).children {
1091 collect_descendants(form, child_id, out);
1092 }
1093}
1094
1095fn build_parent_map(form: &FormTree, root_id: FormNodeId) -> HashMap<FormNodeId, FormNodeId> {
1096 let mut parents = HashMap::new();
1097 populate_parent_map(form, root_id, &mut parents);
1098 parents
1099}
1100
1101fn populate_parent_map(
1102 form: &FormTree,
1103 node_id: FormNodeId,
1104 parents: &mut HashMap<FormNodeId, FormNodeId>,
1105) {
1106 for &child_id in &form.get(node_id).children {
1107 parents.insert(child_id, node_id);
1108 populate_parent_map(form, child_id, parents);
1109 }
1110}
1111
1112fn set_raw_value(form: &mut FormTree, node_id: FormNodeId, value: ScriptValue) -> usize {
1113 let value = match value {
1114 ScriptValue::Null => String::new(),
1115 ScriptValue::String(value) => value,
1116 };
1117
1118 if form.meta(node_id).group_kind == GroupKind::ExclusiveChoice {
1119 let mut changes = 0;
1120 for &child_id in &form.get(node_id).children.clone() {
1121 let item_value = form.meta(child_id).item_value.clone();
1122 let next = if item_value.as_deref() == Some(value.as_str()) {
1123 value.clone()
1124 } else {
1125 String::new()
1126 };
1127 if let FormNodeType::Field { value: field_value } =
1128 &mut form.get_mut(child_id).node_type
1129 {
1130 if *field_value != next {
1131 *field_value = next;
1132 changes += 1;
1133 }
1134 }
1135 }
1136 return changes;
1137 }
1138
1139 if let FormNodeType::Field { value: field_value } = &mut form.get_mut(node_id).node_type {
1140 if *field_value != value {
1141 *field_value = value;
1142 return 1;
1143 }
1144 }
1145
1146 0
1147}
1148
1149fn set_presence(form: &mut FormTree, node_id: FormNodeId, value: ScriptValue) -> usize {
1150 let value = match value {
1151 ScriptValue::Null => return 0,
1152 ScriptValue::String(value) => value,
1153 };
1154 let normalized = value.trim().to_ascii_lowercase();
1155 let new_presence = match normalized.as_str() {
1156 "visible" | "open" => Presence::Visible,
1157 "hidden" => Presence::Hidden,
1158 "invisible" => Presence::Invisible,
1159 "inactive" => Presence::Inactive,
1160 _ => return 0,
1161 };
1162
1163 let meta = form.meta_mut(node_id);
1164 if meta.presence == new_presence {
1165 return 0;
1166 }
1167 meta.presence = new_presence;
1168 1
1169}
1170
1171fn normalize_number(number: f64) -> String {
1172 if number.fract().abs() < f64::EPSILON {
1173 (number as i64).to_string()
1174 } else {
1175 number.to_string()
1176 }
1177}
1178
1179#[derive(Debug, Clone, PartialEq, Eq)]
1180enum ScriptValue {
1181 Null,
1182 String(String),
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187 use super::*;
1188 use xfa_layout_engine::form::{
1189 FieldKind, FormNode, FormNodeMeta, FormNodeStyle, GroupKind, Occur,
1190 };
1191 use xfa_layout_engine::text::FontMetrics;
1192 use xfa_layout_engine::types::{BoxModel, LayoutStrategy};
1193
1194 fn add_node(tree: &mut FormTree, name: &str, node_type: FormNodeType) -> FormNodeId {
1195 tree.add_node(FormNode {
1196 name: name.to_string(),
1197 node_type,
1198 box_model: BoxModel::default(),
1199 layout: LayoutStrategy::TopToBottom,
1200 children: Vec::new(),
1201 occur: Occur::once(),
1202 font: FontMetrics::default(),
1203 calculate: None,
1204 validate: None,
1205 column_widths: Vec::new(),
1206 col_span: 1,
1207 })
1208 }
1209
1210 fn empty_meta() -> FormNodeMeta {
1211 FormNodeMeta {
1212 field_kind: FieldKind::Text,
1213 group_kind: GroupKind::None,
1214 style: FormNodeStyle::default(),
1215 ..Default::default()
1216 }
1217 }
1218
1219 fn formcalc_script(script: &str, activity: &str) -> EventScript {
1220 EventScript::formcalc(script, Some(activity))
1221 }
1222
1223 fn javascript_script(script: &str, activity: &str) -> EventScript {
1224 EventScript::javascript(script, Some(activity))
1225 }
1226
1227 fn other_script(script: &str, activity: &str) -> EventScript {
1228 EventScript::new(
1229 script.to_string(),
1230 ScriptLanguage::Other,
1231 Some(activity.to_string()),
1232 None,
1233 None,
1234 )
1235 }
1236
1237 fn script_policy_fixture(include_js: bool) -> (FormTree, FormNodeId, FormNodeId) {
1238 let mut tree = FormTree::new();
1239 let root = add_node(&mut tree, "root", FormNodeType::Root);
1240 let js_hook = add_node(
1241 &mut tree,
1242 "JsHook",
1243 FormNodeType::Field {
1244 value: String::new(),
1245 },
1246 );
1247 let runner = add_node(
1248 &mut tree,
1249 "Runner",
1250 FormNodeType::Field {
1251 value: String::new(),
1252 },
1253 );
1254 let target = add_node(
1255 &mut tree,
1256 "Target",
1257 FormNodeType::Field {
1258 value: String::new(),
1259 },
1260 );
1261
1262 tree.get_mut(root).children = vec![js_hook, runner, target];
1263 if include_js {
1264 tree.meta_mut(js_hook).event_scripts = vec![javascript_script(
1265 "xfa.host.messageBox('skip');",
1266 "initialize",
1267 )];
1268 }
1269 tree.meta_mut(runner).event_scripts =
1270 vec![formcalc_script(r#"Target.rawValue = "ran""#, "initialize")];
1271
1272 (tree, root, target)
1273 }
1274
1275 fn other_language_policy_fixture() -> (FormTree, FormNodeId, FormNodeId) {
1276 let mut tree = FormTree::new();
1277 let root = add_node(&mut tree, "root", FormNodeType::Root);
1278 let other = add_node(&mut tree, "OtherHook", FormNodeType::Subform);
1279 let runner = add_node(&mut tree, "Runner", FormNodeType::Subform);
1280 let target = add_node(
1281 &mut tree,
1282 "Target",
1283 FormNodeType::Field {
1284 value: String::new(),
1285 },
1286 );
1287
1288 tree.get_mut(root).children = vec![other, runner, target];
1289 tree.meta_mut(other).event_scripts = vec![other_script("MsgBox \"skip\"", "initialize")];
1290 tree.meta_mut(runner).event_scripts =
1291 vec![formcalc_script(r#"Target.rawValue = "ran""#, "initialize")];
1292
1293 (tree, root, target)
1294 }
1295
1296 fn field_value(tree: &FormTree, node_id: FormNodeId) -> &str {
1297 match &tree.get(node_id).node_type {
1298 FormNodeType::Field { value } => value,
1299 _ => panic!("expected field"),
1300 }
1301 }
1302
1303 #[test]
1304 fn change_event_toggles_relative_hidden_subform() {
1305 let mut tree = FormTree::new();
1306 let root = add_node(&mut tree, "root", FormNodeType::Root);
1307 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
1308 let group = add_node(&mut tree, "Choice", FormNodeType::Subform);
1309 let option1 = add_node(
1310 &mut tree,
1311 "Option1",
1312 FormNodeType::Field {
1313 value: "1".to_string(),
1314 },
1315 );
1316 let option2 = add_node(
1317 &mut tree,
1318 "Option2",
1319 FormNodeType::Field {
1320 value: String::new(),
1321 },
1322 );
1323 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
1324
1325 tree.get_mut(root).children = vec![section];
1326 tree.get_mut(section).children = vec![group, details];
1327 tree.get_mut(group).children = vec![option1, option2];
1328
1329 tree.meta_mut(group).group_kind = GroupKind::ExclusiveChoice;
1330 tree.meta_mut(group).event_scripts = vec![formcalc_script(
1331 r#"
1332Details.presence = "hidden"
1333if (this.rawValue == 1) then
1334 Details.presence = "visible"
1335endif
1336"#,
1337 "initialize",
1338 )];
1339 tree.meta_mut(option1).item_value = Some("1".into());
1340 tree.meta_mut(option2).item_value = Some("2".into());
1341 tree.meta_mut(details).presence = Presence::Hidden;
1342
1343 apply_dynamic_scripts(&mut tree, root).unwrap();
1344
1345 assert_eq!(tree.meta(details).presence, Presence::Visible);
1346 }
1347
1348 #[test]
1349 fn calculate_script_on_hidden_block_uses_sibling_values() {
1350 let mut tree = FormTree::new();
1351 let root = add_node(&mut tree, "root", FormNodeType::Root);
1352 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
1353 let option1 = add_node(
1354 &mut tree,
1355 "Opt1",
1356 FormNodeType::Field {
1357 value: "1".to_string(),
1358 },
1359 );
1360 let option2 = add_node(
1361 &mut tree,
1362 "Opt2",
1363 FormNodeType::Field {
1364 value: String::new(),
1365 },
1366 );
1367 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
1368
1369 tree.get_mut(root).children = vec![section];
1370 tree.get_mut(section).children = vec![option1, option2, details];
1371 tree.meta_mut(details).presence = Presence::Hidden;
1372 tree.meta_mut(details).event_scripts = vec![formcalc_script(
1373 r#"
1374this.presence = "hidden"
1375if ((Opt1.rawValue == 1) or (Opt2.rawValue == 1)) then
1376 this.presence = "visible"
1377endif
1378"#,
1379 "calculate",
1380 )];
1381
1382 apply_dynamic_scripts(&mut tree, root).unwrap();
1383
1384 assert_eq!(tree.meta(details).presence, Presence::Visible);
1385 }
1386
1387 #[test]
1388 fn multi_pass_scripts_propagate_raw_values() {
1389 let mut tree = FormTree::new();
1390 let root = add_node(&mut tree, "root", FormNodeType::Root);
1391 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
1392 let controller = add_node(
1393 &mut tree,
1394 "Controller",
1395 FormNodeType::Field {
1396 value: "1".to_string(),
1397 },
1398 );
1399 let target = add_node(
1400 &mut tree,
1401 "Target",
1402 FormNodeType::Field {
1403 value: String::new(),
1404 },
1405 );
1406 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
1407
1408 tree.get_mut(root).children = vec![section];
1409 tree.get_mut(section).children = vec![controller, target, details];
1410
1411 tree.meta_mut(controller).event_scripts = vec![formcalc_script(
1412 r#"
1413if (this.rawValue == 1) then
1414 Target.rawValue = 1
1415endif
1416"#,
1417 "calculate",
1418 )];
1419 tree.meta_mut(details).presence = Presence::Hidden;
1420 tree.meta_mut(details).event_scripts = vec![formcalc_script(
1421 r#"
1422this.presence = "hidden"
1423if (Target.rawValue == 1) then
1424 this.presence = "visible"
1425endif
1426"#,
1427 "calculate",
1428 )];
1429
1430 apply_dynamic_scripts(&mut tree, root).unwrap();
1431
1432 if let FormNodeType::Field { value } = &tree.get(target).node_type {
1433 assert_eq!(value, "1");
1434 } else {
1435 panic!("expected field");
1436 }
1437 assert_eq!(tree.meta(details).presence, Presence::Visible);
1438 }
1439
1440 #[test]
1445 fn som_path_resolves_on_simple_form_tree() {
1446 let mut tree = FormTree::new();
1447 let root = add_node(&mut tree, "root", FormNodeType::Root);
1448 let form1 = add_node(&mut tree, "form1", FormNodeType::Subform);
1449 let subform = add_node(&mut tree, "subform1", FormNodeType::Subform);
1450 let field1 = add_node(
1451 &mut tree,
1452 "field1",
1453 FormNodeType::Field {
1454 value: "hello".to_string(),
1455 },
1456 );
1457
1458 tree.get_mut(root).children = vec![form1];
1459 tree.get_mut(form1).children = vec![subform];
1460 tree.get_mut(subform).children = vec![field1];
1461
1462 tree.meta_mut(root).event_scripts = vec![formcalc_script(
1464 "form1.subform1.field1.rawValue",
1465 "calculate",
1466 )];
1467
1468 let parents = super::build_parent_map(&tree, root);
1469 let resolver = FormTreeSomResolver::new(&mut tree, root, &parents, root);
1470 let target = resolver.resolve_target("form1.subform1.field1.rawValue");
1471 assert!(target.is_some(), "SOM path must resolve to a node");
1472 let target = target.unwrap();
1473 let val = super::read_formcalc_value(&tree, root, &parents, target);
1474 match val {
1475 formcalc_interpreter::value::Value::String(s) => assert_eq!(s, "hello"),
1476 formcalc_interpreter::value::Value::Number(n) => {
1477 panic!("expected string, got number {n}")
1479 }
1480 _ => panic!("expected string value"),
1481 }
1482 }
1483
1484 #[test]
1486 fn invalid_som_path_returns_none_not_panic() {
1487 let mut tree = FormTree::new();
1488 let root = add_node(&mut tree, "root", FormNodeType::Root);
1489 let parents = super::build_parent_map(&tree, root);
1490 let resolver = FormTreeSomResolver::new(&mut tree, root, &parents, root);
1491
1492 let result = resolver.resolve_target("nonexistent.deep.path.rawValue");
1494 assert!(
1495 result.is_none(),
1496 "invalid SOM path must return None, not panic"
1497 );
1498 }
1499
1500 #[test]
1501 fn best_effort_skips_javascript_and_runs_formcalc() {
1502 let (mut tree, root, target) = script_policy_fixture(true);
1503
1504 let outcome = apply_dynamic_scripts(&mut tree, root).unwrap();
1505
1506 assert_eq!(field_value(&tree, target), "ran");
1507 assert!(outcome.js_present);
1508 assert_eq!(outcome.js_skipped, 1);
1509 assert_eq!(outcome.other_skipped, 0);
1510 assert_eq!(outcome.formcalc_run, 1);
1511 assert_eq!(outcome.formcalc_errors, 0);
1512 assert_eq!(outcome.output_quality, OutputQuality::BestEffort);
1513 }
1514
1515 #[test]
1516 fn strict_mode_preserves_javascript_reject() {
1517 let (mut tree, root, target) = script_policy_fixture(true);
1518
1519 let err =
1520 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
1521
1522 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
1523 assert_eq!(field_value(&tree, target), "");
1524 }
1525
1526 #[test]
1527 fn formcalc_only_reports_exact_quality() {
1528 let (mut tree, root, target) = script_policy_fixture(false);
1529
1530 let outcome = apply_dynamic_scripts(&mut tree, root).unwrap();
1531
1532 assert_eq!(field_value(&tree, target), "ran");
1533 assert!(!outcome.js_present);
1534 assert_eq!(outcome.js_skipped, 0);
1535 assert_eq!(outcome.other_skipped, 0);
1536 assert_eq!(outcome.formcalc_run, 1);
1537 assert_eq!(outcome.formcalc_errors, 0);
1538 assert_eq!(outcome.output_quality, OutputQuality::Exact);
1539 }
1540
1541 #[test]
1542 fn other_language_scripts_skip_in_best_effort_and_reject_in_strict() {
1543 let (mut tree, root, target) = other_language_policy_fixture();
1544 let (mut strict_tree, strict_root, _) = other_language_policy_fixture();
1545
1546 let outcome = apply_dynamic_scripts(&mut tree, root).unwrap();
1547 assert_eq!(field_value(&tree, target), "ran");
1548 assert_eq!(outcome.js_skipped, 0);
1549 assert_eq!(outcome.other_skipped, 1);
1550 assert_eq!(outcome.formcalc_run, 1);
1551 assert_eq!(outcome.output_quality, OutputQuality::BestEffort);
1552
1553 let err =
1554 apply_dynamic_scripts_with_mode(&mut strict_tree, strict_root, JsExecutionMode::Strict)
1555 .unwrap_err();
1556 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
1557 }
1558
1559 #[test]
1560 fn javascript_direct_executor_call_is_still_denied() {
1561 let mut tree = FormTree::new();
1562 let root = add_node(&mut tree, "root", FormNodeType::Root);
1563 let trigger = add_node(
1564 &mut tree,
1565 "Trigger",
1566 FormNodeType::Field {
1567 value: String::new(),
1568 },
1569 );
1570 tree.get_mut(root).children = vec![trigger];
1571
1572 let parents = build_parent_map(&tree, root);
1573 let script = javascript_script("xfa.host.messageBox('deny');", "initialize");
1574 let err = execute_event_script(
1575 &mut tree,
1576 root,
1577 &parents,
1578 trigger,
1579 &script,
1580 ScriptPhase::Initialize,
1581 )
1582 .unwrap_err();
1583
1584 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
1585 }
1586
1587 #[test]
1588 fn javascript_resolve_node_call_is_explicitly_denied() {
1589 let mut tree = FormTree::new();
1590 let root = add_node(&mut tree, "root", FormNodeType::Root);
1591 let form = add_node(&mut tree, "formulier1", FormNodeType::Subform);
1592 let admin = add_node(&mut tree, "ADMIN", FormNodeType::Subform);
1593 let lock = add_node(
1594 &mut tree,
1595 "LockForm_AD",
1596 FormNodeType::Field {
1597 value: "1".to_string(),
1598 },
1599 );
1600 let reset = add_node(
1601 &mut tree,
1602 "Reset",
1603 FormNodeType::Field {
1604 value: "1".to_string(),
1605 },
1606 );
1607
1608 tree.get_mut(root).children = vec![form];
1609 tree.get_mut(form).children = vec![admin, reset];
1610 tree.get_mut(admin).children = vec![lock];
1611 tree.meta_mut(reset).event_scripts = vec![javascript_script(
1612 r#"xfa.resolveNode("formulier1.ADMIN.LockForm_AD").rawValue = 0;"#,
1613 "initialize",
1614 )];
1615
1616 let err =
1617 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
1618 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
1619
1620 if let FormNodeType::Field { value } = &tree.get(lock).node_type {
1621 assert_eq!(value, "1");
1622 } else {
1623 panic!("expected field");
1624 }
1625 }
1626
1627 #[test]
1628 fn javascript_utils_hide_if_empty_is_explicitly_denied() {
1629 let mut tree = FormTree::new();
1630 let root = add_node(&mut tree, "root", FormNodeType::Root);
1631 let empty = add_node(
1632 &mut tree,
1633 "EmptyField",
1634 FormNodeType::Field {
1635 value: String::new(),
1636 },
1637 );
1638 tree.get_mut(root).children = vec![empty];
1639 tree.meta_mut(empty).event_scripts =
1640 vec![javascript_script("Utils.hideIfEmpty(this);", "initialize")];
1641
1642 let err =
1643 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
1644 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
1645
1646 assert!(!tree.meta(empty).presence.is_not_visible());
1647 }
1648
1649 #[test]
1650 fn malformed_javascript_payload_is_explicitly_denied_without_panic() {
1651 let mut tree = FormTree::new();
1652 let root = add_node(&mut tree, "root", FormNodeType::Root);
1653 let container = add_node(&mut tree, "Container", FormNodeType::Subform);
1654 let empty = add_node(
1655 &mut tree,
1656 "EmptyField",
1657 FormNodeType::Field {
1658 value: String::new(),
1659 },
1660 );
1661
1662 tree.get_mut(root).children = vec![container];
1663 tree.get_mut(container).children = vec![empty];
1664 tree.meta_mut(empty).event_scripts = vec![javascript_script(
1665 "\0}{{not.valid.javascript(",
1666 "initialize",
1667 )];
1668
1669 let err =
1670 apply_dynamic_scripts_with_mode(&mut tree, root, JsExecutionMode::Strict).unwrap_err();
1671 assert!(matches!(err, XfaError::UnsupportedFeature(feature) if feature == "javascript"));
1672
1673 assert!(!tree.meta(container).presence.is_not_visible());
1674 }
1675
1676 #[test]
1677 fn default_meta_helper_is_constructible() {
1678 let meta = empty_meta();
1679 assert_eq!(meta.group_kind, GroupKind::None);
1680 }
1681
1682 #[test]
1683 fn calculate_event_applies_formcalc_return_value() {
1684 let mut tree = FormTree::new();
1685 let root = add_node(&mut tree, "root", FormNodeType::Root);
1686 let total = add_node(
1687 &mut tree,
1688 "Total",
1689 FormNodeType::Field {
1690 value: String::new(),
1691 },
1692 );
1693
1694 tree.get_mut(root).children = vec![total];
1695 tree.meta_mut(total).event_scripts = vec![formcalc_script("40 + 2", "calculate")];
1696
1697 apply_dynamic_scripts(&mut tree, root).unwrap();
1698
1699 match &tree.get(total).node_type {
1700 FormNodeType::Field { value } => assert_eq!(value, "42"),
1701 _ => panic!("expected field"),
1702 }
1703 }
1704
1705 #[test]
1706 fn calculate_event_resolves_bare_field_names_as_raw_values() {
1707 let mut tree = FormTree::new();
1708 let root = add_node(&mut tree, "root", FormNodeType::Root);
1709 let section = add_node(&mut tree, "Section", FormNodeType::Subform);
1710 let number1 = add_node(
1711 &mut tree,
1712 "Number1",
1713 FormNodeType::Field {
1714 value: "40".to_string(),
1715 },
1716 );
1717 let number2 = add_node(
1718 &mut tree,
1719 "Number2",
1720 FormNodeType::Field {
1721 value: "2".to_string(),
1722 },
1723 );
1724 let total = add_node(
1725 &mut tree,
1726 "Total",
1727 FormNodeType::Field {
1728 value: String::new(),
1729 },
1730 );
1731
1732 tree.get_mut(root).children = vec![section];
1733 tree.get_mut(section).children = vec![number1, number2, total];
1734 tree.meta_mut(total).event_scripts =
1735 vec![formcalc_script("Number1 + Number2", "calculate")];
1736
1737 apply_dynamic_scripts(&mut tree, root).unwrap();
1738
1739 match &tree.get(total).node_type {
1740 FormNodeType::Field { value } => assert_eq!(value, "42"),
1741 _ => panic!("expected field"),
1742 }
1743 }
1744
1745 #[test]
1746 fn click_events_are_skipped_during_flatten() {
1747 let mut tree = FormTree::new();
1748 let root = add_node(&mut tree, "root", FormNodeType::Root);
1749 let trigger = add_node(
1750 &mut tree,
1751 "Trigger",
1752 FormNodeType::Field {
1753 value: "1".to_string(),
1754 },
1755 );
1756 let details = add_node(&mut tree, "Details", FormNodeType::Subform);
1757
1758 tree.get_mut(root).children = vec![trigger, details];
1759 tree.meta_mut(details).presence = Presence::Hidden;
1760 tree.meta_mut(trigger).event_scripts = vec![formcalc_script(
1761 r#"
1762Details.presence = "visible"
1763"#,
1764 "click",
1765 )];
1766
1767 apply_dynamic_scripts(&mut tree, root).unwrap();
1768
1769 assert_eq!(tree.meta(details).presence, Presence::Hidden);
1770 }
1771
1772 #[test]
1773 fn rollback_when_scripts_mostly_error() {
1774 let mut tree = FormTree::new();
1778 let root = add_node(&mut tree, "root", FormNodeType::Root);
1779 let field_a = add_node(
1780 &mut tree,
1781 "FieldA",
1782 FormNodeType::Field {
1783 value: "hello".to_string(),
1784 },
1785 );
1786 let field_b = add_node(
1787 &mut tree,
1788 "FieldB",
1789 FormNodeType::Field {
1790 value: "world".to_string(),
1791 },
1792 );
1793
1794 tree.get_mut(root).children = vec![field_a, field_b];
1795
1796 tree.meta_mut(field_a).event_scripts = vec![formcalc_script("@@INVALID@@", "initialize")];
1798 tree.meta_mut(field_b).event_scripts =
1799 vec![formcalc_script("@@ALSO_BROKEN@@", "initialize")];
1800
1801 apply_dynamic_scripts(&mut tree, root).unwrap();
1802
1803 match &tree.get(field_a).node_type {
1805 FormNodeType::Field { value } => assert_eq!(value, "hello"),
1806 _ => panic!("expected field"),
1807 }
1808 match &tree.get(field_b).node_type {
1809 FormNodeType::Field { value } => assert_eq!(value, "world"),
1810 _ => panic!("expected field"),
1811 }
1812 }
1813
1814 #[test]
1815 fn rollback_when_populated_fields_go_empty() {
1816 let mut tree = FormTree::new();
1819 let root = add_node(&mut tree, "root", FormNodeType::Root);
1820 let field_a = add_node(
1821 &mut tree,
1822 "FieldA",
1823 FormNodeType::Field {
1824 value: "keep".to_string(),
1825 },
1826 );
1827 let field_b = add_node(
1828 &mut tree,
1829 "FieldB",
1830 FormNodeType::Field {
1831 value: "also_keep".to_string(),
1832 },
1833 );
1834
1835 tree.get_mut(root).children = vec![field_a, field_b];
1836
1837 let snapshot = super::snapshot_form(&tree);
1843
1844 if let FormNodeType::Field { value } = &mut tree.get_mut(field_a).node_type {
1846 *value = String::new();
1847 }
1848 if let FormNodeType::Field { value } = &mut tree.get_mut(field_b).node_type {
1849 *value = String::new();
1850 }
1851
1852 assert!(super::should_rollback(&tree, &snapshot, 0, 2));
1853
1854 super::restore_snapshot(&mut tree, &snapshot);
1855
1856 match &tree.get(field_a).node_type {
1857 FormNodeType::Field { value } => assert_eq!(value, "keep"),
1858 _ => panic!("expected field"),
1859 }
1860 match &tree.get(field_b).node_type {
1861 FormNodeType::Field { value } => assert_eq!(value, "also_keep"),
1862 _ => panic!("expected field"),
1863 }
1864 }
1865
1866 #[test]
1867 fn no_rollback_when_scripts_succeed() {
1868 let mut tree = FormTree::new();
1870 let root = add_node(&mut tree, "root", FormNodeType::Root);
1871 let total = add_node(
1872 &mut tree,
1873 "Total",
1874 FormNodeType::Field {
1875 value: String::new(),
1876 },
1877 );
1878
1879 tree.get_mut(root).children = vec![total];
1880 tree.meta_mut(total).event_scripts = vec![formcalc_script("40 + 2", "calculate")];
1881
1882 apply_dynamic_scripts(&mut tree, root).unwrap();
1883
1884 match &tree.get(total).node_type {
1885 FormNodeType::Field { value } => assert_eq!(value, "42"),
1886 _ => panic!("expected field"),
1887 }
1888 }
1889}