1use std::cell::RefCell;
23use std::panic::{catch_unwind, AssertUnwindSafe};
24use std::rc::Rc;
25use std::sync::{
26 atomic::{AtomicBool, AtomicU64, Ordering},
27 Arc, OnceLock,
28};
29use std::time::{Duration, Instant};
30
31use rquickjs::function::Opt;
32use rquickjs::{CatchResultExt, Coerced, Context, Function, Object, Persistent, Runtime};
33use xfa_layout_engine::form::{FormNodeId, FormTree};
34
35use super::{
36 activity_allowed_for_sandbox, HostBindings, RuntimeMetadata, RuntimeOutcome, SandboxError,
37 XfaJsRuntime, DEFAULT_MEMORY_BUDGET_BYTES, DEFAULT_TIME_BUDGET_MS, MAX_SCRIPT_BODY_BYTES,
38};
39
40pub struct QuickJsRuntime {
44 eval_script: Option<Persistent<Function<'static>>>,
45 set_variables_script: Option<Persistent<Function<'static>>>,
48 clear_variables_scripts: Option<Persistent<Function<'static>>>,
51 context: Context,
52 runtime: Runtime,
53 metadata: RuntimeMetadata,
54 time_budget: Duration,
55 memory_budget_bytes: usize,
56 script_deadline: Arc<AtomicU64>,
57 script_started: Arc<AtomicBool>,
58 host: Rc<RefCell<HostBindings>>,
59 bindings_registered: bool,
60}
61
62impl std::fmt::Debug for QuickJsRuntime {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("QuickJsRuntime")
65 .field("metadata", &self.metadata)
66 .field("time_budget_ms", &self.time_budget.as_millis())
67 .field("memory_budget_bytes", &self.memory_budget_bytes)
68 .finish()
69 }
70}
71
72fn parse_node_id_csv(raw: &str) -> Vec<FormNodeId> {
73 raw.split(',')
74 .filter_map(|part| part.trim().parse::<usize>().ok())
75 .map(FormNodeId)
76 .collect()
77}
78
79impl QuickJsRuntime {
80 pub fn new() -> Result<Self, SandboxError> {
82 let runtime =
83 Runtime::new().map_err(|e| SandboxError::ScriptError(format!("rquickjs init: {e}")))?;
84 runtime.set_memory_limit(DEFAULT_MEMORY_BUDGET_BYTES);
85 let context = Context::full(&runtime)
86 .map_err(|e| SandboxError::ScriptError(format!("rquickjs context: {e}")))?;
87
88 let script_deadline = Arc::new(AtomicU64::new(0));
89 let script_started = Arc::new(AtomicBool::new(false));
90
91 let deadline_for_handler = Arc::clone(&script_deadline);
96 let started_for_handler = Arc::clone(&script_started);
97 runtime.set_interrupt_handler(Some(Box::new(move || {
98 if !started_for_handler.load(Ordering::Acquire) {
99 return false;
100 }
101 let deadline_nanos = deadline_for_handler.load(Ordering::Acquire);
102 if deadline_nanos == 0 {
103 return false;
104 }
105 let now_nanos = Instant::now()
106 .checked_duration_since(epoch())
107 .map(|d| d.as_nanos() as u64)
108 .unwrap_or(0);
109 now_nanos >= deadline_nanos
110 })));
111
112 Ok(Self {
113 eval_script: None,
114 set_variables_script: None,
115 clear_variables_scripts: None,
116 context,
117 runtime,
118 metadata: RuntimeMetadata::default(),
119 time_budget: Duration::from_millis(DEFAULT_TIME_BUDGET_MS),
120 memory_budget_bytes: DEFAULT_MEMORY_BUDGET_BYTES,
121 script_deadline,
122 script_started,
123 host: Rc::new(RefCell::new(HostBindings::new())),
124 bindings_registered: false,
125 })
126 }
127
128 pub fn with_time_budget(mut self, budget: Duration) -> Self {
131 self.time_budget = budget;
132 self
133 }
134
135 pub fn with_memory_budget(mut self, bytes: usize) -> Self {
138 self.memory_budget_bytes = bytes;
139 self.runtime.set_memory_limit(bytes);
140 self
141 }
142
143 fn set_deadline(&self) {
144 let deadline = Instant::now()
145 .checked_duration_since(epoch())
146 .map(|d| d + self.time_budget)
147 .unwrap_or(self.time_budget);
148 self.script_deadline
149 .store(deadline.as_nanos() as u64, Ordering::Release);
150 self.script_started.store(true, Ordering::Release);
151 }
152
153 fn clear_deadline(&self) {
154 self.script_started.store(false, Ordering::Release);
155 self.script_deadline.store(0, Ordering::Release);
156 }
157
158 fn register_host_bindings(&mut self) -> Result<(), String> {
159 if self.bindings_registered {
160 return Ok(());
161 }
162
163 let host = Rc::clone(&self.host);
164 let eval_script = self.context.with(|ctx| {
165 let globals = ctx.globals();
166 let internal =
167 Object::new(ctx.clone()).map_err(|e| format!("host internal object: {e}"))?;
168
169 let resolve_host = Rc::clone(&host);
170 let resolve_node_id = Function::new(ctx.clone(), move |path: Opt<Coerced<String>>| {
171 let Some(path) = path.0 else {
172 let _ = resolve_host.borrow_mut().resolve_node("");
173 return -1i32;
174 };
175 resolve_host
176 .borrow_mut()
177 .resolve_node(&path.0)
178 .map(|node_id| node_id.0 as i32)
179 .unwrap_or(-1)
180 })
181 .map_err(|e| format!("resolveNodeId: {e}"))?;
182 internal
183 .set("resolveNodeId", resolve_node_id)
184 .map_err(|e| format!("set resolveNodeId: {e}"))?;
185
186 let resolve_nodes_host = Rc::clone(&host);
187 let resolve_node_ids =
188 Function::new(ctx.clone(), move |path: Opt<Coerced<String>>| -> Vec<i32> {
189 let Some(path) = path.0 else {
190 let _ = resolve_nodes_host.borrow_mut().resolve_nodes("");
191 return Vec::new();
192 };
193 resolve_nodes_host
194 .borrow_mut()
195 .resolve_nodes(&path.0)
196 .into_iter()
197 .map(|node_id| node_id.0 as i32)
198 .collect()
199 })
200 .map_err(|e| format!("resolveNodeIds: {e}"))?;
201 internal
202 .set("resolveNodeIds", resolve_node_ids)
203 .map_err(|e| format!("set resolveNodeIds: {e}"))?;
204
205 let generation_host = Rc::clone(&host);
206 let generation = Function::new(ctx.clone(), move || {
207 generation_host.borrow().generation() as i64
208 })
209 .map_err(|e| format!("generation: {e}"))?;
210 internal
211 .set("generation", generation)
212 .map_err(|e| format!("set generation: {e}"))?;
213
214 let root_host = Rc::clone(&host);
215 let root_node_id = Function::new(ctx.clone(), move |generation: i64| -> i32 {
216 if generation < 0 {
217 return -1;
218 }
219 root_host
220 .borrow_mut()
221 .root_node(generation as u64)
222 .map(|node_id| node_id.0 as i32)
223 .unwrap_or(-1)
224 })
225 .map_err(|e| format!("rootNodeId: {e}"))?;
226 internal
227 .set("rootNodeId", root_node_id)
228 .map_err(|e| format!("set rootNodeId: {e}"))?;
229
230 let current_host = Rc::clone(&host);
231 let current_node = Function::new(ctx.clone(), move || {
232 current_host
233 .borrow()
234 .current_node()
235 .map(|node_id| node_id.0 as i32)
236 .unwrap_or(-1)
237 })
238 .map_err(|e| format!("currentNodeId: {e}"))?;
239 internal
240 .set("currentNodeId", current_node)
241 .map_err(|e| format!("set currentNodeId: {e}"))?;
242
243 let implicit_host = Rc::clone(&host);
244 let resolve_implicit_node_id = Function::new(
245 ctx.clone(),
246 move |current_id: i32, name: Opt<Coerced<String>>| -> i32 {
247 if current_id < 0 {
248 return -1;
249 }
250 let Some(name) = name.0 else {
251 return -1;
252 };
253 implicit_host
254 .borrow_mut()
255 .resolve_implicit(FormNodeId(current_id as usize), &name.0)
256 .map(|node_id| node_id.0 as i32)
257 .unwrap_or(-1)
258 },
259 )
260 .map_err(|e| format!("resolveImplicitNodeId: {e}"))?;
261 internal
262 .set("resolveImplicitNodeId", resolve_implicit_node_id)
263 .map_err(|e| format!("set resolveImplicitNodeId: {e}"))?;
264
265 let implicit_candidates_host = Rc::clone(&host);
266 let resolve_implicit_node_ids = Function::new(
267 ctx.clone(),
268 move |current_id: i32, name: Opt<Coerced<String>>| -> Vec<i32> {
269 if current_id < 0 {
270 return Vec::new();
271 }
272 let Some(name) = name.0 else {
273 return Vec::new();
274 };
275 implicit_candidates_host
276 .borrow_mut()
277 .resolve_implicit_candidates(FormNodeId(current_id as usize), &name.0)
278 .into_iter()
279 .map(|node_id| node_id.0 as i32)
280 .collect::<Vec<_>>()
281 },
282 )
283 .map_err(|e| format!("resolveImplicitNodeIds: {e}"))?;
284 internal
285 .set("resolveImplicitNodeIds", resolve_implicit_node_ids)
286 .map_err(|e| format!("set resolveImplicitNodeIds: {e}"))?;
287
288 let child_host = Rc::clone(&host);
289 let resolve_child_node_id = Function::new(
290 ctx.clone(),
291 move |parent_id: i32, name: Opt<Coerced<String>>| {
292 if parent_id < 0 {
293 return -1i32;
294 }
295 let Some(name) = name.0 else {
296 return -1;
297 };
298 child_host
299 .borrow_mut()
300 .resolve_child(FormNodeId(parent_id as usize), &name.0)
301 .map(|node_id| node_id.0 as i32)
302 .unwrap_or(-1)
303 },
304 )
305 .map_err(|e| format!("resolveChildNodeId: {e}"))?;
306 internal
307 .set("resolveChildNodeId", resolve_child_node_id)
308 .map_err(|e| format!("set resolveChildNodeId: {e}"))?;
309
310 let child_candidates_host = Rc::clone(&host);
311 let resolve_child_node_ids = Function::new(
312 ctx.clone(),
313 move |parent_ids: Opt<Coerced<String>>, name: Opt<Coerced<String>>| -> Vec<i32> {
314 let Some(parent_ids) = parent_ids.0 else {
315 return Vec::new();
316 };
317 let Some(name) = name.0 else {
318 return Vec::new();
319 };
320 child_candidates_host
321 .borrow_mut()
322 .resolve_child_candidates(&parse_node_id_csv(&parent_ids.0), &name.0)
323 .into_iter()
324 .map(|node_id| node_id.0 as i32)
325 .collect()
326 },
327 )
328 .map_err(|e| format!("resolveChildNodeIds: {e}"))?;
329 internal
330 .set("resolveChildNodeIds", resolve_child_node_ids)
331 .map_err(|e| format!("set resolveChildNodeIds: {e}"))?;
332
333 let scoped_candidates_host = Rc::clone(&host);
334 let resolve_scoped_node_ids = Function::new(
335 ctx.clone(),
336 move |scope_ids: Opt<Coerced<String>>, name: Opt<Coerced<String>>| -> Vec<i32> {
337 let Some(scope_ids) = scope_ids.0 else {
338 return Vec::new();
339 };
340 let Some(name) = name.0 else {
341 return Vec::new();
342 };
343 scoped_candidates_host
344 .borrow_mut()
345 .resolve_scoped_candidates(&parse_node_id_csv(&scope_ids.0), &name.0)
346 .into_iter()
347 .map(|node_id| node_id.0 as i32)
348 .collect()
349 },
350 )
351 .map_err(|e| format!("resolveScopedNodeIds: {e}"))?;
352 internal
353 .set("resolveScopedNodeIds", resolve_scoped_node_ids)
354 .map_err(|e| format!("set resolveScopedNodeIds: {e}"))?;
355
356 let get_raw_host = Rc::clone(&host);
357 let get_raw_value = Function::new(
358 ctx.clone(),
359 move |id: i32, generation: i64| -> Option<String> {
360 if id < 0 || generation < 0 {
361 return None;
362 }
363 get_raw_host
364 .borrow_mut()
365 .get_raw_value(FormNodeId(id as usize), generation as u64)
366 },
367 )
368 .map_err(|e| format!("getRawValue: {e}"))?;
369 internal
370 .set("getRawValue", get_raw_value)
371 .map_err(|e| format!("set getRawValue: {e}"))?;
372
373 let set_raw_host = Rc::clone(&host);
374 let set_raw_value = Function::new(
375 ctx.clone(),
376 move |id: i32, generation: i64, value: Coerced<String>| -> bool {
377 if id < 0 || generation < 0 {
378 return false;
379 }
380 set_raw_host.borrow_mut().set_raw_value(
381 FormNodeId(id as usize),
382 value.0,
383 generation as u64,
384 )
385 },
386 )
387 .map_err(|e| format!("setRawValue: {e}"))?;
388 internal
389 .set("setRawValue", set_raw_value)
390 .map_err(|e| format!("set setRawValue: {e}"))?;
391
392 let get_occur_host = Rc::clone(&host);
393 let get_occur_property = Function::new(
394 ctx.clone(),
395 move |id: i32, generation: i64, property: Opt<Coerced<String>>| -> Option<i32> {
396 if id < 0 || generation < 0 {
397 return None;
398 }
399 let property = property.0?;
400 get_occur_host.borrow_mut().get_occur_property(
401 FormNodeId(id as usize),
402 generation as u64,
403 &property.0,
404 )
405 },
406 )
407 .map_err(|e| format!("getOccurProperty: {e}"))?;
408 internal
409 .set("getOccurProperty", get_occur_property)
410 .map_err(|e| format!("set getOccurProperty: {e}"))?;
411
412 let set_occur_host = Rc::clone(&host);
413 let set_occur_property = Function::new(
414 ctx.clone(),
415 move |id: i32,
416 generation: i64,
417 property: Opt<Coerced<String>>,
418 value: Coerced<String>|
419 -> bool {
420 if id < 0 || generation < 0 {
421 return false;
422 }
423 let Some(property) = property.0 else {
424 return false;
425 };
426 set_occur_host.borrow_mut().set_occur_property(
427 FormNodeId(id as usize),
428 generation as u64,
429 &property.0,
430 &value.0,
431 )
432 },
433 )
434 .map_err(|e| format!("setOccurProperty: {e}"))?;
435 internal
436 .set("setOccurProperty", set_occur_property)
437 .map_err(|e| format!("set setOccurProperty: {e}"))?;
438
439 let instance_count_host = Rc::clone(&host);
440 let instance_count =
441 Function::new(ctx.clone(), move |id: i32, generation: i64| -> u32 {
442 if id < 0 || generation < 0 {
443 return 0;
444 }
445 instance_count_host
446 .borrow_mut()
447 .instance_count_for_handle(FormNodeId(id as usize), generation as u64)
448 })
449 .map_err(|e| format!("instanceCount: {e}"))?;
450 internal
451 .set("instanceCount", instance_count)
452 .map_err(|e| format!("set instanceCount: {e}"))?;
453
454 let zero_instance_host = Rc::clone(&host);
455 let has_zero_instance_run = Function::new(
456 ctx.clone(),
457 move |id: i32, generation: i64, name: Opt<Coerced<String>>| -> bool {
458 if id < 0 || generation < 0 {
459 return false;
460 }
461 let Some(name) = name.0 else {
462 return false;
463 };
464 zero_instance_host.borrow_mut().has_zero_instance_run(
465 FormNodeId(id as usize),
466 generation as u64,
467 &name.0,
468 )
469 },
470 )
471 .map_err(|e| format!("hasZeroInstanceRun: {e}"))?;
472 internal
473 .set("hasZeroInstanceRun", has_zero_instance_run)
474 .map_err(|e| format!("set hasZeroInstanceRun: {e}"))?;
475
476 let node_index_host = Rc::clone(&host);
477 let node_index = Function::new(ctx.clone(), move |id: i32, generation: i64| -> u32 {
478 if id < 0 || generation < 0 {
479 return 0;
480 }
481 node_index_host
482 .borrow_mut()
483 .instance_index_for_handle(FormNodeId(id as usize), generation as u64)
484 })
485 .map_err(|e| format!("nodeIndex: {e}"))?;
486 internal
487 .set("nodeIndex", node_index)
488 .map_err(|e| format!("set nodeIndex: {e}"))?;
489
490 let node_name_host = Rc::clone(&host);
491 let node_name = Function::new(ctx.clone(), move |id: i32, generation: i64| -> String {
492 if id < 0 || generation < 0 {
493 return String::new();
494 }
495 node_name_host
496 .borrow()
497 .node_name(FormNodeId(id as usize), generation as u64)
498 .unwrap_or_default()
499 })
500 .map_err(|e| format!("nodeName: {e}"))?;
501 internal
502 .set("nodeName", node_name)
503 .map_err(|e| format!("set nodeName: {e}"))?;
504
505 let scope_chain_host = Rc::clone(&host);
506 let scope_chain = Function::new(
507 ctx.clone(),
508 move |id: i32, generation: i64| -> Vec<String> {
509 if id < 0 || generation < 0 {
510 return vec![];
511 }
512 scope_chain_host
513 .borrow_mut()
514 .subform_scope_chain(FormNodeId(id as usize), generation as u64)
515 },
516 )
517 .map_err(|e| format!("getSubformScopeChain: {e}"))?;
518 internal
519 .set("getSubformScopeChain", scope_chain)
520 .map_err(|e| format!("set getSubformScopeChain: {e}"))?;
521
522 let instance_set_host = Rc::clone(&host);
523 let instance_set = Function::new(
524 ctx.clone(),
525 move |id: i32, generation: i64, n: Opt<i32>| -> i32 {
526 if id < 0 || generation < 0 {
527 return -1;
528 }
529 let n = n.0.unwrap_or(0).max(0) as u32;
530 instance_set_host
531 .borrow_mut()
532 .instance_set_for_handle(FormNodeId(id as usize), generation as u64, n)
533 .map(|count| count as i32)
534 .unwrap_or(-1)
535 },
536 )
537 .map_err(|e| format!("instanceSet: {e}"))?;
538 internal
539 .set("instanceSet", instance_set)
540 .map_err(|e| format!("set instanceSet: {e}"))?;
541
542 let instance_add_host = Rc::clone(&host);
543 let instance_add = Function::new(ctx.clone(), move |id: i32, generation: i64| -> i32 {
544 if id < 0 || generation < 0 {
545 return -1;
546 }
547 instance_add_host
548 .borrow_mut()
549 .instance_add_for_handle(FormNodeId(id as usize), generation as u64)
550 .map(|node_id| node_id.0 as i32)
551 .unwrap_or(-1)
552 })
553 .map_err(|e| format!("instanceAdd: {e}"))?;
554 internal
555 .set("instanceAdd", instance_add)
556 .map_err(|e| format!("set instanceAdd: {e}"))?;
557
558 let instance_remove_host = Rc::clone(&host);
559 let instance_remove = Function::new(
560 ctx.clone(),
561 move |id: i32, generation: i64, index: Opt<i32>| -> bool {
562 if id < 0 || generation < 0 {
563 return false;
564 }
565 let index = index.0.unwrap_or(0).max(0) as u32;
566 instance_remove_host
567 .borrow_mut()
568 .instance_remove_for_handle(
569 FormNodeId(id as usize),
570 generation as u64,
571 index,
572 )
573 .is_ok()
574 },
575 )
576 .map_err(|e| format!("instanceRemove: {e}"))?;
577 internal
578 .set("instanceRemove", instance_remove)
579 .map_err(|e| format!("set instanceRemove: {e}"))?;
580
581 let list_clear_host = Rc::clone(&host);
582 let list_clear = Function::new(ctx.clone(), move |id: i32, generation: i64| -> bool {
583 if id < 0 || generation < 0 {
584 return false;
585 }
586 list_clear_host
587 .borrow_mut()
588 .list_clear_for_handle(FormNodeId(id as usize), generation as u64)
589 .is_ok()
590 })
591 .map_err(|e| format!("listClear: {e}"))?;
592 internal
593 .set("listClear", list_clear)
594 .map_err(|e| format!("set listClear: {e}"))?;
595
596 let list_add_host = Rc::clone(&host);
597 let list_add = Function::new(
598 ctx.clone(),
599 move |id: i32,
600 generation: i64,
601 display: Coerced<String>,
602 save: Opt<Coerced<String>>|
603 -> bool {
604 if id < 0 || generation < 0 {
605 return false;
606 }
607 list_add_host
608 .borrow_mut()
609 .list_add_for_handle(
610 FormNodeId(id as usize),
611 generation as u64,
612 display.0,
613 save.0.map(|s| s.0),
614 )
615 .is_ok()
616 },
617 )
618 .map_err(|e| format!("listAdd: {e}"))?;
619 internal
620 .set("listAdd", list_add)
621 .map_err(|e| format!("set listAdd: {e}"))?;
622
623 let bound_item_host = Rc::clone(&host);
624 let bound_item = Function::new(
625 ctx.clone(),
626 move |id: i32, generation: i64, display: Coerced<String>| -> String {
627 if id < 0 || generation < 0 {
628 return display.0;
629 }
630 bound_item_host.borrow_mut().bound_item_for_handle(
631 FormNodeId(id as usize),
632 generation as u64,
633 display.0,
634 )
635 },
636 )
637 .map_err(|e| format!("boundItem: {e}"))?;
638 internal
639 .set("boundItem", bound_item)
640 .map_err(|e| format!("set boundItem: {e}"))?;
641
642 let num_pages_host = Rc::clone(&host);
643 let num_pages =
644 Function::new(ctx.clone(), move || num_pages_host.borrow_mut().num_pages())
645 .map_err(|e| format!("numPages: {e}"))?;
646 internal
647 .set("numPages", num_pages)
648 .map_err(|e| format!("set numPages: {e}"))?;
649
650 let binding_error_host = Rc::clone(&host);
651 let binding_error = Function::new(ctx.clone(), move || {
652 binding_error_host.borrow_mut().metadata_binding_error();
653 })
654 .map_err(|e| format!("bindingError: {e}"))?;
655 internal
656 .set("bindingError", binding_error)
657 .map_err(|e| format!("set bindingError: {e}"))?;
658
659 let resolve_failure_host = Rc::clone(&host);
660 let resolve_failure = Function::new(ctx.clone(), move || {
661 resolve_failure_host.borrow_mut().metadata_resolve_failure();
662 })
663 .map_err(|e| format!("resolveFailure: {e}"))?;
664 internal
665 .set("resolveFailure", resolve_failure)
666 .map_err(|e| format!("set resolveFailure: {e}"))?;
667
668 let dc_host = Rc::clone(&host);
671 let data_children = Function::new(ctx.clone(), move |raw_id: i32| -> Vec<i32> {
672 if raw_id < 0 {
673 return Vec::new();
674 }
675 dc_host
676 .borrow_mut()
677 .data_children(raw_id as usize)
678 .into_iter()
679 .map(|x| x as i32)
680 .collect()
681 })
682 .map_err(|e| format!("dataChildren: {e}"))?;
683 internal
684 .set("dataChildren", data_children)
685 .map_err(|e| format!("set dataChildren: {e}"))?;
686
687 let dv_host = Rc::clone(&host);
688 let data_value = Function::new(ctx.clone(), move |raw_id: i32| -> Option<String> {
689 if raw_id < 0 {
690 return None;
691 }
692 dv_host.borrow_mut().data_value(raw_id as usize)
693 })
694 .map_err(|e| format!("dataValue: {e}"))?;
695 internal
696 .set("dataValue", data_value)
697 .map_err(|e| format!("set dataValue: {e}"))?;
698
699 let dcbn_host = Rc::clone(&host);
700 let data_child_by_name = Function::new(
701 ctx.clone(),
702 move |parent_raw: i32, name: Opt<Coerced<String>>| -> i32 {
703 if parent_raw < 0 {
704 return -1;
705 }
706 let Some(name) = name.0 else {
707 return -1;
708 };
709 dcbn_host
710 .borrow_mut()
711 .data_child_by_name(parent_raw as usize, &name.0)
712 .map(|x| x as i32)
713 .unwrap_or(-1)
714 },
715 )
716 .map_err(|e| format!("dataChildByName: {e}"))?;
717 internal
718 .set("dataChildByName", data_child_by_name)
719 .map_err(|e| format!("set dataChildByName: {e}"))?;
720
721 let dbr_host = Rc::clone(&host);
722 let data_bound_record = Function::new(
723 ctx.clone(),
724 move |form_node_id: i32, generation: i64| -> i32 {
725 if form_node_id < 0 || generation < 0 {
726 return -1;
727 }
728 dbr_host
729 .borrow_mut()
730 .data_bound_record(FormNodeId(form_node_id as usize), generation as u64)
731 .map(|x| x as i32)
732 .unwrap_or(-1)
733 },
734 )
735 .map_err(|e| format!("dataBoundRecord: {e}"))?;
736 internal
737 .set("dataBoundRecord", data_bound_record)
738 .map_err(|e| format!("set dataBoundRecord: {e}"))?;
739
740 let drn_host = Rc::clone(&host);
741 let data_resolve_node =
742 Function::new(ctx.clone(), move |path: Opt<Coerced<String>>| -> i32 {
743 let Some(path) = path.0 else {
744 return -1;
745 };
746 drn_host
747 .borrow_mut()
748 .data_resolve_node(&path.0)
749 .map(|x| x as i32)
750 .unwrap_or(-1)
751 })
752 .map_err(|e| format!("dataResolveNode: {e}"))?;
753 internal
754 .set("dataResolveNode", data_resolve_node)
755 .map_err(|e| format!("set dataResolveNode: {e}"))?;
756
757 let drns_host = Rc::clone(&host);
758 let data_resolve_nodes =
759 Function::new(ctx.clone(), move |path: Opt<Coerced<String>>| -> Vec<i32> {
760 let Some(path) = path.0 else {
761 return Vec::new();
762 };
763 drns_host
764 .borrow_mut()
765 .data_resolve_nodes(&path.0)
766 .into_iter()
767 .map(|x| x as i32)
768 .collect()
769 })
770 .map_err(|e| format!("dataResolveNodes: {e}"))?;
771 internal
772 .set("dataResolveNodes", data_resolve_nodes)
773 .map_err(|e| format!("set dataResolveNodes: {e}"))?;
774
775 let factory: Function = ctx
778 .eval(PHASE_C_BINDINGS_JS.as_bytes())
779 .map_err(|e| format!("binding factory parse: {e}"))?;
780 let bridge: Object = factory
781 .call((internal,))
782 .catch(&ctx)
783 .map_err(|e| format!("binding factory call: {e}"))?;
784 let xfa: Object = bridge.get("xfa").map_err(|e| format!("get xfa: {e}"))?;
785 let app: Object = bridge.get("app").map_err(|e| format!("get app: {e}"))?;
786 let eval_script: Function = bridge
787 .get("evalScript")
788 .map_err(|e| format!("get evalScript: {e}"))?;
789 let set_variables_script: Function = bridge
790 .get("setVariablesScript")
791 .map_err(|e| format!("get setVariablesScript: {e}"))?;
792 let clear_variables_scripts: Function = bridge
793 .get("clearVariablesScripts")
794 .map_err(|e| format!("get clearVariablesScripts: {e}"))?;
795 globals
796 .set("xfa", xfa)
797 .map_err(|e| format!("set xfa global: {e}"))?;
798 globals
799 .set("app", app)
800 .map_err(|e| format!("set app global: {e}"))?;
801 Ok::<
802 (
803 Persistent<Function<'static>>,
804 Persistent<Function<'static>>,
805 Persistent<Function<'static>>,
806 ),
807 String,
808 >((
809 Persistent::save(&ctx, eval_script),
810 Persistent::save(&ctx, set_variables_script),
811 Persistent::save(&ctx, clear_variables_scripts),
812 ))
813 })?;
814
815 self.eval_script = Some(eval_script.0);
816 self.set_variables_script = Some(eval_script.1);
817 self.clear_variables_scripts = Some(eval_script.2);
818 self.bindings_registered = true;
819 Ok(())
820 }
821
822 fn extract_top_level_idents(body: &str) -> Vec<String> {
829 let mut out: Vec<String> = Vec::new();
830 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
831 let stripped = strip_js_comments(body);
833 for token_kind in [
834 ("var", false),
835 ("let", false),
836 ("const", false),
837 ("function", true),
838 ] {
839 let kw = token_kind.0;
840 let is_fn = token_kind.1;
841 let mut search = stripped.as_str();
842 while let Some(idx) = search.find(kw) {
843 let (before, after) = search.split_at(idx);
844 let head_ok = before
845 .chars()
846 .last()
847 .is_none_or(|c| !c.is_alphanumeric() && c != '_' && c != '$');
848 let tail_after_kw = &after[kw.len()..];
849 let tail_ok = tail_after_kw
850 .chars()
851 .next()
852 .is_some_and(|c| c.is_whitespace());
853 if !(head_ok && tail_ok) {
854 search = &after[1..];
855 continue;
856 }
857 let after_ws = tail_after_kw.trim_start();
859 let mut chars = after_ws.chars();
860 let ident: String = std::iter::once(chars.next())
861 .chain(chars.map(Some))
862 .map_while(|c| c.filter(|c| c.is_alphanumeric() || *c == '_' || *c == '$'))
863 .collect();
864 if ident.is_empty() || ident.chars().next().is_some_and(|c| c.is_ascii_digit()) {
865 search = &after[kw.len()..];
866 continue;
867 }
868 if is_fn {
869 let after_ident = after_ws
870 .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_' || c == '$');
871 if !after_ident.trim_start().starts_with('(') {
872 search = &after[kw.len()..];
873 continue;
874 }
875 }
876 if seen.insert(ident.clone()) {
877 out.push(ident);
878 }
879 search = &after[kw.len()..];
880 }
881 }
882 out
883 }
884
885 fn register_variables_script(
898 &self,
899 name: &str,
900 body: &str,
901 subform_scope: Option<&str>,
902 ) -> Result<(), SandboxError> {
903 let Some(setter) = self.set_variables_script.clone() else {
904 return Ok(());
905 };
906 if body.len() > MAX_SCRIPT_BODY_BYTES {
907 return Err(SandboxError::BodyTooLarge);
908 }
909 let idents = Self::extract_top_level_idents(body);
910 let scope = subform_scope.unwrap_or("").to_string();
911 self.set_deadline();
912 let result = catch_unwind(AssertUnwindSafe(|| {
913 self.context.with(|ctx| -> Result<(), rquickjs::Error> {
914 let setter = setter.restore(&ctx)?;
915 let _: bool = setter.call((name, body, idents, scope))?;
916 Ok(())
917 })
918 }));
919 let deadline_now = self.script_deadline.load(Ordering::Acquire);
923 let now_nanos = Instant::now()
924 .checked_duration_since(epoch())
925 .map(|d| d.as_nanos() as u64)
926 .unwrap_or(0);
927 let timed_out = deadline_now != 0 && now_nanos >= deadline_now;
928 self.clear_deadline();
929 match result {
930 Ok(Ok(())) => Ok(()),
931 Ok(Err(_)) if timed_out => Err(SandboxError::Timeout),
932 Ok(Err(e)) => Err(SandboxError::ScriptError(format!(
933 "variables-script `{name}` register: {e}"
934 ))),
935 Err(_) => Err(SandboxError::PanicCaptured(format!(
936 "panic registering variables-script `{name}`"
937 ))),
938 }
939 }
940
941 fn clear_variables_scripts_global(&self) -> Result<(), SandboxError> {
942 let Some(clearer) = self.clear_variables_scripts.clone() else {
943 return Ok(());
944 };
945 let result = catch_unwind(AssertUnwindSafe(|| {
946 self.context.with(|ctx| -> Result<(), rquickjs::Error> {
947 let clearer = clearer.restore(&ctx)?;
948 let _: () = clearer.call(())?;
949 Ok(())
950 })
951 }));
952 match result {
953 Ok(Ok(())) => Ok(()),
954 Ok(Err(e)) => Err(SandboxError::ScriptError(format!(
955 "variables-script clear: {e}"
956 ))),
957 Err(_) => Err(SandboxError::PanicCaptured(
958 "panic clearing variables-scripts".to_string(),
959 )),
960 }
961 }
962}
963
964fn strip_js_comments(src: &str) -> String {
971 let mut out = String::with_capacity(src.len());
972 let bytes = src.as_bytes();
973 let mut i = 0;
974 while i < bytes.len() {
975 let b = bytes[i];
976 if b == b'"' || b == b'\'' {
977 let quote = b;
979 out.push(b as char);
980 i += 1;
981 while i < bytes.len() {
982 let c = bytes[i];
983 out.push(c as char);
984 if c == b'\\' && i + 1 < bytes.len() {
985 out.push(bytes[i + 1] as char);
986 i += 2;
987 continue;
988 }
989 i += 1;
990 if c == quote {
991 break;
992 }
993 }
994 continue;
995 }
996 if b == b'/' && i + 1 < bytes.len() {
997 let n = bytes[i + 1];
998 if n == b'/' {
999 while i < bytes.len() && bytes[i] != b'\n' {
1001 i += 1;
1002 }
1003 continue;
1004 }
1005 if n == b'*' {
1006 i += 2;
1008 while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
1009 i += 1;
1010 }
1011 i = (i + 2).min(bytes.len());
1012 continue;
1013 }
1014 }
1015 out.push(b as char);
1016 i += 1;
1017 }
1018 out
1019}
1020
1021const PHASE_C_BINDINGS_JS: &str = r#"
1022(function(host) {
1023 function protoGuard() {
1024 return Object.freeze(Object.create(null));
1025 }
1026
1027 function nullProtoObject() {
1028 var obj = Object.create(null);
1029 Object.defineProperty(obj, "__proto__", {
1030 value: protoGuard(),
1031 enumerable: false,
1032 configurable: false,
1033 writable: false
1034 });
1035 return obj;
1036 }
1037
1038 function lookupObject() {
1039 return Object.create(null);
1040 }
1041
1042 var deferredGlobalNames = lookupObject();
1043 [
1044 "app", "arguments", "Array", "Boolean", "Bun", "console", "Date",
1045 "decodeURI", "decodeURIComponent", "Deno", "encodeURI",
1046 "encodeURIComponent", "Error", "eval", "EvalError", "fetch",
1047 "Function", "globalThis", "Infinity", "isFinite", "isNaN", "JSON",
1048 "Map", "Math", "NaN", "Number", "Object", "parseFloat", "parseInt",
1049 "process", "RangeError", "ReferenceError", "RegExp", "require",
1050 "Set", "String", "Symbol", "SyntaxError", "TypeError", "undefined",
1051 "URIError", "WeakMap", "WeakSet", "WebSocket", "XMLHttpRequest",
1052 "xfa", "event"
1053 ].forEach(function(name) {
1054 deferredGlobalNames[name] = true;
1055 });
1056
1057 var reservedHandleProperties = lookupObject();
1058 [
1059 "__defineGetter__", "__defineSetter__", "__lookupGetter__",
1060 "__lookupSetter__", "__proto__", "constructor", "hasOwnProperty",
1061 "isPrototypeOf", "propertyIsEnumerable", "then", "toJSON",
1062 "toLocaleString", "toString", "valueOf"
1063 ].forEach(function(name) {
1064 reservedHandleProperties[name] = true;
1065 });
1066
1067 function shouldDeferGlobalName(name, localNames) {
1068 return name.charAt(0) === "_" ||
1069 localNames[name] === true ||
1070 deferredGlobalNames[name] === true;
1071 }
1072
1073 // Properties that must NOT be deferred so their specific implementations run.
1074 var handlePropertyExclusions = lookupObject();
1075 ["rawValue", "somExpression", "isNull", "clearItems", "addItem", "boundItem",
1076 "$record", "occur", "nodes", "value", "length", "item"].forEach(function(name) {
1077 handlePropertyExclusions[name] = true;
1078 });
1079
1080 function shouldDeferHandleProperty(name) {
1081 if (handlePropertyExclusions[name] === true) {
1082 return false;
1083 }
1084 return name.charAt(0) === "_" || reservedHandleProperties[name] === true;
1085 }
1086
1087 // Adobe silently clears a field when a script writes null/undefined/NaN.
1088 // rquickjs would otherwise coerce these to the literal strings "null"/"NaN".
1089 function coerceRawValue(v) {
1090 if (v === null || v === undefined) return "";
1091 if (typeof v === "number" && isNaN(v)) return "";
1092 return v;
1093 }
1094
1095 function collectLocalNames(body) {
1096 var locals = lookupObject();
1097 var match;
1098 var decls = /\b(?:var|let|const)\s+([^;]+)/g;
1099 while ((match = decls.exec(body)) !== null) {
1100 match[1].split(",").forEach(function(part) {
1101 var ident = /^\s*([A-Za-z_$][0-9A-Za-z_$]*)/.exec(part);
1102 if (ident) {
1103 locals[ident[1]] = true;
1104 }
1105 });
1106 }
1107 var funcs = /\bfunction\s+([A-Za-z_$][0-9A-Za-z_$]*)/g;
1108 while ((match = funcs.exec(body)) !== null) {
1109 locals[match[1]] = true;
1110 }
1111 return locals;
1112 }
1113
1114 function makeInstanceManager(id, generation) {
1115 var manager = nullProtoObject();
1116 Object.defineProperty(manager, "count", {
1117 enumerable: true,
1118 configurable: false,
1119 get: function() {
1120 return host.instanceCount(id, generation);
1121 }
1122 });
1123 Object.defineProperty(manager, "setInstances", {
1124 enumerable: true,
1125 configurable: false,
1126 writable: false,
1127 value: function(n) {
1128 return host.instanceSet(id, generation, n);
1129 }
1130 });
1131 Object.defineProperty(manager, "addInstance", {
1132 enumerable: true,
1133 configurable: false,
1134 writable: false,
1135 value: function() {
1136 var newId = host.instanceAdd(id, generation);
1137 return newId < 0 ? null : makeHandle(newId, generation);
1138 }
1139 });
1140 Object.defineProperty(manager, "removeInstance", {
1141 enumerable: true,
1142 configurable: false,
1143 writable: false,
1144 value: function(idx) {
1145 return host.instanceRemove(id, generation, idx);
1146 }
1147 });
1148 return Object.freeze(manager);
1149 }
1150
1151 function makeEmptyInstanceManager() {
1152 var manager = nullProtoObject();
1153 Object.defineProperty(manager, "count", {
1154 enumerable: true,
1155 configurable: false,
1156 value: 0
1157 });
1158 Object.defineProperty(manager, "setInstances", {
1159 enumerable: true,
1160 configurable: false,
1161 writable: false,
1162 value: function() { return 0; }
1163 });
1164 Object.defineProperty(manager, "addInstance", {
1165 enumerable: true,
1166 configurable: false,
1167 writable: false,
1168 value: function() { return null; }
1169 });
1170 Object.defineProperty(manager, "removeInstance", {
1171 enumerable: true,
1172 configurable: false,
1173 writable: false,
1174 value: function() { return false; }
1175 });
1176 return Object.freeze(manager);
1177 }
1178
1179 function makeOccurrenceHandle(id, generation) {
1180 var occur = nullProtoObject();
1181 ["min", "max", "initial"].forEach(function(name) {
1182 Object.defineProperty(occur, name, {
1183 enumerable: true,
1184 configurable: false,
1185 get: function() {
1186 var value = host.getOccurProperty(id, generation, name);
1187 return value === undefined ? null : value;
1188 },
1189 set: function(value) {
1190 host.setOccurProperty(id, generation, name, value);
1191 }
1192 });
1193 });
1194 return Object.seal(occur);
1195 }
1196
1197 function uniqueNodeIds(ids) {
1198 var out = [];
1199 if (!ids) return out;
1200 for (var i = 0; i < ids.length; i++) {
1201 var id = ids[i] | 0;
1202 if (id < 0) continue;
1203 var seen = false;
1204 for (var j = 0; j < out.length; j++) {
1205 if (out[j] === id) {
1206 seen = true;
1207 break;
1208 }
1209 }
1210 if (!seen) out.push(id);
1211 }
1212 return out;
1213 }
1214
1215 function nodeIdListArg(ids) {
1216 return uniqueNodeIds(ids).join(",");
1217 }
1218
1219 function resolveHandleChildIds(ids, prop) {
1220 var list = nodeIdListArg(ids);
1221 if (list.length === 0) return [];
1222 var childIds = uniqueNodeIds(host.resolveChildNodeIds(list, prop));
1223 if (childIds.length > 0) return childIds;
1224 return uniqueNodeIds(host.resolveScopedNodeIds(list, prop));
1225 }
1226
1227 function makeNodeHandleFromIds(ids, generation) {
1228 var unique = uniqueNodeIds(ids);
1229 if (unique.length === 0) return undefined;
1230 if (unique.length === 1) return makeHandle(unique[0], generation);
1231 return makeCandidateSet(unique, generation);
1232 }
1233
1234 function makeCandidateSet(ids, generation) {
1235 var candidates = uniqueNodeIds(ids);
1236 if (candidates.length === 0) return undefined;
1237 var firstId = candidates[0];
1238 var obj = nullProtoObject();
1239 return new Proxy(obj, {
1240 get: function(target, prop, receiver) {
1241 if (typeof prop !== "string") {
1242 return Reflect.get(target, prop, receiver);
1243 }
1244 if (prop === "rawValue") {
1245 var value = host.getRawValue(firstId, generation);
1246 return value === undefined ? null : value;
1247 }
1248 if (prop === "somExpression") {
1249 return "xfa[0].form[0].placeholder";
1250 }
1251 if (prop === "instanceManager") {
1252 return makeInstanceManager(firstId, generation);
1253 }
1254 if (prop === "occur") {
1255 return makeOccurrenceHandle(firstId, generation);
1256 }
1257 if (prop === "index") {
1258 return host.nodeIndex(firstId, generation);
1259 }
1260 if (prop === "setInstances") {
1261 return function(n) {
1262 return host.instanceSet(firstId, generation, n);
1263 };
1264 }
1265 if (prop === "addInstance") {
1266 return function() {
1267 var newId = host.instanceAdd(firstId, generation);
1268 return newId < 0 ? null : makeHandle(newId, generation);
1269 };
1270 }
1271 if (prop === "removeInstance") {
1272 return function(idx) {
1273 return host.instanceRemove(firstId, generation, idx);
1274 };
1275 }
1276 if (prop === "isNull") {
1277 var raw = host.getRawValue(firstId, generation);
1278 return raw === undefined || raw === null || raw === "";
1279 }
1280 if (prop === "clearItems") {
1281 return function() {
1282 return host.listClear(firstId, generation);
1283 };
1284 }
1285 if (prop === "addItem") {
1286 return function(display, save) {
1287 if (save === undefined) {
1288 return host.listAdd(firstId, generation, String(display));
1289 }
1290 return host.listAdd(firstId, generation, String(display), String(save));
1291 };
1292 }
1293 if (prop === "boundItem") {
1294 return function(displayValue) {
1295 var coerced = displayValue === null || displayValue === undefined ?
1296 "" : String(displayValue);
1297 return host.boundItem(firstId, generation, coerced);
1298 };
1299 }
1300 if (prop === "$record") {
1301 var recRaw = host.dataBoundRecord(firstId, generation);
1302 if (recRaw < 0) return makeNullDataHandle();
1303 return makeDataHandle(recRaw);
1304 }
1305 if (prop === "variables") {
1306 var csNodeName = host.nodeName(firstId, generation);
1307 if (typeof csNodeName === "string" && csNodeName.length > 0 &&
1308 subformVariables[csNodeName] !== undefined) {
1309 return subformVariables[csNodeName];
1310 }
1311 return Object.create(null);
1312 }
1313 if (prop.charAt(0) === "_" && prop.length > 1) {
1314 var bareName = prop.substring(1);
1315 var imIds = uniqueNodeIds(host.resolveChildNodeIds(nodeIdListArg(candidates), bareName));
1316 if (imIds.length > 0) {
1317 return makeInstanceManager(imIds[0], generation);
1318 }
1319 for (var imIdx = 0; imIdx < candidates.length; imIdx++) {
1320 if (host.hasZeroInstanceRun(candidates[imIdx], generation, bareName)) {
1321 return makeEmptyInstanceManager();
1322 }
1323 }
1324 }
1325 if (shouldDeferHandleProperty(prop)) {
1326 return undefined;
1327 }
1328 return makeNodeHandleFromIds(resolveHandleChildIds(candidates, prop), generation);
1329 },
1330 set: function(_target, prop, value) {
1331 if (prop === "rawValue") {
1332 host.setRawValue(firstId, generation, coerceRawValue(value));
1333 }
1334 return true;
1335 },
1336 has: function(target, prop) {
1337 if (typeof prop !== "string") {
1338 return Reflect.has(target, prop);
1339 }
1340 return prop === "rawValue" ||
1341 prop === "somExpression" ||
1342 prop === "instanceManager" ||
1343 prop === "occur" ||
1344 prop === "index" ||
1345 prop === "setInstances" ||
1346 prop === "addInstance" ||
1347 prop === "removeInstance" ||
1348 prop === "isNull" ||
1349 prop === "clearItems" ||
1350 prop === "addItem" ||
1351 prop === "boundItem" ||
1352 Reflect.has(target, prop);
1353 }
1354 });
1355 }
1356
1357 function makeHandle(id, generation) {
1358 var obj = nullProtoObject();
1359 Object.defineProperty(obj, "rawValue", {
1360 enumerable: true,
1361 configurable: false,
1362 get: function() {
1363 var value = host.getRawValue(id, generation);
1364 return value === undefined ? null : value;
1365 },
1366 set: function(value) {
1367 host.setRawValue(id, generation, coerceRawValue(value));
1368 }
1369 });
1370 // Phase C-α: defensive stub. Real Adobe forms call
1371 // `this.somExpression` to obtain the SOM path string. We don't expose
1372 // the real SOM path (introspection capability), but returning a
1373 // placeholder lets viewer-tweak scripts (e.g. acroSOM substr(15))
1374 // proceed without ReferenceError. Mutations via this handle still
1375 // require the rawValue setter, which is the only side-effect channel.
1376 Object.defineProperty(obj, "somExpression", {
1377 enumerable: false,
1378 configurable: false,
1379 get: function() {
1380 return "xfa[0].form[0].placeholder";
1381 }
1382 });
1383 return new Proxy(obj, {
1384 get: function(target, prop, receiver) {
1385 if (typeof prop !== "string") {
1386 return Reflect.get(target, prop, receiver);
1387 }
1388 if (prop === "rawValue" || prop === "somExpression") {
1389 return Reflect.get(target, prop, receiver);
1390 }
1391 if (prop === "instanceManager") {
1392 return makeInstanceManager(id, generation);
1393 }
1394 if (prop === "occur") {
1395 return makeOccurrenceHandle(id, generation);
1396 }
1397 if (prop === "index") {
1398 return host.nodeIndex(id, generation);
1399 }
1400 if (prop === "setInstances") {
1401 return function(n) {
1402 return host.instanceSet(id, generation, n);
1403 };
1404 }
1405 if (prop === "addInstance") {
1406 return function() {
1407 var newId = host.instanceAdd(id, generation);
1408 return newId < 0 ? null : makeHandle(newId, generation);
1409 };
1410 }
1411 if (prop === "removeInstance") {
1412 return function(idx) {
1413 return host.instanceRemove(id, generation, idx);
1414 };
1415 }
1416 if (prop === "isNull") {
1417 var value = host.getRawValue(id, generation);
1418 return value === undefined || value === null || value === "";
1419 }
1420 if (prop === "clearItems") {
1421 return function() {
1422 return host.listClear(id, generation);
1423 };
1424 }
1425 if (prop === "addItem") {
1426 return function(display, save) {
1427 if (save === undefined) {
1428 return host.listAdd(id, generation, String(display));
1429 }
1430 return host.listAdd(id, generation, String(display), String(save));
1431 };
1432 }
1433 // XFA 3.3 §App A `boundItem` — listbox display→save lookup. Used as
1434 // `field.boundItem(xfa.event.newText)` to translate a user-visible
1435 // option label into its underlying save value. Falls back to the
1436 // input string when no match exists (Adobe behaviour).
1437 if (prop === "boundItem") {
1438 return function(displayValue) {
1439 var coerced;
1440 if (displayValue === null || displayValue === undefined) {
1441 coerced = "";
1442 } else {
1443 coerced = String(displayValue);
1444 }
1445 return host.boundItem(id, generation, coerced);
1446 };
1447 }
1448 if (prop === "$record") {
1449 var recRaw = host.dataBoundRecord(id, generation);
1450 if (recRaw < 0) return makeNullDataHandle();
1451 return makeDataHandle(recRaw);
1452 }
1453 // Phase D-ι.2: `subformHandle.variables` returns the namespace object
1454 // holding all `<variables><script>` entries registered for this
1455 // subform by name. Enables `Page2.variables.ValidationScript.fn()`.
1456 if (prop === "variables") {
1457 var nodeName = host.nodeName(id, generation);
1458 if (typeof nodeName === "string" && nodeName.length > 0 &&
1459 subformVariables[nodeName] !== undefined) {
1460 return subformVariables[nodeName];
1461 }
1462 return Object.create(null);
1463 }
1464 // XFA 3.3 §6.4.3.2 underscore shorthand: `_<name>` on a subform
1465 // refers to the instanceManager of the same-named child subform.
1466 // Used in the wild as `parent._child.setInstances(N)`. This MUST
1467 // run before `shouldDeferHandleProperty`, which otherwise returns
1468 // `undefined` for every underscore-prefixed property and makes the
1469 // shorthand unreachable for real bound subforms.
1470 if (prop.charAt(0) === "_" && prop.length > 1) {
1471 var bareName = prop.substring(1);
1472 var imChildIds = uniqueNodeIds(host.resolveChildNodeIds(String(id), bareName));
1473 if (imChildIds.length > 0) {
1474 return makeInstanceManager(imChildIds[0], generation);
1475 }
1476 if (host.hasZeroInstanceRun(id, generation, bareName)) {
1477 return makeEmptyInstanceManager();
1478 }
1479 }
1480 if (shouldDeferHandleProperty(prop)) {
1481 return undefined;
1482 }
1483 return makeNodeHandleFromIds(resolveHandleChildIds([id], prop), generation);
1484 },
1485 set: function(_target, prop, value) {
1486 if (prop === "rawValue") {
1487 host.setRawValue(id, generation, coerceRawValue(value));
1488 }
1489 return true;
1490 },
1491 has: function(target, prop) {
1492 if (typeof prop !== "string") {
1493 return Reflect.has(target, prop);
1494 }
1495 return prop === "rawValue" ||
1496 prop === "somExpression" ||
1497 prop === "instanceManager" ||
1498 prop === "occur" ||
1499 prop === "index" ||
1500 prop === "setInstances" ||
1501 prop === "addInstance" ||
1502 prop === "removeInstance" ||
1503 prop === "isNull" ||
1504 prop === "clearItems" ||
1505 prop === "addItem" ||
1506 prop === "boundItem" ||
1507 Reflect.has(target, prop);
1508 }
1509 });
1510 }
1511
1512 // Phase C-α: viewer-stub that absorbs property writes silently.
1513 // Used as the return value of `event.target.getField()` so
1514 // AcroForm widget-tweak scripts (`field.doNotScroll = true`,
1515 // `field.required = false`, etc.) complete without error and
1516 // without mutating any flatten-relevant state.
1517 function makeViewerStub() {
1518 return new Proxy({}, {
1519 get: function(_t, _prop) { return undefined; },
1520 set: function(_t, _prop, _val) {
1521
1522 return true;
1523 },
1524 has: function() { return true; }
1525 });
1526 }
1527
1528 function toListIndex(value) {
1529 var n = Number(value);
1530 if (!isFinite(n) || n < 0) return -1;
1531 return Math.floor(n);
1532 }
1533
1534 // XFA §8.5: a `$record` reference when there is no bound data node must return
1535 // an empty object that chains safely (`.value` → null, `.nodes.length` → 0)
1536 // rather than null, which would throw TypeError on any property access.
1537 function makeNullDataHandle() {
1538 var emptyNodes = [];
1539 emptyNodes.item = function() { return makeNullDataHandle(); };
1540 Object.freeze(emptyNodes);
1541 var sentinel = nullProtoObject();
1542 Object.defineProperty(sentinel, "value",
1543 { get: function() { return null; }, enumerable: true, configurable: false });
1544 Object.defineProperty(sentinel, "rawValue",
1545 { get: function() { return null; }, enumerable: true, configurable: false });
1546 Object.defineProperty(sentinel, "length",
1547 { get: function() { return 0; }, enumerable: true, configurable: false });
1548 Object.defineProperty(sentinel, "nodes",
1549 { get: function() { return emptyNodes; }, enumerable: true, configurable: false });
1550 sentinel.item = function() { return makeNullDataHandle(); };
1551 return new Proxy(sentinel, {
1552 get: function(target, prop) {
1553 if (prop in target) return target[prop];
1554 if (typeof prop !== "string") return undefined;
1555 return makeNullDataHandle();
1556 }
1557 });
1558 }
1559
1560 // Phase D-γ: Data DOM handle — wraps a raw DataDom node index and exposes
1561 // `.value`, `.nodes`, `.length`, `.item(i)`, and named child access via Proxy.
1562 function makeDataHandle(rawId) {
1563 if (rawId === undefined || rawId < 0) return null;
1564 var handle = nullProtoObject();
1565 Object.defineProperty(handle, "value", {
1566 get: function() {
1567 var v = host.dataValue(rawId);
1568 return (v === undefined || v === null) ? null : v;
1569 },
1570 enumerable: true, configurable: false
1571 });
1572 // rawValue is an alias for value — scripts use both forms on data handles.
1573 Object.defineProperty(handle, "rawValue", {
1574 get: function() {
1575 var v = host.dataValue(rawId);
1576 return (v === undefined || v === null) ? null : v;
1577 },
1578 enumerable: true, configurable: false
1579 });
1580 Object.defineProperty(handle, "length", {
1581 get: function() { return host.dataChildren(rawId).length; },
1582 enumerable: true, configurable: false
1583 });
1584 Object.defineProperty(handle, "nodes", {
1585 get: function() {
1586 var ids = host.dataChildren(rawId);
1587 var arr = [];
1588 for (var i = 0; i < ids.length; i++) arr.push(makeDataHandle(ids[i]));
1589 // Phase D-γ fix: XFA scripts call `nodeList.item(i)` on the array
1590 // returned by `.nodes`. Plain JS arrays have no `.item()` method —
1591 // add one that mirrors the W3C NodeList API. Out-of-bounds indices
1592 // return a null-safe sentinel so `.value` access never throws.
1593 var NULLNODE = Object.freeze({ value: null, rawValue: null });
1594 arr.item = function(idx) {
1595 var index = toListIndex(idx);
1596 if (index < 0 || index >= arr.length) return NULLNODE;
1597 return arr[index];
1598 };
1599 return Object.freeze(arr);
1600 },
1601 enumerable: true, configurable: false
1602 });
1603 handle.item = function(i) {
1604 var ids = host.dataChildren(rawId);
1605 var index = toListIndex(i);
1606 if (index < 0 || index >= ids.length) return null;
1607 return makeDataHandle(ids[index]);
1608 };
1609 return new Proxy(handle, {
1610 get: function(target, prop) {
1611 if (prop in target || typeof prop !== "string") return target[prop];
1612 if (prop === "rawValue" || prop === "value") return target.value;
1613 var childId = host.dataChildByName(rawId, prop);
1614 if (childId < 0) return makeNullDataHandle();
1615 return makeDataHandle(childId);
1616 }
1617 });
1618 }
1619
1620 var xfaHost = nullProtoObject();
1621 Object.defineProperty(xfaHost, "numPages", {
1622 enumerable: true,
1623 configurable: false,
1624 get: function() {
1625 return host.numPages();
1626 }
1627 });
1628 Object.defineProperty(xfaHost, "messageBox", {
1629 enumerable: true,
1630 configurable: false,
1631 writable: false,
1632 value: function() {
1633 host.bindingError();
1634 return null;
1635 }
1636 });
1637
1638 var xfaLayout = nullProtoObject();
1639 Object.defineProperty(xfaLayout, "pageCount", {
1640 enumerable: true,
1641 configurable: false,
1642 writable: false,
1643 value: function() {
1644 return host.numPages();
1645 }
1646 });
1647 // Phase D-γ: xfa.layout.page(node) — page number (1-based) of a form node.
1648 // During static flatten the layout is not yet run, so return a bounded
1649 // placeholder and mark the metadata as approximate.
1650 Object.defineProperty(xfaLayout, "page", {
1651 enumerable: true,
1652 configurable: false,
1653 writable: false,
1654 value: function() {
1655 host.resolveFailure();
1656 return 1;
1657 }
1658 });
1659 // Phase D-γ: xfa.layout.pageSpan(node) — number of pages a node spans.
1660 // Always 1 during static flatten; metadata records the approximation.
1661 Object.defineProperty(xfaLayout, "pageSpan", {
1662 enumerable: true,
1663 configurable: false,
1664 writable: false,
1665 value: function() {
1666 host.resolveFailure();
1667 return 1;
1668 }
1669 });
1670 Object.defineProperty(xfaLayout, "absPage", {
1671 enumerable: true,
1672 configurable: false,
1673 writable: false,
1674 value: function() {
1675 host.bindingError();
1676 return null;
1677 }
1678 });
1679
1680 var xfa = nullProtoObject();
1681 Object.defineProperty(xfa, "host", {
1682 enumerable: true,
1683 configurable: false,
1684 writable: false,
1685 value: Object.freeze(xfaHost)
1686 });
1687 Object.defineProperty(xfa, "layout", {
1688 enumerable: true,
1689 configurable: false,
1690 writable: false,
1691 value: Object.freeze(xfaLayout)
1692 });
1693 Object.defineProperty(xfa, "form", {
1694 enumerable: true,
1695 configurable: false,
1696 get: function() {
1697 var generation = host.generation();
1698 var id = host.rootNodeId(generation);
1699 return id < 0 ? null : makeHandle(id, generation);
1700 }
1701 });
1702 Object.defineProperty(xfa, "resolveNode", {
1703 enumerable: true,
1704 configurable: false,
1705 writable: false,
1706 value: function(path) {
1707 // Phase D-γ: data paths are routed to the DataDom, not the FormTree.
1708 if (typeof path === "string" &&
1709 (path.indexOf("data.") === 0 || path.indexOf("$data.") === 0 ||
1710 path.indexOf("xfa.datasets.data.") === 0)) {
1711 var rawId = host.dataResolveNode(path);
1712 if (rawId < 0) return null;
1713 return makeDataHandle(rawId);
1714 }
1715 var id = host.resolveNodeId(path);
1716 if (id < 0) {
1717 return null;
1718 }
1719 return makeHandle(id, host.generation());
1720 }
1721 });
1722 Object.defineProperty(xfa, "resolveNodes", {
1723 enumerable: true,
1724 configurable: false,
1725 writable: false,
1726 value: function(path) {
1727 // Phase D-γ: data paths are routed to the DataDom, not the FormTree.
1728 if (typeof path === "string" &&
1729 (path.indexOf("data.") === 0 || path.indexOf("$data.") === 0 ||
1730 path.indexOf("xfa.datasets.data.") === 0)) {
1731 var rawIds = host.dataResolveNodes(path);
1732 var out = [];
1733 for (var i = 0; i < rawIds.length; i++) out.push(makeDataHandle(rawIds[i]));
1734 return Object.freeze(out);
1735 }
1736 var generation = host.generation();
1737 var ids = host.resolveNodeIds(path);
1738 var out = [];
1739 for (var i = 0; i < ids.length; i++) {
1740 out.push(makeHandle(ids[i], generation));
1741 }
1742 return Object.freeze(out);
1743 }
1744 });
1745 // Phase D-δ.2: expose `xfa.event` as an alias for the per-script event
1746 // global. Real Adobe Reader populates this with the firing event;
1747 // during static flatten there is no dispatched UI event, so the
1748 // accessor returns the same defensive event-stub that the per-script
1749 // `event` parameter receives — newText/prevText/change default to
1750 // empty strings, target resolves to the firing field handle.
1751 Object.defineProperty(xfa, "event", {
1752 enumerable: true,
1753 configurable: false,
1754 get: function() {
1755 return makeEvent();
1756 }
1757 });
1758
1759 var app = nullProtoObject();
1760 Object.defineProperty(app, "alert", {
1761 enumerable: true,
1762 configurable: false,
1763 writable: false,
1764 value: function() {
1765 host.bindingError();
1766 return null;
1767 }
1768 });
1769 Object.defineProperty(app, "response", {
1770 enumerable: true,
1771 configurable: false,
1772 writable: false,
1773 value: function() {
1774 return "";
1775 }
1776 });
1777 Object.defineProperty(app, "launchURL", {
1778 enumerable: true,
1779 configurable: false,
1780 writable: false,
1781 value: function() {
1782 host.bindingError();
1783 return null;
1784 }
1785 });
1786 // XFA Acrobat SDK §6: app.Application — application-level info object.
1787 // Stubbed as a no-op frozen object; scripts that branch on its presence
1788 // (e.g. PDFIUM-352) no longer throw ReferenceError.
1789 Object.defineProperty(app, "Application", {
1790 enumerable: true,
1791 configurable: false,
1792 writable: false,
1793 value: Object.freeze(nullProtoObject())
1794 });
1795
1796 // Phase C-α: viewer-only `event` global. Real Adobe Reader populates
1797 // this with the firing event; during static flatten there is no
1798 // dispatched UI event, so the object is a defensive stub that:
1799 // - exposes `target` resolving to the firing field handle (≈ `this`),
1800 // so scripts like `event.target.getField(somPath)` complete without
1801 // ReferenceError;
1802 // - exposes `change` as an empty string (the spec default);
1803 // - returns viewer-stubs from `target.getField()` so AcroForm widget
1804 // tweaks (`doNotScroll`, `required`, etc.) silently absorb.
1805 function makeEvent() {
1806 var id = host.currentNodeId();
1807 var fieldHandle = id < 0 ? null : makeHandle(id, host.generation());
1808 var target = nullProtoObject();
1809 Object.defineProperty(target, "getField", {
1810 enumerable: true, configurable: false, writable: false,
1811 value: function() {
1812
1813 return makeViewerStub();
1814 }
1815 });
1816 Object.defineProperty(target, "name", {
1817 enumerable: true, configurable: false,
1818 get: function() { return ""; }
1819 });
1820 Object.defineProperty(target, "self", {
1821 enumerable: true, configurable: false,
1822 get: function() { return fieldHandle; }
1823 });
1824 var ev = nullProtoObject();
1825 Object.defineProperty(ev, "target", {
1826 enumerable: true, configurable: false,
1827 get: function() { return target; }
1828 });
1829 // Phase D-δ.2: stable empty-string defaults for the change-event property
1830 // family so scripts that read `xfa.event.newText` / `prevText` /
1831 // `change` on initialize/calculate (where no real change event occurred)
1832 // do not throw `cannot read property 'X' of undefined`. Adobe populates
1833 // these on actual `change`/`exit` events; we run those activities later
1834 // (or never) and surface deterministic empty defaults instead.
1835 Object.defineProperty(ev, "change", {
1836 enumerable: true, configurable: false,
1837 get: function() { return ""; }
1838 });
1839 Object.defineProperty(ev, "newText", {
1840 enumerable: true, configurable: false,
1841 get: function() { return ""; }
1842 });
1843 Object.defineProperty(ev, "prevText", {
1844 enumerable: true, configurable: false,
1845 get: function() { return ""; }
1846 });
1847 Object.defineProperty(ev, "fullText", {
1848 enumerable: true, configurable: false,
1849 get: function() { return ""; }
1850 });
1851 Object.defineProperty(ev, "selStart", {
1852 enumerable: true, configurable: false,
1853 get: function() { return 0; }
1854 });
1855 Object.defineProperty(ev, "selEnd", {
1856 enumerable: true, configurable: false,
1857 get: function() { return 0; }
1858 });
1859 return Object.freeze(ev);
1860 }
1861
1862 // Phase D-γ: XFA global `util` (Acrobat SDK §Util). Provides date/number
1863 // formatting helpers used by many XFA templates. Only the subset required
1864 // by real-corpus scripts is implemented; unknown methods return "".
1865 //
1866 // util.printd(sFormat, dDate) — format a Date per sFormat using UTC fields.
1867 // Supported tokens: yyyy (year), yy (two-digit year), mm (month 01-12),
1868 // m (1-12), dd (day 01-31), d (1-31), HH (hour 00-23),
1869 // MM (minute 00-59), SS (second 00-59).
1870 // util.printx(cPicture, cValue) — picture format; returns cValue as-is.
1871 // util.scand(sFormat, cDate) — deterministic numeric parser for the same
1872 // token subset; unsupported or invalid input returns Invalid Date.
1873 var xfaUtil = (function() {
1874 var DateCtor = Date;
1875 var dateUtc = Date.UTC;
1876 function pad2(n) { return (n < 10 ? "0" : "") + n; }
1877 function invalidDate() { return new DateCtor(NaN); }
1878 function escapeRegex(text) {
1879 return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1880 }
1881 function parseDate(fmt, input) {
1882 var fmtText, text;
1883 try {
1884 fmtText = String(fmt);
1885 text = String(input);
1886 } catch (_e) {
1887 return invalidDate();
1888 }
1889
1890 var groups = [];
1891 var pattern = "^";
1892 for (var i = 0; i < fmtText.length;) {
1893 var rest = fmtText.substring(i);
1894 if (rest.indexOf("yyyy") === 0) {
1895 pattern += "(\\d{4})";
1896 groups.push("yyyy");
1897 i += 4;
1898 } else if (rest.indexOf("yy") === 0) {
1899 pattern += "(\\d{2})";
1900 groups.push("yy");
1901 i += 2;
1902 } else if (rest.indexOf("mm") === 0) {
1903 pattern += "(\\d{2})";
1904 groups.push("mm");
1905 i += 2;
1906 } else if (rest.indexOf("dd") === 0) {
1907 pattern += "(\\d{2})";
1908 groups.push("dd");
1909 i += 2;
1910 } else if (rest.indexOf("HH") === 0) {
1911 pattern += "(\\d{2})";
1912 groups.push("HH");
1913 i += 2;
1914 } else if (rest.indexOf("MM") === 0) {
1915 pattern += "(\\d{2})";
1916 groups.push("MM");
1917 i += 2;
1918 } else if (rest.indexOf("SS") === 0) {
1919 pattern += "(\\d{2})";
1920 groups.push("SS");
1921 i += 2;
1922 } else if (rest.indexOf("m") === 0) {
1923 pattern += "(\\d{1,2})";
1924 groups.push("m");
1925 i += 1;
1926 } else if (rest.indexOf("d") === 0) {
1927 pattern += "(\\d{1,2})";
1928 groups.push("d");
1929 i += 1;
1930 } else {
1931 pattern += escapeRegex(fmtText.charAt(i));
1932 i += 1;
1933 }
1934 }
1935
1936 var match = new RegExp(pattern + "$").exec(text);
1937 if (!match) return invalidDate();
1938
1939 var year = NaN, month = 1, day = 1, hour = 0, minute = 0, second = 0;
1940 for (var g = 0; g < groups.length; g++) {
1941 var value = parseInt(match[g + 1], 10);
1942 if (!isFinite(value)) return invalidDate();
1943 if (groups[g] === "yyyy") year = value;
1944 else if (groups[g] === "yy") year = 2000 + value;
1945 else if (groups[g] === "mm" || groups[g] === "m") month = value;
1946 else if (groups[g] === "dd" || groups[g] === "d") day = value;
1947 else if (groups[g] === "HH") hour = value;
1948 else if (groups[g] === "MM") minute = value;
1949 else if (groups[g] === "SS") second = value;
1950 }
1951
1952 if (!isFinite(year) || month < 1 || month > 12 || day < 1 || day > 31 ||
1953 hour < 0 || hour > 23 || minute < 0 || minute > 59 ||
1954 second < 0 || second > 59) {
1955 return invalidDate();
1956 }
1957 var out = new DateCtor(dateUtc(year, month - 1, day, hour, minute, second));
1958 if (out.getUTCFullYear() !== year ||
1959 out.getUTCMonth() + 1 !== month ||
1960 out.getUTCDate() !== day ||
1961 out.getUTCHours() !== hour ||
1962 out.getUTCMinutes() !== minute ||
1963 out.getUTCSeconds() !== second) {
1964 return invalidDate();
1965 }
1966 return out;
1967 }
1968 var u = nullProtoObject();
1969 u.printd = function(fmt, date) {
1970 if (!(date instanceof DateCtor) || isNaN(date.getTime())) return "";
1971 var y = date.getUTCFullYear();
1972 var mo = date.getUTCMonth() + 1;
1973 var d = date.getUTCDate();
1974 var h = date.getUTCHours();
1975 var mi = date.getUTCMinutes();
1976 var s = date.getUTCSeconds();
1977 var result = String(fmt);
1978 result = result.replace(/yyyy/g, y)
1979 .replace(/yy/g, String(y).slice(-2))
1980 .replace(/mm/g, pad2(mo))
1981 .replace(/m/g, mo)
1982 .replace(/dd/g, pad2(d))
1983 .replace(/d/g, d)
1984 .replace(/HH/g, pad2(h))
1985 .replace(/MM/g, pad2(mi))
1986 .replace(/SS/g, pad2(s));
1987 return result;
1988 };
1989 u.printx = function(_fmt, val) { return val === null || val === undefined ? "" : String(val); };
1990 u.scand = function(fmt, str) { return parseDate(fmt, str); };
1991 return Object.freeze(u);
1992 }());
1993
1994 // Phase C-α: minimal `console` no-op. Many forms guard with
1995 // `if (typeof console !== "undefined") console.log(...)` and proceed
1996 // when the symbol exists. Stub returns undefined; never writes
1997 // anywhere observable to the script.
1998 var consoleStub = nullProtoObject();
1999 ["log","warn","error","info","debug","trace"].forEach(function(name) {
2000 Object.defineProperty(consoleStub, name, {
2001 enumerable: true, configurable: false, writable: false,
2002 value: function() { return undefined; }
2003 });
2004 });
2005
2006 // Phase D-ι: form-level globals registered from `<variables>` `<script>`
2007 // blocks. Each entry is `name -> frozen object`. Populated by the host
2008 // once per document via `setVariablesScript`; cleared by
2009 // `clearVariablesScripts` at `reset_per_document`.
2010 var variablesScripts = lookupObject();
2011 // Phase D-ι.2: subform-scoped variables. Maps subform name -> namespace
2012 // object containing that subform's named scripts. Enables
2013 // `subformHandle.variables.ScriptName.method()` access paths.
2014 var subformVariables = lookupObject();
2015
2016 function makeImplicitGlobals(body) {
2017 var currentId = host.currentNodeId();
2018 var generation = host.generation();
2019 var localNames = collectLocalNames(String(body));
2020 var cachedHandles = lookupObject();
2021 var cachedImHandles = lookupObject();
2022 var dynamicLocals = lookupObject();
2023 // D-ι.2: ancestor subform names (innermost→outermost) for this script's
2024 // context node. Used to resolve bare names like `partNoScript` that are
2025 // defined in a parent subform's <variables> block.
2026 var scopeChain = (currentId >= 0)
2027 ? host.getSubformScopeChain(currentId, generation)
2028 : [];
2029
2030 function lookup(name) {
2031 if (cachedHandles[name] !== undefined) {
2032 return cachedHandles[name];
2033 }
2034 // Use current node at call time so that functions defined in a
2035 // variables-script IIFE (with captured `currentId`) still resolve SOM
2036 // nodes correctly when invoked from an event script with a different
2037 // active node. During normal event-script execution currentNodeId()
2038 // returns the same value as the captured `currentId`, so there is no
2039 // observable difference for the common case.
2040 var resolveId = host.currentNodeId();
2041 if (resolveId < 0) resolveId = currentId;
2042 var nodeIds = host.resolveImplicitNodeIds(resolveId, name);
2043 if (!nodeIds || nodeIds.length === 0) {
2044 return undefined;
2045 }
2046 var handle = makeNodeHandleFromIds(nodeIds, generation);
2047 cachedHandles[name] = handle;
2048 return handle;
2049 }
2050
2051 // XFA instance manager shorthand: `_NodeName` as a global bare name
2052 // resolves to the instanceManager for `NodeName` from the current context.
2053 // Adobe XFA scripts use patterns like `_PD2.setInstances(0)` at the
2054 // document level. `shouldDeferGlobalName` blocks all `_`-prefixed names
2055 // to prevent internal JS variables (e.g. `_i`) from being hijacked, so
2056 // we must intercept BEFORE that check, but only when the bare name
2057 // actually resolves to a form node.
2058 function lookupInstanceManagerShorthand(prop) {
2059 // Only handle `_X` (single underscore prefix, non-empty suffix) that
2060 // is not a double-underscore builtin (e.g. __proto__) and is not a
2061 // locally declared variable.
2062 if (prop.length < 2 || prop.charAt(1) === "_" || localNames[prop] === true) {
2063 return undefined;
2064 }
2065 var bareName = prop.substring(1);
2066 if (cachedImHandles[bareName] !== undefined) {
2067 return cachedImHandles[bareName];
2068 }
2069 var resolveId = host.currentNodeId();
2070 if (resolveId < 0) resolveId = currentId;
2071 var nodeIds = host.resolveImplicitNodeIds(resolveId, bareName);
2072 if (!nodeIds || nodeIds.length === 0) {
2073 cachedImHandles[bareName] = null;
2074 return null;
2075 }
2076 var im = makeInstanceManager(nodeIds[0], generation);
2077 cachedImHandles[bareName] = im;
2078 return im;
2079 }
2080
2081 return new Proxy(Object.create(null), {
2082 has: function(_target, prop) {
2083 if (typeof prop !== "string") {
2084 return false;
2085 }
2086 if (prop.charAt(0) === "_") {
2087 var _im = lookupInstanceManagerShorthand(prop);
2088 return _im !== null && _im !== undefined;
2089 }
2090 if (shouldDeferGlobalName(prop, localNames)) {
2091 return false;
2092 }
2093 return true;
2094 },
2095 get: function(_target, prop) {
2096 if (typeof prop !== "string") {
2097 return undefined;
2098 }
2099 if (prop.charAt(0) === "_") {
2100 var im = lookupInstanceManagerShorthand(prop);
2101 return (im === null) ? undefined : im;
2102 }
2103 if (shouldDeferGlobalName(prop, localNames)) {
2104 return undefined;
2105 }
2106 // Phase D-γ: $record as a script-level global refers to the data
2107 // record bound to the current field's enclosing subform context.
2108 // Scripts write `var addr = $record.SECTION.nodes;` — we intercept
2109 // this here instead of letting resolveImplicitNodeId fail (-1).
2110 if (prop === "$record") {
2111 var recRaw = host.dataBoundRecord(currentId, generation);
2112 if (recRaw < 0) return makeNullDataHandle();
2113 return makeDataHandle(recRaw);
2114 }
2115 // Phase D-γ: `util` is an XFA global (Acrobat SDK §Util) that provides
2116 // date/number formatting functions. `util.printd(fmt, date)` is widely
2117 // used by XFA templates to format Date objects. We intercept it here so
2118 // scripts can complete without a TypeError instead of throwing and
2119 // aborting all later mutations in the same script body.
2120 if (prop === "util") {
2121 return xfaUtil;
2122 }
2123 if (dynamicLocals[prop] !== undefined) {
2124 return dynamicLocals[prop];
2125 }
2126 // Phase D-ι: form-level named-script globals from <variables>
2127 // outrank the form-tree implicit lookup. Adobe XFA spec §5.5
2128 // exposes `<scriptName>.<topLevelDecl>` to all event/calculate
2129 // scripts in the same document.
2130 if (variablesScripts[prop] !== undefined) {
2131 return variablesScripts[prop];
2132 }
2133 // D-ι.2: walk ancestor subform scope chain (innermost first).
2134 // Variables defined in a parent subform's <variables> block are
2135 // accessible as bare names within all descendant scripts.
2136 for (var _sci = 0; _sci < scopeChain.length; _sci++) {
2137 var _sv = subformVariables[scopeChain[_sci]];
2138 if (_sv !== undefined && _sv[prop] !== undefined) {
2139 return _sv[prop];
2140 }
2141 }
2142 return lookup(prop);
2143 },
2144 set: function(_target, prop, value) {
2145 if (typeof prop !== "string") {
2146 return true;
2147 }
2148 if (shouldDeferGlobalName(prop, localNames)) {
2149 return false;
2150 }
2151 if (cachedHandles[prop] !== undefined) {
2152 return true;
2153 }
2154 dynamicLocals[prop] = value;
2155 return true;
2156 }
2157 });
2158 }
2159
2160 return {
2161 xfa: Object.freeze(xfa),
2162 app: Object.freeze(app),
2163 consoleStub: Object.freeze(consoleStub),
2164 // Phase D-ι: register a `<variables>` `<script name="X">…` block as a
2165 // form-level global. Called by the host once per script body at
2166 // document load. `body` is the raw script source; `identNames` is a
2167 // pre-extracted array of top-level `var` / `function` identifiers
2168 // (Rust-side regex). The body is wrapped in an IIFE that returns a
2169 // frozen object whose properties are those identifiers. Variables
2170 // scripts share the same time/memory budget enforcement as event
2171 // scripts but emit no field mutations of their own. Errors during
2172 // evaluation are absorbed: the namespace remains undefined and
2173 // dependent event scripts will fail naturally at first use.
2174 // Phase D-ι / D-ι.2: register a named `<variables><script>` body.
2175 // `subformName` (4th param, optional) is non-empty for subform-scoped
2176 // scripts; omit or pass "" for root-level scripts.
2177 //
2178 // Root-level scripts (empty subformName) go into the flat
2179 // `variablesScripts` dict only — accessible as `ScriptName.X` from
2180 // any event script in the document.
2181 //
2182 // Subform-scoped scripts go into `subformVariables[subformName][name]`
2183 // ONLY — accessible as `subformHandle.variables.ScriptName.X`. They
2184 // are intentionally NOT written to the flat dict: two subforms may
2185 // define the same script name, and writing both to the flat map would
2186 // let the second registration silently shadow the first.
2187 setVariablesScript: function(name, body, identNames, subformName) {
2188 if (typeof name !== "string" || name.length === 0) return false;
2189 if (typeof body !== "string") return false;
2190 var idents = Array.isArray(identNames) ? identNames : [];
2191 var props = "";
2192 for (var i = 0; i < idents.length; i++) {
2193 var id = idents[i];
2194 if (typeof id !== "string" || id.length === 0) continue;
2195 if (i > 0) props += ",";
2196 props += JSON.stringify(id) + ": typeof " + id +
2197 " !== \"undefined\" ? " + id + " : undefined";
2198 }
2199 var isScoped = typeof subformName === "string" && subformName.length > 0;
2200 // Create the subform dict before the IIFE so the with-binding holds a
2201 // reference to the live object (forward cross-script refs work).
2202 if (isScoped && subformVariables[subformName] === undefined) {
2203 subformVariables[subformName] = lookupObject();
2204 }
2205 try {
2206 // Wrap the body with with(vs) so bare-name references to sibling
2207 // variable scripts resolve at CALL time from the live dictionaries.
2208 // This handles both backward and forward cross-references between
2209 // variables scripts regardless of registration order.
2210 var ns;
2211 if (isScoped) {
2212 ns = (Function("vs", "svs",
2213 "return (function(){\nwith(svs){\nwith(vs){\n" + body +
2214 "\nreturn Object.freeze({" + props + "});\n}}})();"
2215 ))(variablesScripts, subformVariables[subformName]);
2216 } else {
2217 ns = (Function("vs",
2218 "return (function(){\nwith(vs){\n" + body +
2219 "\nreturn Object.freeze({" + props + "});\n}})();"
2220 ))(variablesScripts);
2221 }
2222 if (isScoped) {
2223 subformVariables[subformName][name] = ns;
2224 } else {
2225 variablesScripts[name] = ns;
2226 }
2227 return true;
2228 } catch (_e) {
2229 return false;
2230 }
2231 },
2232 clearVariablesScripts: function() {
2233 var keys = Object.keys(variablesScripts);
2234 for (var i = 0; i < keys.length; i++) {
2235 delete variablesScripts[keys[i]];
2236 }
2237 var skeys = Object.keys(subformVariables);
2238 for (var j = 0; j < skeys.length; j++) {
2239 delete subformVariables[skeys[j]];
2240 }
2241 },
2242 evalScript: function(body) {
2243 var id = host.currentNodeId();
2244 var thisArg = id < 0 ? undefined : makeHandle(id, host.generation());
2245 // Phase C-α: install per-script `event` global in the function
2246 // closure so `event.target` resolves to the current field. Wrapping
2247 // body inside a function lets us pass `event` as a parameter
2248 // without leaking it to globalThis (where it would persist across
2249 // unrelated scripts).
2250 var ev = makeEvent();
2251 var consoleArg = consoleStub;
2252 var globals = makeImplicitGlobals(body);
2253 return (Function(
2254 "event",
2255 "console",
2256 "__globals",
2257 "with(__globals){\n" + String(body) + "\n}"
2258 )).call(thisArg, ev, consoleArg, globals);
2259 }
2260 };
2261})
2262"#;
2263
2264static EPOCH_CELL: OnceLock<Instant> = OnceLock::new();
2268fn epoch() -> Instant {
2269 *EPOCH_CELL.get_or_init(Instant::now)
2270}
2271
2272impl XfaJsRuntime for QuickJsRuntime {
2273 fn init(&mut self) -> Result<(), SandboxError> {
2274 let result = catch_unwind(AssertUnwindSafe(|| {
2278 self.context.with(|ctx| {
2279 let globals = ctx.globals();
2280 for forbidden in [
2284 "fetch",
2285 "XMLHttpRequest",
2286 "WebSocket",
2287 "process",
2288 "require",
2289 "Deno",
2290 "Bun",
2291 ] {
2292 let _ = globals.set(forbidden, rquickjs::Undefined);
2293 }
2294 if let Ok(date_ctor) = globals.get::<_, rquickjs::Object>("Date") {
2296 let zero_now = Function::new(ctx.clone(), || 0i64)
2297 .map_err(|e| format!("date stub: {e}"))?;
2298 let _ = date_ctor.set("now", zero_now);
2299 }
2300 if let Ok(math_ns) = globals.get::<_, rquickjs::Object>("Math") {
2301 let _ = math_ns.set("random", rquickjs::Undefined);
2302 }
2303 Ok::<(), String>(())
2304 })?;
2305 self.register_host_bindings()?;
2306 Ok::<(), String>(())
2307 }));
2308 match result {
2309 Ok(Ok(())) => Ok(()),
2310 Ok(Err(e)) => Err(SandboxError::ScriptError(e)),
2311 Err(_) => Err(SandboxError::PanicCaptured(
2312 "panic while initialising sandbox globals".to_string(),
2313 )),
2314 }
2315 }
2316
2317 fn reset_for_new_document(&mut self) -> Result<(), SandboxError> {
2318 self.metadata = RuntimeMetadata::default();
2319 self.host.borrow_mut().reset_per_document();
2320 self.clear_deadline();
2321 self.runtime.set_memory_limit(self.memory_budget_bytes);
2323 if let Err(e) = self.clear_variables_scripts_global() {
2328 log::debug!("D-ι clear failed: {e:?}");
2329 }
2330 Ok(())
2331 }
2332
2333 #[allow(clippy::not_unsafe_ptr_arg_deref)]
2339 fn set_form_handle(
2340 &mut self,
2341 form: *mut FormTree,
2342 root_id: FormNodeId,
2343 ) -> Result<(), SandboxError> {
2344 self.host.borrow_mut().set_form_handle(form, root_id);
2345 if !form.is_null() {
2351 let scripts: Vec<(Option<String>, String, String)> =
2353 unsafe { (*form).variables_scripts.clone() };
2354 for (subform_scope, name, body) in scripts {
2355 if let Err(e) =
2356 self.register_variables_script(&name, &body, subform_scope.as_deref())
2357 {
2358 log::debug!("D-ι register `{name}` failed: {e:?}");
2359 }
2360 }
2361 }
2362 Ok(())
2363 }
2364
2365 fn set_data_handle(&mut self, dom: *const xfa_dom_resolver::data_dom::DataDom) {
2366 self.host.borrow_mut().set_data_handle(dom);
2367 }
2368
2369 fn reset_per_script(
2370 &mut self,
2371 current_id: FormNodeId,
2372 activity: Option<&str>,
2373 ) -> Result<(), SandboxError> {
2374 self.host
2375 .borrow_mut()
2376 .reset_per_script(current_id, activity);
2377 Ok(())
2378 }
2379
2380 fn set_static_page_count(&mut self, page_count: u32) -> Result<(), SandboxError> {
2381 self.host.borrow_mut().set_static_page_count(page_count);
2382 Ok(())
2383 }
2384
2385 fn execute_script(
2386 &mut self,
2387 activity: Option<&str>,
2388 body: &str,
2389 ) -> Result<RuntimeOutcome, SandboxError> {
2390 if !activity_allowed_for_sandbox(activity) {
2391 return Err(SandboxError::PhaseDenied(
2392 activity.unwrap_or("None").to_string(),
2393 ));
2394 }
2395 if body.len() > MAX_SCRIPT_BODY_BYTES {
2396 self.metadata.runtime_errors = self.metadata.runtime_errors.saturating_add(1);
2397 return Err(SandboxError::BodyTooLarge);
2398 }
2399
2400 self.set_deadline();
2401 let script_owned = body.to_string();
2402 let result = catch_unwind(AssertUnwindSafe(|| {
2406 self.context.with(|ctx| -> Result<(), rquickjs::Error> {
2407 let Some(eval_script) = self.eval_script.clone() else {
2408 return Err(rquickjs::Error::new_from_js_message(
2409 "host bindings",
2410 "Function",
2411 "Phase C eval bridge not registered",
2412 ));
2413 };
2414 let eval_script = eval_script.restore(&ctx)?;
2415 if let Err(e) = eval_script.call::<_, ()>((script_owned,)) {
2416 let exc_msg = if matches!(e, rquickjs::Error::Exception) {
2420 let val = ctx.catch();
2421 if let Some(exc) = val.as_exception() {
2423 exc.message().unwrap_or_else(|| exc.to_string())
2424 } else {
2425 e.to_string()
2426 }
2427 } else {
2428 e.to_string()
2429 };
2430 return Err(rquickjs::Error::new_from_js_message(
2431 "script", "Error", exc_msg,
2432 ));
2433 }
2434 Ok(())
2435 })
2436 }));
2437 let captured_deadline = self.script_deadline.load(Ordering::Acquire);
2440 let captured_now = Instant::now()
2441 .checked_duration_since(epoch())
2442 .map(|d| d.as_nanos() as u64)
2443 .unwrap_or(0);
2444 let timed_out = captured_deadline != 0 && captured_now >= captured_deadline;
2445 self.clear_deadline();
2446
2447 match result {
2448 Ok(Ok(())) => {
2449 self.metadata.executed = self.metadata.executed.saturating_add(1);
2450 let host_metadata = self.host.borrow_mut().take_metadata();
2451 self.metadata.accumulate(host_metadata);
2452 Ok(RuntimeOutcome {
2453 executed: true,
2454 mutated_field_count: host_metadata.mutations,
2455 })
2456 }
2457 Ok(Err(other)) => {
2458 let host_metadata = self.host.borrow_mut().take_metadata();
2459 self.metadata.accumulate(host_metadata);
2460 if timed_out {
2466 self.metadata.timeouts = self.metadata.timeouts.saturating_add(1);
2467 Err(SandboxError::Timeout)
2468 } else {
2469 let msg = other.to_string();
2470 if msg.to_ascii_lowercase().contains("memory") {
2471 self.metadata.oom = self.metadata.oom.saturating_add(1);
2472 Err(SandboxError::OutOfMemory)
2473 } else {
2474 self.metadata.runtime_errors =
2475 self.metadata.runtime_errors.saturating_add(1);
2476 Err(SandboxError::ScriptError(msg))
2477 }
2478 }
2479 }
2480 Err(_) => {
2481 let host_metadata = self.host.borrow_mut().take_metadata();
2482 self.metadata.accumulate(host_metadata);
2483 self.metadata.runtime_errors = self.metadata.runtime_errors.saturating_add(1);
2484 Err(SandboxError::PanicCaptured(
2485 "panic during sandboxed script execution".to_string(),
2486 ))
2487 }
2488 }
2489 }
2490
2491 fn take_metadata(&mut self) -> RuntimeMetadata {
2492 std::mem::take(&mut self.metadata)
2493 }
2494}
2495
2496#[cfg(test)]
2497mod tests {
2498 use super::*;
2499
2500 fn fresh_runtime() -> QuickJsRuntime {
2501 let mut rt = QuickJsRuntime::new().expect("rquickjs init");
2502 rt.init().expect("init");
2503 rt.reset_for_new_document().expect("reset");
2504 rt
2505 }
2506
2507 #[test]
2508 fn harmless_calculate_script_executes() {
2509 let mut rt = fresh_runtime();
2510 let outcome = rt
2511 .execute_script(Some("calculate"), "var x = 1 + 1; x")
2512 .expect("ok");
2513 assert!(outcome.executed);
2514 let md = rt.take_metadata();
2515 assert_eq!(md.executed, 1);
2516 assert!(md.is_clean());
2517 }
2518
2519 #[test]
2520 fn ui_activity_is_phase_denied() {
2521 let mut rt = fresh_runtime();
2522 let err = rt.execute_script(Some("click"), "1+1").unwrap_err();
2523 assert!(matches!(err, SandboxError::PhaseDenied(_)));
2524 }
2525
2526 #[test]
2527 fn oversized_body_rejected_before_parse() {
2528 let mut rt = fresh_runtime();
2529 let body = "1;\n".repeat(MAX_SCRIPT_BODY_BYTES);
2530 let err = rt.execute_script(Some("calculate"), &body).unwrap_err();
2531 assert_eq!(err, SandboxError::BodyTooLarge);
2532 }
2533
2534 #[test]
2535 fn fetch_is_undefined() {
2536 let mut rt = fresh_runtime();
2537 rt.execute_script(
2541 Some("calculate"),
2542 "if (typeof fetch !== 'undefined') throw new Error('fetch leaked');",
2543 )
2544 .expect("must run cleanly with fetch undefined");
2545 }
2546
2547 #[test]
2548 fn require_is_undefined() {
2549 let mut rt = fresh_runtime();
2550 rt.execute_script(
2551 Some("calculate"),
2552 "if (typeof require !== 'undefined') throw new Error('require leaked');",
2553 )
2554 .expect("must run cleanly with require undefined");
2555 }
2556
2557 #[test]
2558 fn process_is_undefined() {
2559 let mut rt = fresh_runtime();
2560 rt.execute_script(
2561 Some("calculate"),
2562 "if (typeof process !== 'undefined') throw new Error('process leaked');",
2563 )
2564 .expect("must run cleanly with process undefined");
2565 }
2566
2567 #[test]
2568 fn date_now_is_zero() {
2569 let mut rt = fresh_runtime();
2570 rt.execute_script(
2571 Some("calculate"),
2572 "if (Date.now() !== 0) throw new Error('Date.now not stubbed');",
2573 )
2574 .expect("Date.now must return 0");
2575 }
2576
2577 #[test]
2578 fn math_random_is_undefined() {
2579 let mut rt = fresh_runtime();
2580 rt.execute_script(
2581 Some("calculate"),
2582 "if (typeof Math.random !== 'undefined') throw new Error('Math.random leaked');",
2583 )
2584 .expect("Math.random must be undefined");
2585 }
2586
2587 #[test]
2588 fn infinite_loop_times_out() {
2589 let mut rt = QuickJsRuntime::new()
2590 .expect("init")
2591 .with_time_budget(Duration::from_millis(50));
2592 rt.init().unwrap();
2593 rt.reset_for_new_document().unwrap();
2594 let err = rt
2595 .execute_script(Some("calculate"), "while(true){}")
2596 .unwrap_err();
2597 assert_eq!(err, SandboxError::Timeout);
2598 let md = rt.take_metadata();
2599 assert_eq!(md.timeouts, 1);
2600 assert_eq!(md.executed, 0);
2601 }
2602
2603 #[test]
2604 fn syntax_error_is_recoverable() {
2605 let mut rt = fresh_runtime();
2606 let err = rt
2607 .execute_script(Some("calculate"), "this is not javascript {{")
2608 .unwrap_err();
2609 assert!(matches!(err, SandboxError::ScriptError(_)));
2610 rt.execute_script(Some("calculate"), "var ok = 1;")
2612 .expect("recovered");
2613 }
2614}