From 54ff776de9d76fb0f0aea2842ac21c228f69f020 Mon Sep 17 00:00:00 2001 From: mtlcl410 Date: Thu, 5 Nov 2026 07:25:53 +0800 Subject: [PATCH] fix(xfa): fit passes stop once the draw budget is spent, and the last pass is checked A draw's fit passes ran on after one had spent the budget, each a full measure, before the draw was refused; each pass now checks the budget before it starts and after it ends. The final layout pass, which expands the embedded values once more, is checked too, so the draw that crosses the budget there is refused even when it is the form's last. (CodeRabbit review on #5852.) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn --- crates/xfa/src/layout/draw.rs | 21 ++++++++++++++++++--- crates/xfa/src/tests.rs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/crates/xfa/src/layout/draw.rs b/crates/xfa/src/layout/draw.rs index 9c0b385c63..ace5bb93ac 100644 --- a/crates/xfa/src/layout/draw.rs +++ b/crates/xfa/src/layout/draw.rs @@ -242,14 +242,29 @@ impl Layouter<'_> { // Text that doesn't fit the height it was given (the template's face is narrower than // the standard one drawn in its place, so it wraps to more lines) is drawn smaller, // down to 70%, rather than over what lies below. - let fits = |k: f64| { + // A pass is not started once the budget is spent, and one that spends it stops the rest. + let fits = |k: f64| -> Result { + self.check_drawn_chars()?; self.drawn_chars.set(self.drawn_chars.get().saturating_add(cost)); - text::measure_height(&rich, rect.w / k, para, &face, &embed) <= rect.h / k + 1.0 + let fit = text::measure_height(&rich, rect.w / k, para, &face, &embed) <= rect.h / k + 1.0; + self.check_drawn_chars()?; + Ok(fit) }; - let scale = if rect.h < 4.0 || fits(1.0) { 1.0 } else { [0.95, 0.9, 0.85, 0.8, 0.75].into_iter().find(|k| fits(*k)).unwrap_or(0.7) }; + let mut scale = 1.0; + if rect.h >= 4.0 && !fits(1.0)? { + scale = 0.7; + for k in [0.95, 0.9, 0.85, 0.8, 0.75] { + if fits(k)? { + scale = k; + break; + } + } + } self.drawn_chars.set(self.drawn_chars.get().saturating_add(cost)); self.check_drawn_chars()?; let mut block = text::layout(&rich, Rect::new(rect.x, rect.y, rect.w / scale, rect.h / scale), para, &face, &embed); + // The layout itself expands the embedded values once more. + self.check_drawn_chars()?; if scale < 1.0 { for s in &mut block.spans { s.x = rect.x + (s.x - rect.x) * scale; diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 29e5e0da0a..d1edc8f6d0 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -713,6 +713,40 @@ fn embedded_values_drawn_in_many_instances_spend_the_draw_budget() { assert!(layout_xml(&one).is_ok(), "a single draw embedding a long value must lay out"); } +/// The last layout pass of a draw counts against the budget too: a draw that fits is measured +/// once and laid out once, each pass expanding its embedded value, and when the form's last draw +/// crosses the budget in that final pass it is still refused (CodeRabbit review on #5852). +#[test] +fn the_draw_that_crosses_the_budget_in_its_last_pass_is_refused() { + // 15,000 characters: with the 1 MiB test budget, the tenth such draw crosses it during its + // final layout pass, after every check before that pass has passed. + let value = "word ".repeat(3000); + let span = r##""##; + let form = |n: usize| { + format!( + r#""# + ) + }; + assert!(layout_xml(&form(9)).is_ok()); + assert!(matches!(layout_xml(&form(10)), Err(XfaError::TooLarge(_))), "the draw crossing the budget in its last pass must be refused"); +} + +/// Once a fit pass spends the budget, no more passes are made: a draw that doesn't fit and embeds +/// a long value is measured once, not seven times, before it is refused (CodeRabbit review on +/// #5852). +#[test] +fn fit_passes_stop_once_the_draw_budget_is_spent() { + let value = "word ".repeat(text::MAX_EMBEDDED / 5 + 1000); + let span = r##""##; + let tpl = format!( + r#""# + ); + crate::layout::EMBED_COPIED.with(|c| c.set(0)); + assert!(matches!(layout_xml(&tpl), Err(XfaError::TooLarge(_)))); + let copied = crate::layout::EMBED_COPIED.with(|c| c.get()); + assert!(copied <= 2 * text::MAX_EMBEDDED as u64, "{copied} bytes copied: more passes after the budget was spent"); +} + #[test] fn a_packet_stream_listed_many_times_is_read_once() { let mut doc = Document::open(Arc::new(shell(""))).unwrap();