From d3fdc2f41c421e915722d0965a65ef0a8523c1b5 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Fri, 30 Oct 2026 21:39:34 +0800 Subject: [PATCH] M12: an XFA template parses up to what a packet the reader accepts can come to as text A UTF-16 packet within the reader's 64 MiB grows by up to half when it becomes UTF-8, so parse() now takes up to 96 MiB rather than 64: a packet read_packets accepts always parses. The refusal gives the size in bytes (review). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv --- crates/xfa/src/packets.rs | 8 ++++++-- crates/xfa/src/parse.rs | 15 ++++++--------- crates/xfa/src/props_tests.rs | 16 +++++++++++----- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/crates/xfa/src/packets.rs b/crates/xfa/src/packets.rs index 8c1a57ab91..73918fdefb 100644 --- a/crates/xfa/src/packets.rs +++ b/crates/xfa/src/packets.rs @@ -5,10 +5,14 @@ use pdfcraft_cos::{Document, Object}; use crate::XfaError; -/// Most decoded packet bytes read in total; a template handed to [`crate::parse`] directly is held -/// to the same. +/// Most decoded packet bytes read in total. pub(crate) const MAX_XDP: usize = 64 << 20; +/// Most bytes of a template [`crate::parse`] takes: the most a packet within [`MAX_XDP`] can come +/// to as UTF-8 text. A UTF-16 packet grows by at most half going over (a two-byte unit becomes at +/// most three bytes; a four-byte pair stays four), so a packet the reader accepts always parses. +pub(crate) const MAX_TEMPLATE_TEXT: usize = MAX_XDP / 2 * 3; + /// What was found in the XFA entry. #[derive(Clone, Debug, Default, PartialEq)] pub struct Packets { diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index efbb23c55f..de50f5102c 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -969,15 +969,12 @@ fn collect_page_roles(nodes: &[Node], out: &mut Vec<(String, PageRole)>, depth: /// Parse a template packet. Returns the template and the warnings met on the way. pub fn parse(xml: &str) -> Result<(Template, Vec), XfaError> { - // As read from a document ([`crate::read_packets`]): a template parsed directly is no bigger, - // so what parsing keeps of it (item lists are held by the field and its properties) stays - // within a few times that. - if xml.len() > crate::packets::MAX_XDP { - return Err(XfaError::TooLarge(format!( - "the template is {} MiB, past the {} MiB the reader accepts", - xml.len() >> 20, - crate::packets::MAX_XDP >> 20 - ))); + // No bigger than a packet the reader accepts can come to as text ([`crate::read_packets`]), so + // what parsing keeps of it (item lists are held by the field and its properties) stays within + // a few times that, whoever calls it. + let limit = crate::packets::MAX_TEMPLATE_TEXT; + if xml.len() > limit { + return Err(XfaError::TooLarge(format!("the template is {} bytes, past the {limit} bytes the reader accepts", xml.len()))); } let depth = crate::xml_nesting_depth(xml); if depth > crate::MAX_XML_DEPTH { diff --git a/crates/xfa/src/props_tests.rs b/crates/xfa/src/props_tests.rs index 8c17fcb0c4..ab46f5a9e3 100644 --- a/crates/xfa/src/props_tests.rs +++ b/crates/xfa/src/props_tests.rs @@ -222,15 +222,21 @@ fn a_caption_s_rich_text_and_a_list_s_items_are_kept() { assert_eq!(f.items.len(), 400, "the list shows the same entries"); } -/// A template handed to `parse` directly is held to the bytes a document's packets may hold: one -/// past them is refused before any of it is parsed, so the item lists a field and its properties -/// keep of it can't grow past a few times that. +/// A template handed to `parse` directly is held to what a document's packets may come to as text: +/// one byte past it is refused before any of it is parsed (the error giving its size in bytes), and +/// a template of exactly that size, as a UTF-16 packet the reader accepted can become, is parsed. #[test] fn a_template_past_the_packet_limit_is_refused_before_parsing() { - let limit = crate::packets::MAX_XDP; + let limit = crate::packets::MAX_TEMPLATE_TEXT; + assert!(limit > crate::packets::MAX_XDP, "room for a UTF-16 packet grown into UTF-8"); let head = r#""; let xml = format!("{head}{}{tail}", "a".repeat(limit + 1 - head.len() - tail.len())); assert_eq!(xml.len(), limit + 1); - assert!(matches!(crate::parse(&xml), Err(crate::XfaError::TooLarge(_)))); + match crate::parse(&xml) { + Err(crate::XfaError::TooLarge(m)) => assert!(m.contains(&format!("{} bytes", limit + 1)), "{m}"), + other => panic!("{other:?}"), + } + let at = format!("{head}{}{tail}", "a".repeat(limit - head.len() - tail.len())); + assert!(crate::parse(&at).is_ok(), "a template at the limit is parsed"); }