fix(xfa): read a packet stream the XFA array lists again only once

The packet reader decoded the stream of every /XFA array entry and
charged only the decoded output to its 64 MB budget, so one stream that
decodes to almost nothing (Flate over hex whitespace), listed many
times, repeated a large decode for free. A stream reference already
read is skipped.
This commit is contained in:
aiongg
2026-10-22 17:08:55 +00:00
parent 2e1c029fa8
commit 306627ee77
2 changed files with 23 additions and 0 deletions
+6
View File
@@ -113,7 +113,13 @@ pub fn read_packets(doc: &Document) -> Result<Option<Packets>, XfaError> {
let needs_rendering = bool_of(catalog.as_dict().and_then(|c| c.get(b"NeedsRendering"))).or_else(|| bool_of(acro.get(b"NeedsRendering")));
let has_fields = acro.get(b"Fields").map(|f| doc.resolve(f)).and_then(|f| f.as_array().map(|a| !a.is_empty())).unwrap_or(false);
let mut total = 0usize;
// A stream listed again adds nothing but another decode, which may cost far more than the
// bytes it yields (a Flate stream of hex whitespace).
let mut seen = std::collections::HashSet::new();
let mut read = |o: &Object| -> Result<Option<String>, XfaError> {
if o.as_ref().is_some_and(|r| !seen.insert(r)) {
return Ok(None);
}
let s = doc.resolve(o);
let Object::Stream(s) = &*s else { return Ok(None) };
let bytes = s.decoded_within(MAX_XDP)?;
+17
View File
@@ -416,6 +416,23 @@ fn empty_repeating_subforms_spend_the_layout_budget() {
assert!(layout_xml(&tpl).is_ok());
}
#[test]
fn a_packet_stream_listed_many_times_is_read_once() {
let mut doc = Document::open(Arc::new(shell("<template><subform/></template>"))).unwrap();
let packet = pdfcraft_cos::ObjRef::new(5, 0);
let items: Vec<Object> =
(0..1000).flat_map(|_| [Object::String(pdfcraft_cos::PdfString::literal(b"template".to_vec())), Object::Ref(packet)]).collect();
let root = doc.root().unwrap();
doc.update_dict(root, |c| {
let mut acro = c.get(b"AcroForm").and_then(Object::as_dict).cloned().unwrap();
acro.set(b"XFA".to_vec(), Object::Array(items));
c.set(b"AcroForm".to_vec(), Object::Dict(acro));
})
.unwrap();
let p = read_packets(&doc).unwrap().unwrap();
assert_eq!(p.xdp.matches("<template").count(), 1);
}
#[test]
fn the_packets_can_be_one_stream_or_named_parts() {
// Named parts, with the template split from the rest.