mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 04:28:20 -06:00
Refactor js for strict Clippy checks
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use super::host::{arg, colour, error, function, host, read, s, set_value, strings, text, value_of, write};
|
||||
use super::model::{DISPLAY_HIDDEN, DISPLAY_VISIBLE, FieldType, Request};
|
||||
use super::*;
|
||||
use super::{Attribute, Context, JsArray, JsObject, JsResult, JsString, JsValue, NativeFunction, ObjectInitializer, js_string};
|
||||
|
||||
macro_rules! getter {
|
||||
($ctx:expr, $name:expr, |$f:ident, $c:ident| $body:expr) => {{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::host::{arg, error, s, text};
|
||||
use super::run::{MAX_PRINTF_OUTPUT_BYTES, MAX_PRINTF_WIDTH_PRECISION};
|
||||
use super::*;
|
||||
use super::{Context, JsDate, JsResult, JsString, JsValue, formcalc};
|
||||
|
||||
/// `util.printf(cFormat, …)`: `%[,n][+ 0#][width][.prec](d|f|s|x)`, where `,0` groups
|
||||
/// thousands with commas (the default), `,1` doesn't, `,2` uses `.` and `,`, `,3` uses `.`
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use super::ast::{Expr, Idx};
|
||||
use super::value::{Val, num_text, parse_num};
|
||||
use super::*;
|
||||
use super::{
|
||||
CLOCK_EVERY, HNode, HashSet, Interp, LIST_TOO_LONG, MAX_LIST, MAX_PARSE_DEPTH, MAX_STEPS, R, RAN_TOO_LONG, child_named, descendants_named,
|
||||
instances, resolve_som,
|
||||
};
|
||||
|
||||
impl Interp<'_> {
|
||||
pub(super) fn step(&mut self) -> R<()> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Func, Rc};
|
||||
|
||||
// ── syntax ──────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::dates::{MAX_DAYS, civil_from_days, days_from_civil, format_date, pars
|
||||
use super::parser::parse;
|
||||
use super::value::{Val, fixed, is_numeric_text, parse_num};
|
||||
use super::words::word_num;
|
||||
use super::*;
|
||||
use super::{Interp, MAX_STRING, R, STRING_TOO_LONG, XfaKind, is_limit};
|
||||
|
||||
impl Interp<'_> {
|
||||
/// A built-in function: its result is never a string past [`MAX_STRING`], and work on
|
||||
@@ -22,6 +22,15 @@ impl Interp<'_> {
|
||||
fn builtin_inner(&mut self, name: &str, args: &[Expr]) -> R<Val> {
|
||||
let lower = name.to_ascii_lowercase();
|
||||
let n = |v: f64| Ok(Val::Num(v));
|
||||
if let Some(value) = self.date_builtin(&lower, args)? {
|
||||
return Ok(value);
|
||||
}
|
||||
if let Some(value) = self.financial_builtin(&lower, args)? {
|
||||
return Ok(value);
|
||||
}
|
||||
if let Some(value) = self.unit_builtin(&lower, args)? {
|
||||
return Ok(value);
|
||||
}
|
||||
Ok(match lower.as_str() {
|
||||
// ── arithmetic
|
||||
"abs" => return n(self.arg_num(args, 0)?.abs()),
|
||||
@@ -271,7 +280,24 @@ impl Interp<'_> {
|
||||
let cleaned: String = s.chars().filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-').collect();
|
||||
parse_num(&cleaned).map_or(Val::Null, Val::Num)
|
||||
}
|
||||
// ── dates and times (days since 1900-01-01 = 1; milliseconds since midnight)
|
||||
"ref" => {
|
||||
let Some(e) = args.first() else { return Ok(Val::Null) };
|
||||
self.eval(e)?
|
||||
}
|
||||
"eval" => {
|
||||
let src = self.arg_str(args, 0)?;
|
||||
self.charge((src.len() / 16) as u64)?;
|
||||
let body = parse(&src)?;
|
||||
let (_, v) = self.block(&body)?;
|
||||
v
|
||||
}
|
||||
"get" | "post" | "put" => return Err(format!("{name}: network access is not available to form scripts")),
|
||||
_ => return Err(format!("unknown function {name}")),
|
||||
})
|
||||
}
|
||||
|
||||
fn date_builtin(&mut self, lower: &str, args: &[Expr]) -> R<Option<Val>> {
|
||||
let value = match lower {
|
||||
"date" => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
@@ -304,7 +330,7 @@ impl Interp<'_> {
|
||||
let days = self.arg_num(args, 0)?;
|
||||
let pic = if args.len() > 1 { self.arg_str(args, 1)? } else { "MMM D, YYYY".into() };
|
||||
if !days.is_finite() || days.abs() >= MAX_DAYS {
|
||||
return Ok(Val::Null);
|
||||
return Ok(Some(Val::Null));
|
||||
}
|
||||
let (y, m, d) = civil_from_days(days as i64);
|
||||
Val::Str(format_date(y, m, d, &pic))
|
||||
@@ -326,7 +352,7 @@ impl Interp<'_> {
|
||||
"num2time" | "num2gmtime" => {
|
||||
let ms = self.arg_num(args, 0)?;
|
||||
if !ms.is_finite() || ms < 0.0 {
|
||||
return Ok(Val::Null);
|
||||
return Ok(Some(Val::Null));
|
||||
}
|
||||
let s = (ms / 1000.0) as u64;
|
||||
Val::Str(format!("{:02}:{:02}:{:02}", (s / 3600) % 24, (s / 60) % 60, s % 60))
|
||||
@@ -344,7 +370,13 @@ impl Interp<'_> {
|
||||
parts.next().and_then(|p| p.trim().parse::<f64>().ok()).filter(|v| v.is_finite() && *v >= 0.0).unwrap_or(0.0).min(1_000_000.0);
|
||||
Val::Num(((h * 3600 + m * 60) * 1000) as f64 + (sec * 1000.0).round())
|
||||
}
|
||||
// ── financial
|
||||
_ => return Ok(None),
|
||||
};
|
||||
Ok(Some(value))
|
||||
}
|
||||
|
||||
fn financial_builtin(&mut self, lower: &str, args: &[Expr]) -> R<Option<Val>> {
|
||||
let value = match lower {
|
||||
"fv" => {
|
||||
let (p, r, k) = (self.arg_num(args, 0)?, self.arg_num(args, 1)?, self.arg_num(args, 2)?);
|
||||
Val::Num(if r == 0.0 { p * k } else { p * (((1.0 + r).powf(k) - 1.0) / r) })
|
||||
@@ -428,7 +460,13 @@ impl Interp<'_> {
|
||||
}
|
||||
Val::Num((lo + hi) / 2.0 * 12.0)
|
||||
}
|
||||
// ── units and references
|
||||
_ => return Ok(None),
|
||||
};
|
||||
Ok(Some(value))
|
||||
}
|
||||
|
||||
fn unit_builtin(&mut self, lower: &str, args: &[Expr]) -> R<Option<Val>> {
|
||||
let value = match lower {
|
||||
"unittype" => {
|
||||
let s = self.arg_str(args, 0)?;
|
||||
let unit: String = s.trim().chars().skip_while(|c| c.is_ascii_digit() || *c == '.' || *c == '-' || *c == '+').collect();
|
||||
@@ -459,19 +497,8 @@ impl Interp<'_> {
|
||||
let target = if to.trim().is_empty() { unit.clone() } else { to };
|
||||
Val::Num(v / per_in(&unit) * per_in(&target))
|
||||
}
|
||||
"ref" => {
|
||||
let Some(e) = args.first() else { return Ok(Val::Null) };
|
||||
self.eval(e)?
|
||||
}
|
||||
"eval" => {
|
||||
let src = self.arg_str(args, 0)?;
|
||||
self.charge((src.len() / 16) as u64)?;
|
||||
let body = parse(&src)?;
|
||||
let (_, v) = self.block(&body)?;
|
||||
v
|
||||
}
|
||||
"get" | "post" | "put" => return Err(format!("{name}: network access is not available to form scripts")),
|
||||
_ => return Err(format!("unknown function {name}")),
|
||||
})
|
||||
_ => return Ok(None),
|
||||
};
|
||||
Ok(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use super::ast::{Expr, Stmt};
|
||||
use super::value::{Val, is_numeric_text, num_text};
|
||||
use super::*;
|
||||
use super::{
|
||||
CALLS_TOO_DEEP, Flow, Func, HashMap, HashSet, Interp, LIST_TOO_LONG, LOOPED_TOO_MUCH, MAX_LIST, R, Rc, TOO_DEEP, XfaEffect, child_named, clip,
|
||||
descendants_named, instances, reset_names, resolve_som,
|
||||
};
|
||||
|
||||
impl Interp<'_> {
|
||||
// ── expressions
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::MAX_STRING;
|
||||
|
||||
// ── lexer ───────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::ast::{Expr, Idx, Stmt};
|
||||
use super::lexer::{Tok, Token, lex};
|
||||
use super::*;
|
||||
use super::{Func, MAX_CHAIN, MAX_PARSE_DEPTH, Rc};
|
||||
|
||||
struct Parser {
|
||||
toks: Vec<Token>,
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::document::{field_object, get_field, get_nth_field_name, noop, num_fie
|
||||
use super::format::{printd, printf, printx, scand};
|
||||
use super::host::{arg, function, host, s, text, text_line};
|
||||
use super::model::{DISPLAY_HIDDEN, DISPLAY_NO_PRINT, DISPLAY_NO_VIEW, DISPLAY_VISIBLE, Event, Panel, Request};
|
||||
use super::*;
|
||||
use super::{Attribute, Context, JsArray, JsObject, JsResult, JsString, JsValue, NativeFunction, ObjectInitializer, js_string};
|
||||
|
||||
// ── app, util, console ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use super::model::{DocInfo, FieldState, FieldType, Request};
|
||||
use super::run::MAX_ARRAY_LENGTH;
|
||||
use super::*;
|
||||
use super::{
|
||||
Arc, BTreeSet, Context, FunctionObjectBuilder, JsArray, JsError, JsFunction, JsNativeError, JsResult, JsString, JsValue, NativeFunction, RefCell,
|
||||
xfa,
|
||||
};
|
||||
|
||||
pub(crate) struct Host {
|
||||
pub(crate) fields: Arc<Vec<FieldState>>,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::globals::{event_object, install};
|
||||
use super::host::{Host, Shared, text};
|
||||
use super::model::{DocInfo, Event, FieldState, Limits, Outcome};
|
||||
use super::*;
|
||||
use super::{Arc, Attribute, BTreeSet, Context, HostHooks, JsNativeError, JsResult, JsString, Realm, RefCell, Source, js_string};
|
||||
|
||||
/// Longest script, in bytes.
|
||||
pub(crate) const MAX_SCRIPT_BYTES: usize = 256 * 1024;
|
||||
|
||||
+123
-50
@@ -1017,6 +1017,14 @@ mod formcalc {
|
||||
|
||||
#[test]
|
||||
fn built_in_functions_cover_the_common_set() {
|
||||
assert_numeric_and_logical_builtins();
|
||||
assert_string_builtins();
|
||||
assert_format_and_date_builtins();
|
||||
assert_financial_and_unit_builtins();
|
||||
assert_builtin_errors();
|
||||
}
|
||||
|
||||
fn assert_numeric_and_logical_builtins() {
|
||||
assert_eq!(calc("Abs(-2.5)"), "2.5");
|
||||
assert_eq!(calc(r#"Decode("a%2Fb", "url")"#), "a/b");
|
||||
assert_eq!(calc(r#"Decode("a%+fb", "url")"#), "a%+fb");
|
||||
@@ -1034,6 +1042,9 @@ mod formcalc {
|
||||
assert_eq!(calc(r#"Within(5, 1, 10)"#), "1");
|
||||
assert_eq!(calc(r#"HasValue("")"#), "0");
|
||||
assert_eq!(calc(r#"HasValue("x")"#), "1");
|
||||
}
|
||||
|
||||
fn assert_string_builtins() {
|
||||
assert_eq!(calc(r#"At("abcd", "c")"#), "3");
|
||||
assert_eq!(calc(r#"Left("hello", 2)"#), "he");
|
||||
assert_eq!(calc(r#"Right("hello", 2)"#), "lo");
|
||||
@@ -1050,6 +1061,11 @@ mod formcalc {
|
||||
assert_eq!(calc(r#"WordNum(42)"#), "Forty-Two");
|
||||
assert_eq!(calc(r#"WordNum(1.999, 2)"#), "Two Dollars And 00 Cents");
|
||||
assert_eq!(calc(r#"WordNum(1.01, 2)"#), "One Dollar And 01 Cent");
|
||||
assert_eq!(calc(r#"Encode("a b&c", "url")"#), "a%20b%26c");
|
||||
assert_eq!(calc(r#"Decode("a%20b", "url")"#), "a b");
|
||||
}
|
||||
|
||||
fn assert_format_and_date_builtins() {
|
||||
assert_eq!(calc(r#"Format("zzz,zz9.99", 1234.5)"#), "1,234.50");
|
||||
assert_eq!(calc(r#"Format("$z,zz9.99", -5)"#), "-$5.00");
|
||||
assert_eq!(calc(r#"Format("MM/DD/YYYY", "2024-02-29")"#), "02/29/2024");
|
||||
@@ -1065,6 +1081,9 @@ mod formcalc {
|
||||
assert_eq!(calc(r#"IsoTime2Num("12:30:45.250")"#), "45045250");
|
||||
assert_eq!(calc(r#"IsoTime2Num("T12:30:45")"#), "45045000");
|
||||
assert_eq!(calc(r#"Num2Time(3661000)"#), "01:01:01");
|
||||
}
|
||||
|
||||
fn assert_financial_and_unit_builtins() {
|
||||
assert_eq!(calc("Round(Pmt(10000, 0.01, 12), 2)"), "888.49");
|
||||
assert_eq!(calc("Round(FV(100, 0.01, 12), 2)"), "1268.25");
|
||||
assert_eq!(calc("Round(PV(100, 0.01, 12), 2)"), "1125.51");
|
||||
@@ -1073,8 +1092,9 @@ mod formcalc {
|
||||
assert_eq!(calc(r#"UnitValue("72points", "in")"#), "1");
|
||||
assert_eq!(calc(r#"UnitValue("25.4millimeters", "inches")"#), "1");
|
||||
assert_eq!(calc(r#"UnitValue("1in", "points")"#), "72");
|
||||
assert_eq!(calc(r#"Encode("a b&c", "url")"#), "a%20b%26c");
|
||||
assert_eq!(calc(r#"Decode("a%20b", "url")"#), "a b");
|
||||
}
|
||||
|
||||
fn assert_builtin_errors() {
|
||||
assert!(go(r#"Get("http://example.com")"#, "form1[0].page1[0].total[0]", "calculate").error.unwrap().contains("network"));
|
||||
assert!(go("Nope(1)", "form1[0].page1[0].total[0]", "calculate").error.unwrap().contains("unknown function"));
|
||||
}
|
||||
@@ -1147,6 +1167,13 @@ s"#
|
||||
|
||||
#[test]
|
||||
fn form_objects_resolve_assign_and_record_effects() {
|
||||
assert_formcalc_reads_fields_and_rows();
|
||||
assert_formcalc_reads_runtime_properties();
|
||||
assert_formcalc_records_primary_effects();
|
||||
assert_formcalc_updates_instances_and_records_requests();
|
||||
}
|
||||
|
||||
fn assert_formcalc_reads_fields_and_rows() {
|
||||
// Fields deref to their values; `$` is the current field.
|
||||
assert_eq!(calc("qty * price"), "10");
|
||||
assert_eq!(calc("$.name"), "total");
|
||||
@@ -1170,7 +1197,6 @@ s"#
|
||||
assert_eq!(in_row("Sum(row[*].amount)"), "15");
|
||||
assert_eq!(in_row("row[1].amount"), "5");
|
||||
assert_eq!(in_row("row[-1].amount"), "10");
|
||||
// Past the ends there is no object, so a property of it is an error (as in Acrobat).
|
||||
let past = go("row[+1].amount", "form1[0].page1[0].table[0].row[1].amount[0]", "calculate");
|
||||
assert!(past.error.unwrap().contains("no object"));
|
||||
assert_eq!(in_row("Exists(row[-5])"), "0");
|
||||
@@ -1182,12 +1208,18 @@ s"#
|
||||
assert_eq!(calc("table._row.count"), "2");
|
||||
assert_eq!(calc("Exists(qty) & not Exists(nothing)"), "1");
|
||||
assert_eq!(calc("HasValue(total)"), "0");
|
||||
}
|
||||
|
||||
fn assert_formcalc_reads_runtime_properties() {
|
||||
assert_eq!(calc("$host.numPages"), "3");
|
||||
assert_eq!(calc("$layout.pageCount()"), "3");
|
||||
assert_eq!(calc("$event.name"), "calculate");
|
||||
assert_eq!(calc("xfa.resolveNode(\"page1.price\").rawValue"), "2.5");
|
||||
assert_eq!(calc("$.border.fill.color.value"), "");
|
||||
assert!(go("nothing + 1", "form1[0].page1[0].total[0]", "calculate").error.unwrap().contains("unknown name"));
|
||||
}
|
||||
|
||||
fn assert_formcalc_records_primary_effects() {
|
||||
// Assignment to a field (by name and by path), a property and a variable.
|
||||
let o = go(
|
||||
r#"page1.qty = 6
|
||||
@@ -1215,7 +1247,9 @@ table._row.count"#,
|
||||
XfaEffect::AddInstance { som: "form1[0].page1[0].table[0].row[2]".into() },
|
||||
]
|
||||
);
|
||||
// Instance managers: remove, setInstances, count assignment, and the floor of one.
|
||||
}
|
||||
|
||||
fn assert_formcalc_updates_instances_and_records_requests() {
|
||||
let o = go(
|
||||
"table._row.removeInstance(0)\ntable._row.setInstances(1)\ntable._row.count = 0\ntable._row.count",
|
||||
"form1[0].page1[0].go[0]",
|
||||
@@ -1238,7 +1272,7 @@ $host.gotoURL("https://example.org")"#,
|
||||
);
|
||||
assert_eq!(
|
||||
o.effects,
|
||||
vec![XfaEffect::ResetData(vec!["qty".into(), "price".into()]), XfaEffect::Print, XfaEffect::LaunchUrl("https://example.org".into())]
|
||||
vec![XfaEffect::ResetData(vec!["qty".into(), "price".into()]), XfaEffect::Print, XfaEffect::LaunchUrl("https://example.org".into()),]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1279,58 +1313,79 @@ $host.gotoURL("https://example.org")"#,
|
||||
|
||||
#[test]
|
||||
fn hostile_formcalc_scripts_end_with_an_error_and_bounded_output() {
|
||||
let total = "form1[0].page1[0].total[0]";
|
||||
let fast = Limits { loop_iterations: 10_000, recursion: 32 };
|
||||
let ev = XfaEvent { activity: "calculate".into(), target: total.into(), ..Default::default() };
|
||||
let doc = XfaDoc::default();
|
||||
let run = |s: &str| run_formcalc(s, &ev, &doc, &form(), fast);
|
||||
assert!(run("while (1) do endwhile").error.unwrap().contains("looped"));
|
||||
assert!(run("for i = 1 upto 1000000 do endfor").error.unwrap().contains("looped"));
|
||||
assert!(run("func f(n) do f(n + 1) endfunc\nf(1)").error.unwrap().contains("nested"));
|
||||
assert!(run(&format!("{}1{}", "(".repeat(500), ")".repeat(500))).error.is_some());
|
||||
assert_hostile_control_flow_is_bounded();
|
||||
assert_hostile_numeric_inputs_are_safe();
|
||||
assert_hostile_outputs_are_bounded();
|
||||
assert_hostile_nesting_is_bounded();
|
||||
assert_hostile_deadlines_and_parse_errors();
|
||||
assert_hostile_instance_growth_is_bounded();
|
||||
assert_hostile_message_count_is_bounded();
|
||||
assert_hostile_small_stack_is_safe();
|
||||
}
|
||||
|
||||
fn assert_hostile_control_flow_is_bounded() {
|
||||
assert!(run_hostile_formcalc("while (1) do endwhile").error.unwrap().contains("looped"));
|
||||
assert!(run_hostile_formcalc("for i = 1 upto 1000000 do endfor").error.unwrap().contains("looped"));
|
||||
assert!(run_hostile_formcalc("func f(n) do f(n + 1) endfunc\nf(1)").error.unwrap().contains("nested"));
|
||||
assert!(run_hostile_formcalc(&format!("{}1{}", "(".repeat(500), ")".repeat(500))).error.is_some());
|
||||
// A 120k-term chain is refused at parse time (its tree would be as deep as it is long).
|
||||
assert!(run(&format!("1{}", "+1".repeat(120_000))).error.unwrap().contains("too long"));
|
||||
assert!(run(&format!("$form{}", ".a".repeat(120_000))).error.unwrap().contains("too long"));
|
||||
assert!(run("nothing.x = 1").error.unwrap().contains("unknown name"));
|
||||
assert!(run("$form.nothing = 1").error.unwrap().contains("no nothing"));
|
||||
assert!(run_hostile_formcalc(&format!("1{}", "+1".repeat(120_000))).error.unwrap().contains("too long"));
|
||||
assert!(run_hostile_formcalc(&format!("$form{}", ".a".repeat(120_000))).error.unwrap().contains("too long"));
|
||||
assert!(run_hostile_formcalc("nothing.x = 1").error.unwrap().contains("unknown name"));
|
||||
assert!(run_hostile_formcalc("$form.nothing = 1").error.unwrap().contains("no nothing"));
|
||||
// Values go into fields only: a subform or the form itself is refused, nothing is recorded.
|
||||
let o = run("details = \"junk\"");
|
||||
let o = run_hostile_formcalc("details = \"junk\"");
|
||||
assert!(o.error.unwrap().contains("not a field"));
|
||||
assert!(o.effects.is_empty());
|
||||
assert!(run("$form = \"junk\"").error.unwrap().contains("not a field"));
|
||||
assert!(run_hostile_formcalc("$form = \"junk\"").error.unwrap().contains("not a field"));
|
||||
}
|
||||
|
||||
fn assert_hostile_numeric_inputs_are_safe() {
|
||||
// Hostile numbers into the string functions.
|
||||
assert_eq!(run(r#"Stuff("abc", 2, 1e300, "X")"#).result.as_deref(), Some("aX"));
|
||||
assert_eq!(run(r#"Stuff("abc", -1e300, 1, "X")"#).result.as_deref(), Some("Xbc"));
|
||||
assert_eq!(run(r#"Substr("abc", 1e300, 1e300)"#).result.as_deref(), Some(""));
|
||||
assert_eq!(run(r#"Left("abc", -1e300)"#).result.as_deref(), Some(""));
|
||||
assert!(run(r#"Str(1e300, 1e300, 1e300)"#).error.is_none());
|
||||
assert!(run(r#"Round(1e300, 1e300)"#).error.is_none());
|
||||
assert!(run(r#"Choose(1e300, 1)"#).error.is_none());
|
||||
assert_eq!(run_hostile_formcalc(r#"Stuff("abc", 2, 1e300, "X")"#).result.as_deref(), Some("aX"));
|
||||
assert_eq!(run_hostile_formcalc(r#"Stuff("abc", -1e300, 1, "X")"#).result.as_deref(), Some("Xbc"));
|
||||
assert_eq!(run_hostile_formcalc(r#"Substr("abc", 1e300, 1e300)"#).result.as_deref(), Some(""));
|
||||
assert_eq!(run_hostile_formcalc(r#"Left("abc", -1e300)"#).result.as_deref(), Some(""));
|
||||
assert!(run_hostile_formcalc(r#"Str(1e300, 1e300, 1e300)"#).error.is_none());
|
||||
assert!(run_hostile_formcalc(r#"Round(1e300, 1e300)"#).error.is_none());
|
||||
assert!(run_hostile_formcalc(r#"Choose(1e300, 1)"#).error.is_none());
|
||||
}
|
||||
|
||||
fn assert_hostile_outputs_are_bounded() {
|
||||
// Strings can't grow past the cap through any function, and messages and values are clipped.
|
||||
assert!(
|
||||
run("var s = Space(1000000)\nfor i = 1 upto 10 do s = Encode(Replace(s, \" \", \"&\"), \"html\") endfor\nLen(s)")
|
||||
run_hostile_formcalc("var s = Space(1000000)\nfor i = 1 upto 10 do s = Encode(Replace(s, \" \", \"&\"), \"html\") endfor\nLen(s)")
|
||||
.error
|
||||
.unwrap()
|
||||
.contains("too long")
|
||||
);
|
||||
assert!(run("Upper(Concat(Space(600000), Space(600000)))").error.unwrap().contains("too long"));
|
||||
let o = run("$host.messageBox(Space(100000))");
|
||||
assert!(run_hostile_formcalc("Upper(Concat(Space(600000), Space(600000)))").error.unwrap().contains("too long"));
|
||||
let o = run_hostile_formcalc("$host.messageBox(Space(100000))");
|
||||
assert!(matches!(o.effects.as_slice(), [XfaEffect::MessageBox(m)] if m.chars().count() <= 4100), "{:?}", o.effects.len());
|
||||
let o = run("qty = Space(100000)");
|
||||
let o = run_hostile_formcalc("qty = Space(100000)");
|
||||
assert!(matches!(o.effects.as_slice(), [XfaEffect::SetValue { value, .. }] if value.chars().count() == 65_536), "{:?}", o.effects.len());
|
||||
assert!(o.notes.iter().any(|n| n.contains("cut")), "{:?}", o.notes);
|
||||
}
|
||||
|
||||
fn assert_hostile_nesting_is_bounded() {
|
||||
// A comment of dashes is not deep nesting.
|
||||
assert_eq!(run(&format!("; {}\n1 + 1", "-".repeat(70))).result.as_deref(), Some("2"));
|
||||
assert_eq!(run_hostile_formcalc(&format!("; {}\n1 + 1", "-".repeat(70))).result.as_deref(), Some("2"));
|
||||
// Parentheses don't reset the chain count: a tree is never deeper than the cap.
|
||||
let mut nested = "1+1+1+1+1+1+1+1+1+1".to_string();
|
||||
for _ in 0..30 {
|
||||
nested = format!("({nested}{})", "+1".repeat(40));
|
||||
}
|
||||
assert!(run(&nested).error.unwrap().contains("too long"));
|
||||
assert_eq!(run("(1+2)*(3+4) + (5+6)").result.as_deref(), Some("32"));
|
||||
assert!(run_hostile_formcalc(&nested).error.unwrap().contains("too long"));
|
||||
assert_eq!(run_hostile_formcalc("(1+2)*(3+4) + (5+6)").result.as_deref(), Some("32"));
|
||||
// Nested statements inside recursion count against the depth limit.
|
||||
let deep = format!("func f(n) do\n{}return f(n - 1)\n{}endfunc\nf(1000)", "if (n > 0) then\n".repeat(20), "endif\n".repeat(20));
|
||||
assert!(run(&deep).error.unwrap().contains("nested"));
|
||||
assert!(run_hostile_formcalc(&deep).error.unwrap().contains("nested"));
|
||||
}
|
||||
|
||||
fn assert_hostile_deadlines_and_parse_errors() {
|
||||
let total = "form1[0].page1[0].total[0]";
|
||||
let ev = XfaEvent { activity: "calculate".into(), target: total.into(), ..Default::default() };
|
||||
let doc = XfaDoc::default();
|
||||
// A deadline already passed stops the script at its first look at the clock.
|
||||
let o = crate::formcalc::run_formcalc_at(
|
||||
"for i = 1 upto 100000 do Eval(\"1\") endfor",
|
||||
@@ -1341,29 +1396,40 @@ $host.gotoURL("https://example.org")"#,
|
||||
Some(std::time::Duration::ZERO),
|
||||
);
|
||||
assert!(o.error.unwrap().contains("too long"));
|
||||
assert!(run(&format!("{}1{}", "-".repeat(5000), "")).error.is_some());
|
||||
assert!(run("var s = \"x\"\nfor i = 1 upto 100 do s = Concat(s, s) endfor\nLen(s)").error.unwrap().contains("too long"));
|
||||
assert!(run("Space(1e12)").error.is_none());
|
||||
assert!(run("Replace(Space(100000), \" \", Space(100000))").error.unwrap().contains("too long"));
|
||||
assert_eq!(run(r#"Format("YYYY-MM-DD", 1e300)"#).error, None);
|
||||
assert_eq!(run(r#"Date2Num("99999999999999999999-01-01", "YYYYYYYYYYYYYYYYYYYY-MM-DD")"#).error, None);
|
||||
assert_eq!(run(r#"Time2Num("99999999999999999999:0:0")"#).error, None);
|
||||
assert_eq!(run(r#"Num2Date(1e300, "YYYY")"#).result.as_deref(), Some(""));
|
||||
assert!(run("for i = 1 upto 1000 do nothing endfor").error.unwrap().contains("unknown name"));
|
||||
assert!(run("\"unterminated").error.unwrap().contains("unterminated"));
|
||||
assert!(run("1 ) 2").error.is_some());
|
||||
assert!(run("Eval(\"Eval(\"\"Eval(\"\"\"\"1\"\"\"\")\"\")\")").error.is_none());
|
||||
assert!(run_hostile_formcalc(&format!("{}1{}", "-".repeat(5000), "")).error.is_some());
|
||||
assert!(run_hostile_formcalc("var s = \"x\"\nfor i = 1 upto 100 do s = Concat(s, s) endfor\nLen(s)").error.unwrap().contains("too long"));
|
||||
assert!(run_hostile_formcalc("Space(1e12)").error.is_none());
|
||||
assert!(run_hostile_formcalc("Replace(Space(100000), \" \", Space(100000))").error.unwrap().contains("too long"));
|
||||
assert_eq!(run_hostile_formcalc(r#"Format("YYYY-MM-DD", 1e300)"#).error, None);
|
||||
assert_eq!(run_hostile_formcalc(r#"Date2Num("99999999999999999999-01-01", "YYYYYYYYYYYYYYYYYYYY-MM-DD")"#).error, None);
|
||||
assert_eq!(run_hostile_formcalc(r#"Time2Num("99999999999999999999:0:0")"#).error, None);
|
||||
assert_eq!(run_hostile_formcalc(r#"Num2Date(1e300, "YYYY")"#).result.as_deref(), Some(""));
|
||||
assert!(run_hostile_formcalc("for i = 1 upto 1000 do nothing endfor").error.unwrap().contains("unknown name"));
|
||||
assert!(run_hostile_formcalc("\"unterminated").error.unwrap().contains("unterminated"));
|
||||
assert!(run_hostile_formcalc("1 ) 2").error.is_some());
|
||||
assert!(run_hostile_formcalc("Eval(\"Eval(\"\"Eval(\"\"\"\"1\"\"\"\")\"\")\")").error.is_none());
|
||||
}
|
||||
|
||||
fn assert_hostile_instance_growth_is_bounded() {
|
||||
// Adding rows forever stops at the instance cap with the effects it managed.
|
||||
let o = run("for i = 1 upto 100000 do table._row.addInstance(1) endfor\ntable._row.count");
|
||||
let o = run_hostile_formcalc("for i = 1 upto 100000 do table._row.addInstance(1) endfor\ntable._row.count");
|
||||
assert!(o.error.unwrap().contains("looped"));
|
||||
assert!(o.effects.iter().filter(|e| matches!(e, XfaEffect::AddInstance { .. })).count() <= 1000, "{}", o.effects.len());
|
||||
let o = run("for i = 1 upto 5000 do table._row.addInstance(1) endfor\ntable._row.count");
|
||||
let o = run_hostile_formcalc("for i = 1 upto 5000 do table._row.addInstance(1) endfor\ntable._row.count");
|
||||
assert_eq!(o.error, None);
|
||||
assert_eq!(o.result.as_deref(), Some("1002"));
|
||||
}
|
||||
|
||||
fn assert_hostile_message_count_is_bounded() {
|
||||
// Message boxes past the cap are dropped and noted.
|
||||
let o = run("for i = 1 upto 500 do $host.messageBox(i) endfor");
|
||||
let o = run_hostile_formcalc("for i = 1 upto 500 do $host.messageBox(i) endfor");
|
||||
assert_eq!(o.effects.iter().filter(|e| matches!(e, XfaEffect::MessageBox(_))).count(), 100);
|
||||
assert!(!o.notes.is_empty());
|
||||
}
|
||||
|
||||
fn assert_hostile_small_stack_is_safe() {
|
||||
let ev = XfaEvent { activity: "calculate".into(), target: "form1[0].page1[0].total[0]".into(), ..Default::default() };
|
||||
let doc = XfaDoc::default();
|
||||
// On a small stack (the wasm build's), the limits stop the script before the stack does.
|
||||
let small = std::thread::Builder::new()
|
||||
.stack_size(1 << 20)
|
||||
@@ -1400,6 +1466,13 @@ $host.gotoURL("https://example.org")"#,
|
||||
);
|
||||
assert!(o.abandoned || o.error.is_some(), "{o:?}");
|
||||
}
|
||||
|
||||
fn run_hostile_formcalc(script: &str) -> XfaOutcome {
|
||||
let ev = XfaEvent { activity: "calculate".into(), target: "form1[0].page1[0].total[0]".into(), ..Default::default() };
|
||||
let doc = XfaDoc::default();
|
||||
let fast = Limits { loop_iterations: 10_000, recursion: 32 };
|
||||
run_formcalc(script, &ev, &doc, &form(), fast)
|
||||
}
|
||||
}
|
||||
|
||||
mod session {
|
||||
|
||||
@@ -2,7 +2,10 @@ use super::host::host;
|
||||
use super::instances::instance_manager;
|
||||
use super::model::{MAX_DEPTH, XfaDoc, XfaEffect, XfaEvent, clip, reset_names};
|
||||
use super::nodes::{node_index, node_object, noop, resolve_som, resolver, sink};
|
||||
use super::*;
|
||||
use super::{
|
||||
Attribute, Context, JsResult, JsString, JsValue, NativeFunction, ObjectInitializer, arg, js_string, platform, printd, printf, printx, s, scand,
|
||||
text,
|
||||
};
|
||||
|
||||
// ── xfa.host, xfa.layout, xfa.event, app, console ───────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::model::{
|
||||
MAX_ALERTS, MAX_CONSOLE, MAX_DEPTH, MAX_EFFECTS, MAX_NODES, MAX_PAYLOAD, MAX_VALUE, XfaDoc, XfaEffect, XfaKind, XfaNode, clip, clip_value,
|
||||
effect_key, payload,
|
||||
};
|
||||
use super::*;
|
||||
use super::{Context, HashMap, JsResult, RefCell, error};
|
||||
|
||||
/// One node of the flattened form, shared by the JavaScript and FormCalc engines.
|
||||
pub(crate) struct HNode {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::host::{HNode, XHost, host};
|
||||
use super::model::{MAX_ADDED, MAX_DEPTH, MAX_NODES, XfaEffect};
|
||||
use super::nodes::{instances, node_object};
|
||||
use super::*;
|
||||
use super::{Attribute, Context, JsArray, JsObject, JsResult, JsValue, NativeFunction, ObjectInitializer, arg, function, js_string, s, text};
|
||||
|
||||
pub(super) fn list_object(ctx: &mut Context, items: &[usize]) -> JsResult<JsValue> {
|
||||
let vals: Vec<JsValue> = items.iter().map(|i| JsValue::from(*i as f64)).collect();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::HashSet;
|
||||
|
||||
/// What kind of template object a node is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -2,7 +2,10 @@ use super::globals::{recalculate, relayout};
|
||||
use super::host::{HNode, XHost, host};
|
||||
use super::instances::{instance_manager, list_object};
|
||||
use super::model::{MAX_DEPTH, XfaEffect};
|
||||
use super::*;
|
||||
use super::{
|
||||
Attribute, Context, JsObject, JsProxy, JsResult, JsString, JsValue, NativeFunction, ObjectInitializer, arg, as_number, error, function,
|
||||
js_string, s, text,
|
||||
};
|
||||
|
||||
// ── node objects ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::globals::install;
|
||||
use super::host::{Shared, XHost};
|
||||
use super::model::{XfaDoc, XfaEvent, XfaNode, XfaOutcome, clip_result};
|
||||
use super::*;
|
||||
use super::{Attribute, Context, JsResult, Limits, RefCell, Source, js_string, refuse, s, text};
|
||||
|
||||
/// Run `script` for `event` on the form `root` (the root subform), from node `event.target`.
|
||||
pub fn run_xfa(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, limits: Limits) -> XfaOutcome {
|
||||
|
||||
Reference in New Issue
Block a user