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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
This commit is contained in:
mtlcl410andClaude Opus 5.5 committed 2026-10-30 21:39:34 +08:00
1 parent 0481b98cda
commit d3fdc2f41c
3 files changed
+23 -16

No files matched your search

+6 -2
View File
@@ -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 {
+6 -9
View File
@@ -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<String>), 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 {
+11 -5
View File
@@ -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#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form"><field name="f"><items><text>"#;
let tail = "</text></items></field></subform></template>";
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");
}