diff --git a/crates/xfa/src/layout.rs b/crates/xfa/src/layout.rs index 326e7f741..b4d331255 100644 --- a/crates/xfa/src/layout.rs +++ b/crates/xfa/src/layout.rs @@ -769,6 +769,9 @@ impl Layouter<'_> { self.warn_truncated(node, &ctx.som, instances); let ctx_parent = ctx; for i in 0..instances { + // Every instance spends budget, so nested repeats that place nothing still stop. + self.measures.set(self.measures.get().saturating_add(1)); + self.check_measures()?; let ctx = ctx_parent.with_som(&sf.common, sib + i); let (rect, _, _) = self.cursor(); let (x, w) = avail.unwrap_or((rect.x, rect.w)); diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 84c9fc00f..c5de7ad88 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -404,6 +404,18 @@ fn repeated_pictures_are_bounded_in_total_bytes() { assert!(layout_xml(&ok).is_ok(), "one small picture must lay out"); } +#[test] +fn empty_repeating_subforms_spend_the_layout_budget() { + // Four nested levels of 50 empty instances are 6.25 million instances that place nothing + // and measure nothing; each one counts against the layout budget. + let level = ""; + let tpl = format!("", level.repeat(4), "".repeat(4)); + assert!(matches!(layout_xml(&tpl), Err(XfaError::TooLarge(_)))); + // Three levels (125,000 instances) still lay out. + let tpl = format!("", level.repeat(3), "".repeat(3)); + assert!(layout_xml(&tpl).is_ok()); +} + #[test] fn the_packets_can_be_one_stream_or_named_parts() { // Named parts, with the template split from the rest.