mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(js): cap a form script's alerts, console lines and requests
The AcroForm script host kept every alert, console line and request a script made, each as long as the script made it. A loop printing a long script-built string held a copy per iteration. The host applies the XFA host's limits: message and console counts, the effect count for requests, each message, URL and field name cut to a line, and a byte budget across all of them, with a closing console line saying how many were left out.
This commit is contained in:
+91
-13
@@ -203,6 +203,66 @@ struct Host {
|
||||
console: Vec<String>,
|
||||
requests: Vec<Request>,
|
||||
doc: DocInfo,
|
||||
/// Bytes of text kept in alerts, console lines and requests, against [`xfa::MAX_PAYLOAD`].
|
||||
kept: usize,
|
||||
/// Alerts, console lines and requests the limits left out.
|
||||
dropped: usize,
|
||||
}
|
||||
|
||||
impl Host {
|
||||
/// Room for `bytes` more bytes of kept text: charged when there is, a drop counted when not.
|
||||
fn room(&mut self, bytes: usize) -> bool {
|
||||
match self.kept.checked_add(bytes).filter(|k| *k <= xfa::MAX_PAYLOAD) {
|
||||
Some(k) => {
|
||||
self.kept = k;
|
||||
true
|
||||
}
|
||||
None => {
|
||||
self.dropped = self.dropped.saturating_add(1);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn alert(&mut self, m: String) {
|
||||
if self.alerts.len() >= xfa::MAX_ALERTS {
|
||||
self.dropped = self.dropped.saturating_add(1);
|
||||
} else if self.room(m.len()) {
|
||||
self.alerts.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
fn print(&mut self, m: String) {
|
||||
if self.console.len() >= xfa::MAX_CONSOLE {
|
||||
self.dropped = self.dropped.saturating_add(1);
|
||||
} else if self.room(m.len()) {
|
||||
self.console.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
/// Record `r`, its names and URLs cut to a message's length, up to [`xfa::MAX_EFFECTS`]
|
||||
/// requests and the run's byte budget.
|
||||
fn request(&mut self, r: Request) {
|
||||
if self.requests.len() >= xfa::MAX_EFFECTS {
|
||||
self.dropped = self.dropped.saturating_add(1);
|
||||
return;
|
||||
}
|
||||
let r = match r {
|
||||
Request::Reset(names) => Request::Reset(names.into_iter().map(xfa::clip).collect()),
|
||||
Request::LaunchUrl(u) => Request::LaunchUrl(xfa::clip(u)),
|
||||
Request::Submit(u) => Request::Submit(xfa::clip(u)),
|
||||
Request::Focus(n) => Request::Focus(xfa::clip(n)),
|
||||
other => other,
|
||||
};
|
||||
let bytes = match &r {
|
||||
Request::Reset(names) => names.iter().fold(0usize, |n, s| n.saturating_add(s.len()).saturating_add(std::mem::size_of::<String>())),
|
||||
Request::LaunchUrl(u) | Request::Submit(u) | Request::Focus(u) => u.len(),
|
||||
_ => 0,
|
||||
};
|
||||
if self.room(bytes) {
|
||||
self.requests.push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Shared = RefCell<Host>;
|
||||
@@ -223,6 +283,19 @@ fn text(v: &JsValue, ctx: &mut Context) -> JsResult<String> {
|
||||
Ok(v.to_string(ctx)?.to_std_string_escaped())
|
||||
}
|
||||
|
||||
/// [`text`] cut to a message's length ([`xfa::clip`]), without first copying all of a longer
|
||||
/// string.
|
||||
fn text_line(v: &JsValue, ctx: &mut Context) -> JsResult<String> {
|
||||
if v.is_undefined() || v.is_null() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
let js = v.to_string(ctx)?;
|
||||
// Two UTF-16 units per character at most: the clip still sees more than a line's worth.
|
||||
let keep = xfa::MAX_LINE.saturating_add(1).saturating_mul(2);
|
||||
let js = if js.len() > keep { js.get(..keep).unwrap_or_default() } else { js };
|
||||
Ok(xfa::clip(js.to_std_string_escaped()))
|
||||
}
|
||||
|
||||
fn arg(args: &[JsValue], i: usize) -> JsValue {
|
||||
args.get(i).cloned().unwrap_or_default()
|
||||
}
|
||||
@@ -440,7 +513,7 @@ fn field_object(ctx: &mut Context, name: &str) -> JsObject {
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
fn set_focus(_: &JsValue, _: &[JsValue], name: &JsString, c: &mut Context) -> JsResult<JsValue> {
|
||||
host(c)?.borrow_mut().requests.push(Request::Focus(name.to_std_string_escaped()));
|
||||
host(c)?.borrow_mut().request(Request::Focus(name.to_std_string_escaped()));
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
@@ -494,12 +567,12 @@ fn get_nth_field_name(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsRes
|
||||
|
||||
fn reset_form(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let names = strings(&arg(args, 0), ctx)?;
|
||||
host(ctx)?.borrow_mut().requests.push(Request::Reset(names));
|
||||
host(ctx)?.borrow_mut().request(Request::Reset(names));
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
fn print(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
host(ctx)?.borrow_mut().requests.push(Request::Print);
|
||||
host(ctx)?.borrow_mut().request(Request::Print);
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
@@ -512,7 +585,7 @@ fn submit_form(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsV
|
||||
}
|
||||
_ => text(&a, ctx)?,
|
||||
};
|
||||
host(ctx)?.borrow_mut().requests.push(Request::Submit(url));
|
||||
host(ctx)?.borrow_mut().request(Request::Submit(url));
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
@@ -534,38 +607,38 @@ fn page_num_set(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<Js
|
||||
let last = h.doc.num_pages.saturating_sub(1);
|
||||
let p = (n.max(0.0) as usize).min(last);
|
||||
h.doc.page = p;
|
||||
h.requests.push(Request::GoToPage(p));
|
||||
h.request(Request::GoToPage(p));
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
// ── app, util, console ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A message argument: a string, or an object with `cMsg`.
|
||||
/// A message argument: a string, or an object with `cMsg`, cut to a message's length.
|
||||
fn message(v: &JsValue, key: &str, ctx: &mut Context) -> JsResult<String> {
|
||||
match v.as_object() {
|
||||
Some(o) if !o.is_array() && !o.is_callable() => {
|
||||
let m = o.get(JsString::from(key), ctx)?;
|
||||
text(&m, ctx)
|
||||
text_line(&m, ctx)
|
||||
}
|
||||
_ => text(v, ctx),
|
||||
_ => text_line(v, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
fn alert(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let m = message(&arg(args, 0), "cMsg", ctx)?;
|
||||
host(ctx)?.borrow_mut().alerts.push(m);
|
||||
host(ctx)?.borrow_mut().alert(m);
|
||||
// The OK button.
|
||||
Ok(JsValue::from(1))
|
||||
}
|
||||
|
||||
fn beep(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
host(ctx)?.borrow_mut().requests.push(Request::Beep);
|
||||
host(ctx)?.borrow_mut().request(Request::Beep);
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
fn launch_url(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let u = message(&arg(args, 0), "cURL", ctx)?;
|
||||
host(ctx)?.borrow_mut().requests.push(Request::LaunchUrl(u));
|
||||
host(ctx)?.borrow_mut().request(Request::LaunchUrl(u));
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
@@ -575,8 +648,8 @@ fn response(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
|
||||
}
|
||||
|
||||
fn println(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let m = text(&arg(args, 0), ctx)?;
|
||||
host(ctx)?.borrow_mut().console.push(m);
|
||||
let m = text_line(&arg(args, 0), ctx)?;
|
||||
host(ctx)?.borrow_mut().print(m);
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
@@ -1187,6 +1260,8 @@ fn run_here(script: &str, event: &Event, doc: &DocInfo, fields: Arc<Vec<FieldSta
|
||||
console: Vec::new(),
|
||||
requests: Vec::new(),
|
||||
doc: doc.clone(),
|
||||
kept: 0,
|
||||
dropped: 0,
|
||||
}));
|
||||
let mut out = Outcome { rc: true, value: event.value.clone(), change: event.change.clone(), ..Default::default() };
|
||||
let result = (|| -> JsResult<()> {
|
||||
@@ -1217,6 +1292,9 @@ fn run_here(script: &str, event: &Event, doc: &DocInfo, fields: Arc<Vec<FieldSta
|
||||
out.alerts = h.alerts;
|
||||
out.console = h.console;
|
||||
out.requests = h.requests;
|
||||
if h.dropped > 0 {
|
||||
out.console.push(format!("the script's messages, console lines and requests passed their limits; {} more were left out", h.dropped));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -244,6 +244,35 @@ fn document_requests_and_console() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Bytes of text a run handed back in its messages, console lines and requests.
|
||||
fn kept_bytes(o: &Outcome) -> usize {
|
||||
let req = |r: &Request| match r {
|
||||
Request::Reset(names) => names.iter().map(String::len).sum(),
|
||||
Request::Submit(u) | Request::LaunchUrl(u) | Request::Focus(u) => u.len(),
|
||||
_ => 0,
|
||||
};
|
||||
o.console.iter().chain(&o.alerts).map(String::len).sum::<usize>() + o.requests.iter().map(req).sum::<usize>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn console_alerts_and_requests_are_capped_in_count_and_bytes() {
|
||||
let o = go(
|
||||
"var s = 'x'; while (s.length < 8192) s += s; \
|
||||
for (var i = 0; i < 1500; i++) { console.println(s); app.alert(s); } \
|
||||
var a = []; for (var i = 0; i < 2000; i++) a.push(s); \
|
||||
for (var i = 0; i < 5; i++) this.resetForm(a); \
|
||||
for (var i = 0; i < 12000; i++) app.launchURL('u');",
|
||||
&Event::field("Mouse Up", "agree", ""),
|
||||
);
|
||||
assert_eq!(o.error, None);
|
||||
assert!(o.console.len() <= crate::xfa::MAX_CONSOLE + 1, "{} console lines", o.console.len());
|
||||
assert!(o.alerts.len() <= crate::xfa::MAX_ALERTS, "{} alerts", o.alerts.len());
|
||||
assert!(o.requests.len() <= crate::xfa::MAX_EFFECTS, "{} requests", o.requests.len());
|
||||
assert!(o.console.iter().chain(&o.alerts).all(|m| m.chars().count() <= 4_097));
|
||||
assert!(kept_bytes(&o) <= crate::xfa::MAX_PAYLOAD, "{} bytes kept", kept_bytes(&o));
|
||||
assert!(o.console.last().is_some_and(|l| l.contains("left out")), "{:?}", o.console.last());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printd_reads_a_long_literal_run_in_linear_work() {
|
||||
crate::PRINTD_SCANNED.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
@@ -158,7 +158,10 @@ pub const MAX_ALERTS: usize = 100;
|
||||
/// Most console lines one run may print.
|
||||
pub const MAX_CONSOLE: usize = 1_000;
|
||||
/// Longest message or console line kept, in characters.
|
||||
const MAX_LINE: usize = 4_096;
|
||||
pub(crate) const MAX_LINE: usize = 4_096;
|
||||
/// Most bytes of text one run may hand back or write: messages, console lines, URLs, names
|
||||
/// and the values it sets. Past it, more are left out and noted.
|
||||
pub(crate) const MAX_PAYLOAD: usize = 32 << 20;
|
||||
|
||||
/// Longest value a script may put in a field (characters); longer ones are cut and noted.
|
||||
const MAX_VALUE: usize = 65_536;
|
||||
|
||||
Reference in New Issue
Block a user