mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(redact): form entries and page content share one budget per page
The interpreter entered a form again at every Do with only a nesting limit, so a few shared forms drawing each other produced exponential work in apply, sanitize and verify. Scope carries a per-page DecodeBudget for form entries and form bytes; past it, apply refuses the page and verification counts the form as possible residue. Page content streams are decoded against a page budget too.
This commit is contained in:
@@ -21,7 +21,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
|
||||
use pdfcraft_content::{Matrix, Op, Pieces, contains, num, overlaps, serialize_ops, string};
|
||||
use pdfcraft_cos::{CosError, Dict, Document, ObjRef, Object, Stream};
|
||||
use pdfcraft_cos::{CosError, DecodeBudget, Dict, Document, MAX_PAGE_FORMS, ObjRef, Object, Stream};
|
||||
|
||||
use crate::{RedactError, Report, image, page_streams, tags};
|
||||
use pdfcraft_fonts::pdf::Metrics;
|
||||
@@ -102,6 +102,9 @@ pub(crate) struct Scope<'a> {
|
||||
fonts: HashMap<Vec<u8>, Rc<Metrics>>,
|
||||
used_names: Vec<Vec<u8>>,
|
||||
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.
|
||||
budget: DecodeBudget,
|
||||
}
|
||||
|
||||
impl<'a> Scope<'a> {
|
||||
@@ -120,6 +123,7 @@ impl<'a> Scope<'a> {
|
||||
fonts: HashMap::new(),
|
||||
used_names: Vec::new(),
|
||||
depth: 0,
|
||||
budget: DecodeBudget::page(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1201,8 +1205,25 @@ fn xobject(
|
||||
out.doomed.push(r);
|
||||
return Some(None);
|
||||
}
|
||||
let data = match s.decoded() {
|
||||
let data = if scope.budget.enter_form() {
|
||||
scope.budget.decode(s)
|
||||
} else {
|
||||
Err(CosError::Filter(format!("the page draws more than {MAX_PAGE_FORMS} form XObjects")))
|
||||
};
|
||||
let data = match data {
|
||||
Ok(d) => d,
|
||||
// Past the page's budget nothing more is read: apply refuses the page, and
|
||||
// verification counts the form as content that may remain.
|
||||
Err(e) if scope.budget.exhausted() => {
|
||||
if scope.mode == Mode::Apply {
|
||||
out.error = Some(e);
|
||||
} else if scope.everywhere() {
|
||||
scope.layer_blocks += 1;
|
||||
} else {
|
||||
out.residue += 1;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
Err(_) if scope.mode == Mode::Apply => {
|
||||
scope.report.forms_removed += 1;
|
||||
out.replaced.push(name);
|
||||
|
||||
@@ -219,7 +219,8 @@ pub(crate) fn marks_checked(doc: &Document) -> (Vec<Mark>, Vec<usize>) {
|
||||
(out, unreadable)
|
||||
}
|
||||
|
||||
/// 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`].
|
||||
pub(crate) fn page_streams(doc: &Document, page: &Dict, index: usize) -> Result<(Vec<Object>, Vec<Vec<u8>>), RedactError> {
|
||||
let list: Vec<Object> = match page.get(b"Contents") {
|
||||
None => Vec::new(),
|
||||
@@ -229,9 +230,10 @@ 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) {
|
||||
Object::Stream(s) => data.push(s.decoded().map_err(|_| RedactError::Unreadable(index + 1))?),
|
||||
Object::Stream(s) => data.push(budget.decode(s).map_err(|_| RedactError::Unreadable(index + 1))?),
|
||||
Object::Null => data.push(Vec::new()),
|
||||
_ => return Err(RedactError::Unreadable(index + 1)),
|
||||
}
|
||||
|
||||
@@ -2472,3 +2472,20 @@ fn a_tj_surplus_arrays_undrawn_glyphs_are_not_seen_under_a_mark() {
|
||||
assert_eq!(under(&mut doc, 0, &[[45.0, 95.0, 50.0, 110.0]]), 0, "nothing is drawn past LASTARR's 35pt width");
|
||||
assert_eq!(under(&mut doc, 0, &[[10.0, 95.0, 45.0, 110.0]]), 7, "LASTARR's 7 glyphs, the one the page draws");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_entries_share_one_budget_per_page() {
|
||||
// 20 draws of a form that draws the next 20 times, four levels down: 160,000 glyphs from a
|
||||
// few hundred bytes.
|
||||
let draws = "/X Do ".repeat(20);
|
||||
let mut objs = vec![stream("/Type /XObject /Subtype /Form /Resources << /Font << /F1 5 0 R >> >>", b"BT /F1 10 Tf 10 100 Td (A) Tj ET")];
|
||||
for inner in 7..10 {
|
||||
objs.push(stream(&format!("/Type /XObject /Subtype /Form /Resources << /XObject << /X {inner} 0 R >> >>"), draws.as_bytes()));
|
||||
}
|
||||
let mut doc = one_page(draws.as_bytes(), "/XObject << /X 10 0 R >>", objs);
|
||||
let residue = under(&mut doc, 0, &[[0.0, 90.0, 50.0, 120.0]]);
|
||||
assert!(residue > 0 && residue <= pdfcraft_cos::MAX_PAGE_FORMS, "{residue} glyphs counted");
|
||||
// Applying refuses the page rather than leave forms past the budget unread.
|
||||
mark(&mut doc, 0, &[[0.0, 90.0, 50.0, 120.0]], "");
|
||||
assert!(apply(&mut doc, None).is_err());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user