M12: an XFA page area keeps at most 128 content areas, so a subform split across them lays out within its budget (#5843) (#5844)

This commit is contained in:
mtlcl410 authored and GitHub committed 2026-11-09 08:39:31 +08:00
1 parent 67a6037be1
commit 359cef8b0d
3 files changed
+88 -1

No files matched your search

+9 -1
View File
@@ -1,6 +1,6 @@
use super::common::{clamp_opt, divide, is_row, moved, solid_extent, splittable};
use super::items::Item;
use super::{Ctx, Frame, Layout, Layouter, MAX_DEPTH, Node, Rect, SomIndices, Subform, XfaError, data_path, instance_count};
use super::{Ctx, Frame, Layout, Layouter, MAX_DEPTH, MAX_MEASURES, Node, Rect, SomIndices, Subform, XfaError, data_path, instance_count};
impl Layouter<'_> {
/// Place `node` as one block of size (`w`, `h`) at the cursor: in this content area when it fits,
@@ -75,6 +75,14 @@ impl Layouter<'_> {
let mut seg_top = top;
let mut first_segment = true;
loop {
// Each pass rescans every item still to be placed. A block split across very many
// content areas, and so across very many pages, would do this once per page, costing
// time quadratic in its items; the still-unplaced count spends the work budget so a
// crafted template stops rather than run for seconds before the page limit catches it.
self.measures.set(self.measures.get().saturating_add(rest.len()));
if self.measures.get() > MAX_MEASURES {
return Err(XfaError::TooLarge("a subform is split across too many content areas to lay out".into()));
}
let (rect, _, placed) = self.cursor();
let limit = rect.bottom() + shift;
let mut cut = limit;
+16
View File
@@ -21,6 +21,10 @@ const MAX_IMAGE: usize = 32 << 20;
const MAX_ITEMS: usize = 1000;
/// Longest text run kept.
const MAX_TEXT: usize = 1 << 20;
/// Most content areas kept on one page area. A master page has a handful (the most in the
/// 500-form corpus is nine); laying a splittable subform out across tens of thousands of them,
/// which `next_area` walks before starting a new page, costs time quadratic in the paragraphs.
const MAX_CONTENT_AREAS: usize = 128;
/// Parse a measurement such as `12.7mm`, `72pt`, `1in`, `2.54cm` into points. Bare numbers are
/// points. `None` for anything else (including `em`).
@@ -609,7 +613,19 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> PageS
attr_measure(c, "h").unwrap_or(height).clamp(1.0, height),
)
})
.take(MAX_CONTENT_AREAS + 1)
.collect();
if content.len() > MAX_CONTENT_AREAS {
content.truncate(MAX_CONTENT_AREAS);
// Said once however many warnings came before: other warnings fill the shared cap of
// 100 without limit of their own, and this one must not go silent behind them. It is
// kept first, so whether it was said is one comparison, not a scan of every warning
// for every page area (nothing else is put first).
let said = format!("a page area has more than {MAX_CONTENT_AREAS} content areas; the rest are left out");
if warnings.first() != Some(&said) {
warnings.insert(0, said);
}
}
if content.is_empty() {
content.push(Rect::new(0.0, 0.0, width, height));
}
+63
View File
@@ -188,6 +188,46 @@ fn the_template_parses_with_fonts_captions_and_rich_text() {
assert!(last.break_before_page);
}
/// A page area that declares far more content areas than any real master keeps only the first
/// 128, with a warning. `split_block` walks every content area of a page before starting a new
/// one, re-scanning the unplaced items each time, so tens of thousands of them cost time
/// quadratic in a splittable subform's paragraphs. Capping the count bounds that walk.
#[test]
fn a_page_area_keeps_at_most_a_hundred_and_twenty_eight_content_areas() {
let areas = (0..200).map(|i| format!("<contentArea x=\"0.5in\" y=\"{}pt\" w=\"7in\" h=\"1pt\"/>", i)).collect::<String>();
let xml = format!(
r##"<?xml version="1.0" encoding="UTF-8"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet>
<pageArea name="front"><medium stock="letter" short="8.5in" long="11in"/>{areas}</pageArea>
</pageSet>
</subform>
</template>
</xdp:xdp>
"##
);
let (tpl, warnings) = parse(&xml).unwrap();
let ps = tpl.root.page_set.as_ref().unwrap();
assert_eq!(ps.areas[0].content.len(), 128, "the content areas are capped");
assert!(warnings.iter().any(|w| w.contains("content areas")), "a warning records the cap: {warnings:?}");
// Still said after a hundred other warnings (CodeRabbit review on #5844): images that can't
// be decoded, parsed before the page set, fill the warnings' shared cap first.
let bad = "<draw w=\"1in\" h=\"1in\"><value><image contentType=\"image/png\">!!!</image></value></draw>".repeat(120);
let crowded = xml.replace("<subform name=\"form\" layout=\"tb\">", &format!("<subform name=\"form\" layout=\"tb\">{bad}"));
let (_, warnings) = parse(&crowded).unwrap();
assert!(warnings.len() > 100, "the other warnings came first: {}", warnings.len());
assert!(warnings.iter().any(|w| w.contains("content areas")), "the cap is still recorded");
// Many capped page areas say it once, found without scanning every warning (it comes first).
let page_area = |n: usize| format!(r#"<pageArea name="p{n}"><medium stock="letter" short="8.5in" long="11in"/>{areas}</pageArea>"#);
let many: String = (0..50).map(page_area).collect();
let crowded = crowded.replacen(&page_area(0).replace("p0", "front"), &many, 1);
let (_, warnings) = parse(&crowded).unwrap();
assert_eq!(warnings.iter().filter(|w| w.contains("content areas")).count(), 1, "said once");
assert!(warnings[0].contains("content areas"), "kept first: {:?}", warnings.first());
}
#[test]
fn rich_text_max_length_reaches_the_field_and_layout() {
for (attribute, expected) in [
@@ -678,6 +718,29 @@ fn empty_repeating_subforms_spend_the_layout_budget() {
assert!(layout_xml(&tpl).is_ok());
}
#[test]
fn a_block_split_across_very_many_pages_spends_the_layout_budget() {
// One-point content areas slice a flowed subform's paragraphs one line to a page, so the
// block is split across hundreds of pages; split_block rescans the still-unplaced
// paragraphs once per page, which is quadratic in their number. Fewer paragraphs than this
// lay out (188 pages here), so the page limit does not catch it; the rescans spend the
// work budget instead.
let areas: String = (0..128).map(|i| format!("<contentArea x=\"0.5in\" y=\"{i}pt\" w=\"7in\" h=\"1pt\"/>")).collect();
let paras: String = (0..2500).map(|i| format!("<p>Paragraph number {i} with a few words to fill a line.</p>")).collect();
let tpl = format!(
"<template><subform name=\"form\" layout=\"tb\"><pageSet><pageArea name=\"p\">\
<medium stock=\"letter\" short=\"8.5in\" long=\"11in\"/>{areas}</pageArea></pageSet>\
<subform name=\"body\" layout=\"tb\"><field name=\"t\" w=\"7in\"><ui><textEdit multiLine=\"1\"/></ui>\
<value><exData contentType=\"text/html\"><body xmlns=\"http://www.w3.org/1999/xhtml\">{paras}</body></exData></value>\
</field></subform></subform></template>"
);
match layout_xml(&tpl) {
// It says what ran out, not that the form is nested too deeply (CodeRabbit review on #5844).
Err(XfaError::TooLarge(m)) => assert!(m.contains("split across"), "{m}"),
other => panic!("a block split across very many pages must be refused: {other:?}"),
}
}
#[test]
fn a_packet_stream_listed_many_times_is_read_once() {
let mut doc = Document::open(Arc::new(shell("<template><subform/></template>"))).unwrap();