pub mod host;
pub mod null;
#[cfg(feature = "xfa-js-sandboxed")]
pub mod rquickjs_backend;
pub use host::{
HostBindings, MutationLogEntry, MAX_INSTANCES_PER_SUBFORM, MAX_ITEMS_PER_LISTBOX,
MAX_MUTATIONS_PER_DOC, MAX_RESOLVE_CALLS_PER_SCRIPT, MAX_RESOLVE_RESULTS, MAX_SOM_DEPTH,
};
pub use null::NullRuntime;
#[cfg(feature = "xfa-js-sandboxed")]
pub use rquickjs_backend::QuickJsRuntime;
use xfa_dom_resolver::data_dom::DataDom;
use xfa_layout_engine::form::{FormNodeId, FormTree};
#[derive(Debug, Clone, Default)]
pub struct RuntimeOutcome {
pub executed: bool,
pub mutated_field_count: usize,
}
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum SandboxError {
#[error("sandboxed runtime not compiled in")]
NotCompiledIn,
#[error("script body exceeds size cap")]
BodyTooLarge,
#[error("script time budget exceeded")]
Timeout,
#[error("document memory budget exceeded")]
OutOfMemory,
#[error("call stack overflow")]
StackOverflow,
#[error("activity {0:?} denied for sandbox dispatch")]
PhaseDenied(String),
#[error("no host bindings registered (Phase B skeleton)")]
NoBindings,
#[error("sandbox panic captured: {0}")]
PanicCaptured(String),
#[error("script error: {0}")]
ScriptError(String),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RuntimeMetadata {
pub executed: usize,
pub runtime_errors: usize,
pub timeouts: usize,
pub oom: usize,
pub host_calls: usize,
pub mutations: usize,
pub instance_writes: usize,
pub list_writes: usize,
pub binding_errors: usize,
pub resolve_failures: usize,
pub data_reads: usize,
}
impl RuntimeMetadata {
pub fn is_clean(&self) -> bool {
self.runtime_errors == 0
&& self.timeouts == 0
&& self.oom == 0
&& self.binding_errors == 0
&& self.resolve_failures == 0
}
pub fn accumulate(&mut self, other: RuntimeMetadata) {
self.executed = self.executed.saturating_add(other.executed);
self.runtime_errors = self.runtime_errors.saturating_add(other.runtime_errors);
self.timeouts = self.timeouts.saturating_add(other.timeouts);
self.oom = self.oom.saturating_add(other.oom);
self.host_calls = self.host_calls.saturating_add(other.host_calls);
self.mutations = self.mutations.saturating_add(other.mutations);
self.instance_writes = self.instance_writes.saturating_add(other.instance_writes);
self.list_writes = self.list_writes.saturating_add(other.list_writes);
self.binding_errors = self.binding_errors.saturating_add(other.binding_errors);
self.resolve_failures = self.resolve_failures.saturating_add(other.resolve_failures);
self.data_reads = self.data_reads.saturating_add(other.data_reads);
}
}
pub const DEFAULT_TIME_BUDGET_MS: u64 = 100;
pub const DEFAULT_MEMORY_BUDGET_BYTES: usize = 32 * 1024 * 1024;
pub const MAX_SCRIPT_BODY_BYTES: usize = 64 * 1024;
pub const SANDBOX_ACTIVITY_ALLOWLIST: &[&str] = &[
"initialize",
"calculate",
"validate",
"docReady",
"layoutReady",
];
pub fn activity_allowed_for_sandbox(activity: Option<&str>) -> bool {
matches!(activity, Some(a) if SANDBOX_ACTIVITY_ALLOWLIST.contains(&a))
}
pub trait XfaJsRuntime {
fn init(&mut self) -> Result<(), SandboxError>;
fn reset_for_new_document(&mut self) -> Result<(), SandboxError>;
fn set_form_handle(
&mut self,
_form: *mut FormTree,
_root_id: FormNodeId,
) -> Result<(), SandboxError> {
Ok(())
}
fn set_data_handle(&mut self, _dom: *const DataDom) {}
fn reset_per_script(
&mut self,
_current_id: FormNodeId,
_activity: Option<&str>,
) -> Result<(), SandboxError> {
Ok(())
}
fn set_static_page_count(&mut self, _page_count: u32) -> Result<(), SandboxError> {
Ok(())
}
fn execute_script(
&mut self,
activity: Option<&str>,
body: &str,
) -> Result<RuntimeOutcome, SandboxError>;
fn take_metadata(&mut self) -> RuntimeMetadata;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allowlist_accepts_initialize_and_calculate() {
assert!(activity_allowed_for_sandbox(Some("initialize")));
assert!(activity_allowed_for_sandbox(Some("calculate")));
assert!(activity_allowed_for_sandbox(Some("validate")));
assert!(activity_allowed_for_sandbox(Some("docReady")));
assert!(activity_allowed_for_sandbox(Some("layoutReady")));
}
#[test]
fn allowlist_rejects_ui_and_submit_activities() {
for ui in [
"click",
"mouseEnter",
"mouseExit",
"enter",
"exit",
"preSubmit",
"postSubmit",
"ready",
] {
assert!(
!activity_allowed_for_sandbox(Some(ui)),
"{ui} must not be allowed",
);
}
assert!(!activity_allowed_for_sandbox(None));
}
#[test]
fn metadata_is_clean_when_zero() {
assert!(RuntimeMetadata::default().is_clean());
let mut m = RuntimeMetadata::default();
m.executed = 5;
assert!(m.is_clean(), "executed counter does not flip cleanliness");
m.runtime_errors = 1;
assert!(!m.is_clean());
}
#[test]
fn budget_constants_are_sane() {
assert!(MAX_SCRIPT_BODY_BYTES >= 4096);
assert!(DEFAULT_TIME_BUDGET_MS >= 25);
assert!(DEFAULT_MEMORY_BUDGET_BYTES >= 1024 * 1024);
}
}