mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 22:41:47 -06:00
Redaction charges a page's content streams and its forms to one decode budget
This commit is contained in:
@@ -106,13 +106,16 @@ pub(crate) struct Scope<'a> {
|
|||||||
fonts: HashMap<Vec<u8>, Rc<Metrics>>,
|
fonts: HashMap<Vec<u8>, Rc<Metrics>>,
|
||||||
used_names: Vec<Vec<u8>>,
|
used_names: Vec<Vec<u8>>,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
/// Form entries and decoded form bytes left for this page: shared forms drawn from other
|
/// Form entries and decoded bytes left for this page, its content streams already charged:
|
||||||
/// shared forms would otherwise multiply the work with every level.
|
/// shared forms drawn from other shared forms would otherwise multiply the work with every
|
||||||
|
/// level.
|
||||||
budget: DecodeBudget,
|
budget: DecodeBudget,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Scope<'a> {
|
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 {
|
Scope {
|
||||||
rects,
|
rects,
|
||||||
mode,
|
mode,
|
||||||
@@ -127,7 +130,7 @@ impl<'a> Scope<'a> {
|
|||||||
fonts: HashMap::new(),
|
fonts: HashMap::new(),
|
||||||
used_names: Vec::new(),
|
used_names: Vec::new(),
|
||||||
depth: 0,
|
depth: 0,
|
||||||
budget: DecodeBudget::page(),
|
budget,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,6 +230,17 @@ pub(crate) fn marks_checked(doc: &Document) -> (Vec<Mark>, Vec<usize>) {
|
|||||||
/// A page's content streams: (the references or inline objects as listed, their decoded data),
|
/// A page's content streams: (the references or inline objects as listed, their decoded data),
|
||||||
/// decoded within one page's [`pdfcraft_cos::DecodeBudget`].
|
/// decoded within one page's [`pdfcraft_cos::DecodeBudget`].
|
||||||
pub(crate) fn page_streams(doc: &Document, page: &Dict, index: usize) -> Result<(Vec<Object>, Vec<Vec<u8>>), RedactError> {
|
pub(crate) fn page_streams(doc: &Document, page: &Dict, index: usize) -> Result<(Vec<Object>, Vec<Vec<u8>>), 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<Object>, Vec<Vec<u8>>), RedactError> {
|
||||||
let list: Vec<Object> = match page.get(b"Contents") {
|
let list: Vec<Object> = match page.get(b"Contents") {
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
Some(c) => match &*doc.resolve(c) {
|
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 data = Vec::new();
|
||||||
let mut budget = pdfcraft_cos::DecodeBudget::page();
|
|
||||||
for o in &list {
|
for o in &list {
|
||||||
match &*doc.resolve(o) {
|
match &*doc.resolve(o) {
|
||||||
// Strict: a tolerant decode keeps what precedes corruption and drops the rest, which
|
// 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<Report, Reda
|
|||||||
let rects: Vec<[f64; 4]> = page_marks.iter().flat_map(|m| m.rects.iter().copied()).collect();
|
let rects: Vec<[f64; 4]> = page_marks.iter().flat_map(|m| m.rects.iter().copied()).collect();
|
||||||
|
|
||||||
// 1. Content.
|
// 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 resources = page.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default();
|
||||||
let out = {
|
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)
|
process(doc, &mut scope, &data, &resources, pdfcraft_content::Matrix::IDENTITY)
|
||||||
};
|
};
|
||||||
// Fail closed: the interpreter lays vertical-writing text out horizontally, so it cannot
|
// 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<Report, Reda
|
|||||||
for &(pi, page_marks) in &groups {
|
for &(pi, page_marks) in &groups {
|
||||||
let page = pdfcraft_model::pages(doc).swap_remove(pi);
|
let page = pdfcraft_model::pages(doc).swap_remove(pi);
|
||||||
let rects: Vec<[f64; 4]> = page_marks.iter().flat_map(|m| m.rects.iter().copied()).collect();
|
let rects: Vec<[f64; 4]> = 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.
|
// The overlay stream draws text of its own: the overlay label is allowed.
|
||||||
let original: Vec<Vec<u8>> =
|
let original: Vec<Vec<u8>> =
|
||||||
list.iter().zip(data).filter(|(o, _)| !o.as_ref().is_some_and(|r| report.overlays.contains(&r))).map(|(_, d)| d).collect();
|
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 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 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;
|
residue += process(doc, &mut scope, &original, &resources, pdfcraft_content::Matrix::IDENTITY).residue;
|
||||||
}
|
}
|
||||||
if residue > 0 {
|
if residue > 0 {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use pdfcraft_content::Matrix;
|
|||||||
use pdfcraft_cos::{Dict, Document, ObjRef, Object};
|
use pdfcraft_cos::{Dict, Document, ObjRef, Object};
|
||||||
|
|
||||||
use crate::interp::{Mode, Scope, process};
|
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.
|
/// The categories of Acrobat's Remove Hidden Information panel that PdfCraft handles.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[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();
|
let n = pdfcraft_model::pages(doc).len();
|
||||||
for pi in 0..n {
|
for pi in 0..n {
|
||||||
let page = pdfcraft_model::pages(doc).swap_remove(pi);
|
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 {
|
if write {
|
||||||
return Err(RedactError::Unreadable(pi + 1));
|
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 resources = page.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default();
|
||||||
let mut report = Report::default();
|
let mut report = Report::default();
|
||||||
let mode = if write { Mode::Apply } else { Mode::Verify };
|
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 {
|
if text {
|
||||||
let c = page.crop(doc);
|
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])]);
|
scope.hidden_text = Some([c[0].min(c[2]), c[1].min(c[3]), c[0].max(c[2]), c[1].max(c[3])]);
|
||||||
|
|||||||
@@ -48,10 +48,11 @@ fn content(doc: &Document, page: usize) -> String {
|
|||||||
/// Glyphs (and inline images) of page `page` under `rects` (the verifier's count).
|
/// Glyphs (and inline images) of page `page` under `rects` (the verifier's count).
|
||||||
fn under(doc: &mut Document, page: usize, rects: &[[f64; 4]]) -> usize {
|
fn under(doc: &mut Document, page: usize, rects: &[[f64; 4]]) -> usize {
|
||||||
let p = pdfcraft_model::pages(doc).swap_remove(page);
|
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 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 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
|
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<Hidden, usize> = scan(&doc).into_iter().collect();
|
let counts: std::collections::HashMap<Hidden, usize> = scan(&doc).into_iter().collect();
|
||||||
assert_eq!(counts[&Hidden::Attachments], 1, "the walk went past its node budget to the last kid");
|
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");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user