From d0c56fdfee614328d31b351d24f52128b4f8dd46 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 8 Nov 2026 23:37:23 +0700 Subject: [PATCH] Refuse the form whose merged resources pass the page's limit The resource limit was checked before a form's resources were merged into the page's, so one form could take the page past it and every later form was then refused, even one whose resources were all the page's already. Check the merged dictionary instead. --- crates/edit/src/tests.rs | 36 ++++++++++++++++++++++++++++++++++++ crates/edit/src/ungroup.rs | 5 +++++ 2 files changed, 41 insertions(+) diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index e61bd87d3e..97127db949 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -12720,6 +12720,42 @@ fn grouped_source_text_preparation_edits_original_operators_without_changing_sha assert_eq!(text::forms::prepare(&mut doc, 0).unwrap().expanded, 0); } +/// A text form whose resources would take the page's past the limit is refused itself, so a later +/// form whose resources are all the page's already is still prepared. +#[test] +fn grouped_text_preparation_refuses_the_form_that_would_pass_the_resource_limit() { + let mut doc = page_with_forms( + "/A Do /B Do", + &[("A".into(), "BT /X 14 Tf 40 500 Td (a) Tj ET".into()), ("B".into(), "BT /F1 14 Tf 40 400 Td (b) Tj ET".into())], + ); + let helvetica = Object::Ref(pdfcraft_cos::ObjRef::new(5, 0)); + doc.update_dict(pdfcraft_cos::ObjRef::new(3, 0), |page| { + let Some(Object::Dict(mut resources)) = page.get(b"Resources").cloned() else { panic!("fixture") }; + let Some(Object::Dict(mut fonts)) = resources.get(b"Font").cloned() else { panic!("fixture") }; + // With F1, the 4 096 the limit allows. + for i in 0..4095 { + fonts.set(format!("P{i}").into_bytes(), helvetica.clone()); + } + resources.set(b"Font".to_vec(), Object::Dict(fonts)); + page.set(b"Resources".to_vec(), Object::Dict(resources)); + }) + .unwrap(); + // A names Helvetica /X, which the page doesn't: merging it adds a name. B's /F1 is the page's. + for (object, name) in [(7, "X"), (8, "F1")] { + let Object::Stream(mut form) = doc.get(pdfcraft_cos::ObjRef::new(object, 0)).as_ref().clone() else { panic!("fixture") }; + let mut fonts = Dict::new(); + fonts.set(name.as_bytes().to_vec(), helvetica.clone()); + let mut resources = Dict::new(); + resources.set(b"Font".to_vec(), Object::Dict(fonts)); + form.dict.set(b"Resources".to_vec(), Object::Dict(resources)); + doc.set(pdfcraft_cos::ObjRef::new(object, 0), Object::Stream(form)); + } + let prepared = text::forms::prepare(&mut doc, 0).unwrap(); + assert_eq!(prepared.expanded, 1); + assert!(prepared.refused.len() == 1 && prepared.refused[0].starts_with("A:"), "{:?}", prepared.refused); + assert!(text::text_lines(&doc, 0).unwrap().iter().any(|l| l.text == "b")); +} + #[test] fn grouped_text_preparation_keeps_unsafe_compositing_paths_and_malformed_forms_intact() { for (page_ops, form_ops, extra) in [ diff --git a/crates/edit/src/ungroup.rs b/crates/edit/src/ungroup.rs index f0e7dc0ebe..1b1916a1c8 100644 --- a/crates/edit/src/ungroup.rs +++ b/crates/edit/src/ungroup.rs @@ -240,6 +240,11 @@ pub(crate) fn ungroup_ops( mine.set(fresh.clone(), value.clone()); map.push((key.to_vec(), fresh)); } + // Counted after the merge: a page taken past the limit would refuse every later form, + // even one whose resources are all the page's already. + if mine.len() > MAX_RESOURCES { + return Err(refuse("too many resources")); + } merged.set(cat.to_vec(), Object::Dict(mine)); if !map.is_empty() { renames.push((cat, map));