From 0481b98cda4a32936a7ffb96fa609a5aaee03aa1 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:51:57 +0800 Subject: [PATCH] M12: an XFA template parsed directly is held to the packet size limit parse() refuses a template larger than the 64 MiB a document's packets may hold (read_packets' limit), before any of it is parsed, so what parsing keeps of it, item lists held by the field and its properties among them, stays within a few times that whoever calls it (review). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv --- crates/xfa/src/packets.rs | 5 +++-- crates/xfa/src/parse.rs | 10 ++++++++++ crates/xfa/src/props_tests.rs | 13 +++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/xfa/src/packets.rs b/crates/xfa/src/packets.rs index f79b071c0e..8c1a57ab91 100644 --- a/crates/xfa/src/packets.rs +++ b/crates/xfa/src/packets.rs @@ -5,8 +5,9 @@ use pdfcraft_cos::{Document, Object}; use crate::XfaError; -/// Most decoded packet bytes read in total. -const MAX_XDP: usize = 64 << 20; +/// Most decoded packet bytes read in total; a template handed to [`crate::parse`] directly is held +/// to the same. +pub(crate) const MAX_XDP: usize = 64 << 20; /// What was found in the XFA entry. #[derive(Clone, Debug, Default, PartialEq)] diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index d99f875368..efbb23c55f 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -969,6 +969,16 @@ 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 + ))); + } let depth = crate::xml_nesting_depth(xml); if depth > crate::MAX_XML_DEPTH { return Err(XfaError::Malformed(format!( diff --git a/crates/xfa/src/props_tests.rs b/crates/xfa/src/props_tests.rs index 81ba5114b5..8c17fcb0c4 100644 --- a/crates/xfa/src/props_tests.rs +++ b/crates/xfa/src/props_tests.rs @@ -221,3 +221,16 @@ fn a_caption_s_rich_text_and_a_list_s_items_are_kept() { assert_eq!(f.common.props.elements[2].attrs, [("save".to_string(), "1".to_string())]); 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. +#[test] +fn a_template_past_the_packet_limit_is_refused_before_parsing() { + let limit = crate::packets::MAX_XDP; + 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(_)))); +}