diff --git a/crates/redact/src/interp.rs b/crates/redact/src/interp.rs index 0a3b730f60..9851c2e8c0 100644 --- a/crates/redact/src/interp.rs +++ b/crates/redact/src/interp.rs @@ -106,13 +106,16 @@ pub(crate) struct Scope<'a> { fonts: HashMap, Rc>, used_names: Vec>, depth: usize, - /// Form entries and decoded form bytes left for this page: shared forms drawn from other - /// shared forms would otherwise multiply the work with every level. + /// Form entries and decoded bytes left for this page, its content streams already charged: + /// shared forms drawn from other shared forms would otherwise multiply the work with every + /// level. budget: DecodeBudget, } impl<'a> Scope<'a> { - pub fn new(rects: &'a [[f64; 4]], mode: Mode, report: &'a mut Report) -> Self { + /// A scope whose form walk spends `budget`, what is left of the page's allowance after its + /// content streams were decoded. + pub fn new(rects: &'a [[f64; 4]], mode: Mode, report: &'a mut Report, budget: DecodeBudget) -> Self { Scope { rects, mode, @@ -127,7 +130,7 @@ impl<'a> Scope<'a> { fonts: HashMap::new(), used_names: Vec::new(), depth: 0, - budget: DecodeBudget::page(), + budget, } } diff --git a/crates/redact/src/lib.rs b/crates/redact/src/lib.rs index f847977201..fce9f432a9 100644 --- a/crates/redact/src/lib.rs +++ b/crates/redact/src/lib.rs @@ -230,6 +230,17 @@ pub(crate) fn marks_checked(doc: &Document) -> (Vec, Vec) { /// A page's content streams: (the references or inline objects as listed, their decoded data), /// decoded within one page's [`pdfcraft_cos::DecodeBudget`]. pub(crate) fn page_streams(doc: &Document, page: &Dict, index: usize) -> Result<(Vec, Vec>), RedactError> { + page_streams_within(doc, page, index, &mut pdfcraft_cos::DecodeBudget::page()) +} + +/// [`page_streams`], decoded against `budget`, which the walk of the page's forms then goes on +/// spending. +pub(crate) fn page_streams_within( + doc: &Document, + page: &Dict, + index: usize, + budget: &mut pdfcraft_cos::DecodeBudget, +) -> Result<(Vec, Vec>), RedactError> { let list: Vec = match page.get(b"Contents") { None => Vec::new(), Some(c) => match &*doc.resolve(c) { @@ -238,7 +249,6 @@ pub(crate) fn page_streams(doc: &Document, page: &Dict, index: usize) -> Result< }, }; let mut data = Vec::new(); - let mut budget = pdfcraft_cos::DecodeBudget::page(); for o in &list { match &*doc.resolve(o) { // Strict: a tolerant decode keeps what precedes corruption and drops the rest, which @@ -396,10 +406,11 @@ pub fn apply(doc: &mut Document, pages: Option<&[usize]>) -> Result = page_marks.iter().flat_map(|m| m.rects.iter().copied()).collect(); // 1. Content. - let (list, data) = page_streams(doc, &page.dict, pi)?; + let mut budget = pdfcraft_cos::DecodeBudget::page(); + let (list, data) = page_streams_within(doc, &page.dict, pi, &mut budget)?; let resources = page.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default(); let out = { - let mut scope = Scope::new(&rects, Mode::Apply, &mut report); + let mut scope = Scope::new(&rects, Mode::Apply, &mut report, budget); process(doc, &mut scope, &data, &resources, pdfcraft_content::Matrix::IDENTITY) }; // Fail closed: the interpreter lays vertical-writing text out horizontally, so it cannot @@ -601,13 +612,14 @@ pub fn apply(doc: &mut Document, pages: Option<&[usize]>) -> Result = page_marks.iter().flat_map(|m| m.rects.iter().copied()).collect(); - let (list, data) = page_streams(doc, &page.dict, pi)?; + let mut budget = pdfcraft_cos::DecodeBudget::page(); + let (list, data) = page_streams_within(doc, &page.dict, pi, &mut budget)?; // The overlay stream draws text of its own: the overlay label is allowed. let original: Vec> = list.iter().zip(data).filter(|(o, _)| !o.as_ref().is_some_and(|r| report.overlays.contains(&r))).map(|(_, d)| d).collect(); let resources = page.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default(); let mut scratch = Report::default(); - let mut scope = Scope::new(&rects, Mode::Verify, &mut scratch); + let mut scope = Scope::new(&rects, Mode::Verify, &mut scratch, budget); residue += process(doc, &mut scope, &original, &resources, pdfcraft_content::Matrix::IDENTITY).residue; } if residue > 0 { diff --git a/crates/redact/src/sanitize.rs b/crates/redact/src/sanitize.rs index 5b2918f01c..0b6c04fdf0 100644 --- a/crates/redact/src/sanitize.rs +++ b/crates/redact/src/sanitize.rs @@ -8,7 +8,7 @@ use pdfcraft_content::Matrix; use pdfcraft_cos::{Dict, Document, ObjRef, Object}; use crate::interp::{Mode, Scope, process}; -use crate::{RedactError, Report, annots_of, listed_annotations, page_streams}; +use crate::{RedactError, Report, annots_of, listed_annotations, page_streams, page_streams_within}; /// The categories of Acrobat's Remove Hidden Information panel that PdfCraft handles. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -356,7 +356,8 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re let n = pdfcraft_model::pages(doc).len(); for pi in 0..n { let page = pdfcraft_model::pages(doc).swap_remove(pi); - let Ok((list, data)) = page_streams(doc, &page.dict, pi) else { + let mut budget = pdfcraft_cos::DecodeBudget::page(); + let Ok((list, data)) = page_streams_within(doc, &page.dict, pi, &mut budget) else { if write { return Err(RedactError::Unreadable(pi + 1)); } @@ -368,7 +369,7 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re let resources = page.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default(); let mut report = Report::default(); let mode = if write { Mode::Apply } else { Mode::Verify }; - let mut scope = Scope::new(&[], mode, &mut report); + let mut scope = Scope::new(&[], mode, &mut report, budget); if text { let c = page.crop(doc); scope.hidden_text = Some([c[0].min(c[2]), c[1].min(c[3]), c[0].max(c[2]), c[1].max(c[3])]); diff --git a/crates/redact/src/tests.rs b/crates/redact/src/tests.rs index 550903e494..2478e0b31b 100644 --- a/crates/redact/src/tests.rs +++ b/crates/redact/src/tests.rs @@ -48,10 +48,11 @@ fn content(doc: &Document, page: usize) -> String { /// Glyphs (and inline images) of page `page` under `rects` (the verifier's count). fn under(doc: &mut Document, page: usize, rects: &[[f64; 4]]) -> usize { let p = pdfcraft_model::pages(doc).swap_remove(page); - let (_, data) = page_streams(doc, &p.dict, page).unwrap(); + let mut budget = pdfcraft_cos::DecodeBudget::page(); + let (_, data) = page_streams_within(doc, &p.dict, page, &mut budget).unwrap(); let res = p.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default(); let mut rep = Report::default(); - let mut scope = Scope::new(rects, Mode::Verify, &mut rep); + let mut scope = Scope::new(rects, Mode::Verify, &mut rep, budget); process(doc, &mut scope, &data, &res, pdfcraft_content::Matrix::IDENTITY).residue } @@ -2713,3 +2714,34 @@ fn repeated_name_tree_kids_count_towards_the_node_budget() { let counts: std::collections::HashMap = scan(&doc).into_iter().collect(); assert_eq!(counts[&Hidden::Attachments], 1, "the walk went past its node budget to the last kid"); } + +/// Content streams and the forms they draw spend one page allowance: 130 MiB of content and a +/// 130 MiB form are each under it, together past it. +#[test] +fn content_and_forms_share_one_budget_per_page() { + use pdfcraft_cos::{Dict, Stream}; + let mut doc = one_page(b"", "", vec![]); + let mut content = b"BT /F1 10 Tf 10 100 Td (SECRET) Tj ET /X Do\n%".to_vec(); + content.resize(130 << 20, b'x'); + let content = doc.add(Object::Stream(Stream::flate(Dict::new(), &content))); + let mut form = Dict::new(); + form.set(b"Type".to_vec(), Object::name("XObject")); + form.set(b"Subtype".to_vec(), Object::name("Form")); + let mut body = b"%".to_vec(); + body.resize(130 << 20, b'x'); + let form = doc.add(Object::Stream(Stream::flate(form, &body))); + doc.update_dict(ObjRef::new(3, 0), |d| { + let mut xobjects = Dict::new(); + xobjects.set(b"X".to_vec(), Object::Ref(form)); + let mut fonts = Dict::new(); + fonts.set(b"F1".to_vec(), Object::Ref(ObjRef::new(5, 0))); + let mut res = Dict::new(); + res.set(b"Font".to_vec(), Object::Dict(fonts)); + res.set(b"XObject".to_vec(), Object::Dict(xobjects)); + d.set(b"Resources".to_vec(), Object::Dict(res)); + d.set(b"Contents".to_vec(), Object::Ref(content)); + }) + .unwrap(); + mark(&mut doc, 0, &[[0.0, 90.0, 50.0, 120.0]], ""); + assert!(apply(&mut doc, None).is_err(), "the form past the page's allowance was read"); +}