diff --git a/crates/js/src/tests.rs b/crates/js/src/tests.rs index 8ce338b2e..3201641b8 100644 --- a/crates/js/src/tests.rs +++ b/crates/js/src/tests.rs @@ -455,6 +455,27 @@ mod xfa_model { assert_eq!(o.effects, vec![XfaEffect::SetValue { som: "form1[0].page1[0].total[0]".into(), value: "5".into() }]); } + /// A long value written again and again stops at the run's byte budget, and the write that + /// does not fit is left out of the field the script reads as well as of the effect. + #[test] + fn a_value_past_the_byte_budget_is_left_out_of_reads_and_effects_alike() { + let o = go( + "var s = 'x'; while (s.length < 65536) s += s; \ + for (var i = 0; i < 300; i++) details.note.rawValue = ('00000' + i).slice(-6) + s.slice(6); \ + details.note.rawValue.slice(0, 6)", + "form1[0].page1[0].total[0]", + "calculate", + ); + assert_eq!(o.error, None, "{o:?}"); + let set: Vec<&str> = o + .effects + .iter() + .filter_map(|e| if let XfaEffect::SetValue { value, .. } = e { Some(value.get(..6).unwrap_or(value)) } else { None }) + .collect(); + assert_eq!(set.len(), 1); + assert_eq!(o.result.as_deref(), set.first().copied()); + } + /// The script runs through `eval`, so code built from strings at run time is held to the /// script's own size and nesting checks instead of being refused outright. #[test] diff --git a/crates/js/src/xfa.rs b/crates/js/src/xfa.rs index ae38658e0..99a1dd45c 100644 --- a/crates/js/src/xfa.rs +++ b/crates/js/src/xfa.rs @@ -315,7 +315,15 @@ impl XHost { /// earlier one; past [`MAX_EFFECTS`] (or [`MAX_ALERTS`] message boxes, or [`MAX_PAYLOAD`] /// bytes of text) effects are counted and dropped. pub(crate) fn push(&mut self, e: XfaEffect) { - let e = match e { + let e = self.clip_effect(e); + if self.room(payload(&e)) { + self.record(e); + } + } + + /// `e` with its text cut to the lengths an effect keeps. + fn clip_effect(&mut self, e: XfaEffect) -> XfaEffect { + match e { XfaEffect::MessageBox(m) => XfaEffect::MessageBox(clip(m)), XfaEffect::LaunchUrl(u) => XfaEffect::LaunchUrl(clip(u)), XfaEffect::SetFocus(s) => XfaEffect::SetFocus(clip(s)), @@ -328,10 +336,11 @@ impl XHost { XfaEffect::SetValue { som, value } } other => other, - }; - if !self.room(payload(&e)) { - return; } + } + + /// Keep `e`, whose bytes are already charged, against the count limits. + fn record(&mut self, e: XfaEffect) { let key = match &e { XfaEffect::SetValue { som, .. } => Some((0u8, som.clone())), XfaEffect::SetPresence { som, .. } => Some((1, som.clone())), @@ -415,16 +424,17 @@ impl XHost { if cut { self.clipped_values += 1; } - // The snapshot's copy counts as well as the effect's: past the budget the write is left - // out of both. - if !self.room(text.len()) { + // The snapshot's copy and the effect's are charged together, before either is kept: + // past the byte budget the write is left out of both. + let effect = XfaEffect::SetValue { som, value: text.clone() }; + if !self.room(text.len().saturating_add(payload(&effect))) { return true; } if let Some(n) = self.nodes.get_mut(i) { - n.value = text.clone(); + n.value = text; n.formatted = None; } - self.push(XfaEffect::SetValue { som, value: text }); + self.record(effect); true } }