diff --git a/crates/redact/src/tests/surviving_references.rs b/crates/redact/src/tests/surviving_references.rs index 0864c701bd..b2e59a6465 100644 --- a/crates/redact/src/tests/surviving_references.rs +++ b/crates/redact/src/tests/surviving_references.rs @@ -386,6 +386,32 @@ fn a_type3_font_only_an_appearance_without_resources_selects_stays_on_the_page() assert!(saved_first_page_lists_font(&doc, b"T4"), "the page no longer lists the font the appearance selects"); } +/// The operators of a page's appearances count toward one total, as the content's do. Two +/// appearances under the cap alone pass it together, and the redaction is refused rather than +/// judged on part of what draws through the page's fonts. +#[test] +fn a_page_s_appearances_share_one_operator_total() { + let big = || stream("/Type /XObject /Subtype /Form /BBox [0 0 100 20]", b"n\n".repeat(1_100_000)); + let mut doc = pdf(vec![ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources 5 0 R /Annots [7 0 R 8 0 R] >>".to_vec(), + stream("", b"BT /T3 10 Tf 10 100 Td (AC) Tj ET"), + b"<< /Font << /T3 6 0 R >> >>".to_vec(), + type3_abc(11), + b"<< /Type /Annot /Subtype /FreeText /Rect [150 200 250 220] /P 3 0 R /AP << /N 9 0 R >> >>".to_vec(), + b"<< /Type /Annot /Subtype /FreeText /Rect [150 240 250 260] /P 3 0 R /AP << /N 10 0 R >> >>".to_vec(), + big(), + big(), + stream("", b"% GLYPH_A"), + stream("", b"% GLYPH_B"), + stream("", b"% GLYPH_C"), + ]); + mark(&mut doc, 0, &[[9.0, 95.0, 14.9, 110.0]], ""); + let refused = apply(&mut doc, None); + assert!(refused.is_err(), "two appearances over the operator cap together were judged in part: {refused:?}"); +} + /// Whether the first page of a full save of `doc` lists the font `name` in its resources. fn saved_first_page_lists_font(doc: &Document, name: &[u8]) -> bool { let bytes = write_full(doc, &SaveOptions::default()).unwrap(); diff --git a/crates/redact/src/type3.rs b/crates/redact/src/type3.rs index 32145c426a..78916c012f 100644 --- a/crates/redact/src/type3.rs +++ b/crates/redact/src/type3.rs @@ -494,8 +494,14 @@ pub(crate) fn prune_type3_from_resources(doc: &Document, res: &mut Dict, content // other files are not refused over them. if fonts.iter().any(|(_, f)| doc.resolve(f).as_dict().is_some_and(|d| d.name(b"Subtype") == Some(b"Type3"))) { if let Some(page) = page { + // One total for all of them, as for the content: each appearance under the cap could + // still add up to far more than it. for data in resourceless_appearances(doc, page)? { - ops.extend(crate::interp::resource_ops(doc, res, std::slice::from_ref(&data))?); + let more = crate::interp::resource_ops(doc, res, std::slice::from_ref(&data))?; + if ops.len().saturating_add(more.len()) > 2_000_000 { + return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully inspect annotation appearances".into() }); + } + ops.extend(more); } } let known = ops.len();