diff --git a/crates/xfa/src/packets.rs b/crates/xfa/src/packets.rs index 875cdc894..f79b071c0 100644 --- a/crates/xfa/src/packets.rs +++ b/crates/xfa/src/packets.rs @@ -113,7 +113,13 @@ pub fn read_packets(doc: &Document) -> Result, 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, 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)?; diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index c5de7ad88..03a7d2c5a 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -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(""))).unwrap(); + let packet = pdfcraft_cos::ObjRef::new(5, 0); + let items: Vec = + (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("