1use std::cell::RefCell;
29use std::panic::{catch_unwind, AssertUnwindSafe};
30use std::rc::Rc;
31use std::sync::{
32 atomic::{AtomicBool, AtomicU64, Ordering},
33 Arc, OnceLock,
34};
35use std::time::{Duration, Instant};
36
37use rquickjs::function::Opt;
38use rquickjs::{CatchResultExt, Coerced, Context, Function, Object, Persistent, Runtime};
39use xfa_layout_engine::form::{FormNodeId, FormTree};
40
41use super::regex_guard::{scan_script_for_redos, RegexScanVerdict};
42use super::{
43 activity_allowed_for_sandbox_with_gate, walltime_fallback_multiplier, HostBindings,
44 RuntimeMetadata, RuntimeOutcome, SandboxError, XfaJsRuntime, DEFAULT_MEMORY_BUDGET_BYTES,
45 DEFAULT_TIME_BUDGET_MS, MAX_SCRIPT_BODY_BYTES, MAX_VARIABLES_SCRIPT_BODY_BYTES,
46};
47
48pub struct QuickJsRuntime {
52 eval_script: Option<Persistent<Function<'static>>>,
53 set_variables_script: Option<Persistent<Function<'static>>>,
56 clear_variables_scripts: Option<Persistent<Function<'static>>>,
59 set_variables_data_item: Option<Persistent<Function<'static>>>,
65 context: Context,
66 runtime: Runtime,
67 metadata: RuntimeMetadata,
68 time_budget: Duration,
69 memory_budget_bytes: usize,
70 script_deadline: Arc<AtomicU64>,
71 script_started: Arc<AtomicBool>,
72 script_start_nanos: Arc<AtomicU64>,
80 walltime_fallback_multiplier: u32,
85 host: Rc<RefCell<HostBindings>>,
86 bindings_registered: bool,
87}
88
89impl std::fmt::Debug for QuickJsRuntime {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.debug_struct("QuickJsRuntime")
92 .field("metadata", &self.metadata)
93 .field("time_budget_ms", &self.time_budget.as_millis())
94 .field("memory_budget_bytes", &self.memory_budget_bytes)
95 .finish()
96 }
97}
98
99fn parse_node_id_csv(raw: &str) -> Vec<FormNodeId> {
100 raw.split(',')
101 .filter_map(|part| part.trim().parse::<usize>().ok())
102 .map(FormNodeId)
103 .collect()
104}
105
106impl QuickJsRuntime {
107 pub fn new() -> Result<Self, SandboxError> {
109 let runtime =
110 Runtime::new().map_err(|e| SandboxError::ScriptError(format!("rquickjs init: {e}")))?;
111 runtime.set_memory_limit(DEFAULT_MEMORY_BUDGET_BYTES);
112 let context = Context::full(&runtime)
113 .map_err(|e| SandboxError::ScriptError(format!("rquickjs context: {e}")))?;
114
115 let script_deadline = Arc::new(AtomicU64::new(0));
116 let script_started = Arc::new(AtomicBool::new(false));
117 let script_start_nanos = Arc::new(AtomicU64::new(0));
121
122 let deadline_for_handler = Arc::clone(&script_deadline);
127 let started_for_handler = Arc::clone(&script_started);
128 runtime.set_interrupt_handler(Some(Box::new(move || {
129 if !started_for_handler.load(Ordering::Acquire) {
130 return false;
131 }
132 let deadline_nanos = deadline_for_handler.load(Ordering::Acquire);
133 if deadline_nanos == 0 {
134 return false;
135 }
136 let now_nanos = Instant::now()
137 .checked_duration_since(epoch())
138 .map(|d| d.as_nanos() as u64)
139 .unwrap_or(0);
140 now_nanos >= deadline_nanos
141 })));
142
143 Ok(Self {
144 eval_script: None,
145 set_variables_script: None,
146 clear_variables_scripts: None,
147 set_variables_data_item: None,
148 context,
149 runtime,
150 metadata: RuntimeMetadata::default(),
151 time_budget: Duration::from_millis(DEFAULT_TIME_BUDGET_MS),
152 memory_budget_bytes: DEFAULT_MEMORY_BUDGET_BYTES,
153 script_deadline,
154 script_started,
155 script_start_nanos,
156 walltime_fallback_multiplier: walltime_fallback_multiplier(),
157 host: Rc::new(RefCell::new(HostBindings::new())),
158 bindings_registered: false,
159 })
160 }
161
162 pub fn with_time_budget(mut self, budget: Duration) -> Self {
165 self.time_budget = budget;
166 self
167 }
168
169 pub fn with_memory_budget(mut self, bytes: usize) -> Self {
172 self.memory_budget_bytes = bytes;
173 self.runtime.set_memory_limit(bytes);
174 self
175 }
176
177 pub fn with_walltime_fallback_multiplier(mut self, multiplier: u32) -> Self {
185 self.walltime_fallback_multiplier = multiplier.max(2);
186 self
187 }
188
189 fn set_deadline(&self) {
190 let now = Instant::now()
191 .checked_duration_since(epoch())
192 .unwrap_or(Duration::ZERO);
193 let deadline = now + self.time_budget;
194 self.script_deadline
195 .store(deadline.as_nanos() as u64, Ordering::Release);
196 self.script_start_nanos
200 .store(now.as_nanos() as u64, Ordering::Release);
201 self.script_started.store(true, Ordering::Release);
202 }
203
204 fn clear_deadline(&self) {
205 self.script_started.store(false, Ordering::Release);
206 self.script_deadline.store(0, Ordering::Release);
207 }
210
211 fn classify_timeout_or_walltime(&self, now_nanos: u64) -> SandboxError {
228 let start = self.script_start_nanos.load(Ordering::Acquire);
229 if start == 0 || now_nanos <= start {
230 return SandboxError::Timeout;
234 }
235 let elapsed_nanos = now_nanos - start;
236 let threshold_nanos = (self.time_budget.as_nanos() as u64)
237 .saturating_mul(self.walltime_fallback_multiplier as u64);
238 if elapsed_nanos >= threshold_nanos {
239 let elapsed_ms = elapsed_nanos / 1_000_000;
240 let budget_ms = self.time_budget.as_millis();
241 SandboxError::WallTimeExceeded(format!(
242 "{elapsed_ms} ms ≥ {mult}× budget ({budget_ms} ms)",
243 mult = self.walltime_fallback_multiplier
244 ))
245 } else {
246 SandboxError::Timeout
247 }
248 }
249
250 fn register_host_bindings(&mut self) -> Result<(), String> {
251 if self.bindings_registered {
252 return Ok(());
253 }
254
255 let host = Rc::clone(&self.host);
256 let eval_script = self.context.with(|ctx| {
257 let globals = ctx.globals();
258 let internal =
259 Object::new(ctx.clone()).map_err(|e| format!("host internal object: {e}"))?;
260
261 let resolve_host = Rc::clone(&host);
262 let resolve_node_id = Function::new(ctx.clone(), move |path: Opt<Coerced<String>>| {
263 let Some(path) = path.0 else {
264 let _ = resolve_host.borrow_mut().resolve_node("");
265 return -1i32;
266 };
267 resolve_host
268 .borrow_mut()
269 .resolve_node(&path.0)
270 .map(|node_id| node_id.0 as i32)
271 .unwrap_or(-1)
272 })
273 .map_err(|e| format!("resolveNodeId: {e}"))?;
274 internal
275 .set("resolveNodeId", resolve_node_id)
276 .map_err(|e| format!("set resolveNodeId: {e}"))?;
277
278 let resolve_nodes_host = Rc::clone(&host);
279 let resolve_node_ids =
280 Function::new(ctx.clone(), move |path: Opt<Coerced<String>>| -> Vec<i32> {
281 let Some(path) = path.0 else {
282 let _ = resolve_nodes_host.borrow_mut().resolve_nodes("");
283 return Vec::new();
284 };
285 resolve_nodes_host
286 .borrow_mut()
287 .resolve_nodes(&path.0)
288 .into_iter()
289 .map(|node_id| node_id.0 as i32)
290 .collect()
291 })
292 .map_err(|e| format!("resolveNodeIds: {e}"))?;
293 internal
294 .set("resolveNodeIds", resolve_node_ids)
295 .map_err(|e| format!("set resolveNodeIds: {e}"))?;
296
297 let generation_host = Rc::clone(&host);
298 let generation = Function::new(ctx.clone(), move || {
299 generation_host.borrow().generation() as i64
300 })
301 .map_err(|e| format!("generation: {e}"))?;
302 internal
303 .set("generation", generation)
304 .map_err(|e| format!("set generation: {e}"))?;
305
306 let current_host = Rc::clone(&host);
307 let current_node = Function::new(ctx.clone(), move || {
308 current_host
309 .borrow()
310 .current_node()
311 .map(|node_id| node_id.0 as i32)
312 .unwrap_or(-1)
313 })
314 .map_err(|e| format!("currentNodeId: {e}"))?;
315 internal
316 .set("currentNodeId", current_node)
317 .map_err(|e| format!("set currentNodeId: {e}"))?;
318
319 let implicit_host = Rc::clone(&host);
320 let resolve_implicit_node_id = Function::new(
321 ctx.clone(),
322 move |current_id: i32, name: Opt<Coerced<String>>| -> i32 {
323 if current_id < 0 {
324 return -1;
325 }
326 let Some(name) = name.0 else {
327 return -1;
328 };
329 implicit_host
330 .borrow_mut()
331 .resolve_implicit(FormNodeId(current_id as usize), &name.0)
332 .map(|node_id| node_id.0 as i32)
333 .unwrap_or(-1)
334 },
335 )
336 .map_err(|e| format!("resolveImplicitNodeId: {e}"))?;
337 internal
338 .set("resolveImplicitNodeId", resolve_implicit_node_id)
339 .map_err(|e| format!("set resolveImplicitNodeId: {e}"))?;
340
341 let implicit_candidates_host = Rc::clone(&host);
342 let resolve_implicit_node_ids = Function::new(
343 ctx.clone(),
344 move |current_id: i32, name: Opt<Coerced<String>>| -> Vec<i32> {
345 if current_id < 0 {
346 return Vec::new();
347 }
348 let Some(name) = name.0 else {
349 return Vec::new();
350 };
351 implicit_candidates_host
352 .borrow_mut()
353 .resolve_implicit_candidates(FormNodeId(current_id as usize), &name.0)
354 .into_iter()
355 .map(|node_id| node_id.0 as i32)
356 .collect()
357 },
358 )
359 .map_err(|e| format!("resolveImplicitNodeIds: {e}"))?;
360 internal
361 .set("resolveImplicitNodeIds", resolve_implicit_node_ids)
362 .map_err(|e| format!("set resolveImplicitNodeIds: {e}"))?;
363
364 let implicit_quiet_host = Rc::clone(&host);
370 let resolve_implicit_node_ids_quiet = Function::new(
371 ctx.clone(),
372 move |current_id: i32, name: Opt<Coerced<String>>| -> Vec<i32> {
373 if current_id < 0 {
374 return Vec::new();
375 }
376 let Some(name) = name.0 else {
377 return Vec::new();
378 };
379 implicit_quiet_host
380 .borrow_mut()
381 .resolve_implicit_candidates_quiet(FormNodeId(current_id as usize), &name.0)
382 .into_iter()
383 .map(|node_id| node_id.0 as i32)
384 .collect()
385 },
386 )
387 .map_err(|e| format!("resolveImplicitNodeIdsQuiet: {e}"))?;
388 internal
389 .set(
390 "resolveImplicitNodeIdsQuiet",
391 resolve_implicit_node_ids_quiet,
392 )
393 .map_err(|e| format!("set resolveImplicitNodeIdsQuiet: {e}"))?;
394
395 let child_host = Rc::clone(&host);
396 let resolve_child_node_id = Function::new(
397 ctx.clone(),
398 move |parent_id: i32, name: Opt<Coerced<String>>| {
399 if parent_id < 0 {
400 return -1i32;
401 }
402 let Some(name) = name.0 else {
403 return -1;
404 };
405 child_host
406 .borrow_mut()
407 .resolve_child(FormNodeId(parent_id as usize), &name.0)
408 .map(|node_id| node_id.0 as i32)
409 .unwrap_or(-1)
410 },
411 )
412 .map_err(|e| format!("resolveChildNodeId: {e}"))?;
413 internal
414 .set("resolveChildNodeId", resolve_child_node_id)
415 .map_err(|e| format!("set resolveChildNodeId: {e}"))?;
416
417 let child_candidates_host = Rc::clone(&host);
418 let resolve_child_node_ids = Function::new(
419 ctx.clone(),
420 move |parent_ids: Opt<Coerced<String>>, name: Opt<Coerced<String>>| -> Vec<i32> {
421 let Some(parent_ids) = parent_ids.0 else {
422 return Vec::new();
423 };
424 let Some(name) = name.0 else {
425 return Vec::new();
426 };
427 child_candidates_host
428 .borrow_mut()
429 .resolve_child_candidates(&parse_node_id_csv(&parent_ids.0), &name.0)
430 .into_iter()
431 .map(|node_id| node_id.0 as i32)
432 .collect()
433 },
434 )
435 .map_err(|e| format!("resolveChildNodeIds: {e}"))?;
436 internal
437 .set("resolveChildNodeIds", resolve_child_node_ids)
438 .map_err(|e| format!("set resolveChildNodeIds: {e}"))?;
439
440 let child_quiet_host = Rc::clone(&host);
446 let resolve_child_node_ids_quiet = Function::new(
447 ctx.clone(),
448 move |parent_ids: Opt<Coerced<String>>, name: Opt<Coerced<String>>| -> Vec<i32> {
449 let Some(parent_ids) = parent_ids.0 else {
450 return Vec::new();
451 };
452 let Some(name) = name.0 else {
453 return Vec::new();
454 };
455 child_quiet_host
456 .borrow_mut()
457 .resolve_child_candidates_quiet(&parse_node_id_csv(&parent_ids.0), &name.0)
458 .into_iter()
459 .map(|node_id| node_id.0 as i32)
460 .collect()
461 },
462 )
463 .map_err(|e| format!("resolveChildNodeIdsQuiet: {e}"))?;
464 internal
465 .set("resolveChildNodeIdsQuiet", resolve_child_node_ids_quiet)
466 .map_err(|e| format!("set resolveChildNodeIdsQuiet: {e}"))?;
467
468 let implicit_hinted_host = Rc::clone(&host);
475 let resolve_implicit_node_ids_hinted = Function::new(
476 ctx.clone(),
477 move |current_id: i32,
478 name: Opt<Coerced<String>>,
479 hint: Opt<Coerced<String>>|
480 -> Vec<i32> {
481 if current_id < 0 {
482 return Vec::new();
483 }
484 let Some(name) = name.0 else {
485 return Vec::new();
486 };
487 let hint = hint.0.map(|s| s.0).unwrap_or_default();
488 implicit_hinted_host
489 .borrow_mut()
490 .resolve_implicit_candidates_hinted(
491 FormNodeId(current_id as usize),
492 &name.0,
493 &hint,
494 )
495 .into_iter()
496 .map(|node_id| node_id.0 as i32)
497 .collect()
498 },
499 )
500 .map_err(|e| format!("resolveImplicitNodeIdsHinted: {e}"))?;
501 internal
502 .set(
503 "resolveImplicitNodeIdsHinted",
504 resolve_implicit_node_ids_hinted,
505 )
506 .map_err(|e| format!("set resolveImplicitNodeIdsHinted: {e}"))?;
507
508 let child_hinted_host = Rc::clone(&host);
509 let resolve_child_node_ids_hinted = Function::new(
510 ctx.clone(),
511 move |parent_ids: Opt<Coerced<String>>,
512 name: Opt<Coerced<String>>,
513 hint: Opt<Coerced<String>>|
514 -> Vec<i32> {
515 let Some(parent_ids) = parent_ids.0 else {
516 return Vec::new();
517 };
518 let Some(name) = name.0 else {
519 return Vec::new();
520 };
521 let hint = hint.0.map(|s| s.0).unwrap_or_default();
522 child_hinted_host
523 .borrow_mut()
524 .resolve_child_candidates_hinted(
525 &parse_node_id_csv(&parent_ids.0),
526 &name.0,
527 &hint,
528 )
529 .into_iter()
530 .map(|node_id| node_id.0 as i32)
531 .collect()
532 },
533 )
534 .map_err(|e| format!("resolveChildNodeIdsHinted: {e}"))?;
535 internal
536 .set("resolveChildNodeIdsHinted", resolve_child_node_ids_hinted)
537 .map_err(|e| format!("set resolveChildNodeIdsHinted: {e}"))?;
538
539 let scoped_candidates_host = Rc::clone(&host);
540 let resolve_scoped_node_ids = Function::new(
541 ctx.clone(),
542 move |scope_ids: Opt<Coerced<String>>, name: Opt<Coerced<String>>| -> Vec<i32> {
543 let Some(scope_ids) = scope_ids.0 else {
544 return Vec::new();
545 };
546 let Some(name) = name.0 else {
547 return Vec::new();
548 };
549 scoped_candidates_host
550 .borrow_mut()
551 .resolve_scoped_candidates(&parse_node_id_csv(&scope_ids.0), &name.0)
552 .into_iter()
553 .map(|node_id| node_id.0 as i32)
554 .collect()
555 },
556 )
557 .map_err(|e| format!("resolveScopedNodeIds: {e}"))?;
558 internal
559 .set("resolveScopedNodeIds", resolve_scoped_node_ids)
560 .map_err(|e| format!("set resolveScopedNodeIds: {e}"))?;
561
562 let full_chain_host = Rc::clone(&host);
571 let resolve_with_full_chain = Function::new(
572 ctx.clone(),
573 move |parent_ids_csv: Opt<Coerced<String>>,
574 current_id: i32,
575 chain_csv: Opt<Coerced<String>>|
576 -> Vec<i32> {
577 let parents: Vec<FormNodeId> = parent_ids_csv
578 .0
579 .map(|s| parse_node_id_csv(&s.0))
580 .unwrap_or_default();
581 let chain: Vec<String> = chain_csv
582 .0
583 .map(|s| s.0.split(',').map(|part| part.trim().to_string()).collect())
584 .unwrap_or_default();
585 let implicit_origin = if current_id >= 0 {
586 Some(FormNodeId(current_id as usize))
587 } else {
588 None
589 };
590 full_chain_host
591 .borrow_mut()
592 .resolve_with_full_chain(&parents, &chain, implicit_origin)
593 .into_iter()
594 .map(|node_id| node_id.0 as i32)
595 .collect()
596 },
597 )
598 .map_err(|e| format!("resolveWithFullChain: {e}"))?;
599 internal
600 .set("resolveWithFullChain", resolve_with_full_chain)
601 .map_err(|e| format!("set resolveWithFullChain: {e}"))?;
602
603 let full_chain_strict_host = Rc::clone(&host);
609 let resolve_with_full_chain_strict = Function::new(
610 ctx.clone(),
611 move |parent_ids_csv: Opt<Coerced<String>>,
612 current_id: i32,
613 chain_csv: Opt<Coerced<String>>|
614 -> Vec<i32> {
615 let parents: Vec<FormNodeId> = parent_ids_csv
616 .0
617 .map(|s| parse_node_id_csv(&s.0))
618 .unwrap_or_default();
619 let chain: Vec<String> = chain_csv
620 .0
621 .map(|s| s.0.split(',').map(|part| part.trim().to_string()).collect())
622 .unwrap_or_default();
623 let implicit_origin = if current_id >= 0 {
624 Some(FormNodeId(current_id as usize))
625 } else {
626 None
627 };
628 full_chain_strict_host
629 .borrow_mut()
630 .resolve_with_full_chain_strict(&parents, &chain, implicit_origin)
631 .into_iter()
632 .map(|node_id| node_id.0 as i32)
633 .collect()
634 },
635 )
636 .map_err(|e| format!("resolveWithFullChainStrict: {e}"))?;
637 internal
638 .set("resolveWithFullChainStrict", resolve_with_full_chain_strict)
639 .map_err(|e| format!("set resolveWithFullChainStrict: {e}"))?;
640
641 let get_raw_host = Rc::clone(&host);
642 let get_raw_value = Function::new(
643 ctx.clone(),
644 move |id: i32, generation: i64| -> Option<String> {
645 if id < 0 || generation < 0 {
646 return None;
647 }
648 get_raw_host
649 .borrow_mut()
650 .get_raw_value(FormNodeId(id as usize), generation as u64)
651 },
652 )
653 .map_err(|e| format!("getRawValue: {e}"))?;
654 internal
655 .set("getRawValue", get_raw_value)
656 .map_err(|e| format!("set getRawValue: {e}"))?;
657
658 let set_raw_host = Rc::clone(&host);
659 let set_raw_value = Function::new(
660 ctx.clone(),
661 move |id: i32, generation: i64, value: Coerced<String>| -> bool {
662 if id < 0 || generation < 0 {
663 return false;
664 }
665 set_raw_host.borrow_mut().set_raw_value(
666 FormNodeId(id as usize),
667 value.0,
668 generation as u64,
669 )
670 },
671 )
672 .map_err(|e| format!("setRawValue: {e}"))?;
673 internal
674 .set("setRawValue", set_raw_value)
675 .map_err(|e| format!("set setRawValue: {e}"))?;
676
677 let instance_count_host = Rc::clone(&host);
678 let instance_count =
679 Function::new(ctx.clone(), move |id: i32, generation: i64| -> u32 {
680 if id < 0 || generation < 0 {
681 return 0;
682 }
683 instance_count_host
684 .borrow_mut()
685 .instance_count_for_handle(FormNodeId(id as usize), generation as u64)
686 })
687 .map_err(|e| format!("instanceCount: {e}"))?;
688 internal
689 .set("instanceCount", instance_count)
690 .map_err(|e| format!("set instanceCount: {e}"))?;
691
692 let zero_instance_host = Rc::clone(&host);
693 let has_zero_instance_run = Function::new(
694 ctx.clone(),
695 move |id: i32, generation: i64, name: Opt<Coerced<String>>| -> bool {
696 if id < 0 || generation < 0 {
697 return false;
698 }
699 let Some(name) = name.0 else {
700 return false;
701 };
702 zero_instance_host.borrow_mut().has_zero_instance_run(
703 FormNodeId(id as usize),
704 generation as u64,
705 &name.0,
706 )
707 },
708 )
709 .map_err(|e| format!("hasZeroInstanceRun: {e}"))?;
710 internal
711 .set("hasZeroInstanceRun", has_zero_instance_run)
712 .map_err(|e| format!("set hasZeroInstanceRun: {e}"))?;
713
714 let node_index_host = Rc::clone(&host);
715 let node_index = Function::new(ctx.clone(), move |id: i32, generation: i64| -> u32 {
716 if id < 0 || generation < 0 {
717 return 0;
718 }
719 node_index_host
720 .borrow_mut()
721 .instance_index_for_handle(FormNodeId(id as usize), generation as u64)
722 })
723 .map_err(|e| format!("nodeIndex: {e}"))?;
724 internal
725 .set("nodeIndex", node_index)
726 .map_err(|e| format!("set nodeIndex: {e}"))?;
727
728 let node_name_host = Rc::clone(&host);
729 let node_name = Function::new(ctx.clone(), move |id: i32, generation: i64| -> String {
730 if id < 0 || generation < 0 {
731 return String::new();
732 }
733 node_name_host
734 .borrow()
735 .node_name(FormNodeId(id as usize), generation as u64)
736 .unwrap_or_default()
737 })
738 .map_err(|e| format!("nodeName: {e}"))?;
739 internal
740 .set("nodeName", node_name)
741 .map_err(|e| format!("set nodeName: {e}"))?;
742
743 let occur_resolve_host = Rc::clone(&host);
745 let occur_resolve =
746 Function::new(ctx.clone(), move |id: i32, generation: i64| -> bool {
747 if id < 0 || generation < 0 {
748 return false;
749 }
750 occur_resolve_host
751 .borrow_mut()
752 .occur_resolve(FormNodeId(id as usize), generation as u64)
753 })
754 .map_err(|e| format!("occurResolve: {e}"))?;
755 internal
756 .set("occurResolve", occur_resolve)
757 .map_err(|e| format!("set occurResolve: {e}"))?;
758
759 let occur_read_host = Rc::clone(&host);
761 let occur_read = Function::new(
762 ctx.clone(),
763 move |id: i32, generation: i64, prop: String| -> i64 {
764 if id < 0 || generation < 0 {
765 return -1;
766 }
767 occur_read_host.borrow_mut().occur_read(
768 FormNodeId(id as usize),
769 generation as u64,
770 &prop,
771 )
772 },
773 )
774 .map_err(|e| format!("occurRead: {e}"))?;
775 internal
776 .set("occurRead", occur_read)
777 .map_err(|e| format!("set occurRead: {e}"))?;
778
779 let occur_capture_host = Rc::clone(&host);
782 let occur_capture = Function::new(
783 ctx.clone(),
784 move |id: i32, generation: i64, prop: String, value: i64| -> bool {
785 if id < 0 || generation < 0 {
786 return false;
787 }
788 occur_capture_host.borrow_mut().occur_capture(
789 FormNodeId(id as usize),
790 generation as u64,
791 &prop,
792 value,
793 )
794 },
795 )
796 .map_err(|e| format!("occurCapture: {e}"))?;
797 internal
798 .set("occurCapture", occur_capture)
799 .map_err(|e| format!("set occurCapture: {e}"))?;
800
801 let is_container_host = Rc::clone(&host);
806 let node_is_container =
807 Function::new(ctx.clone(), move |id: i32, generation: i64| -> bool {
808 if id < 0 || generation < 0 {
809 return false;
810 }
811 is_container_host
812 .borrow_mut()
813 .node_is_container(FormNodeId(id as usize), generation as u64)
814 })
815 .map_err(|e| format!("nodeIsContainer: {e}"))?;
816 internal
817 .set("nodeIsContainer", node_is_container)
818 .map_err(|e| format!("set nodeIsContainer: {e}"))?;
819
820 let parent_node_host = Rc::clone(&host);
825 let parent_of_node =
826 Function::new(ctx.clone(), move |id: i32, generation: i64| -> i32 {
827 if id < 0 || generation < 0 {
828 return -1;
829 }
830 parent_node_host
831 .borrow_mut()
832 .parent_of_node(FormNodeId(id as usize), generation as u64)
833 .map(|node_id| node_id.0 as i32)
834 .unwrap_or(-1)
835 })
836 .map_err(|e| format!("parentOfNode: {e}"))?;
837 internal
838 .set("parentOfNode", parent_of_node)
839 .map_err(|e| format!("set parentOfNode: {e}"))?;
840
841 let instance_set_host = Rc::clone(&host);
842 let instance_set = Function::new(
843 ctx.clone(),
844 move |id: i32, generation: i64, n: Opt<i32>| -> i32 {
845 if id < 0 || generation < 0 {
846 return -1;
847 }
848 let n = n.0.unwrap_or(0).max(0) as u32;
849 instance_set_host
850 .borrow_mut()
851 .instance_set_for_handle(FormNodeId(id as usize), generation as u64, n)
852 .map(|count| count as i32)
853 .unwrap_or(-1)
854 },
855 )
856 .map_err(|e| format!("instanceSet: {e}"))?;
857 internal
858 .set("instanceSet", instance_set)
859 .map_err(|e| format!("set instanceSet: {e}"))?;
860
861 let instance_add_host = Rc::clone(&host);
862 let instance_add = Function::new(ctx.clone(), move |id: i32, generation: i64| -> i32 {
863 if id < 0 || generation < 0 {
864 return -1;
865 }
866 instance_add_host
867 .borrow_mut()
868 .instance_add_for_handle(FormNodeId(id as usize), generation as u64)
869 .map(|node_id| node_id.0 as i32)
870 .unwrap_or(-1)
871 })
872 .map_err(|e| format!("instanceAdd: {e}"))?;
873 internal
874 .set("instanceAdd", instance_add)
875 .map_err(|e| format!("set instanceAdd: {e}"))?;
876
877 let instance_remove_host = Rc::clone(&host);
878 let instance_remove = Function::new(
879 ctx.clone(),
880 move |id: i32, generation: i64, index: Opt<i32>| -> bool {
881 if id < 0 || generation < 0 {
882 return false;
883 }
884 let index = index.0.unwrap_or(0).max(0) as u32;
885 instance_remove_host
886 .borrow_mut()
887 .instance_remove_for_handle(
888 FormNodeId(id as usize),
889 generation as u64,
890 index,
891 )
892 .is_ok()
893 },
894 )
895 .map_err(|e| format!("instanceRemove: {e}"))?;
896 internal
897 .set("instanceRemove", instance_remove)
898 .map_err(|e| format!("set instanceRemove: {e}"))?;
899
900 let list_clear_host = Rc::clone(&host);
901 let list_clear = Function::new(ctx.clone(), move |id: i32, generation: i64| -> bool {
902 if id < 0 || generation < 0 {
903 return false;
904 }
905 list_clear_host
906 .borrow_mut()
907 .list_clear_for_handle(FormNodeId(id as usize), generation as u64)
908 .is_ok()
909 })
910 .map_err(|e| format!("listClear: {e}"))?;
911 internal
912 .set("listClear", list_clear)
913 .map_err(|e| format!("set listClear: {e}"))?;
914
915 let list_add_host = Rc::clone(&host);
916 let list_add = Function::new(
917 ctx.clone(),
918 move |id: i32,
919 generation: i64,
920 display: Coerced<String>,
921 save: Opt<Coerced<String>>|
922 -> bool {
923 if id < 0 || generation < 0 {
924 return false;
925 }
926 list_add_host
927 .borrow_mut()
928 .list_add_for_handle(
929 FormNodeId(id as usize),
930 generation as u64,
931 display.0,
932 save.0.map(|s| s.0),
933 )
934 .is_ok()
935 },
936 )
937 .map_err(|e| format!("listAdd: {e}"))?;
938 internal
939 .set("listAdd", list_add)
940 .map_err(|e| format!("set listAdd: {e}"))?;
941
942 let bound_item_host = Rc::clone(&host);
943 let bound_item = Function::new(
944 ctx.clone(),
945 move |id: i32, generation: i64, display: Coerced<String>| -> String {
946 if id < 0 || generation < 0 {
947 return display.0;
948 }
949 bound_item_host.borrow_mut().bound_item_for_handle(
950 FormNodeId(id as usize),
951 generation as u64,
952 display.0,
953 )
954 },
955 )
956 .map_err(|e| format!("boundItem: {e}"))?;
957 internal
958 .set("boundItem", bound_item)
959 .map_err(|e| format!("set boundItem: {e}"))?;
960
961 let get_display_items_host = Rc::clone(&host);
966 let get_display_items = Function::new(
967 ctx.clone(),
968 move |id: i32, generation: i64| -> Vec<String> {
969 if id < 0 || generation < 0 {
970 return Vec::new();
971 }
972 get_display_items_host
973 .borrow_mut()
974 .get_display_items(FormNodeId(id as usize), generation as u64)
975 },
976 )
977 .map_err(|e| format!("getDisplayItems: {e}"))?;
978 internal
979 .set("getDisplayItems", get_display_items)
980 .map_err(|e| format!("set getDisplayItems: {e}"))?;
981
982 let num_pages_host = Rc::clone(&host);
983 let num_pages =
984 Function::new(ctx.clone(), move || num_pages_host.borrow_mut().num_pages())
985 .map_err(|e| format!("numPages: {e}"))?;
986 internal
987 .set("numPages", num_pages)
988 .map_err(|e| format!("set numPages: {e}"))?;
989
990 let binding_error_host = Rc::clone(&host);
991 let binding_error = Function::new(ctx.clone(), move || {
992 binding_error_host.borrow_mut().metadata_binding_error();
993 })
994 .map_err(|e| format!("bindingError: {e}"))?;
995 internal
996 .set("bindingError", binding_error)
997 .map_err(|e| format!("set bindingError: {e}"))?;
998
999 let resolve_failure_host = Rc::clone(&host);
1000 let resolve_failure = Function::new(ctx.clone(), move || {
1001 resolve_failure_host.borrow_mut().metadata_resolve_failure();
1002 })
1003 .map_err(|e| format!("resolveFailure: {e}"))?;
1004 internal
1005 .set("resolveFailure", resolve_failure)
1006 .map_err(|e| format!("set resolveFailure: {e}"))?;
1007
1008 let som_data_root_hit_host = Rc::clone(&host);
1010 let som_data_root_hit = Function::new(ctx.clone(), move || {
1011 som_data_root_hit_host
1012 .borrow_mut()
1013 .metadata_som_data_root_hit();
1014 })
1015 .map_err(|e| format!("somDataRootHit: {e}"))?;
1016 internal
1017 .set("somDataRootHit", som_data_root_hit)
1018 .map_err(|e| format!("set somDataRootHit: {e}"))?;
1019
1020 let som_items_path_hit_host = Rc::clone(&host);
1022 let som_items_path_hit = Function::new(ctx.clone(), move || {
1023 som_items_path_hit_host
1024 .borrow_mut()
1025 .metadata_som_items_path_hit();
1026 })
1027 .map_err(|e| format!("somItemsPathHit: {e}"))?;
1028 internal
1029 .set("somItemsPathHit", som_items_path_hit)
1030 .map_err(|e| format!("set somItemsPathHit: {e}"))?;
1031
1032 let unsupported_host_call_host = Rc::clone(&host);
1039 let unsupported_host_call =
1040 Function::new(ctx.clone(), move |_capability: Opt<Coerced<String>>| {
1041 unsupported_host_call_host
1042 .borrow_mut()
1043 .metadata_unsupported_host_call();
1044 })
1045 .map_err(|e| format!("unsupportedHostCall: {e}"))?;
1046 internal
1047 .set("unsupportedHostCall", unsupported_host_call)
1048 .map_err(|e| format!("set unsupportedHostCall: {e}"))?;
1049
1050 let probe_skip_host = Rc::clone(&host);
1054 let probe_skip = Function::new(ctx.clone(), move || {
1055 probe_skip_host.borrow_mut().metadata_probe_skip();
1056 })
1057 .map_err(|e| format!("probeSkip: {e}"))?;
1058 internal
1059 .set("probeSkip", probe_skip)
1060 .map_err(|e| format!("set probeSkip: {e}"))?;
1061
1062 let dc_host = Rc::clone(&host);
1065 let data_children = Function::new(ctx.clone(), move |raw_id: i32| -> Vec<i32> {
1066 if raw_id < 0 {
1067 return Vec::new();
1068 }
1069 dc_host
1070 .borrow_mut()
1071 .data_children(raw_id as usize)
1072 .into_iter()
1073 .map(|x| x as i32)
1074 .collect()
1075 })
1076 .map_err(|e| format!("dataChildren: {e}"))?;
1077 internal
1078 .set("dataChildren", data_children)
1079 .map_err(|e| format!("set dataChildren: {e}"))?;
1080
1081 let dv_host = Rc::clone(&host);
1082 let data_value = Function::new(ctx.clone(), move |raw_id: i32| -> Option<String> {
1083 if raw_id < 0 {
1084 return None;
1085 }
1086 dv_host.borrow_mut().data_value(raw_id as usize)
1087 })
1088 .map_err(|e| format!("dataValue: {e}"))?;
1089 internal
1090 .set("dataValue", data_value)
1091 .map_err(|e| format!("set dataValue: {e}"))?;
1092
1093 let dcbn_host = Rc::clone(&host);
1094 let data_child_by_name = Function::new(
1095 ctx.clone(),
1096 move |parent_raw: i32, name: Opt<Coerced<String>>| -> i32 {
1097 if parent_raw < 0 {
1098 return -1;
1099 }
1100 let Some(name) = name.0 else {
1101 return -1;
1102 };
1103 dcbn_host
1104 .borrow_mut()
1105 .data_child_by_name(parent_raw as usize, &name.0)
1106 .map(|x| x as i32)
1107 .unwrap_or(-1)
1108 },
1109 )
1110 .map_err(|e| format!("dataChildByName: {e}"))?;
1111 internal
1112 .set("dataChildByName", data_child_by_name)
1113 .map_err(|e| format!("set dataChildByName: {e}"))?;
1114
1115 let dbr_host = Rc::clone(&host);
1116 let data_bound_record = Function::new(
1117 ctx.clone(),
1118 move |form_node_id: i32, generation: i64| -> i32 {
1119 if form_node_id < 0 || generation < 0 {
1120 return -1;
1121 }
1122 dbr_host
1123 .borrow_mut()
1124 .data_bound_record(FormNodeId(form_node_id as usize), generation as u64)
1125 .map(|x| x as i32)
1126 .unwrap_or(-1)
1127 },
1128 )
1129 .map_err(|e| format!("dataBoundRecord: {e}"))?;
1130 internal
1131 .set("dataBoundRecord", data_bound_record)
1132 .map_err(|e| format!("set dataBoundRecord: {e}"))?;
1133
1134 let drn_host = Rc::clone(&host);
1135 let data_resolve_node =
1136 Function::new(ctx.clone(), move |path: Opt<Coerced<String>>| -> i32 {
1137 let Some(path) = path.0 else {
1138 return -1;
1139 };
1140 drn_host
1141 .borrow_mut()
1142 .data_resolve_node(&path.0)
1143 .map(|x| x as i32)
1144 .unwrap_or(-1)
1145 })
1146 .map_err(|e| format!("dataResolveNode: {e}"))?;
1147 internal
1148 .set("dataResolveNode", data_resolve_node)
1149 .map_err(|e| format!("set dataResolveNode: {e}"))?;
1150
1151 let idan_host = Rc::clone(&host);
1159 let is_declared_absent_node =
1160 Function::new(ctx.clone(), move |name: Opt<Coerced<String>>| -> bool {
1161 let Some(name) = name.0 else {
1162 return false;
1163 };
1164 idan_host.borrow().is_declared_absent_node(&name.0)
1165 })
1166 .map_err(|e| format!("isDeclaredAbsentNode: {e}"))?;
1167 internal
1168 .set("isDeclaredAbsentNode", is_declared_absent_node)
1169 .map_err(|e| format!("set isDeclaredAbsentNode: {e}"))?;
1170
1171 let drns_host = Rc::clone(&host);
1172 let data_resolve_nodes =
1173 Function::new(ctx.clone(), move |path: Opt<Coerced<String>>| -> Vec<i32> {
1174 let Some(path) = path.0 else {
1175 return Vec::new();
1176 };
1177 drns_host
1178 .borrow_mut()
1179 .data_resolve_nodes(&path.0)
1180 .into_iter()
1181 .map(|x| x as i32)
1182 .collect()
1183 })
1184 .map_err(|e| format!("dataResolveNodes: {e}"))?;
1185 internal
1186 .set("dataResolveNodes", data_resolve_nodes)
1187 .map_err(|e| format!("set dataResolveNodes: {e}"))?;
1188
1189 let factory: Function = ctx
1192 .eval(PHASE_C_BINDINGS_JS.as_bytes())
1193 .map_err(|e| format!("binding factory parse: {e}"))?;
1194 let bridge: Object = factory
1195 .call((internal,))
1196 .catch(&ctx)
1197 .map_err(|e| format!("binding factory call: {e}"))?;
1198 let xfa: Object = bridge.get("xfa").map_err(|e| format!("get xfa: {e}"))?;
1199 let app: Object = bridge.get("app").map_err(|e| format!("get app: {e}"))?;
1200 let form: Object = bridge.get("form").map_err(|e| format!("get form: {e}"))?;
1204 let eval_script: Function = bridge
1205 .get("evalScript")
1206 .map_err(|e| format!("get evalScript: {e}"))?;
1207 let set_variables_script: Function = bridge
1208 .get("setVariablesScript")
1209 .map_err(|e| format!("get setVariablesScript: {e}"))?;
1210 let clear_variables_scripts: Function = bridge
1211 .get("clearVariablesScripts")
1212 .map_err(|e| format!("get clearVariablesScripts: {e}"))?;
1213 let set_variables_data_item: Function = bridge
1214 .get("setVariablesDataItem")
1215 .map_err(|e| format!("get setVariablesDataItem: {e}"))?;
1216 globals
1217 .set("xfa", xfa)
1218 .map_err(|e| format!("set xfa global: {e}"))?;
1219 globals
1220 .set("app", app)
1221 .map_err(|e| format!("set app global: {e}"))?;
1222 globals
1223 .set("form", form)
1224 .map_err(|e| format!("set form global: {e}"))?;
1225 Ok::<
1226 (
1227 Persistent<Function<'static>>,
1228 Persistent<Function<'static>>,
1229 Persistent<Function<'static>>,
1230 Persistent<Function<'static>>,
1231 ),
1232 String,
1233 >((
1234 Persistent::save(&ctx, eval_script),
1235 Persistent::save(&ctx, set_variables_script),
1236 Persistent::save(&ctx, clear_variables_scripts),
1237 Persistent::save(&ctx, set_variables_data_item),
1238 ))
1239 })?;
1240
1241 self.eval_script = Some(eval_script.0);
1242 self.set_variables_script = Some(eval_script.1);
1243 self.clear_variables_scripts = Some(eval_script.2);
1244 self.set_variables_data_item = Some(eval_script.3);
1245 self.bindings_registered = true;
1246 Ok(())
1247 }
1248
1249 fn extract_top_level_idents(body: &str) -> Vec<String> {
1256 let mut out: Vec<String> = Vec::new();
1257 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1258 let stripped = strip_js_comments(body);
1260 for token_kind in [
1261 ("var", false),
1262 ("let", false),
1263 ("const", false),
1264 ("function", true),
1265 ] {
1266 let kw = token_kind.0;
1267 let is_fn = token_kind.1;
1268 let mut search = stripped.as_str();
1269 while let Some(idx) = search.find(kw) {
1270 let (before, after) = search.split_at(idx);
1271 let head_ok = before
1272 .chars()
1273 .last()
1274 .is_none_or(|c| !c.is_alphanumeric() && c != '_' && c != '$');
1275 let tail_after_kw = &after[kw.len()..];
1276 let tail_ok = tail_after_kw
1277 .chars()
1278 .next()
1279 .is_some_and(|c| c.is_whitespace());
1280 if !(head_ok && tail_ok) {
1281 search = &after[1..];
1282 continue;
1283 }
1284 let after_ws = tail_after_kw.trim_start();
1286 let mut chars = after_ws.chars();
1287 let ident: String = std::iter::once(chars.next())
1288 .chain(chars.map(Some))
1289 .map_while(|c| c.filter(|c| c.is_alphanumeric() || *c == '_' || *c == '$'))
1290 .collect();
1291 if ident.is_empty() || ident.chars().next().is_some_and(|c| c.is_ascii_digit()) {
1292 search = &after[kw.len()..];
1293 continue;
1294 }
1295 if is_fn {
1296 let after_ident = after_ws
1297 .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_' || c == '$');
1298 if !after_ident.trim_start().starts_with('(') {
1299 search = &after[kw.len()..];
1300 continue;
1301 }
1302 }
1303 if seen.insert(ident.clone()) {
1304 out.push(ident);
1305 }
1306 search = &after[kw.len()..];
1307 }
1308 }
1309 out
1310 }
1311
1312 fn register_variables_script(
1330 &self,
1331 name: &str,
1332 body: &str,
1333 subform_scope: Option<&str>,
1334 expose_global: bool,
1335 ) -> Result<bool, SandboxError> {
1336 let Some(setter) = self.set_variables_script.clone() else {
1337 return Ok(false);
1338 };
1339 if body.len() > MAX_VARIABLES_SCRIPT_BODY_BYTES {
1349 return Err(SandboxError::BodyTooLarge);
1350 }
1351 if let RegexScanVerdict::Reject { reason } = scan_script_for_redos(body) {
1356 return Err(SandboxError::RegexRejected(reason));
1357 }
1358 let idents = Self::extract_top_level_idents(body);
1359 let scope = subform_scope.unwrap_or("").to_string();
1360 self.set_deadline();
1361 let result = catch_unwind(AssertUnwindSafe(|| {
1362 self.context.with(|ctx| -> Result<bool, rquickjs::Error> {
1363 let setter = setter.restore(&ctx)?;
1364 let ok: bool = setter.call((name, body, idents, scope, expose_global))?;
1365 Ok(ok)
1366 })
1367 }));
1368 let deadline_now = self.script_deadline.load(Ordering::Acquire);
1372 let now_nanos = Instant::now()
1373 .checked_duration_since(epoch())
1374 .map(|d| d.as_nanos() as u64)
1375 .unwrap_or(0);
1376 let timed_out = deadline_now != 0 && now_nanos >= deadline_now;
1377 self.clear_deadline();
1378 match result {
1379 Ok(Ok(ok)) => Ok(ok),
1380 Ok(Err(_)) if timed_out => Err(self.classify_timeout_or_walltime(now_nanos)),
1385 Ok(Err(e)) => Err(SandboxError::ScriptError(format!(
1386 "variables-script `{name}` register: {e}"
1387 ))),
1388 Err(_) => Err(SandboxError::PanicCaptured(format!(
1389 "panic registering variables-script `{name}`"
1390 ))),
1391 }
1392 }
1393
1394 fn register_variables_data_item(
1400 &self,
1401 name: &str,
1402 initial: &str,
1403 subform_scope: Option<&str>,
1404 ) -> Result<(), SandboxError> {
1405 let Some(setter) = self.set_variables_data_item.clone() else {
1406 return Ok(());
1407 };
1408 let initial_owned = initial.to_string();
1409 let scope = subform_scope.unwrap_or("").to_string();
1410 let name_owned = name.to_string();
1411 let result = catch_unwind(AssertUnwindSafe(|| {
1412 self.context.with(|ctx| -> Result<(), rquickjs::Error> {
1413 let setter = setter.restore(&ctx)?;
1414 let _: bool = setter.call((name_owned, initial_owned, scope))?;
1415 Ok(())
1416 })
1417 }));
1418 match result {
1419 Ok(Ok(())) => Ok(()),
1420 Ok(Err(e)) => Err(SandboxError::ScriptError(format!(
1421 "variables data item `{name}` register: {e}"
1422 ))),
1423 Err(_) => Err(SandboxError::PanicCaptured(format!(
1424 "panic registering variables data item `{name}`"
1425 ))),
1426 }
1427 }
1428
1429 fn clear_variables_scripts_global(&self) -> Result<(), SandboxError> {
1430 let Some(clearer) = self.clear_variables_scripts.clone() else {
1431 return Ok(());
1432 };
1433 let result = catch_unwind(AssertUnwindSafe(|| {
1434 self.context.with(|ctx| -> Result<(), rquickjs::Error> {
1435 let clearer = clearer.restore(&ctx)?;
1436 let _: () = clearer.call(())?;
1437 Ok(())
1438 })
1439 }));
1440 match result {
1441 Ok(Ok(())) => Ok(()),
1442 Ok(Err(e)) => Err(SandboxError::ScriptError(format!(
1443 "variables-script clear: {e}"
1444 ))),
1445 Err(_) => Err(SandboxError::PanicCaptured(
1446 "panic clearing variables-scripts".to_string(),
1447 )),
1448 }
1449 }
1450}
1451
1452fn strip_js_comments(src: &str) -> String {
1459 let mut out = String::with_capacity(src.len());
1460 let bytes = src.as_bytes();
1461 let mut i = 0;
1462 while i < bytes.len() {
1463 let b = bytes[i];
1464 if b == b'"' || b == b'\'' {
1465 let quote = b;
1467 out.push(b as char);
1468 i += 1;
1469 while i < bytes.len() {
1470 let c = bytes[i];
1471 out.push(c as char);
1472 if c == b'\\' && i + 1 < bytes.len() {
1473 out.push(bytes[i + 1] as char);
1474 i += 2;
1475 continue;
1476 }
1477 i += 1;
1478 if c == quote {
1479 break;
1480 }
1481 }
1482 continue;
1483 }
1484 if b == b'/' && i + 1 < bytes.len() {
1485 let n = bytes[i + 1];
1486 if n == b'/' {
1487 while i < bytes.len() && bytes[i] != b'\n' {
1489 i += 1;
1490 }
1491 continue;
1492 }
1493 if n == b'*' {
1494 i += 2;
1496 while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
1497 i += 1;
1498 }
1499 i = (i + 2).min(bytes.len());
1500 continue;
1501 }
1502 }
1503 out.push(b as char);
1504 i += 1;
1505 }
1506 out
1507}
1508
1509const PHASE_C_BINDINGS_JS: &str = r##"
1510(function(host) {
1511 function protoGuard() {
1512 return Object.freeze(Object.create(null));
1513 }
1514
1515 function nullProtoObject() {
1516 var obj = Object.create(null);
1517 Object.defineProperty(obj, "__proto__", {
1518 value: protoGuard(),
1519 enumerable: false,
1520 configurable: false,
1521 writable: false
1522 });
1523 return obj;
1524 }
1525
1526 function lookupObject() {
1527 return Object.create(null);
1528 }
1529
1530 var deferredGlobalNames = lookupObject();
1531 [
1532 "app", "arguments", "Array", "Boolean", "Bun", "console", "Date",
1533 "decodeURI", "decodeURIComponent", "Deno", "encodeURI",
1534 "encodeURIComponent", "Error", "eval", "EvalError", "fetch",
1535 "Function", "globalThis", "Infinity", "isFinite", "isNaN", "JSON",
1536 "Map", "Math", "NaN", "Number", "Object", "parseFloat", "parseInt",
1537 "process", "RangeError", "ReferenceError", "RegExp", "require",
1538 "Set", "String", "Symbol", "SyntaxError", "TypeError", "undefined",
1539 "URIError", "WeakMap", "WeakSet", "WebSocket", "XMLHttpRequest",
1540 // JS2-01 (Sprint 2 Batch B): `form` is a top-level alias of `xfa.form`.
1541 // It must defer to globalThis so the host SOM resolver does not first
1542 // try to resolve `form` as a form-tree node, find nothing, and then
1543 // surface a `js_resolve_failure` for what is really a viewer global.
1544 "xfa", "event", "form"
1545 ].forEach(function(name) {
1546 deferredGlobalNames[name] = true;
1547 });
1548
1549 var reservedHandleProperties = lookupObject();
1550 [
1551 "__defineGetter__", "__defineSetter__", "__lookupGetter__",
1552 "__lookupSetter__", "__proto__", "constructor", "hasOwnProperty",
1553 "isPrototypeOf", "propertyIsEnumerable", "then", "toJSON",
1554 "toLocaleString", "toString", "valueOf"
1555 ].forEach(function(name) {
1556 reservedHandleProperties[name] = true;
1557 });
1558
1559 function shouldDeferGlobalName(name, localNames) {
1560 return name.charAt(0) === "_" ||
1561 localNames[name] === true ||
1562 deferredGlobalNames[name] === true;
1563 }
1564
1565 // Properties that must NOT be deferred so their specific implementations run.
1566 var handlePropertyExclusions = lookupObject();
1567 ["rawValue", "somExpression", "isNull", "clearItems", "addItem", "boundItem",
1568 "$record", "nodes", "value", "length", "item", "choiceList", "#items"].forEach(function(name) {
1569 handlePropertyExclusions[name] = true;
1570 });
1571
1572 function shouldDeferHandleProperty(name) {
1573 if (handlePropertyExclusions[name] === true) {
1574 return false;
1575 }
1576 return name.charAt(0) === "_" || reservedHandleProperties[name] === true;
1577 }
1578
1579 function collectLocalNames(body) {
1580 var locals = lookupObject();
1581 var match;
1582 var decls = /\b(?:var|let|const)\s+([^;]+)/g;
1583 while ((match = decls.exec(body)) !== null) {
1584 match[1].split(",").forEach(function(part) {
1585 var ident = /^\s*([A-Za-z_$][0-9A-Za-z_$]*)/.exec(part);
1586 if (ident) {
1587 locals[ident[1]] = true;
1588 }
1589 });
1590 }
1591 var funcs = /\bfunction\s+([A-Za-z_$][0-9A-Za-z_$]*)/g;
1592 while ((match = funcs.exec(body)) !== null) {
1593 locals[match[1]] = true;
1594 }
1595 return locals;
1596 }
1597
1598 // XFA-DATA-M3C: `<handle>.ui` returns the ui-config sub-object that
1599 // Adobe templates expose for choiceList / textEdit / checkButton widget
1600 // tweaking. During static flatten the widget tree is not materialised, so
1601 // we hand back a minimal stub whose `choiceList` is an empty frozen
1602 // array (matching the bare `handle.choiceList` fallback). Property
1603 // writes are absorbed silently so scripts like
1604 // `field.ui.choiceList.commitOn = "exit"` complete without TypeError.
1605 function makeUiStub() {
1606 var stub = nullProtoObject();
1607 Object.defineProperty(stub, "choiceList", {
1608 enumerable: true,
1609 configurable: false,
1610 get: function() { return Object.freeze([]); }
1611 });
1612 Object.defineProperty(stub, "textEdit", {
1613 enumerable: true,
1614 configurable: false,
1615 get: function() { return Object.freeze({}); }
1616 });
1617 Object.defineProperty(stub, "checkButton", {
1618 enumerable: true,
1619 configurable: false,
1620 get: function() { return Object.freeze({}); }
1621 });
1622 return new Proxy(stub, {
1623 get: function(target, prop) {
1624 if (typeof prop !== "string") return undefined;
1625 if (prop in target) return target[prop];
1626 // Sub-property writes / reads are absorbed: scripts walking
1627 // unknown widget config (e.g. `ui.imageEdit.<x>`) get a chainable
1628 // viewer-stub rather than crashing on undefined.
1629 return makeViewerStub();
1630 },
1631 set: function(_t, _p, _v) { return true; },
1632 has: function(_t, _p) { return true; }
1633 });
1634 }
1635
1636 // D5: minimal `node.occur` handle. Reads `min`/`max`/`initial` from the
1637 // structural template (host.occurRead; -1 = unlimited/unknown). Writes to
1638 // `min`/`max` are CAPTURED (host.occurCapture) as mutation intent and then
1639 // discarded — no layout/pagination effect in D5. This stops
1640 // `subform.occur.min = N` from throwing `cannot set property 'min' of
1641 // undefined` so the script proceeds; the captured intent is for the next
1642 // milestone's bounded apply path.
1643 function makeOccurHandle(id, generation) {
1644 var occ = nullProtoObject();
1645 function defineOccurProp(name) {
1646 Object.defineProperty(occ, name, {
1647 enumerable: true,
1648 configurable: false,
1649 get: function() {
1650 return host.occurRead(id, generation, name);
1651 },
1652 set: function(value) {
1653 var n = (typeof value === "number") ? value : parseInt(value, 10);
1654 if (isNaN(n)) { n = 0; }
1655 host.occurCapture(id, generation, name, n);
1656 }
1657 });
1658 }
1659 defineOccurProp("min");
1660 defineOccurProp("max");
1661 defineOccurProp("initial");
1662 return occ;
1663 }
1664
1665 function makeInstanceManager(id, generation) {
1666 var manager = nullProtoObject();
1667 Object.defineProperty(manager, "count", {
1668 enumerable: true,
1669 configurable: false,
1670 get: function() {
1671 return host.instanceCount(id, generation);
1672 }
1673 });
1674 Object.defineProperty(manager, "setInstances", {
1675 enumerable: true,
1676 configurable: false,
1677 writable: false,
1678 value: function(n) {
1679 return host.instanceSet(id, generation, n);
1680 }
1681 });
1682 Object.defineProperty(manager, "addInstance", {
1683 enumerable: true,
1684 configurable: false,
1685 writable: false,
1686 value: function() {
1687 var newId = host.instanceAdd(id, generation);
1688 return newId < 0 ? null : makeHandle(newId, generation);
1689 }
1690 });
1691 Object.defineProperty(manager, "removeInstance", {
1692 enumerable: true,
1693 configurable: false,
1694 writable: false,
1695 value: function(idx) {
1696 return host.instanceRemove(id, generation, idx);
1697 }
1698 });
1699 return Object.freeze(manager);
1700 }
1701
1702 function makeEmptyInstanceManager() {
1703 var manager = nullProtoObject();
1704 Object.defineProperty(manager, "count", {
1705 enumerable: true,
1706 configurable: false,
1707 value: 0
1708 });
1709 Object.defineProperty(manager, "setInstances", {
1710 enumerable: true,
1711 configurable: false,
1712 writable: false,
1713 value: function() { return 0; }
1714 });
1715 Object.defineProperty(manager, "addInstance", {
1716 enumerable: true,
1717 configurable: false,
1718 writable: false,
1719 value: function() { return null; }
1720 });
1721 Object.defineProperty(manager, "removeInstance", {
1722 enumerable: true,
1723 configurable: false,
1724 writable: false,
1725 value: function() { return false; }
1726 });
1727 return Object.freeze(manager);
1728 }
1729
1730 function uniqueNodeIds(ids) {
1731 var out = [];
1732 if (!ids) return out;
1733 for (var i = 0; i < ids.length; i++) {
1734 var id = ids[i] | 0;
1735 if (id < 0) continue;
1736 var seen = false;
1737 for (var j = 0; j < out.length; j++) {
1738 if (out[j] === id) {
1739 seen = true;
1740 break;
1741 }
1742 }
1743 if (!seen) out.push(id);
1744 }
1745 return out;
1746 }
1747
1748 function nodeIdListArg(ids) {
1749 return uniqueNodeIds(ids).join(",");
1750 }
1751
1752 function resolveHandleChildIds(ids, prop) {
1753 var list = nodeIdListArg(ids);
1754 if (list.length === 0) return [];
1755 var childIds = uniqueNodeIds(host.resolveChildNodeIds(list, prop));
1756 if (childIds.length > 0) return childIds;
1757 return uniqueNodeIds(host.resolveScopedNodeIds(list, prop));
1758 }
1759
1760 function makeNodeHandleFromIds(ids, generation) {
1761 var unique = uniqueNodeIds(ids);
1762 if (unique.length === 0) return undefined;
1763 if (unique.length === 1) return makeHandle(unique[0], generation);
1764 return makeCandidateSet(unique, generation);
1765 }
1766
1767 // Phase D-θ: single-segment SOM lookahead. The proxy chain `F.P1.X.rawValue`
1768 // can resolve the leading `F.P1` correctly yet pick the wrong `P1` when
1769 // multiple same-named siblings exist — the resolver lacks the next-segment
1770 // context needed to disambiguate. These helpers return a one-step deferred
1771 // wrapper that, on its next property access, asks the host's hinted
1772 // resolver to drop candidates whose subtree does not contain the lookahead
1773 // name. Terminal properties (rawValue, instanceManager, …) bypass the hint
1774 // path entirely so single-token reads remain byte-for-byte identical.
1775 function isTerminalHandleProp(name) {
1776 if (handlePropertyExclusions[name] === true) return true;
1777 if (reservedHandleProperties[name] === true) return true;
1778 if (name.charAt(0) === "_") return true;
1779 switch (name) {
1780 case "rawValue":
1781 case "somExpression":
1782 case "instanceManager":
1783 case "index":
1784 case "setInstances":
1785 case "addInstance":
1786 case "removeInstance":
1787 case "isNull":
1788 case "clearItems":
1789 case "addItem":
1790 case "boundItem":
1791 case "#items":
1792 case "$record":
1793 case "variables":
1794 case "nodes":
1795 case "length":
1796 case "value":
1797 case "item":
1798 return true;
1799 default:
1800 return false;
1801 }
1802 }
1803
1804 function makeImplicitDeferred(currentId, name, eagerIds, generation) {
1805 function realize(hint) {
1806 if (!hint) return eagerIds;
1807 var refined = uniqueNodeIds(host.resolveImplicitNodeIdsHinted(currentId, name, hint));
1808 return refined.length > 0 ? refined : eagerIds;
1809 }
1810 var sentinel = nullProtoObject();
1811 return new Proxy(sentinel, {
1812 get: function(target, prop, receiver) {
1813 if (typeof prop !== "string") {
1814 return Reflect.get(target, prop, receiver);
1815 }
1816 // XFA-DATA2-02: short-circuit caption / resolveNode pair before any
1817 // host call. See captionSentinel + handleResolveNode definitions.
1818 if (prop === "caption") { return captionSentinel; }
1819 if (prop === "resolveNode") { return handleResolveNode; }
1820 if (prop === "resolveNodes") { return handleResolveNodes; }
1821 var ids = isTerminalHandleProp(prop) ? eagerIds : realize(prop);
1822 var handle = makeNodeHandleFromIds(ids, generation);
1823 if (handle === undefined) return undefined;
1824 return handle[prop];
1825 },
1826 set: function(_target, prop, value) {
1827 var handle = makeNodeHandleFromIds(eagerIds, generation);
1828 if (handle === undefined) return true;
1829 handle[prop] = value;
1830 return true;
1831 },
1832 has: function(_target, prop) {
1833 if (typeof prop !== "string") return Reflect.has(sentinel, prop);
1834 return true;
1835 }
1836 });
1837 }
1838
1839 function makeChildDeferred(parentIds, childName, eagerChildIds, generation) {
1840 var parentList = nodeIdListArg(parentIds);
1841 function realize(hint) {
1842 if (!hint || parentList.length === 0) return eagerChildIds;
1843 var refined = uniqueNodeIds(host.resolveChildNodeIdsHinted(parentList, childName, hint));
1844 return refined.length > 0 ? refined : eagerChildIds;
1845 }
1846 var sentinel = nullProtoObject();
1847 return new Proxy(sentinel, {
1848 get: function(target, prop, receiver) {
1849 if (typeof prop !== "string") {
1850 return Reflect.get(target, prop, receiver);
1851 }
1852 // XFA-DATA2-02: short-circuit caption / resolveNode pair before any
1853 // host call. See captionSentinel + handleResolveNode definitions.
1854 if (prop === "caption") { return captionSentinel; }
1855 if (prop === "resolveNode") { return handleResolveNode; }
1856 if (prop === "resolveNodes") { return handleResolveNodes; }
1857 var ids = isTerminalHandleProp(prop) ? eagerChildIds : realize(prop);
1858 var handle = makeNodeHandleFromIds(ids, generation);
1859 if (handle === undefined) return undefined;
1860 return handle[prop];
1861 },
1862 set: function(_target, prop, value) {
1863 var handle = makeNodeHandleFromIds(eagerChildIds, generation);
1864 if (handle === undefined) return true;
1865 handle[prop] = value;
1866 return true;
1867 },
1868 has: function(_target, prop) {
1869 if (typeof prop !== "string") return Reflect.has(sentinel, prop);
1870 return true;
1871 }
1872 });
1873 }
1874
1875 function makeChainHandle(parentIds, childName, generation) {
1876 var eagerChildIds = resolveHandleChildIds(parentIds, childName);
1877 if (eagerChildIds.length === 0) return undefined;
1878 if (eagerChildIds.length === 1) {
1879 // Singleton child with a single parent leaves the host resolver no
1880 // alternatives to filter even when a hint is provided — same-name
1881 // sibling disambiguation is impossible. Skip the wrapper so the
1882 // common path keeps its original Proxy identity and resolve budget.
1883 return makeHandle(eagerChildIds[0], generation);
1884 }
1885 // Phase D-θ.2: same-name siblings exist; defer through the full-chain
1886 // accumulator so a deeper segment (still unread) can disambiguate.
1887 return makeChainProxy(parentIds, [childName], eagerChildIds, generation, -1);
1888 }
1889
1890 // Phase D-θ.2: lazy chain proxy. Accumulates a SOM chain (string[]) as the
1891 // script walks property access; on the first terminal property read it
1892 // asks the host to resolve the FULL accumulated chain in one call,
1893 // letting the host backtrack and pick the same-name candidate at any
1894 // depth whose subtree actually completes the chain.
1895 //
1896 // Compared to D-θ.1 (single-segment hint), this lets `A.B.C.D.rawValue`
1897 // pick the correct same-name `A` not only when one is the parent of `B`
1898 // but specifically the one whose subtree contains the full B.C.D chain.
1899 //
1900 // Parameters:
1901 // parentIds — entry parent set; empty array means implicit walk.
1902 // chain — string[] of segments accumulated so far (>=1).
1903 // eagerIds — fallback ids returned if the host's full-chain
1904 // resolve produces no candidate at any depth
1905 // (preserves D-θ.1 byte-for-byte behaviour for
1906 // chains the host cannot improve on).
1907 // generation — handle generation to bind to.
1908 // currentId — implicit-walk anchor (used when parentIds is empty).
1909 //
1910 // Terminal property names trigger immediate resolution. Non-terminal
1911 // string property names return a NEW chain proxy with the property name
1912 // appended; no host call happens at that point.
1913 function makeChainProxy(parentIds, chain, eagerIds, generation, currentId) {
1914 var sentinel = nullProtoObject();
1915 function fullChain(extraSeg) {
1916 var parentCsv = nodeIdListArg(parentIds);
1917 var chainArr = chain;
1918 if (extraSeg !== undefined) {
1919 chainArr = chain.slice();
1920 chainArr.push(extraSeg);
1921 }
1922 var chainCsv = chainArr.join(",");
1923 var resolved = uniqueNodeIds(
1924 host.resolveWithFullChain(parentCsv, currentId | 0, chainCsv)
1925 );
1926 if (resolved.length === 0) return eagerIds;
1927 return resolved;
1928 }
1929 return new Proxy(sentinel, {
1930 get: function(target, prop, receiver) {
1931 if (typeof prop !== "string") {
1932 // Symbol.toPrimitive and similar non-string keys must force
1933 // terminal resolution so coercion (e.g. via Number(handle)) sees
1934 // the resolved candidate's primitive view rather than the proxy
1935 // sentinel itself.
1936 var primIds = fullChain();
1937 var primHandle = makeNodeHandleFromIds(primIds, generation);
1938 if (primHandle === undefined) {
1939 return Reflect.get(target, prop, receiver);
1940 }
1941 return primHandle[prop];
1942 }
1943 // XFA-DATA2-02: caption / resolveNode / resolveNodes short-circuit on
1944 // the chain proxy. Without this branch the chain accumulator would
1945 // append "caption" as a non-terminal segment, call
1946 // resolveWithFullChainStrict, miss, and return `undefined` —
1947 // `field.caption.value` then TypeErrors. Same null/empty-list
1948 // contract as the global pair.
1949 if (prop === "caption") {
1950 return captionSentinel;
1951 }
1952 if (prop === "resolveNode") {
1953 return handleResolveNode;
1954 }
1955 if (prop === "resolveNodes") {
1956 return handleResolveNodes;
1957 }
1958 if (isTerminalHandleProp(prop)) {
1959 var ids = fullChain();
1960 var handle = makeNodeHandleFromIds(ids, generation);
1961 if (handle === undefined) return undefined;
1962 return handle[prop];
1963 }
1964 // Non-terminal: probe the extended chain in STRICT mode. If the
1965 // host cannot complete `[chain..., prop]` at full depth anywhere
1966 // in the form, return `undefined` — mirroring the eager D-θ.1
1967 // behaviour where `A.B` resolved to `undefined` when no `A.B`
1968 // existed. When the probe succeeds, return a new chain proxy
1969 // with the segment appended; the lazy accumulation lets a
1970 // deeper segment still disambiguate which same-name `A` to keep.
1971 //
1972 // D-θ.2 probe-skip: when there is exactly one parent and the chain
1973 // so far has exactly one segment, no same-name sibling ambiguity is
1974 // possible — the parent uniquely identifies the node, and there are
1975 // no alternative subtrees for the host to choose between. In that
1976 // case the strict probe would always return the same set as eagerIds,
1977 // so we skip the host round-trip and immediately build the next proxy
1978 // using eagerIds as the fallback. The host-call budget is preserved
1979 // and disambiguation is unaffected because a single-parent / single-
1980 // segment chain cannot be disambiguated further by the host anyway.
1981 var nextChain = chain.slice();
1982 nextChain.push(prop);
1983 if (parentIds.length === 1 && chain.length === 1) {
1984 host.probeSkip();
1985 return makeChainProxy(parentIds, nextChain, eagerIds, generation, currentId);
1986 }
1987 var probe = host.resolveWithFullChainStrict(
1988 nodeIdListArg(parentIds),
1989 currentId | 0,
1990 nextChain.join(",")
1991 );
1992 if (!probe || probe.length === 0) {
1993 return undefined;
1994 }
1995 // The probe result becomes the new eager-fallback so future
1996 // probes that the host cannot improve on still degrade
1997 // gracefully. We must NOT collapse to a single makeHandle here:
1998 // the chain may continue to grow and disambiguate further.
1999 var nextEager = uniqueNodeIds(probe);
2000 return makeChainProxy(parentIds, nextChain, nextEager, generation, currentId);
2001 },
2002 set: function(_target, prop, value) {
2003 // Writes are always terminal: resolve the full chain and forward.
2004 var ids = fullChain();
2005 var handle = makeNodeHandleFromIds(ids, generation);
2006 if (handle === undefined) return true;
2007 handle[prop] = value;
2008 return true;
2009 },
2010 has: function(_target, prop) {
2011 if (typeof prop !== "string") return Reflect.has(sentinel, prop);
2012 return true;
2013 }
2014 });
2015 }
2016
2017 function makeCandidateSet(ids, generation) {
2018 var candidates = uniqueNodeIds(ids);
2019 if (candidates.length === 0) return undefined;
2020 var firstId = candidates[0];
2021 var obj = nullProtoObject();
2022 return new Proxy(obj, {
2023 get: function(target, prop, receiver) {
2024 if (typeof prop !== "string") {
2025 return Reflect.get(target, prop, receiver);
2026 }
2027 if (prop === "rawValue") {
2028 var value = host.getRawValue(firstId, generation);
2029 return value === undefined ? null : value;
2030 }
2031 if (prop === "somExpression") {
2032 return "xfa[0].form[0].placeholder";
2033 }
2034 if (prop === "instanceManager") {
2035 return makeInstanceManager(firstId, generation);
2036 }
2037 if (prop === "occur") {
2038 host.occurResolve(firstId, generation);
2039 return makeOccurHandle(firstId, generation);
2040 }
2041 if (prop === "index") {
2042 return host.nodeIndex(firstId, generation);
2043 }
2044 // XFA-DATA-M3C: `<candidateSet>.parent` walks one form-tree level up
2045 // from the leading candidate. Same semantics as `<handle>.parent`;
2046 // see makeHandle for rationale.
2047 if (prop === "parent") {
2048 var parentId = host.parentOfNode(firstId, generation);
2049 if (parentId < 0) return undefined;
2050 return makeHandle(parentId, generation);
2051 }
2052 if (prop === "setInstances") {
2053 return function(n) {
2054 return host.instanceSet(firstId, generation, n);
2055 };
2056 }
2057 if (prop === "addInstance") {
2058 return function() {
2059 var newId = host.instanceAdd(firstId, generation);
2060 // WP-3 F3: return a chainable null-safe sentinel rather than null
2061 // so scripts that immediately access `.index` / `.value` on the
2062 // newly-added handle do not throw when the instance manager
2063 // refuses (min/max reached, unbound).
2064 return newId < 0 ? makeNullDataHandle() : makeHandle(newId, generation);
2065 };
2066 }
2067 if (prop === "removeInstance") {
2068 return function(idx) {
2069 return host.instanceRemove(firstId, generation, idx);
2070 };
2071 }
2072 if (prop === "isNull") {
2073 var raw = host.getRawValue(firstId, generation);
2074 return raw === undefined || raw === null || raw === "";
2075 }
2076 if (prop === "clearItems") {
2077 return function() {
2078 return host.listClear(firstId, generation);
2079 };
2080 }
2081 if (prop === "addItem") {
2082 return function(display, save) {
2083 if (save === undefined) {
2084 return host.listAdd(firstId, generation, String(display));
2085 }
2086 return host.listAdd(firstId, generation, String(display), String(save));
2087 };
2088 }
2089 if (prop === "boundItem") {
2090 return function(displayValue) {
2091 var coerced = displayValue === null || displayValue === undefined ?
2092 "" : String(displayValue);
2093 return host.boundItem(firstId, generation, coerced);
2094 };
2095 }
2096 if (prop === "$record") {
2097 var recRaw = host.dataBoundRecord(firstId, generation);
2098 if (recRaw < 0) return makeNullDataHandle();
2099 return makeDataHandle(recRaw);
2100 }
2101 if (prop === "variables") {
2102 var csNodeName = host.nodeName(firstId, generation);
2103 if (typeof csNodeName === "string" && csNodeName.length > 0 &&
2104 subformVariables[csNodeName] !== undefined) {
2105 return subformVariables[csNodeName];
2106 }
2107 return Object.create(null);
2108 }
2109 if (prop.charAt(0) === "_" && prop.length > 1) {
2110 var bareName = prop.substring(1);
2111 var imIds = uniqueNodeIds(
2112 host.resolveChildNodeIdsQuiet(nodeIdListArg(candidates), bareName)
2113 );
2114 if (imIds.length > 0) {
2115 return makeInstanceManager(imIds[0], generation);
2116 }
2117 for (var imIdx = 0; imIdx < candidates.length; imIdx++) {
2118 if (host.hasZeroInstanceRun(candidates[imIdx], generation, bareName)) {
2119 return makeEmptyInstanceManager();
2120 }
2121 }
2122 // XFA-DATA-M3C: same fallthrough as makeHandle underscore branch —
2123 // expose an empty instanceManager for schema-optional same-named
2124 // siblings that the merged tree omits, keeping schema-bound
2125 // scripts chainable. Restricted to container candidates so
2126 // field/draw candidate sets keep the original `undefined`
2127 // semantics for underscored property reads.
2128 if (host.nodeIsContainer(firstId, generation)) {
2129 return makeEmptyInstanceManager();
2130 }
2131 }
2132 // WP-3: choiceList is a listbox/combobox property not surfaced through
2133 // the FormTree. Return an empty frozen array.
2134 if (prop === "choiceList") {
2135 return Object.freeze([]);
2136 }
2137 // BE-1: `#items` on a candidate-set — mirrors the makeHandle path.
2138 // Uses the first candidate as the representative field id (same
2139 // convention as boundItem / clearItems / addItem above).
2140 if (prop === "#items") {
2141 var csDisplayItems = host.getDisplayItems(firstId, generation);
2142 if (csDisplayItems.length > 0) {
2143 host.somItemsPathHit();
2144 }
2145 return makeItemsSubstitute(csDisplayItems);
2146 }
2147 // XFA-DATA-M3C: `<candidateSet>.ui` returns the widget-config stub.
2148 // See makeUiStub on makeHandle for rationale.
2149 if (prop === "ui") {
2150 return makeUiStub();
2151 }
2152 if (prop === "formattedValue") {
2153 var fvc = host.getRawValue(firstId, generation);
2154 return fvc === undefined || fvc === null ? "" : String(fvc);
2155 }
2156 if (prop === "execEvent") {
2157 return function() { return undefined; };
2158 }
2159 // XFA-DATA2-02: caption / resolveNode / resolveNodes intercept on the
2160 // candidate-set path. Same rationale as makeHandle: skip the child
2161 // resolver host call and its resolve_failures bump, return a frozen
2162 // sentinel + the chainable method pair. Null contract preserved.
2163 if (prop === "caption") {
2164 return captionSentinel;
2165 }
2166 if (prop === "resolveNode") {
2167 return handleResolveNode;
2168 }
2169 if (prop === "resolveNodes") {
2170 return handleResolveNodes;
2171 }
2172 if (shouldDeferHandleProperty(prop)) {
2173 return undefined;
2174 }
2175 return makeChainHandle(candidates, prop, generation);
2176 },
2177 set: function(_target, prop, value) {
2178 // WP-3: `value` is an alias for rawValue on the set side.
2179 if (prop === "rawValue" || prop === "value") {
2180 host.setRawValue(firstId, generation, value);
2181 }
2182 return true;
2183 },
2184 has: function(target, prop) {
2185 if (typeof prop !== "string") {
2186 return Reflect.has(target, prop);
2187 }
2188 return prop === "rawValue" ||
2189 prop === "somExpression" ||
2190 prop === "instanceManager" ||
2191 prop === "index" ||
2192 prop === "parent" ||
2193 prop === "setInstances" ||
2194 prop === "addInstance" ||
2195 prop === "removeInstance" ||
2196 prop === "isNull" ||
2197 prop === "clearItems" ||
2198 prop === "addItem" ||
2199 prop === "boundItem" ||
2200 Reflect.has(target, prop);
2201 }
2202 });
2203 }
2204
2205 // XFA-DATA2-02: shared caption sentinel — singleton, frozen, null-safe.
2206 //
2207 // XFA templates commonly inspect `field.caption.value` or
2208 // `field.caption.text` to drive labels and conditional formatting. The
2209 // merged FormTree does not carry the optional `<caption>` sub-element, so
2210 // looking up `caption` as a normal child name would call
2211 // `resolve_child_candidates` → NoMatch → bump `resolve_failures`, return
2212 // `undefined`, and the very next `.value` access would throw TypeError
2213 // ("cannot read property 'value' of undefined") and abort the rest of the
2214 // script's initializers.
2215 //
2216 // We intercept `caption` BEFORE the child-resolver call and return a
2217 // chainable sentinel whose terminal reads are empty strings and whose
2218 // deeper reads keep chaining via `makeNullDataHandle`. This is purely
2219 // read-side: writes are silently absorbed (caption sub-element is a
2220 // viewer-only field, never a flatten mutation channel). The sentinel is
2221 // allocated once at factory build-time and frozen, so per-call access is
2222 // an `Object.freeze`d Proxy hit with no heap allocation.
2223 var captionSentinel = (function() {
2224 var base = nullProtoObject();
2225 Object.defineProperty(base, "value", { enumerable: true, configurable: false,
2226 get: function() { return ""; }, set: function(_v) { /* viewer-only */ } });
2227 Object.defineProperty(base, "text", { enumerable: true, configurable: false,
2228 get: function() { return ""; }, set: function(_v) { /* viewer-only */ } });
2229 Object.defineProperty(base, "name", { enumerable: true, configurable: false,
2230 get: function() { return "caption"; } });
2231 Object.defineProperty(base, "rawValue",{ enumerable: true, configurable: false,
2232 get: function() { return ""; }, set: function(_v) { /* viewer-only */ } });
2233 Object.defineProperty(base, "isNull", { enumerable: true, configurable: false,
2234 get: function() { return true; } });
2235 return new Proxy(base, {
2236 get: function(target, prop) {
2237 if (prop in target || typeof prop !== "string") return target[prop];
2238 // Unknown reads stay chainable so `caption.font.typeface` etc. do
2239 // not throw. The sentinel is read-only and write-absorbing.
2240 return makeNullDataHandle();
2241 },
2242 set: function(_t, _p, _v) { return true; },
2243 has: function() { return true; }
2244 });
2245 })();
2246
2247 // XFA-DATA2-02: shared `resolveNode` / `resolveNodes` instance methods.
2248 //
2249 // Beyond the global `xfa.resolveNode(path)`, XFA scripts also call these
2250 // as methods on individual handles (`field.resolveNode("Subform.X")`,
2251 // `subform.resolveNodes("$.field[*]")`). Without an explicit shortcut these
2252 // property reads would fall through to `makeChainHandle`, miss in the
2253 // child resolver and inflate `js_resolve_failures` — the very pattern the
2254 // 60df78fe corpus replay flagged.
2255 //
2256 // Behaviour mirrors the global pair, including the data-path routing for
2257 // `data.`, `$data.`, and `xfa.datasets.data.` prefixes. The Cluster C
2258 // null-return contract is preserved: a miss in the host returns native
2259 // `null` for the singular form and a frozen empty array for the plural.
2260 // BE-1: XFA-compatible substitute for the <items> element returned by
2261 // resolveNode("FieldName.#items"). In a live viewer the result is an XFA
2262 // node whose .nodes collection contains one <text> child per option label;
2263 // each child exposes .value / .rawValue. Scripts iterate:
2264 // for (var i=0; i<items.nodes.length; i++) { items.nodes.item(i).value }
2265 // We materialise that shape from the host display-strings array so existing
2266 // scripts complete without TypeError and can read the label list.
2267 function makeItemsSubstitute(displayStrings) {
2268 var nodes = [];
2269 for (var k = 0; k < displayStrings.length; k++) {
2270 (function(label) {
2271 var textNode = nullProtoObject();
2272 Object.defineProperty(textNode, "value", {
2273 enumerable: true, configurable: false, writable: false, value: label
2274 });
2275 Object.defineProperty(textNode, "rawValue", {
2276 enumerable: false, configurable: false, writable: false, value: label
2277 });
2278 nodes.push(Object.freeze(textNode));
2279 })(displayStrings[k]);
2280 }
2281 var frozenNodes = Object.freeze(nodes);
2282 var nodesCollection = nullProtoObject();
2283 Object.defineProperty(nodesCollection, "length", {
2284 enumerable: true, configurable: false, get: function() { return frozenNodes.length; }
2285 });
2286 Object.defineProperty(nodesCollection, "item", {
2287 enumerable: false, configurable: false, writable: false,
2288 value: function(idx) { return frozenNodes[idx] || null; }
2289 });
2290 var substitute = nullProtoObject();
2291 Object.defineProperty(substitute, "nodes", {
2292 enumerable: true, configurable: false, writable: false, value: Object.freeze(nodesCollection)
2293 });
2294 Object.defineProperty(substitute, "length", {
2295 enumerable: true, configurable: false, get: function() { return frozenNodes.length; }
2296 });
2297 return Object.freeze(substitute);
2298 }
2299
2300 function handleResolveNode(path) {
2301 if (typeof path === "string" &&
2302 (path.indexOf("data.") === 0 || path.indexOf("$data.") === 0 ||
2303 path.indexOf("xfa.datasets.data.") === 0)) {
2304 var rawId = host.dataResolveNode(path);
2305 if (rawId < 0) return null;
2306 return makeDataHandle(rawId);
2307 }
2308 // BE-1: intercept `FieldName.#items` (XFA 3.3 §7.7 / §8.1).
2309 // `#items` is the SOM class reference for a choiceList's <items> element.
2310 // The form-tree resolver has no node for it; we resolve the field part,
2311 // fetch its display labels, and return an XFA-compatible substitute.
2312 // Guard: path.length > 7 prevents false-positive when lastIndexOf returns
2313 // -1 and path.length-7 is also -1 (paths shorter than 8 chars).
2314 if (typeof path === "string" && path.length > 7 &&
2315 path.lastIndexOf(".#items") === path.length - 7) {
2316 var fieldPath = path.substring(0, path.length - 7);
2317 var fid = host.resolveNodeId(fieldPath);
2318 if (fid >= 0) {
2319 var displayItems = host.getDisplayItems(fid, host.generation());
2320 if (displayItems.length > 0) {
2321 host.somItemsPathHit();
2322 }
2323 return makeItemsSubstitute(displayItems);
2324 }
2325 return null;
2326 }
2327 var nid = host.resolveNodeId(path);
2328 if (nid < 0) return null;
2329 return makeHandle(nid, host.generation());
2330 }
2331 function handleResolveNodes(path) {
2332 if (typeof path === "string" &&
2333 (path.indexOf("data.") === 0 || path.indexOf("$data.") === 0 ||
2334 path.indexOf("xfa.datasets.data.") === 0)) {
2335 var rawIds = host.dataResolveNodes(path);
2336 var out = [];
2337 for (var i = 0; i < rawIds.length; i++) out.push(makeDataHandle(rawIds[i]));
2338 return Object.freeze(out);
2339 }
2340 var generation = host.generation();
2341 var ids = host.resolveNodeIds(path);
2342 var out2 = [];
2343 for (var j = 0; j < ids.length; j++) {
2344 out2.push(makeHandle(ids[j], generation));
2345 }
2346 return Object.freeze(out2);
2347 }
2348
2349 function makeHandle(id, generation) {
2350 var obj = nullProtoObject();
2351 Object.defineProperty(obj, "rawValue", {
2352 enumerable: true,
2353 configurable: false,
2354 get: function() {
2355 var value = host.getRawValue(id, generation);
2356 return value === undefined ? null : value;
2357 },
2358 set: function(value) {
2359 host.setRawValue(id, generation, value);
2360 }
2361 });
2362 // Phase C-α: defensive stub. Real Adobe forms call
2363 // `this.somExpression` to obtain the SOM path string. We don't expose
2364 // the real SOM path (introspection capability), but returning a
2365 // placeholder lets viewer-tweak scripts (e.g. acroSOM substr(15))
2366 // proceed without ReferenceError. Mutations via this handle still
2367 // require the rawValue setter, which is the only side-effect channel.
2368 Object.defineProperty(obj, "somExpression", {
2369 enumerable: false,
2370 configurable: false,
2371 get: function() {
2372 return "xfa[0].form[0].placeholder";
2373 }
2374 });
2375 return new Proxy(obj, {
2376 get: function(target, prop, receiver) {
2377 if (typeof prop !== "string") {
2378 return Reflect.get(target, prop, receiver);
2379 }
2380 if (prop === "rawValue" || prop === "somExpression") {
2381 return Reflect.get(target, prop, receiver);
2382 }
2383 if (prop === "instanceManager") {
2384 return makeInstanceManager(id, generation);
2385 }
2386 if (prop === "occur") {
2387 host.occurResolve(id, generation);
2388 return makeOccurHandle(id, generation);
2389 }
2390 if (prop === "index") {
2391 return host.nodeIndex(id, generation);
2392 }
2393 // XFA-DATA-M3C: `<handle>.parent` returns the form-tree parent.
2394 // Adobe scripts commonly chain `parent.somExpression`,
2395 // `parent.index`, or `parent._Sibling.setInstances(...)` to walk
2396 // one level up from a field or subform without authoring an
2397 // explicit SOM path.
2398 if (prop === "parent") {
2399 var parentId = host.parentOfNode(id, generation);
2400 if (parentId < 0) return undefined;
2401 return makeHandle(parentId, generation);
2402 }
2403 if (prop === "setInstances") {
2404 return function(n) {
2405 return host.instanceSet(id, generation, n);
2406 };
2407 }
2408 if (prop === "addInstance") {
2409 return function() {
2410 var newId = host.instanceAdd(id, generation);
2411 // WP-3 F3: chainable null-safe sentinel on failure (see candidateSet).
2412 return newId < 0 ? makeNullDataHandle() : makeHandle(newId, generation);
2413 };
2414 }
2415 if (prop === "removeInstance") {
2416 return function(idx) {
2417 return host.instanceRemove(id, generation, idx);
2418 };
2419 }
2420 if (prop === "isNull") {
2421 var value = host.getRawValue(id, generation);
2422 return value === undefined || value === null || value === "";
2423 }
2424 if (prop === "clearItems") {
2425 return function() {
2426 return host.listClear(id, generation);
2427 };
2428 }
2429 if (prop === "addItem") {
2430 return function(display, save) {
2431 if (save === undefined) {
2432 return host.listAdd(id, generation, String(display));
2433 }
2434 return host.listAdd(id, generation, String(display), String(save));
2435 };
2436 }
2437 // XFA 3.3 §App A `boundItem` — listbox display→save lookup. Used as
2438 // `field.boundItem(xfa.event.newText)` to translate a user-visible
2439 // option label into its underlying save value. Falls back to the
2440 // input string when no match exists (Adobe behaviour).
2441 if (prop === "boundItem") {
2442 return function(displayValue) {
2443 var coerced;
2444 if (displayValue === null || displayValue === undefined) {
2445 coerced = "";
2446 } else {
2447 coerced = String(displayValue);
2448 }
2449 return host.boundItem(id, generation, coerced);
2450 };
2451 }
2452 if (prop === "$record") {
2453 var recRaw = host.dataBoundRecord(id, generation);
2454 if (recRaw < 0) return makeNullDataHandle();
2455 return makeDataHandle(recRaw);
2456 }
2457 // Phase D-ι.2: `subformHandle.variables` returns the namespace object
2458 // holding all `<variables><script>` entries registered for this
2459 // subform by name. Enables `Page2.variables.ValidationScript.fn()`.
2460 if (prop === "variables") {
2461 var nodeName = host.nodeName(id, generation);
2462 if (typeof nodeName === "string" && nodeName.length > 0 &&
2463 subformVariables[nodeName] !== undefined) {
2464 return subformVariables[nodeName];
2465 }
2466 return Object.create(null);
2467 }
2468 // XFA 3.3 §6.4.3.2 underscore shorthand: `_<name>` on a subform
2469 // refers to the instanceManager of the same-named child subform.
2470 // Used in the wild as `parent._child.setInstances(N)`. This MUST
2471 // run before `shouldDeferHandleProperty`, which otherwise returns
2472 // `undefined` for every underscore-prefixed property and makes the
2473 // shorthand unreachable for real bound subforms.
2474 if (prop.charAt(0) === "_" && prop.length > 1) {
2475 var bareName = prop.substring(1);
2476 var imChildIds = uniqueNodeIds(host.resolveChildNodeIdsQuiet(String(id), bareName));
2477 if (imChildIds.length > 0) {
2478 return makeInstanceManager(imChildIds[0], generation);
2479 }
2480 if (host.hasZeroInstanceRun(id, generation, bareName)) {
2481 return makeEmptyInstanceManager();
2482 }
2483 // XFA-DATA-M3C: when the same-named child subform is absent from
2484 // the merged form tree (occur.initial=0, optional bind, or
2485 // never-instantiated schema option), Adobe still hands the
2486 // script an empty instanceManager so `parent._Foo.setInstances(N)`
2487 // is a chainable no-op. This sentinel is ONLY safe on container
2488 // handles (root, subform, area, subformSet, exclGroup) — XFA
2489 // 3.3 §6.4.3.2 limits the underscore-shorthand to those node
2490 // classes. Fields and draws stay `undefined` so existing
2491 // narrow-handle tests (m3b_phaseC_bindings::field_handle_is_frozen_and_narrow)
2492 // keep their property-isolation contract.
2493 if (host.nodeIsContainer(id, generation)) {
2494 return makeEmptyInstanceManager();
2495 }
2496 }
2497 // WP-3: choiceList is a listbox/combobox property not surfaced through
2498 // the FormTree. Return an empty frozen array.
2499 if (prop === "choiceList") {
2500 return Object.freeze([]);
2501 }
2502 // XFA-DATA-M3C: `<handle>.ui` exposes the widget-config sub-object.
2503 // We materialise a chainable stub (see makeUiStub) so scripts like
2504 // `field.ui.choiceList.commitOn = "exit"` complete without throwing.
2505 if (prop === "ui") {
2506 return makeUiStub();
2507 }
2508 // XFA-DATA-M3C: `<handle>.formattedValue` (Adobe SDK §JS A) reads
2509 // the field value formatted by its picture clause. Static flatten
2510 // has no live picture-clause formatter; return the raw value so
2511 // scripts can compare-and-branch without TypeError.
2512 if (prop === "formattedValue") {
2513 var fv = host.getRawValue(id, generation);
2514 return fv === undefined || fv === null ? "" : String(fv);
2515 }
2516 // XFA-DATA-M3C: `<handle>.execEvent("activity")` (Adobe SDK) fires
2517 // an event handler. Static flatten cannot dispatch new events
2518 // mid-script; absorb the call as a no-op returning undefined.
2519 if (prop === "execEvent") {
2520 return function() { return undefined; };
2521 }
2522 // XFA-DATA2-02: caption sub-element + resolveNode/resolveNodes
2523 // instance methods. Intercepted BEFORE shouldDeferHandleProperty +
2524 // makeChainHandle so the child-resolver host call (and its
2525 // resolve_failures bump) never happens. See captionSentinel /
2526 // handleResolveNode/Nodes definitions above for rationale.
2527 if (prop === "caption") {
2528 return captionSentinel;
2529 }
2530 if (prop === "resolveNode") {
2531 return handleResolveNode;
2532 }
2533 if (prop === "resolveNodes") {
2534 return handleResolveNodes;
2535 }
2536 // BE-1: `#items` — XFA 3.3 §7.7 / §8.1 choiceList item labels.
2537 // Scripts access `fieldHandle.#items` (e.g. `Wojewodztwo.#items`) to
2538 // read the display-label collection. Returns an XFA-compatible
2539 // substitute (makeItemsSubstitute) so scripts can use both direct
2540 // array indexing and the `.nodes` / `.nodes.item(i)` access pattern.
2541 // `#` bypasses `shouldDeferHandleProperty` via `handlePropertyExclusions`.
2542 if (prop === "#items") {
2543 var displayItems = host.getDisplayItems(id, generation);
2544 if (displayItems.length > 0) {
2545 host.somItemsPathHit();
2546 }
2547 return makeItemsSubstitute(displayItems);
2548 }
2549 if (shouldDeferHandleProperty(prop)) {
2550 return undefined;
2551 }
2552 return makeChainHandle([id], prop, generation);
2553 },
2554 set: function(_target, prop, value) {
2555 // WP-3: `value` is an alias for rawValue on the set side.
2556 if (prop === "rawValue" || prop === "value") {
2557 host.setRawValue(id, generation, value);
2558 }
2559 return true;
2560 },
2561 has: function(target, prop) {
2562 if (typeof prop !== "string") {
2563 return Reflect.has(target, prop);
2564 }
2565 return prop === "rawValue" ||
2566 prop === "somExpression" ||
2567 prop === "instanceManager" ||
2568 prop === "index" ||
2569 prop === "parent" ||
2570 prop === "ui" ||
2571 prop === "formattedValue" ||
2572 prop === "execEvent" ||
2573 prop === "setInstances" ||
2574 prop === "addInstance" ||
2575 prop === "removeInstance" ||
2576 prop === "isNull" ||
2577 prop === "clearItems" ||
2578 prop === "addItem" ||
2579 prop === "boundItem" ||
2580 prop === "caption" ||
2581 prop === "resolveNode" ||
2582 prop === "resolveNodes" ||
2583 prop === "#items" ||
2584 Reflect.has(target, prop);
2585 }
2586 });
2587 }
2588
2589 // Phase C-α: viewer-stub that absorbs property writes silently.
2590 // Used as the return value of `event.target.getField()` so
2591 // AcroForm widget-tweak scripts (`field.doNotScroll = true`,
2592 // `field.required = false`, etc.) complete without error and
2593 // without mutating any flatten-relevant state.
2594 function makeViewerStub() {
2595 return new Proxy({}, {
2596 get: function(_t, _prop) { return undefined; },
2597 set: function(_t, _prop, _val) {
2598
2599 return true;
2600 },
2601 has: function() { return true; }
2602 });
2603 }
2604
2605 function toListIndex(value) {
2606 var n = Number(value);
2607 if (!isFinite(n) || n < 0) return -1;
2608 return Math.floor(n);
2609 }
2610
2611 // XFA §8.5: a `$record` reference when there is no bound data node must return
2612 // an empty object that chains safely (`.value` → null, `.nodes.length` → 0)
2613 // rather than null, which would throw TypeError on any property access.
2614 function makeNullDataHandle() {
2615 var emptyNodes = [];
2616 // XFA null-safe: item() on an empty sentinel list returns a chainable null handle,
2617 // not native null, so callers can do nodes.item(0).value without TypeError.
2618 emptyNodes.item = function() { return makeNullDataHandle(); };
2619 Object.freeze(emptyNodes);
2620 var sentinel = nullProtoObject();
2621 // WP-3: configurable:true so the Proxy set-trap can return true without
2622 // triggering the ECMAScript "non-configurable accessor without setter" invariant.
2623 Object.defineProperty(sentinel, "value",
2624 { get: function() { return null; }, enumerable: true, configurable: true });
2625 Object.defineProperty(sentinel, "rawValue",
2626 { get: function() { return null; }, enumerable: true, configurable: true });
2627 Object.defineProperty(sentinel, "length",
2628 { get: function() { return 0; }, enumerable: true, configurable: true });
2629 Object.defineProperty(sentinel, "nodes",
2630 { get: function() { return emptyNodes; }, enumerable: true, configurable: true });
2631 // WP-3: instance.index on an unbound node → 0
2632 Object.defineProperty(sentinel, "index",
2633 { get: function() { return 0; }, enumerable: true, configurable: true });
2634 sentinel.item = function() { return makeNullDataHandle(); };
2635 return new Proxy(sentinel, {
2636 get: function(target, prop) {
2637 if (prop in target) return target[prop];
2638 if (typeof prop !== "string") return undefined;
2639 return makeNullDataHandle();
2640 },
2641 // WP-3: absorb all writes — null data handles are read-only sentinels.
2642 set: function(_target, _prop, _value) {
2643 return true;
2644 }
2645 });
2646 }
2647
2648 // BE-1 tranche #1: benign "absent declared node" façade. Returned by the
2649 // implicit-globals `lookup` for a bare identifier that fails the scope
2650 // resolve but names a template-declared container (gated by the host
2651 // `isDeclaredAbsentNode`). Mirrors Adobe semantics: a reference to a
2652 // declared-but-absent node is an EMPTY node — `isNull === true`, `rawValue`/
2653 // `value` null, empty `nodes`, an empty `instanceManager`, and every further
2654 // SOM segment chains to another empty node. Writes are absorbed. This lets a
2655 // guarded script (`if (!Sub.Child.Field.isNull) {…} else {…}`) evaluate the
2656 // guard to `false` and run its else branch (e.g. `_X.setInstances(0)`,
2657 // `presence = "invisible"`) instead of throwing on `undefined`. Modelled on
2658 // `makeNullDataHandle`; the explicit boolean `isNull` is essential — without
2659 // it, `.isNull` would chain to a truthy façade object and invert the guard.
2660 function makeAbsentNodeHandle() {
2661 var sentinel = nullProtoObject();
2662 Object.defineProperty(sentinel, "isNull",
2663 { get: function() { return true; }, enumerable: true, configurable: true });
2664 Object.defineProperty(sentinel, "value",
2665 { get: function() { return null; }, enumerable: true, configurable: true });
2666 Object.defineProperty(sentinel, "rawValue",
2667 { get: function() { return null; }, enumerable: true, configurable: true });
2668 Object.defineProperty(sentinel, "length",
2669 { get: function() { return 0; }, enumerable: true, configurable: true });
2670 var emptyNodes = [];
2671 emptyNodes.item = function() { return makeAbsentNodeHandle(); };
2672 Object.freeze(emptyNodes);
2673 Object.defineProperty(sentinel, "nodes",
2674 { get: function() { return emptyNodes; }, enumerable: true, configurable: true });
2675 Object.defineProperty(sentinel, "instanceManager",
2676 { get: function() { return makeEmptyInstanceManager(); }, enumerable: true, configurable: true });
2677 sentinel.item = function() { return makeAbsentNodeHandle(); };
2678 return new Proxy(sentinel, {
2679 get: function(target, prop) {
2680 if (prop in target) return target[prop];
2681 if (typeof prop !== "string") return undefined;
2682 // `_<Name>` underscore-shorthand → an empty instanceManager, so chained
2683 // `Sub._Child.setInstances(n)` on an absent node is a safe no-op.
2684 if (prop.charAt(0) === "_" && prop.length > 1) return makeEmptyInstanceManager();
2685 // any further SOM segment chains to another empty node.
2686 return makeAbsentNodeHandle();
2687 },
2688 // absorb writes (presence, rawValue, mandatory, …) — empty nodes are inert.
2689 set: function(_target, _prop, _value) { return true; }
2690 });
2691 }
2692
2693 // Phase D-γ: Data DOM handle — wraps a raw DataDom node index and exposes
2694 // `.value`, `.nodes`, `.length`, `.item(i)`, and named child access via Proxy.
2695 function makeDataHandle(rawId) {
2696 // WP-3: return null-safe sentinel so callers can chain .value / .nodes safely.
2697 if (rawId === undefined || rawId < 0) return makeNullDataHandle();
2698 var handle = nullProtoObject();
2699 Object.defineProperty(handle, "value", {
2700 get: function() {
2701 var v = host.dataValue(rawId);
2702 return (v === undefined || v === null) ? null : v;
2703 },
2704 enumerable: true, configurable: false
2705 });
2706 // rawValue is an alias for value — scripts use both forms on data handles.
2707 Object.defineProperty(handle, "rawValue", {
2708 get: function() {
2709 var v = host.dataValue(rawId);
2710 return (v === undefined || v === null) ? null : v;
2711 },
2712 enumerable: true, configurable: false
2713 });
2714 Object.defineProperty(handle, "length", {
2715 get: function() { return host.dataChildren(rawId).length; },
2716 enumerable: true, configurable: false
2717 });
2718 Object.defineProperty(handle, "nodes", {
2719 get: function() {
2720 var ids = host.dataChildren(rawId);
2721 var arr = [];
2722 for (var i = 0; i < ids.length; i++) arr.push(makeDataHandle(ids[i]));
2723 // Phase D-γ fix: XFA scripts call `nodeList.item(i)` on the array
2724 // returned by `.nodes`. Plain JS arrays have no `.item()` method —
2725 // add one that mirrors the W3C NodeList API. Out-of-bounds indices
2726 // return a null-safe sentinel so `.value` access never throws.
2727 var NULLNODE = Object.freeze({ value: null, rawValue: null });
2728 arr.item = function(idx) {
2729 var index = toListIndex(idx);
2730 if (index < 0 || index >= arr.length) return NULLNODE;
2731 return arr[index];
2732 };
2733 return Object.freeze(arr);
2734 },
2735 enumerable: true, configurable: false
2736 });
2737 handle.item = function(i) {
2738 var ids = host.dataChildren(rawId);
2739 var index = toListIndex(i);
2740 if (index < 0 || index >= ids.length) return null;
2741 return makeDataHandle(ids[index]);
2742 };
2743 return new Proxy(handle, {
2744 get: function(target, prop) {
2745 if (prop in target || typeof prop !== "string") return target[prop];
2746 if (prop === "rawValue" || prop === "value") return target.value;
2747 var childId = host.dataChildByName(rawId, prop);
2748 if (childId < 0) return makeNullDataHandle();
2749 return makeDataHandle(childId);
2750 }
2751 });
2752 }
2753
2754 // Phase E (XFA-JS-HOST-STUBS): `xfa.host` is the most heavily used Adobe
2755 // Reader viewer namespace. The previous frozen stub only exposed
2756 // `numPages` + `messageBox`, so initializer scripts that touch
2757 // `xfa.host.title = "..."`, `xfa.host.openList(...)`, `xfa.host.beep()`,
2758 // etc. raised TypeError on the very first property access and inflated
2759 // `runtime_errors`. We expose a Proxy that:
2760 // * resolves known read-only viewer properties to deterministic defaults
2761 // (numPages, version, language, platform, name, title, validationsEnabled,
2762 // calculationsEnabled, currentPage, pageCount);
2763 // * absorbs every other write silently (viewer-only state has no flatten
2764 // side-effects);
2765 // * returns a null-safe sentinel for unknown reads so chains like
2766 // `xfa.host.someVendorExt.message` don't TypeError;
2767 // * exposes the interactive function family (messageBox, openList, beep,
2768 // response, print, gotoURL, setFocus, exportData, importData,
2769 // resetData) as safe-default thunks that ALSO bump the
2770 // `unsupported_host_calls` counter so dispatch keeps observability
2771 // without claiming a fake success (no "user clicked OK" lies).
2772 // No filesystem / network / process syscalls reach the host — every
2773 // interactive thunk is a pure JS no-op that delegates to host.unsupported
2774 // for accounting only. See benchmarks/JS_SANDBOX_SECURITY_AUDIT.md.
2775 var XFA_HOST_INTERACTIVE_CALLS = {
2776 "messageBox": { kind: "ret", value: 0 },
2777 "openList": { kind: "ret", value: -1 },
2778 "beep": { kind: "ret", value: undefined },
2779 "response": { kind: "ret", value: "" },
2780 "print": { kind: "ret", value: undefined },
2781 "gotoURL": { kind: "ret", value: undefined },
2782 "setFocus": { kind: "ret", value: undefined },
2783 "exportData": { kind: "ret", value: undefined },
2784 "importData": { kind: "ret", value: undefined },
2785 "resetData": { kind: "ret", value: undefined },
2786 "documentCountInBatch": { kind: "ret", value: 1 },
2787 "documentInBatch": { kind: "ret", value: 0 },
2788 // JS2-01 (Sprint 2 Batch B): closeDoc is referenced by Canadian IMM
2789 // and Quebec dol4n templates as an interactive viewer entry point.
2790 // Static flatten cannot dismiss a document; safe-default undefined
2791 // with `unsupported_host_calls` accounting keeps observability without
2792 // a TypeError. Pure JS no-op — no filesystem / network reach the host.
2793 "closeDoc": { kind: "ret", value: undefined }
2794 };
2795
2796 // Read-side defaults for viewer-readable host properties. These are pure
2797 // accessors — no host call leaves the sandbox. The values mirror Adobe
2798 // Reader behaviour during static, non-interactive rendering.
2799 function makeXfaHostBase() {
2800 var base = nullProtoObject();
2801 Object.defineProperty(base, "numPages", {
2802 enumerable: true, configurable: false,
2803 get: function() { return host.numPages(); }
2804 });
2805 Object.defineProperty(base, "currentPage", {
2806 enumerable: true, configurable: true,
2807 get: function() { return 0; },
2808 set: function(_v) { /* viewer-only */ }
2809 });
2810 Object.defineProperty(base, "pageCount", {
2811 enumerable: true, configurable: false,
2812 get: function() { return host.numPages(); }
2813 });
2814 // Static deterministic identity strings. We deliberately avoid claiming
2815 // Acrobat compatibility — scripts that branch on `xfa.host.name` for
2816 // Adobe-specific behaviour should fall through to a non-Acrobat path.
2817 var SCALAR_DEFAULTS = {
2818 "version": "PDFluent-XFA",
2819 "language": "ENU",
2820 "platform": "PDFluent",
2821 "name": "PDFluent",
2822 "title": "",
2823 "appType": "Reader",
2824 "variation": "Reader",
2825 "calculationsEnabled": true,
2826 "validationsEnabled": true,
2827 "runtimeHighlight": false,
2828 "runtimeHighlightColor": "",
2829 "viewerType": "PDFluent",
2830 "fullScreen": false
2831 };
2832 Object.keys(SCALAR_DEFAULTS).forEach(function(k) {
2833 var v = SCALAR_DEFAULTS[k];
2834 Object.defineProperty(base, k, {
2835 enumerable: true, configurable: true,
2836 get: function() { return v; },
2837 set: function(_v) { /* viewer-only, silently absorb */ }
2838 });
2839 });
2840 return base;
2841 }
2842
2843 // Install interactive function thunks. Each thunk increments the
2844 // unsupported-host-call counter and returns a deterministic safe default.
2845 function installXfaHostInteractive(base) {
2846 Object.keys(XFA_HOST_INTERACTIVE_CALLS).forEach(function(name) {
2847 var spec = XFA_HOST_INTERACTIVE_CALLS[name];
2848 Object.defineProperty(base, name, {
2849 enumerable: true, configurable: false, writable: false,
2850 value: function() {
2851 host.unsupportedHostCall(name);
2852 return spec.value;
2853 }
2854 });
2855 });
2856 }
2857
2858 var xfaHostBase = makeXfaHostBase();
2859 installXfaHostInteractive(xfaHostBase);
2860 // Wrap in a Proxy so unknown property reads return a null-safe sentinel
2861 // and unknown writes silently absorb. This is the "no TypeError" guarantee
2862 // for vendor-specific xfa.host extensions referenced by templated scripts.
2863 var xfaHost = new Proxy(xfaHostBase, {
2864 get: function(target, prop) {
2865 if (prop in target || typeof prop !== "string") return target[prop];
2866 // Unknown read — never throw, never claim a value. Sentinel only.
2867 return makeNullDataHandle();
2868 },
2869 set: function(_t, _p, _v) { return true; },
2870 has: function() { return true; }
2871 });
2872
2873 var xfaLayout = nullProtoObject();
2874 Object.defineProperty(xfaLayout, "pageCount", {
2875 enumerable: true,
2876 configurable: false,
2877 writable: false,
2878 value: function() {
2879 return host.numPages();
2880 }
2881 });
2882 // Phase D-γ: xfa.layout.page(node) — page number (1-based) of a form node.
2883 // During static flatten the layout is not yet run, so return a bounded
2884 // placeholder and mark the metadata as approximate.
2885 Object.defineProperty(xfaLayout, "page", {
2886 enumerable: true,
2887 configurable: false,
2888 writable: false,
2889 value: function() {
2890 host.resolveFailure();
2891 return 1;
2892 }
2893 });
2894 // Phase D-γ: xfa.layout.pageSpan(node) — number of pages a node spans.
2895 // Always 1 during static flatten; metadata records the approximation.
2896 Object.defineProperty(xfaLayout, "pageSpan", {
2897 enumerable: true,
2898 configurable: false,
2899 writable: false,
2900 value: function() {
2901 host.resolveFailure();
2902 return 1;
2903 }
2904 });
2905 Object.defineProperty(xfaLayout, "absPage", {
2906 enumerable: true,
2907 configurable: false,
2908 writable: false,
2909 value: function() {
2910 host.bindingError();
2911 return null;
2912 }
2913 });
2914
2915 var xfa = nullProtoObject();
2916 Object.defineProperty(xfa, "host", {
2917 enumerable: true,
2918 configurable: false,
2919 writable: false,
2920 // Note: deliberately NOT Object.freeze'd. `xfaHost` is already a Proxy
2921 // with sealed semantics (writes absorbed, unknown reads return sentinels).
2922 // Freezing would force every Set trap to throw under strict-mode scripts.
2923 value: xfaHost
2924 });
2925 Object.defineProperty(xfa, "layout", {
2926 enumerable: true,
2927 configurable: false,
2928 writable: false,
2929 value: Object.freeze(xfaLayout)
2930 });
2931
2932 // Phase E (XFA-JS-HOST-STUBS): viewer / interaction sub-namespaces. Each
2933 // is a Proxy that silently absorbs writes and returns chainable sentinels
2934 // on read so scripts can complete:
2935 //
2936 // xfa.viewer — Reader UI state (zoom, scrollbar, toolbar, ...)
2937 // xfa.appState — Reader-wide preference cache
2938 // xfa.appearanceFilter — accessibility / high-contrast hints
2939 // xfa.connection — `<connection>` outbound data binding stubs
2940 // xfa.signature — interactive digital-signature panel
2941 // xfa.aliasNode — script-time DOM alias rebinding
2942 // xfa.form — top-level FormDOM root reference (we expose the
2943 // same resolveNode/resolveNodes pair so the script
2944 // feels uniform; legacy property reads sentinel)
2945 //
2946 // These never call out to the host other than for accounting; no
2947 // filesystem / network / process syscalls reach the host bindings. Reads
2948 // for known property names return spec-conforming defaults; everything
2949 // else falls back to the null-safe sentinel chain.
2950 function makeViewerStubNamespace(staticReads) {
2951 var base = nullProtoObject();
2952 if (staticReads) {
2953 Object.keys(staticReads).forEach(function(k) {
2954 var v = staticReads[k];
2955 Object.defineProperty(base, k, {
2956 enumerable: true, configurable: true,
2957 get: function() { return v; },
2958 set: function(_v) { /* viewer-only — silent absorb */ }
2959 });
2960 });
2961 }
2962 return new Proxy(base, {
2963 get: function(target, prop) {
2964 if (prop in target || typeof prop !== "string") return target[prop];
2965 return makeNullDataHandle();
2966 },
2967 set: function(_t, _p, _v) { return true; },
2968 has: function() { return true; }
2969 });
2970 }
2971
2972 function makeInteractiveStubNamespace(funcs) {
2973 // funcs: { "sign": defaultRet, "verify": defaultRet, ... }
2974 var base = nullProtoObject();
2975 Object.keys(funcs).forEach(function(name) {
2976 var def = funcs[name];
2977 Object.defineProperty(base, name, {
2978 enumerable: true, configurable: false, writable: false,
2979 value: function() {
2980 host.unsupportedHostCall(name);
2981 return def;
2982 }
2983 });
2984 });
2985 return new Proxy(base, {
2986 get: function(target, prop) {
2987 if (prop in target || typeof prop !== "string") return target[prop];
2988 return makeNullDataHandle();
2989 },
2990 set: function(_t, _p, _v) { return true; },
2991 has: function() { return true; }
2992 });
2993 }
2994
2995 Object.defineProperty(xfa, "viewer", {
2996 enumerable: true, configurable: false, writable: false,
2997 value: makeViewerStubNamespace({
2998 "zoomType": "FitWidth",
2999 "zoom": 100,
3000 "scrollbar": "auto",
3001 "toolbar": true,
3002 "menubar": true,
3003 "statusbar": true
3004 })
3005 });
3006 Object.defineProperty(xfa, "appState", {
3007 enumerable: true, configurable: false, writable: false,
3008 value: makeViewerStubNamespace({
3009 "highlightRequiredFields": false,
3010 "fieldHighlightColor": "",
3011 "ariaEnabled": false
3012 })
3013 });
3014 Object.defineProperty(xfa, "appearanceFilter", {
3015 enumerable: true, configurable: false, writable: false,
3016 value: makeViewerStubNamespace(null)
3017 });
3018 Object.defineProperty(xfa, "aliasNode", {
3019 enumerable: true, configurable: false, writable: false,
3020 value: makeViewerStubNamespace(null)
3021 });
3022 Object.defineProperty(xfa, "connection", {
3023 enumerable: true, configurable: false, writable: false,
3024 // `<connection>` calls hit a real service endpoint in Adobe Reader.
3025 // Static flatten cannot honour that — every method is unsupported.
3026 value: makeInteractiveStubNamespace({
3027 "execute": null,
3028 "open": null,
3029 "close": null,
3030 "send": null
3031 })
3032 });
3033 Object.defineProperty(xfa, "signature", {
3034 enumerable: true, configurable: false, writable: false,
3035 // Digital signatures require an interactive certificate picker.
3036 value: makeInteractiveStubNamespace({
3037 "sign": false,
3038 "verify": "unknown",
3039 "enumerate": ""
3040 })
3041 });
3042 // WP-3 F3: `xfa.validate` is a viewer-only namespace controlling form-wide
3043 // validation behaviour (Reader UI prompts on field constraint violations).
3044 // Adobe initializers commonly do `xfa.validate.override = 0` /
3045 // `xfa.validate.max = N` to suppress modal dialogs that have no meaning
3046 // in a static flatten context. Expose a Proxy that silently absorbs every
3047 // property write (`override`, `max`, `messageMode`, etc.) and returns
3048 // empty defaults on read, so scripts complete without TypeError.
3049 Object.defineProperty(xfa, "validate", {
3050 enumerable: true,
3051 configurable: false,
3052 writable: false,
3053 value: new Proxy(nullProtoObject(), {
3054 get: function(_t, prop) {
3055 if (prop === "override" || prop === "max") return 0;
3056 if (prop === "messageMode") return "";
3057 if (typeof prop !== "string") return undefined;
3058 // Unknown reads return a chainable null-safe sentinel so deeper
3059 // access like `xfa.validate.scriptTest.message` does not throw.
3060 return makeNullDataHandle();
3061 },
3062 set: function(_t, _prop, _value) { return true; },
3063 has: function() { return true; }
3064 })
3065 });
3066 Object.defineProperty(xfa, "resolveNode", {
3067 enumerable: true,
3068 configurable: false,
3069 writable: false,
3070 value: function(path) {
3071 // Phase D-γ: data paths are routed to the DataDom, not the FormTree.
3072 if (typeof path === "string" &&
3073 (path.indexOf("data.") === 0 || path.indexOf("$data.") === 0 ||
3074 path.indexOf("xfa.datasets.data.") === 0)) {
3075 var rawId = host.dataResolveNode(path);
3076 if (rawId < 0) return null;
3077 return makeDataHandle(rawId);
3078 }
3079 // BE-1: intercept `FieldName.#items` — same logic as handleResolveNode.
3080 // Scripts call `xfa.resolveNode("Wojewodztwo.#items")` to get the items
3081 // collection for a choiceList; the form-tree SOM cannot resolve `#items`
3082 // as a node, so we resolve the field, fetch its display labels, and
3083 // return an XFA-compatible substitute (makeItemsSubstitute).
3084 // Guard: path.length > 7 prevents false-positive match when
3085 // lastIndexOf returns -1 and path.length-7 is also -1.
3086 if (typeof path === "string" && path.length > 7 &&
3087 path.lastIndexOf(".#items") === path.length - 7) {
3088 var fieldPath = path.substring(0, path.length - 7);
3089 var fid = host.resolveNodeId(fieldPath);
3090 if (fid >= 0) {
3091 var xrDisplayItems = host.getDisplayItems(fid, host.generation());
3092 if (xrDisplayItems.length > 0) {
3093 host.somItemsPathHit();
3094 }
3095 return makeItemsSubstitute(xrDisplayItems);
3096 }
3097 return null;
3098 }
3099 var id = host.resolveNodeId(path);
3100 if (id < 0) {
3101 return null;
3102 }
3103 return makeHandle(id, host.generation());
3104 }
3105 });
3106 // JS2-01 (Sprint 2 Batch B): `xfa.form` is the FormDOM root surface
3107 // Adobe Reader exposes (XFA 3.3 §6.1.4 — the result of merging template
3108 // + datasets). The previous build only commented that it should be
3109 // exposed (rquickjs_backend.rs:2477) but no actual binding existed —
3110 // scripts that did `xfa.form.resolveNode("Form1.Subform1")` or
3111 // `form.resolveNode(...)` ran into "cannot read property of undefined"
3112 // or "form is not defined". We expose a Proxy that:
3113 // * forwards `resolveNode` / `resolveNodes` to the existing
3114 // `xfa.resolveNode` / `xfa.resolveNodes` pair (cluster C contract
3115 // preserved — missing paths still return `null`, not a sentinel);
3116 // * absorbs unknown property writes (viewer-only flags) silently;
3117 // * returns a chainable null-safe sentinel for unknown reads.
3118 // No new Rust closure is registered; the binding is pure JS routing.
3119 function makeFormNamespace() {
3120 var base = nullProtoObject();
3121 Object.defineProperty(base, "resolveNode", {
3122 enumerable: true, configurable: false, writable: false,
3123 value: function(path) { return xfa.resolveNode(path); }
3124 });
3125 Object.defineProperty(base, "resolveNodes", {
3126 enumerable: true, configurable: false, writable: false,
3127 value: function(path) { return xfa.resolveNodes(path); }
3128 });
3129 // Adobe's `form` exposes `recalculate(true/false)` as a no-op trigger
3130 // for the calculation cascade. Templates pass `1` (force) commonly.
3131 // Static flatten already ran calculate scripts — accept the call,
3132 // return undefined, no counter (it is not an interactive prompt).
3133 Object.defineProperty(base, "recalculate", {
3134 enumerable: true, configurable: false, writable: false,
3135 value: function() { return undefined; }
3136 });
3137 Object.defineProperty(base, "execValidate", {
3138 enumerable: true, configurable: false, writable: false,
3139 value: function() { return true; }
3140 });
3141 Object.defineProperty(base, "execInitialize", {
3142 enumerable: true, configurable: false, writable: false,
3143 value: function() { return undefined; }
3144 });
3145 Object.defineProperty(base, "execCalculate", {
3146 enumerable: true, configurable: false, writable: false,
3147 value: function() { return undefined; }
3148 });
3149 return new Proxy(base, {
3150 get: function(target, prop) {
3151 if (prop in target || typeof prop !== "string") return target[prop];
3152 return makeNullDataHandle();
3153 },
3154 set: function(_t, _p, _v) { return true; },
3155 has: function() { return true; }
3156 });
3157 }
3158 var formNamespace = makeFormNamespace();
3159 Object.defineProperty(xfa, "form", {
3160 enumerable: true,
3161 configurable: false,
3162 writable: false,
3163 value: formNamespace
3164 });
3165 Object.defineProperty(xfa, "resolveNodes", {
3166 enumerable: true,
3167 configurable: false,
3168 writable: false,
3169 value: function(path) {
3170 // Phase D-γ: data paths are routed to the DataDom, not the FormTree.
3171 if (typeof path === "string" &&
3172 (path.indexOf("data.") === 0 || path.indexOf("$data.") === 0 ||
3173 path.indexOf("xfa.datasets.data.") === 0)) {
3174 var rawIds = host.dataResolveNodes(path);
3175 var out = [];
3176 for (var i = 0; i < rawIds.length; i++) out.push(makeDataHandle(rawIds[i]));
3177 return Object.freeze(out);
3178 }
3179 var generation = host.generation();
3180 var ids = host.resolveNodeIds(path);
3181 var out = [];
3182 for (var i = 0; i < ids.length; i++) {
3183 out.push(makeHandle(ids[i], generation));
3184 }
3185 return Object.freeze(out);
3186 }
3187 });
3188 // Phase D-δ.2: expose `xfa.event` as an alias for the per-script event
3189 // global. Real Adobe Reader populates this with the firing event;
3190 // during static flatten there is no dispatched UI event, so the
3191 // accessor returns the same defensive event-stub that the per-script
3192 // `event` parameter receives — newText/prevText/change default to
3193 // empty strings, target resolves to the firing field handle.
3194 Object.defineProperty(xfa, "event", {
3195 enumerable: true,
3196 configurable: false,
3197 get: function() {
3198 return makeEvent();
3199 }
3200 });
3201
3202 // Phase E (XFA-JS-HOST-STUBS): Acrobat / Reader `app` global. Adobe
3203 // initializer scripts commonly do:
3204 // app.calculate.override = true; // viewer-only flag — silent absorb
3205 // app.runtimeHighlight = false; // viewer-only flag — silent absorb
3206 // app.alert("submitted"); app.launchURL("…") // interactive — counted
3207 //
3208 // The previous frozen `app` only stubbed two functions; every other
3209 // property touch surfaced as TypeError. Wrap the whole namespace in a
3210 // Proxy with the same semantics as `xfa.host`:
3211 // * known viewer properties resolve to deterministic defaults;
3212 // * known interactive functions return a safe default + bump
3213 // unsupported_host_calls (NOT runtime_errors);
3214 // * unknown reads return a null-safe sentinel chain;
3215 // * unknown writes are absorbed.
3216 //
3217 // `app.calculate` is itself an absorbing sub-Proxy — every assignment
3218 // (`app.calculate.override = true`, `app.calculate.suspend = false`, ...)
3219 // is viewer-only and has no flatten side-effect. We document this as an
3220 // explicit silent no-op (not UnsupportedHostCapability) because
3221 // suppressing the viewer's calculation cascade is the script author's
3222 // ASKING the viewer to stop, not a UI prompt to the user. See
3223 // benchmarks/JS_SANDBOX_SECURITY_AUDIT.md for the full classification.
3224 var APP_INTERACTIVE_CALLS = {
3225 "alert": { kind: "ret", value: 0 }, // dialog OK pressed = 0
3226 "launchURL": { kind: "ret", value: undefined },
3227 "execMenuItem":{ kind: "ret", value: undefined },
3228 "beep": { kind: "ret", value: undefined },
3229 "openDoc": { kind: "ret", value: null },
3230 "response": { kind: "ret", value: "" },
3231 "mailMsg": { kind: "ret", value: undefined },
3232 // JS2-01 (Sprint 2 Batch B): Adobe Acrobat SDK exposes `app.messageBox`
3233 // as a richer alias of `app.alert` (modal dialog, returns the pressed
3234 // button index — 0 = OK). Safe default mirrors `app.alert`. No new
3235 // capability surface; pure JS no-op with counter accounting.
3236 "messageBox": { kind: "ret", value: 0 },
3237 // JS2-01: `app.closeDoc` is the Acrobat alias of `xfa.host.closeDoc`.
3238 // Templates targeting both Reader and Acrobat reference both spellings;
3239 // we counter-bump on each call and return undefined.
3240 "closeDoc": { kind: "ret", value: undefined }
3241 };
3242 function makeAppBase() {
3243 var base = nullProtoObject();
3244 // Read-side static defaults — never call out, never lie about identity.
3245 var SCALAR_DEFAULTS = {
3246 "viewerType": "PDFluent",
3247 "viewerVariation":"Reader",
3248 "viewerVersion": 0,
3249 "language": "ENU",
3250 "platform": "PDFluent",
3251 "fs": null, // file system gateway — explicitly null
3252 "media": null, // multimedia controller — explicitly null
3253 "fullscreen": false,
3254 "runtimeHighlight": false,
3255 "runtimeHighlightColor": ""
3256 };
3257 Object.keys(SCALAR_DEFAULTS).forEach(function(k) {
3258 var v = SCALAR_DEFAULTS[k];
3259 Object.defineProperty(base, k, {
3260 enumerable: true, configurable: true,
3261 get: function() { return v; },
3262 set: function(_v) { /* viewer-only, silently absorb */ }
3263 });
3264 });
3265 // app.calculate sub-namespace: writes absorb, reads return defaults.
3266 Object.defineProperty(base, "calculate", {
3267 enumerable: true, configurable: false, writable: false,
3268 value: makeViewerStubNamespace({
3269 "override": true,
3270 "suspend": false
3271 })
3272 });
3273 // Install interactive function thunks (same pattern as xfa.host).
3274 Object.keys(APP_INTERACTIVE_CALLS).forEach(function(name) {
3275 var spec = APP_INTERACTIVE_CALLS[name];
3276 Object.defineProperty(base, name, {
3277 enumerable: true, configurable: false, writable: false,
3278 value: function() {
3279 host.unsupportedHostCall(name);
3280 return spec.value;
3281 }
3282 });
3283 });
3284 return base;
3285 }
3286 var app = new Proxy(makeAppBase(), {
3287 get: function(target, prop) {
3288 if (prop in target || typeof prop !== "string") return target[prop];
3289 return makeNullDataHandle();
3290 },
3291 set: function(_t, _p, _v) { return true; },
3292 has: function() { return true; }
3293 });
3294
3295 // Phase C-α: viewer-only `event` global. Real Adobe Reader populates
3296 // this with the firing event; during static flatten there is no
3297 // dispatched UI event, so the object is a defensive stub that:
3298 // - exposes `target` resolving to the firing field handle (≈ `this`),
3299 // so scripts like `event.target.getField(somPath)` complete without
3300 // ReferenceError;
3301 // - exposes `change` as an empty string (the spec default);
3302 // - returns viewer-stubs from `target.getField()` so AcroForm widget
3303 // tweaks (`doNotScroll`, `required`, etc.) silently absorb.
3304 function makeEvent() {
3305 var id = host.currentNodeId();
3306 var fieldHandle = id < 0 ? null : makeHandle(id, host.generation());
3307 var target = nullProtoObject();
3308 Object.defineProperty(target, "getField", {
3309 enumerable: true, configurable: false, writable: false,
3310 value: function() {
3311
3312 return makeViewerStub();
3313 }
3314 });
3315 Object.defineProperty(target, "name", {
3316 enumerable: true, configurable: false,
3317 get: function() { return ""; }
3318 });
3319 Object.defineProperty(target, "self", {
3320 enumerable: true, configurable: false,
3321 get: function() { return fieldHandle; }
3322 });
3323 var ev = nullProtoObject();
3324 Object.defineProperty(ev, "target", {
3325 enumerable: true, configurable: false,
3326 get: function() { return target; }
3327 });
3328 // Phase D-δ.2: stable empty-string defaults for the change-event property
3329 // family so scripts that read `xfa.event.newText` / `prevText` /
3330 // `change` on initialize/calculate (where no real change event occurred)
3331 // do not throw `cannot read property 'X' of undefined`. Adobe populates
3332 // these on actual `change`/`exit` events; we run those activities later
3333 // (or never) and surface deterministic empty defaults instead.
3334 Object.defineProperty(ev, "change", {
3335 enumerable: true, configurable: false,
3336 get: function() { return ""; }
3337 });
3338 Object.defineProperty(ev, "newText", {
3339 enumerable: true, configurable: false,
3340 get: function() { return ""; }
3341 });
3342 Object.defineProperty(ev, "prevText", {
3343 enumerable: true, configurable: false,
3344 get: function() { return ""; }
3345 });
3346 Object.defineProperty(ev, "fullText", {
3347 enumerable: true, configurable: false,
3348 get: function() { return ""; }
3349 });
3350 Object.defineProperty(ev, "selStart", {
3351 enumerable: true, configurable: false,
3352 get: function() { return 0; }
3353 });
3354 Object.defineProperty(ev, "selEnd", {
3355 enumerable: true, configurable: false,
3356 get: function() { return 0; }
3357 });
3358 // JS2-01 (Sprint 2 Batch B): the Phase E security audit §3.9 promised
3359 // that `event` exposes the full Adobe Acrobat SDK event surface
3360 // (`target`, `change`, `newText`, `prevText`, `fullText`, `selStart`,
3361 // `selEnd`, plus `name`, `type`, `shift`, `modifier`, `commitKey`,
3362 // `willCommit`, `rc`, `keyDown`, `value`, `reenter`). Only the first
3363 // seven were implemented; scripts that probe `event.name` for the
3364 // firing activity (`if (event.name === "calculate") {...}`) silently
3365 // saw `undefined`, and scripts that chained off `event.X` could trip
3366 // a "not a function" if `X` was expected to be callable.
3367 //
3368 // All values are deterministic spec defaults that match Adobe's
3369 // behaviour during a non-interactive flatten (no firing event):
3370 // name — current activity if known, else ""
3371 // type — event type label (Adobe ≈ "Field"), "" if unknown
3372 // shift — modifier-key state (false)
3373 // modifier — modifier-key state (false)
3374 // commitKey — 0 (no commit key pressed)
3375 // willCommit — false (no commit pending)
3376 // rc — true (accept-by-default — same as Adobe's validate)
3377 // keyDown — false (no key pressed)
3378 // value — "" (current widget value, unknown during static flatten)
3379 // reenter — false (Acrobat re-entry flag)
3380 //
3381 // Read-only via getter, like the existing seven. The frozen ev object
3382 // means writes to these from scripts running in strict mode would throw
3383 // a TypeError — but no real script writes event scalars (the only
3384 // observed pattern is `event.value = ...` on a stale `event` reference
3385 // which is undefined, not the frozen object). If we ever observe
3386 // legitimate writes, swap getter for configurable getter+setter.
3387 Object.defineProperty(ev, "name", {
3388 enumerable: true, configurable: false,
3389 get: function() { return ""; }
3390 });
3391 Object.defineProperty(ev, "type", {
3392 enumerable: true, configurable: false,
3393 get: function() { return ""; }
3394 });
3395 Object.defineProperty(ev, "shift", {
3396 enumerable: true, configurable: false,
3397 get: function() { return false; }
3398 });
3399 Object.defineProperty(ev, "modifier", {
3400 enumerable: true, configurable: false,
3401 get: function() { return false; }
3402 });
3403 Object.defineProperty(ev, "commitKey", {
3404 enumerable: true, configurable: false,
3405 get: function() { return 0; }
3406 });
3407 Object.defineProperty(ev, "willCommit", {
3408 enumerable: true, configurable: false,
3409 get: function() { return false; }
3410 });
3411 Object.defineProperty(ev, "rc", {
3412 enumerable: true, configurable: false,
3413 get: function() { return true; }
3414 });
3415 Object.defineProperty(ev, "keyDown", {
3416 enumerable: true, configurable: false,
3417 get: function() { return false; }
3418 });
3419 Object.defineProperty(ev, "value", {
3420 enumerable: true, configurable: false,
3421 get: function() { return ""; }
3422 });
3423 Object.defineProperty(ev, "reenter", {
3424 enumerable: true, configurable: false,
3425 get: function() { return false; }
3426 });
3427 return Object.freeze(ev);
3428 }
3429
3430 // Phase D-γ: XFA global `util` (Acrobat SDK §Util). Provides date/number
3431 // formatting helpers used by many XFA templates. Only the subset required
3432 // by real-corpus scripts is implemented; unknown methods return "".
3433 //
3434 // util.printd(sFormat, dDate) — format a Date per sFormat using UTC fields.
3435 // Supported tokens: yyyy (year), yy (two-digit year), mm (month 01-12),
3436 // m (1-12), dd (day 01-31), d (1-31), HH (hour 00-23),
3437 // MM (minute 00-59), SS (second 00-59).
3438 // util.printx(cPicture, cValue) — picture format; returns cValue as-is.
3439 // util.scand(sFormat, cDate) — deterministic numeric parser for the same
3440 // token subset; unsupported or invalid input returns Invalid Date.
3441 var xfaUtil = (function() {
3442 var DateCtor = Date;
3443 var dateUtc = Date.UTC;
3444 function pad2(n) { return (n < 10 ? "0" : "") + n; }
3445 function invalidDate() { return new DateCtor(NaN); }
3446 function escapeRegex(text) {
3447 return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3448 }
3449 function parseDate(fmt, input) {
3450 var fmtText, text;
3451 try {
3452 fmtText = String(fmt);
3453 text = String(input);
3454 } catch (_e) {
3455 return invalidDate();
3456 }
3457
3458 var groups = [];
3459 var pattern = "^";
3460 for (var i = 0; i < fmtText.length;) {
3461 var rest = fmtText.substring(i);
3462 if (rest.indexOf("yyyy") === 0) {
3463 pattern += "(\\d{4})";
3464 groups.push("yyyy");
3465 i += 4;
3466 } else if (rest.indexOf("yy") === 0) {
3467 pattern += "(\\d{2})";
3468 groups.push("yy");
3469 i += 2;
3470 } else if (rest.indexOf("mm") === 0) {
3471 pattern += "(\\d{2})";
3472 groups.push("mm");
3473 i += 2;
3474 } else if (rest.indexOf("dd") === 0) {
3475 pattern += "(\\d{2})";
3476 groups.push("dd");
3477 i += 2;
3478 } else if (rest.indexOf("HH") === 0) {
3479 pattern += "(\\d{2})";
3480 groups.push("HH");
3481 i += 2;
3482 } else if (rest.indexOf("MM") === 0) {
3483 pattern += "(\\d{2})";
3484 groups.push("MM");
3485 i += 2;
3486 } else if (rest.indexOf("SS") === 0) {
3487 pattern += "(\\d{2})";
3488 groups.push("SS");
3489 i += 2;
3490 } else if (rest.indexOf("m") === 0) {
3491 pattern += "(\\d{1,2})";
3492 groups.push("m");
3493 i += 1;
3494 } else if (rest.indexOf("d") === 0) {
3495 pattern += "(\\d{1,2})";
3496 groups.push("d");
3497 i += 1;
3498 } else {
3499 pattern += escapeRegex(fmtText.charAt(i));
3500 i += 1;
3501 }
3502 }
3503
3504 var match = new RegExp(pattern + "$").exec(text);
3505 if (!match) return invalidDate();
3506
3507 var year = NaN, month = 1, day = 1, hour = 0, minute = 0, second = 0;
3508 for (var g = 0; g < groups.length; g++) {
3509 var value = parseInt(match[g + 1], 10);
3510 if (!isFinite(value)) return invalidDate();
3511 if (groups[g] === "yyyy") year = value;
3512 else if (groups[g] === "yy") year = 2000 + value;
3513 else if (groups[g] === "mm" || groups[g] === "m") month = value;
3514 else if (groups[g] === "dd" || groups[g] === "d") day = value;
3515 else if (groups[g] === "HH") hour = value;
3516 else if (groups[g] === "MM") minute = value;
3517 else if (groups[g] === "SS") second = value;
3518 }
3519
3520 if (!isFinite(year) || month < 1 || month > 12 || day < 1 || day > 31 ||
3521 hour < 0 || hour > 23 || minute < 0 || minute > 59 ||
3522 second < 0 || second > 59) {
3523 return invalidDate();
3524 }
3525 var out = new DateCtor(dateUtc(year, month - 1, day, hour, minute, second));
3526 if (out.getUTCFullYear() !== year ||
3527 out.getUTCMonth() + 1 !== month ||
3528 out.getUTCDate() !== day ||
3529 out.getUTCHours() !== hour ||
3530 out.getUTCMinutes() !== minute ||
3531 out.getUTCSeconds() !== second) {
3532 return invalidDate();
3533 }
3534 return out;
3535 }
3536 var u = nullProtoObject();
3537 u.printd = function(fmt, date) {
3538 if (!(date instanceof DateCtor) || isNaN(date.getTime())) return "";
3539 var y = date.getUTCFullYear();
3540 var mo = date.getUTCMonth() + 1;
3541 var d = date.getUTCDate();
3542 var h = date.getUTCHours();
3543 var mi = date.getUTCMinutes();
3544 var s = date.getUTCSeconds();
3545 var result = String(fmt);
3546 result = result.replace(/yyyy/g, y)
3547 .replace(/yy/g, String(y).slice(-2))
3548 .replace(/mm/g, pad2(mo))
3549 .replace(/m/g, mo)
3550 .replace(/dd/g, pad2(d))
3551 .replace(/d/g, d)
3552 .replace(/HH/g, pad2(h))
3553 .replace(/MM/g, pad2(mi))
3554 .replace(/SS/g, pad2(s));
3555 return result;
3556 };
3557 u.printx = function(_fmt, val) { return val === null || val === undefined ? "" : String(val); };
3558 u.scand = function(fmt, str) { return parseDate(fmt, str); };
3559 return Object.freeze(u);
3560 }());
3561
3562 // Phase C-α: minimal `console` no-op. Many forms guard with
3563 // `if (typeof console !== "undefined") console.log(...)` and proceed
3564 // when the symbol exists. Stub returns undefined; never writes
3565 // anywhere observable to the script.
3566 var consoleStub = nullProtoObject();
3567 ["log","warn","error","info","debug","trace"].forEach(function(name) {
3568 Object.defineProperty(consoleStub, name, {
3569 enumerable: true, configurable: false, writable: false,
3570 value: function() { return undefined; }
3571 });
3572 });
3573
3574 // Phase D-ι: form-level globals registered from `<variables>` `<script>`
3575 // blocks. Each entry is `name -> frozen object`. Populated by the host
3576 // once per document via `setVariablesScript`; cleared by
3577 // `clearVariablesScripts` at `reset_per_document`.
3578 var variablesScripts = lookupObject();
3579 // Phase D-ι.2: subform-scoped variables. Maps subform name -> namespace
3580 // object containing that subform's named scripts. Enables
3581 // `subformHandle.variables.ScriptName.method()` access paths.
3582 var subformVariables = lookupObject();
3583 // W3-D RETRY: form-level mutable string data items registered from
3584 // `<variables>` `<text name="X">value</text>` blocks (XFA 3.3 §5.5.2).
3585 // Each entry is `name -> { value: <string> }`. Adobe Reader exposes these
3586 // as global mutable string containers — the canonical IMM5709 pattern
3587 // is `<text name="globValidatePressed"/>` referenced from event scripts
3588 // as `globValidatePressed.value = "true";`. Populated by
3589 // `setVariablesDataItem`; cleared by `clearVariablesScripts` (shared
3590 // teardown — same per-document lifecycle).
3591 var variablesDataItems = lookupObject();
3592 // W3-D RETRY: subform-scoped variant of `variablesDataItems`, parallel to
3593 // `subformVariables` for scripts. Reserved for future subform-level data
3594 // items; currently populated only at root scope but the structure is
3595 // here so an upcoming pass can light it up without further refactor.
3596 var subformVariablesDataItems = lookupObject();
3597 // D4: flat first-wins exposure of subform-scoped named `<script>` objects to
3598 // bare-identifier lookup. The host only writes an entry here for names that
3599 // are unique across all subforms (ambiguous names are withheld — fail-closed
3600 // — and counted Rust-side). Consulted by the `with` proxy AFTER
3601 // `variablesScripts` / `variablesDataItems` (root scope wins) and BEFORE the
3602 // host SOM resolver, so a subform-scoped helper such as `countryScript`
3603 // resolves as a bare global instead of falling through to a SOM NoMatch.
3604 var subformScriptsByName = lookupObject();
3605
3606 function makeImplicitGlobals(body) {
3607 var currentId = host.currentNodeId();
3608 var generation = host.generation();
3609 var localNames = collectLocalNames(String(body));
3610 var cachedHandles = lookupObject();
3611 var dynamicLocals = lookupObject();
3612
3613 function lookup(name) {
3614 if (cachedHandles[name] !== undefined) {
3615 return cachedHandles[name];
3616 }
3617 var nodeIds = uniqueNodeIds(host.resolveImplicitNodeIds(currentId, name));
3618 if (nodeIds.length === 0) {
3619 // BE-1 tranche #1: a bare implicit identifier that fails the scope
3620 // resolve but names a template-declared container resolves to a benign
3621 // EMPTY node (Adobe semantics: isNull===true), not `undefined`. This
3622 // lets guarded scripts such as
3623 // if (!Podmiot1.OsobaFizyczna.PESEL.isNull) {...} else {...}
3624 // take the empty (else) branch and run their setInstances(0)/presence
3625 // writes instead of throwing "cannot read property '…' of undefined"
3626 // and aborting. Undeclared names still return `undefined`, preserving
3627 // the D-θ.1 `A.B === undefined` byte-identity contract for genuine
3628 // misses. Gated host-side by `isDeclaredAbsentNode`; sandboxed-only.
3629 if (host.isDeclaredAbsentNode(name)) {
3630 var facade = makeAbsentNodeHandle();
3631 cachedHandles[name] = facade;
3632 return facade;
3633 }
3634 return undefined;
3635 }
3636 // Phase D-θ.2: wrap an implicit hit in the full-chain accumulator. The
3637 // resulting proxy accumulates SOM property names without contacting
3638 // the host until a terminal property is read; only then is the full
3639 // chain resolved with backtracking. This lets `A.B.C.D.rawValue`
3640 // disambiguate the same-name `A` whose subtree completes the chain,
3641 // rather than relying on a single-segment hint as in D-θ.1. Terminal
3642 // properties still resolve byte-for-byte identically to the
3643 // un-hinted walk so single-token reads keep their existing
3644 // semantics.
3645 var handle = makeChainProxy([], [name], nodeIds, generation, currentId);
3646 cachedHandles[name] = handle;
3647 return handle;
3648 }
3649
3650 return new Proxy(Object.create(null), {
3651 has: function(_target, prop) {
3652 if (typeof prop !== "string") {
3653 return false;
3654 }
3655 // XFA-DATA-M3C: `_<Name>` and `parent` are XFA-defined globals that
3656 // must be visible to `with()` lookup even when the local-names
3657 // collector treats them as locals (they shadow no real var/let).
3658 if (prop === "parent") {
3659 return true;
3660 }
3661 if (prop.charAt(0) === "_" && prop.length > 1 &&
3662 localNames[prop] !== true) {
3663 // Probe — only claim presence when the bare-name resolves to a
3664 // real subform/container; otherwise fall through to the standard
3665 // defer rules so unrelated underscored locals stay undefined.
3666 // Quiet probe: a miss here is the schema-optional path and must
3667 // not count as a script-level resolve failure.
3668 var bare = prop.substring(1);
3669 if (uniqueNodeIds(host.resolveImplicitNodeIdsQuiet(currentId, bare)).length > 0 ||
3670 host.hasZeroInstanceRun(currentId, generation, bare)) {
3671 return true;
3672 }
3673 }
3674 if (shouldDeferGlobalName(prop, localNames)) {
3675 return false;
3676 }
3677 return true;
3678 },
3679 get: function(_target, prop) {
3680 if (typeof prop !== "string") {
3681 return undefined;
3682 }
3683 // XFA-DATA-M3C: bare `parent` resolves to the form-tree parent of
3684 // the current script node. Common in calculate/initialize scripts
3685 // for `parent.index`, `parent.rawValue`, and chain navigation that
3686 // mirrors Adobe's implicit-scope walk one step upward.
3687 if (prop === "parent") {
3688 var parentId = host.parentOfNode(currentId, generation);
3689 if (parentId < 0) return undefined;
3690 return makeHandle(parentId, generation);
3691 }
3692 // XFA 3.3 §6.4.3.2 underscore shorthand at the global scope:
3693 // `_<Name>` referenced as a bare identifier denotes the
3694 // instanceManager of a same-named subform reachable from the
3695 // current implicit scope. Adobe Reader exposes this both as a
3696 // child property (`parent._Foo`) and as a bare global. Without
3697 // this branch the `shouldDeferGlobalName` rule below returns
3698 // `undefined` for every underscored bare ident and the calling
3699 // script throws `ReferenceError: _Foo is not defined`.
3700 //
3701 // Only triggers when the target subform actually exists; otherwise
3702 // fall through so unrelated underscored locals keep their
3703 // existing deferred-undefined semantics.
3704 if (prop.charAt(0) === "_" && prop.length > 1 &&
3705 localNames[prop] !== true) {
3706 var bareName = prop.substring(1);
3707 // Quiet probe: schema-optional misses are NOT a script-level
3708 // resolve failure. We surface either a live manager, an empty
3709 // manager (zero-instance run or never-instantiated optional
3710 // subform), or fall through to the defer rules.
3711 var imIds = uniqueNodeIds(
3712 host.resolveImplicitNodeIdsQuiet(currentId, bareName)
3713 );
3714 if (imIds.length > 0) {
3715 return makeInstanceManager(imIds[0], generation);
3716 }
3717 if (host.hasZeroInstanceRun(currentId, generation, bareName)) {
3718 return makeEmptyInstanceManager();
3719 }
3720 }
3721 if (shouldDeferGlobalName(prop, localNames)) {
3722 return undefined;
3723 }
3724 // Phase D-γ: $record as a script-level global refers to the data
3725 // record bound to the current field's enclosing subform context.
3726 // Scripts write `var addr = $record.SECTION.nodes;` — we intercept
3727 // this here instead of letting resolveImplicitNodeId fail (-1).
3728 if (prop === "$record") {
3729 var recRaw = host.dataBoundRecord(currentId, generation);
3730 if (recRaw < 0) return makeNullDataHandle();
3731 return makeDataHandle(recRaw);
3732 }
3733 // BE-1: `$data` as a bare global refers to the data-DOM root node
3734 // (XFA 3.3 §3.3.2: `$data` == `xfa.datasets.data`). Scripts write
3735 // e.g. `var root = $data; root.child.value` — we intercept here
3736 // before the SOM resolver so the data-dom root is returned directly.
3737 // `data_resolve_node("$data")` resolves to the root of the DataDom
3738 // (SomRoot::Data + zero segments -> start node) via resolve_data_som.
3739 if (prop === "$data") {
3740 var dataRootRaw = host.dataResolveNode("$data");
3741 if (dataRootRaw < 0) return makeNullDataHandle();
3742 host.somDataRootHit();
3743 return makeDataHandle(dataRootRaw);
3744 }
3745 // Phase D-γ: `util` is an XFA global (Acrobat SDK §Util) that provides
3746 // date/number formatting functions. `util.printd(fmt, date)` is widely
3747 // used by XFA templates to format Date objects. We intercept it here so
3748 // scripts can complete without a TypeError instead of throwing and
3749 // aborting all later mutations in the same script body.
3750 if (prop === "util") {
3751 return xfaUtil;
3752 }
3753 if (dynamicLocals[prop] !== undefined) {
3754 return dynamicLocals[prop];
3755 }
3756 // Phase D-ι: form-level named-script globals from <variables>
3757 // outrank the form-tree implicit lookup. Adobe XFA spec §5.5
3758 // exposes `<scriptName>.<topLevelDecl>` to all event/calculate
3759 // scripts in the same document.
3760 if (variablesScripts[prop] !== undefined) {
3761 return variablesScripts[prop];
3762 }
3763 // W3-D RETRY: form-level data items (XFA 3.3 §5.5.2) outrank the
3764 // form-tree implicit lookup, same as scripts. This lets templates
3765 // like Canadian IMM5709 reference `globValidatePressed.value`
3766 // without the bare ident falling through to the SOM resolver as a
3767 // `js_resolve_failure`. Scripts take precedence over data items in
3768 // the unlikely case both share a name (spec-undefined; we follow
3769 // declaration order, which is `setVariablesScript` first in the
3770 // host loop).
3771 if (variablesDataItems[prop] !== undefined) {
3772 return variablesDataItems[prop];
3773 }
3774 // D4: subform-scoped named script objects (unique-name only) resolve as
3775 // bare globals here, after root-scope scripts/data items and before the
3776 // host SOM resolver. This is the minimal SOM resolution for the
3777 // `countryScript` / `partNoScript` cluster (XFA 3.3 §5.5 subform
3778 // variables). Ambiguous names were never written here (fail-closed).
3779 if (subformScriptsByName[prop] !== undefined) {
3780 return subformScriptsByName[prop];
3781 }
3782 return lookup(prop);
3783 },
3784 set: function(_target, prop, value) {
3785 if (typeof prop !== "string") {
3786 return true;
3787 }
3788 if (shouldDeferGlobalName(prop, localNames)) {
3789 return false;
3790 }
3791 if (cachedHandles[prop] !== undefined) {
3792 return true;
3793 }
3794 dynamicLocals[prop] = value;
3795 return true;
3796 }
3797 });
3798 }
3799
3800 return {
3801 // Phase E (XFA-JS-HOST-STUBS): `xfa` itself is still frozen because all
3802 // of its properties were installed with `configurable: false` and writes
3803 // go through inner-Proxy `set` traps that we control. `app` is a Proxy
3804 // whose underlying target is non-extensible after we install the
3805 // function thunks; freezing the Proxy itself would force the absorbing
3806 // `set` trap to throw a TypeError (proxy invariants: writes to
3807 // non-extensible targets must reject), defeating the whole point of the
3808 // silent-absorb design. We therefore expose `app` unfrozen — its safety
3809 // is enforced by the Proxy traps, not by Object.freeze.
3810 xfa: Object.freeze(xfa),
3811 app: app,
3812 // JS2-01 (Sprint 2 Batch B): top-level `form` global alias for
3813 // `xfa.form`. Adobe Reader exposes both spellings; templates such as
3814 // corpus forms reference bare `form.X`. The alias is identity-equal
3815 // to `xfa.form` (same Proxy), so cluster C contract preservation is
3816 // shared between the two surfaces. No new Rust closure introduced.
3817 form: formNamespace,
3818 consoleStub: Object.freeze(consoleStub),
3819 // Phase D-ι: register a `<variables>` `<script name="X">…` block as a
3820 // form-level global. Called by the host once per script body at
3821 // document load. `body` is the raw script source; `identNames` is a
3822 // pre-extracted array of top-level `var` / `function` identifiers
3823 // (Rust-side regex). The body is wrapped in an IIFE that returns a
3824 // frozen object whose properties are those identifiers. Variables
3825 // scripts share the same time/memory budget enforcement as event
3826 // scripts but emit no field mutations of their own. Errors during
3827 // evaluation are absorbed: the namespace remains undefined and
3828 // dependent event scripts will fail naturally at first use.
3829 // Phase D-ι / D-ι.2: register a named `<variables><script>` body.
3830 // `subformName` (4th param, optional) is non-empty for subform-scoped
3831 // scripts; omit or pass "" for root-level scripts.
3832 //
3833 // Root-level scripts (empty subformName) go into the flat
3834 // `variablesScripts` dict only — accessible as `ScriptName.X` from
3835 // any event script in the document.
3836 //
3837 // Subform-scoped scripts go into `subformVariables[subformName][name]`
3838 // ONLY — accessible as `subformHandle.variables.ScriptName.X`. They
3839 // are intentionally NOT written to the flat dict: two subforms may
3840 // define the same script name, and writing both to the flat map would
3841 // let the second registration silently shadow the first.
3842 setVariablesScript: function(name, body, identNames, subformName, exposeGlobal) {
3843 if (typeof name !== "string" || name.length === 0) return false;
3844 if (typeof body !== "string") return false;
3845 var idents = Array.isArray(identNames) ? identNames : [];
3846 var props = "";
3847 for (var i = 0; i < idents.length; i++) {
3848 var id = idents[i];
3849 if (typeof id !== "string" || id.length === 0) continue;
3850 if (i > 0) props += ",";
3851 props += JSON.stringify(id) + ": typeof " + id +
3852 " !== \"undefined\" ? " + id + " : undefined";
3853 }
3854 // XFA-DATA2-02: inject `console` and `util` into the variables-script
3855 // closure so the form-level helper functions registered here can
3856 // reference them lexically and the dependent event scripts that call
3857 // those helpers do not blow up with `console is not defined` /
3858 // `util is not defined`. Both are the same sandbox-safe singletons
3859 // exposed to event scripts (silent no-op console, deterministic util).
3860 var wrapper = "(function(__console, __util){\n" +
3861 "var console = __console; var util = __util;\n" +
3862 body +
3863 "\nreturn Object.freeze({" + props + "});\n})";
3864 try {
3865 var ns = (Function("return " + wrapper))()(consoleStub, xfaUtil);
3866 if (typeof subformName === "string" && subformName.length > 0) {
3867 if (subformVariables[subformName] === undefined) {
3868 subformVariables[subformName] = lookupObject();
3869 }
3870 subformVariables[subformName][name] = ns;
3871 // D4: also expose to bare-identifier lookup when the host marked this
3872 // name unique across subforms (ambiguous names are withheld).
3873 if (exposeGlobal === true && subformScriptsByName[name] === undefined) {
3874 subformScriptsByName[name] = ns;
3875 }
3876 } else {
3877 variablesScripts[name] = ns;
3878 }
3879 return true;
3880 } catch (_e) {
3881 return false;
3882 }
3883 },
3884 clearVariablesScripts: function() {
3885 var keys = Object.keys(variablesScripts);
3886 for (var i = 0; i < keys.length; i++) {
3887 delete variablesScripts[keys[i]];
3888 }
3889 var skeys = Object.keys(subformVariables);
3890 for (var j = 0; j < skeys.length; j++) {
3891 delete subformVariables[skeys[j]];
3892 }
3893 // D4: clear the flat subform-script exposure map on the same per-document
3894 // lifecycle as the scoped maps.
3895 var ssnkeys = Object.keys(subformScriptsByName);
3896 for (var n = 0; n < ssnkeys.length; n++) {
3897 delete subformScriptsByName[ssnkeys[n]];
3898 }
3899 // W3-D RETRY: data items share the same per-document lifecycle as
3900 // scripts. Dropping them here keeps a single bridge entry-point at
3901 // `reset_for_new_document` and prevents per-document drift.
3902 var dkeys = Object.keys(variablesDataItems);
3903 for (var k = 0; k < dkeys.length; k++) {
3904 delete variablesDataItems[dkeys[k]];
3905 }
3906 var sdkeys = Object.keys(subformVariablesDataItems);
3907 for (var m = 0; m < sdkeys.length; m++) {
3908 delete subformVariablesDataItems[sdkeys[m]];
3909 }
3910 },
3911 // W3-D RETRY: register a `<variables>` `<text name="X">value</text>`
3912 // data item (XFA 3.3 §5.5.2) as a form-level mutable string container.
3913 // Idempotent per (subformName?, name): a later registration overwrites
3914 // the earlier one, mirroring Adobe Reader's last-wins template merge.
3915 //
3916 // The returned shape is `{ value: <string> }` — minimal because real
3917 // templates only read/write `.value`. Writes coerce non-string inputs
3918 // to string (matching Adobe Reader's text-field semantics) so a
3919 // calculate script that does `globValidatePressed.value = true` does
3920 // not silently store a boolean.
3921 setVariablesDataItem: function(name, initial, subformName) {
3922 if (typeof name !== "string" || name.length === 0) return false;
3923 var initialStr = (typeof initial === "string") ? initial : "";
3924 // Use a closure-bound private slot so the `value` getter / setter
3925 // can coerce on write without exposing a plain-object property that
3926 // would let `delete item.value` succeed.
3927 var item = (function(initStr) {
3928 var current = initStr;
3929 var holder = Object.create(null);
3930 Object.defineProperty(holder, "value", {
3931 enumerable: true,
3932 configurable: false,
3933 get: function() { return current; },
3934 set: function(v) {
3935 current = (v === undefined || v === null) ? "" : String(v);
3936 }
3937 });
3938 // XFA 3.3 §5.5.2: data items also expose `.name`. Read-only.
3939 Object.defineProperty(holder, "name", {
3940 enumerable: true, configurable: false, writable: false,
3941 value: name
3942 });
3943 return holder;
3944 })(initialStr);
3945 if (typeof subformName === "string" && subformName.length > 0) {
3946 if (subformVariablesDataItems[subformName] === undefined) {
3947 subformVariablesDataItems[subformName] = lookupObject();
3948 }
3949 subformVariablesDataItems[subformName][name] = item;
3950 } else {
3951 variablesDataItems[name] = item;
3952 }
3953 return true;
3954 },
3955 evalScript: function(body) {
3956 var id = host.currentNodeId();
3957 var thisArg = id < 0 ? undefined : makeHandle(id, host.generation());
3958 // Phase C-α: install per-script `event` global in the function
3959 // closure so `event.target` resolves to the current field. Wrapping
3960 // body inside a function lets us pass `event` as a parameter
3961 // without leaking it to globalThis (where it would persist across
3962 // unrelated scripts).
3963 var ev = makeEvent();
3964 var consoleArg = consoleStub;
3965 var globals = makeImplicitGlobals(body);
3966 return (Function(
3967 "event",
3968 "console",
3969 "__globals",
3970 "with(__globals){\n" + String(body) + "\n}"
3971 )).call(thisArg, ev, consoleArg, globals);
3972 }
3973 };
3974})
3975"##;
3976
3977static EPOCH_CELL: OnceLock<Instant> = OnceLock::new();
3981fn epoch() -> Instant {
3982 *EPOCH_CELL.get_or_init(Instant::now)
3983}
3984
3985impl XfaJsRuntime for QuickJsRuntime {
3986 fn init(&mut self) -> Result<(), SandboxError> {
3987 let result = catch_unwind(AssertUnwindSafe(|| {
3991 self.context.with(|ctx| {
3992 let globals = ctx.globals();
3993 for forbidden in [
3997 "fetch",
3998 "XMLHttpRequest",
3999 "WebSocket",
4000 "process",
4001 "require",
4002 "Deno",
4003 "Bun",
4004 ] {
4005 let _ = globals.set(forbidden, rquickjs::Undefined);
4006 }
4007 if let Ok(date_ctor) = globals.get::<_, rquickjs::Object>("Date") {
4009 let zero_now = Function::new(ctx.clone(), || 0i64)
4010 .map_err(|e| format!("date stub: {e}"))?;
4011 let _ = date_ctor.set("now", zero_now);
4012 }
4013 if let Ok(math_ns) = globals.get::<_, rquickjs::Object>("Math") {
4014 let _ = math_ns.set("random", rquickjs::Undefined);
4015 }
4016 Ok::<(), String>(())
4017 })?;
4018 self.register_host_bindings()?;
4019 Ok::<(), String>(())
4020 }));
4021 match result {
4022 Ok(Ok(())) => Ok(()),
4023 Ok(Err(e)) => Err(SandboxError::ScriptError(e)),
4024 Err(_) => Err(SandboxError::PanicCaptured(
4025 "panic while initialising sandbox globals".to_string(),
4026 )),
4027 }
4028 }
4029
4030 fn reset_for_new_document(&mut self) -> Result<(), SandboxError> {
4031 self.metadata = RuntimeMetadata::default();
4032 self.host.borrow_mut().reset_per_document();
4033 self.clear_deadline();
4034 self.runtime.set_memory_limit(self.memory_budget_bytes);
4036 if let Err(e) = self.clear_variables_scripts_global() {
4041 log::debug!("D-ι clear failed: {e:?}");
4042 }
4043 Ok(())
4044 }
4045
4046 #[allow(clippy::not_unsafe_ptr_arg_deref)]
4052 fn set_form_handle(
4053 &mut self,
4054 form: *mut FormTree,
4055 root_id: FormNodeId,
4056 ) -> Result<(), SandboxError> {
4057 self.host.borrow_mut().set_form_handle(form, root_id);
4058 if !form.is_null() {
4064 let scripts: Vec<(Option<String>, String, String)> =
4066 unsafe { (*form).variables_scripts.clone() };
4067 self.metadata.variables_scripts_collected = self
4070 .metadata
4071 .variables_scripts_collected
4072 .saturating_add(scripts.len());
4073 let mut subform_name_counts: std::collections::HashMap<String, usize> =
4078 std::collections::HashMap::new();
4079 for (subform_scope, name, _) in &scripts {
4080 if subform_scope.is_some() {
4081 *subform_name_counts.entry(name.clone()).or_insert(0) += 1;
4082 }
4083 }
4084 let ambiguous_subform_names = subform_name_counts.values().filter(|&&c| c > 1).count();
4085 self.metadata.som_lookup_ambiguous = self
4086 .metadata
4087 .som_lookup_ambiguous
4088 .saturating_add(ambiguous_subform_names);
4089 for (subform_scope, name, body) in scripts {
4090 if subform_scope.is_some() {
4091 self.metadata.script_objects_subform_scoped = self
4092 .metadata
4093 .script_objects_subform_scoped
4094 .saturating_add(1);
4095 }
4096 let expose_global = subform_scope.is_some()
4098 && subform_name_counts.get(&name).copied().unwrap_or(0) == 1;
4099 match self.register_variables_script(
4100 &name,
4101 &body,
4102 subform_scope.as_deref(),
4103 expose_global,
4104 ) {
4105 Ok(true) => {
4106 self.metadata.script_objects_registered =
4107 self.metadata.script_objects_registered.saturating_add(1);
4108 if expose_global {
4111 self.metadata.som_subform_scripts_exposed =
4112 self.metadata.som_subform_scripts_exposed.saturating_add(1);
4113 }
4114 }
4115 Ok(false) => {
4116 self.metadata.script_objects_register_failed = self
4117 .metadata
4118 .script_objects_register_failed
4119 .saturating_add(1);
4120 log::debug!("D-ι register `{name}` returned false (JS eval failed)");
4121 }
4122 Err(e) => {
4123 self.metadata.script_objects_register_failed = self
4124 .metadata
4125 .script_objects_register_failed
4126 .saturating_add(1);
4127 log::debug!("D-ι register `{name}` failed: {e:?}");
4128 }
4129 }
4130 }
4131 let data_items: Vec<(Option<String>, String, String)> =
4137 unsafe { (*form).variables_data_items.clone() };
4138 self.metadata.variables_data_items_collected = self
4140 .metadata
4141 .variables_data_items_collected
4142 .saturating_add(data_items.len());
4143 for (subform_scope, name, initial) in data_items {
4144 match self.register_variables_data_item(&name, &initial, subform_scope.as_deref()) {
4145 Ok(()) => {
4146 self.metadata.script_objects_registered =
4147 self.metadata.script_objects_registered.saturating_add(1);
4148 }
4149 Err(e) => {
4150 self.metadata.script_objects_register_failed = self
4151 .metadata
4152 .script_objects_register_failed
4153 .saturating_add(1);
4154 log::debug!("W3-D register data item `{name}` failed: {e:?}");
4155 }
4156 }
4157 }
4158 }
4159 Ok(())
4160 }
4161
4162 fn set_data_handle(&mut self, dom: *const xfa_dom_resolver::data_dom::DataDom) {
4163 self.host.borrow_mut().set_data_handle(dom);
4164 }
4165
4166 fn set_declared_subform_names(&mut self, names: std::collections::HashSet<String>) {
4167 self.host.borrow_mut().set_declared_subform_names(names);
4168 }
4169
4170 fn reset_per_script(
4171 &mut self,
4172 current_id: FormNodeId,
4173 activity: Option<&str>,
4174 ) -> Result<(), SandboxError> {
4175 self.host
4176 .borrow_mut()
4177 .reset_per_script(current_id, activity);
4178 Ok(())
4179 }
4180
4181 fn set_static_page_count(&mut self, page_count: u32) -> Result<(), SandboxError> {
4182 self.host.borrow_mut().set_static_page_count(page_count);
4183 Ok(())
4184 }
4185
4186 fn set_presave_gate(&mut self, enabled: bool) {
4187 self.host.borrow_mut().set_presave_gate(enabled);
4190 }
4191
4192 fn execute_script(
4193 &mut self,
4194 activity: Option<&str>,
4195 body: &str,
4196 ) -> Result<RuntimeOutcome, SandboxError> {
4197 let presave_gate = self.host.borrow().presave_gate();
4204 if !activity_allowed_for_sandbox_with_gate(activity, presave_gate) {
4205 return Err(SandboxError::PhaseDenied(
4206 activity.unwrap_or("None").to_string(),
4207 ));
4208 }
4209 if body.len() > MAX_SCRIPT_BODY_BYTES {
4210 self.metadata.runtime_errors = self.metadata.runtime_errors.saturating_add(1);
4211 return Err(SandboxError::BodyTooLarge);
4212 }
4213
4214 if let RegexScanVerdict::Reject { reason } = scan_script_for_redos(body) {
4220 self.metadata.runtime_errors = self.metadata.runtime_errors.saturating_add(1);
4221 return Err(SandboxError::RegexRejected(reason));
4222 }
4223
4224 self.set_deadline();
4225 let script_owned = body.to_string();
4226 let result = catch_unwind(AssertUnwindSafe(|| {
4230 self.context.with(|ctx| -> Result<(), rquickjs::Error> {
4231 let Some(eval_script) = self.eval_script.clone() else {
4232 return Err(rquickjs::Error::new_from_js_message(
4233 "host bindings",
4234 "Function",
4235 "Phase C eval bridge not registered",
4236 ));
4237 };
4238 let eval_script = eval_script.restore(&ctx)?;
4239 if let Err(e) = eval_script.call::<_, ()>((script_owned,)) {
4240 let exc_msg = if matches!(e, rquickjs::Error::Exception) {
4244 let val = ctx.catch();
4245 if let Some(exc) = val.as_exception() {
4247 exc.message().unwrap_or_else(|| exc.to_string())
4248 } else {
4249 e.to_string()
4250 }
4251 } else {
4252 e.to_string()
4253 };
4254 return Err(rquickjs::Error::new_from_js_message(
4255 "script", "Error", exc_msg,
4256 ));
4257 }
4258 Ok(())
4259 })
4260 }));
4261 let captured_deadline = self.script_deadline.load(Ordering::Acquire);
4264 let captured_now = Instant::now()
4265 .checked_duration_since(epoch())
4266 .map(|d| d.as_nanos() as u64)
4267 .unwrap_or(0);
4268 let timed_out = captured_deadline != 0 && captured_now >= captured_deadline;
4269 self.clear_deadline();
4270
4271 match result {
4272 Ok(Ok(())) => {
4273 self.metadata.executed = self.metadata.executed.saturating_add(1);
4274 let host_metadata = self.host.borrow_mut().take_metadata();
4275 self.metadata.accumulate(host_metadata);
4276 Ok(RuntimeOutcome {
4277 executed: true,
4278 mutated_field_count: host_metadata.mutations,
4279 })
4280 }
4281 Ok(Err(other)) => {
4282 let host_metadata = self.host.borrow_mut().take_metadata();
4283 self.metadata.accumulate(host_metadata);
4284 if timed_out {
4290 self.metadata.timeouts = self.metadata.timeouts.saturating_add(1);
4297 Err(self.classify_timeout_or_walltime(captured_now))
4298 } else {
4299 let msg = other.to_string();
4300 if msg.to_ascii_lowercase().contains("memory") {
4301 self.metadata.oom = self.metadata.oom.saturating_add(1);
4302 Err(SandboxError::OutOfMemory)
4303 } else {
4304 self.metadata.runtime_errors =
4305 self.metadata.runtime_errors.saturating_add(1);
4306 Err(SandboxError::ScriptError(msg))
4307 }
4308 }
4309 }
4310 Err(_) => {
4311 let host_metadata = self.host.borrow_mut().take_metadata();
4312 self.metadata.accumulate(host_metadata);
4313 self.metadata.runtime_errors = self.metadata.runtime_errors.saturating_add(1);
4314 Err(SandboxError::PanicCaptured(
4315 "panic during sandboxed script execution".to_string(),
4316 ))
4317 }
4318 }
4319 }
4320
4321 fn take_metadata(&mut self) -> RuntimeMetadata {
4322 std::mem::take(&mut self.metadata)
4323 }
4324
4325 fn take_occur_mutations(&mut self) -> Vec<(usize, String, i64)> {
4326 self.host
4327 .borrow_mut()
4328 .take_occur_mutations()
4329 .into_iter()
4330 .map(|(id, prop, value)| (id.0, prop, value))
4331 .collect()
4332 }
4333
4334 fn take_diag_logs(&mut self) -> crate::js_runtime::RuntimeDiagLogs {
4335 self.host.borrow_mut().take_diag_logs()
4336 }
4337}
4338
4339#[cfg(test)]
4340mod tests {
4341 use super::*;
4342
4343 fn fresh_runtime() -> QuickJsRuntime {
4344 let mut rt = QuickJsRuntime::new().expect("rquickjs init");
4345 rt.init().expect("init");
4346 rt.reset_for_new_document().expect("reset");
4347 rt
4348 }
4349
4350 #[test]
4351 fn harmless_calculate_script_executes() {
4352 let mut rt = fresh_runtime();
4353 let outcome = rt
4354 .execute_script(Some("calculate"), "var x = 1 + 1; x")
4355 .expect("ok");
4356 assert!(outcome.executed);
4357 let md = rt.take_metadata();
4358 assert_eq!(md.executed, 1);
4359 assert!(md.is_clean());
4360 }
4361
4362 #[test]
4368 fn absent_declared_subform_resolves_to_empty_node() {
4369 let mut rt = fresh_runtime();
4370 let mut names = std::collections::HashSet::new();
4371 names.insert("Podmiot1".to_string());
4372 rt.set_declared_subform_names(names);
4373 rt.execute_script(
4375 Some("calculate"),
4376 r#"
4377 if (Podmiot1.OsobaFizyczna.PESEL.isNull !== true)
4378 throw new Error('expected isNull === true on absent declared node');
4379 var tookElse = false;
4380 if (!Podmiot1.OsobaFizyczna.PESEL.isNull) { tookElse = false; }
4381 else { tookElse = true; }
4382 if (!tookElse) throw new Error('guard did not take the empty (else) branch');
4383 // Chained writes on the empty node must be inert (no throw).
4384 Podmiot1.OsobaFizyczna.presence = "invisible";
4385 Podmiot1.OsobaFizyczna.PESEL.rawValue = 2;
4386 if (Podmiot1.nodes.length !== 0) throw new Error('expected empty .nodes');
4387 "#,
4388 )
4389 .expect("absent declared subform must resolve to a benign empty node");
4390 }
4391
4392 #[test]
4397 fn undeclared_bare_name_stays_undefined() {
4398 let mut rt = fresh_runtime();
4399 rt.execute_script(
4401 Some("calculate"),
4402 "if (typeof NemoNonexistent7 !== 'undefined') \
4403 throw new Error('D-theta.1 violated: undeclared name became defined');",
4404 )
4405 .expect("undeclared bare name must remain undefined");
4406 let mut names = std::collections::HashSet::new();
4408 names.insert("SomeOtherSubform".to_string());
4409 rt.set_declared_subform_names(names);
4410 rt.execute_script(
4411 Some("calculate"),
4412 "if (typeof NemoNonexistent7 !== 'undefined') \
4413 throw new Error('D-theta.1 violated: undeclared name became defined (declared set present)');",
4414 )
4415 .expect("undeclared bare name must remain undefined even when other names are declared");
4416 }
4417
4418 #[test]
4419 fn ui_activity_is_phase_denied() {
4420 let mut rt = fresh_runtime();
4421 let err = rt.execute_script(Some("click"), "1+1").unwrap_err();
4422 assert!(matches!(err, SandboxError::PhaseDenied(_)));
4423 }
4424
4425 #[test]
4426 fn oversized_body_rejected_before_parse() {
4427 let mut rt = fresh_runtime();
4428 let body = "1;\n".repeat(MAX_SCRIPT_BODY_BYTES);
4429 let err = rt.execute_script(Some("calculate"), &body).unwrap_err();
4430 assert_eq!(err, SandboxError::BodyTooLarge);
4431 }
4432
4433 #[test]
4434 fn fetch_is_undefined() {
4435 let mut rt = fresh_runtime();
4436 rt.execute_script(
4440 Some("calculate"),
4441 "if (typeof fetch !== 'undefined') throw new Error('fetch leaked');",
4442 )
4443 .expect("must run cleanly with fetch undefined");
4444 }
4445
4446 #[test]
4447 fn require_is_undefined() {
4448 let mut rt = fresh_runtime();
4449 rt.execute_script(
4450 Some("calculate"),
4451 "if (typeof require !== 'undefined') throw new Error('require leaked');",
4452 )
4453 .expect("must run cleanly with require undefined");
4454 }
4455
4456 #[test]
4457 fn process_is_undefined() {
4458 let mut rt = fresh_runtime();
4459 rt.execute_script(
4460 Some("calculate"),
4461 "if (typeof process !== 'undefined') throw new Error('process leaked');",
4462 )
4463 .expect("must run cleanly with process undefined");
4464 }
4465
4466 #[test]
4467 fn date_now_is_zero() {
4468 let mut rt = fresh_runtime();
4469 rt.execute_script(
4470 Some("calculate"),
4471 "if (Date.now() !== 0) throw new Error('Date.now not stubbed');",
4472 )
4473 .expect("Date.now must return 0");
4474 }
4475
4476 #[test]
4477 fn math_random_is_undefined() {
4478 let mut rt = fresh_runtime();
4479 rt.execute_script(
4480 Some("calculate"),
4481 "if (typeof Math.random !== 'undefined') throw new Error('Math.random leaked');",
4482 )
4483 .expect("Math.random must be undefined");
4484 }
4485
4486 #[test]
4487 fn infinite_loop_times_out() {
4488 let mut rt = QuickJsRuntime::new()
4489 .expect("init")
4490 .with_time_budget(Duration::from_millis(50));
4491 rt.init().unwrap();
4492 rt.reset_for_new_document().unwrap();
4493 let err = rt
4494 .execute_script(Some("calculate"), "while(true){}")
4495 .unwrap_err();
4496 assert_eq!(err, SandboxError::Timeout);
4497 let md = rt.take_metadata();
4498 assert_eq!(md.timeouts, 1);
4499 assert_eq!(md.executed, 0);
4500 }
4501
4502 #[test]
4503 fn syntax_error_is_recoverable() {
4504 let mut rt = fresh_runtime();
4505 let err = rt
4506 .execute_script(Some("calculate"), "this is not javascript {{")
4507 .unwrap_err();
4508 assert!(matches!(err, SandboxError::ScriptError(_)));
4509 rt.execute_script(Some("calculate"), "var ok = 1;")
4511 .expect("recovered");
4512 }
4513
4514 #[test]
4515 fn null_receiver_silent_setter() {
4516 let mut rt = fresh_runtime();
4517 rt.execute_script(
4518 Some("calculate"),
4519 r#"
4520$record.NONEXISTENT_FIELD.rawValue = "hello";
4521$record.NONEXISTENT_FIELD.value = "world";
4522"#,
4523 )
4524 .expect("WP-3 null_receiver_silent_setter: set on null DataHandle must not throw");
4525 }
4526
4527 #[test]
4528 fn null_record_returns_empty_nodelist() {
4529 let mut rt = fresh_runtime();
4530 rt.execute_script(
4531 Some("calculate"),
4532 r#"
4533var nodes = $record.FIELD.nodes;
4534if (nodes.length !== 0) {
4535 throw new Error("expected nodes.length == 0, got " + nodes.length);
4536}
4537// item() on an empty null-sentinel NodeList returns a chainable null handle (not
4538// native null) so callers can safely do .item(0).value without TypeError.
4539var itemVal = nodes.item(0).value;
4540if (itemVal !== null) {
4541 throw new Error("expected nodes.item(0).value == null, got " + itemVal);
4542}
4543"#,
4544 )
4545 .expect("WP-3 null_record_returns_empty_nodelist: $record.FIELD.nodes must be empty and chainable");
4546 }
4547
4548 #[test]
4554 fn xfa_validate_stub_absorbs_writes_silently() {
4555 let mut rt = fresh_runtime();
4556 rt.execute_script(
4557 Some("initialize"),
4558 r#"
4559xfa.validate.override = 0;
4560xfa.validate.max = 5;
4561xfa.validate.messageMode = "warning";
4562xfa.validate.scriptTest.message = "ignored";
4563xfa.validate.scriptTest.nested.deeper = true;
4564// Reads must return harmless defaults, never throw.
4565if (xfa.validate.override !== 0) throw new Error("override default");
4566if (xfa.validate.max !== 0) throw new Error("max default");
4567if (xfa.validate.messageMode !== "") throw new Error("messageMode default");
4568"#,
4569 )
4570 .expect("WP-3 F3 xfa_validate_stub: writes must absorb and reads must not throw");
4571 }
4572
4573 #[test]
4579 fn add_instance_failure_returns_chainable_null_handle() {
4580 let mut rt = fresh_runtime();
4581 rt.execute_script(
4584 Some("initialize"),
4585 r#"
4586// Resolve a non-existent subform, addInstance() on an unbound instance
4587// manager must yield a chainable handle whose `.index` is a number and
4588// whose `.rawValue` setter is silent.
4589var im = xfa.resolveNode("NoSuchSubform");
4590if (im !== null) {
4591 // If a real handle was returned (shouldn't be), addInstance still must
4592 // either return a real handle or a null-safe sentinel — never throw.
4593 var added = im.addInstance();
4594 if (typeof added.index !== "number") {
4595 throw new Error("added.index must be a number, got " + typeof added.index);
4596 }
4597 added.rawValue = "chained"; // silent on sentinel
4598 added.value = "chained"; // silent on sentinel
4599}
4600"#,
4601 )
4602 .expect("WP-3 F3 add_instance_failure: chainable sentinel on failure path");
4603 }
4604}