mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(audit): page content and forms share one decode budget per page
The cover walk decoded every /Contents entry and every form draw with a fresh 1 GiB allowance; the operator bound does not count whitespace, so a large blank form drawn many times meant hours of decoding. The walk charges a per-page DecodeBudget with the strict decoder, and spending it fails the audit.
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
//! Positions compare lexicographically in paint order.
|
||||
|
||||
use pdfcraft_content::{Matrix, Op, Pieces, parse};
|
||||
use pdfcraft_cos::{Dict, Document, Object};
|
||||
use pdfcraft_cos::{DecodeBudget, Dict, Document, Object};
|
||||
|
||||
use crate::AuditError;
|
||||
|
||||
@@ -109,6 +109,8 @@ struct Walker<'a> {
|
||||
pieces: &'a Pieces,
|
||||
out: Vec<FilledRect>,
|
||||
ops_seen: usize,
|
||||
/// Decoded bytes left for the page, its forms included: spending it fails the audit.
|
||||
budget: DecodeBudget,
|
||||
}
|
||||
|
||||
impl<'a> Walker<'a> {
|
||||
@@ -272,7 +274,7 @@ impl<'a> Walker<'a> {
|
||||
.unwrap_or(Matrix::IDENTITY);
|
||||
let sub_resources = dict.get(b"Resources").map(|o| self.doc.resolve(o)).and_then(|o| o.as_dict().cloned()).unwrap_or_default();
|
||||
// A form stream that cannot be decoded fails the audit: it might hide a cover.
|
||||
let data = stream.decoded_strict()?;
|
||||
let data = self.budget.decode_strict(stream)?;
|
||||
let ops = parse(&data).ops;
|
||||
// The XObject matrix maps form space into the user space active at `Do`.
|
||||
let mut sub_gs = gs.clone();
|
||||
@@ -304,7 +306,8 @@ fn ext_alpha(doc: &Document, resources: &Dict, name: &[u8]) -> f64 {
|
||||
/// Every opaque filled rectangle drawn by a page's content (and its form XObjects), with paint
|
||||
/// positions. A page's content streams are walked once, joined, with one graphics state, so
|
||||
/// state set in an earlier stream is still in force in the later ones. Any content stream that
|
||||
/// cannot be decoded is an audit failure, never an empty result.
|
||||
/// cannot be decoded, or content and forms that decode past one page's [`DecodeBudget`] in all,
|
||||
/// is an audit failure, never an empty result.
|
||||
pub fn filled_rects(doc: &Document, page: &Dict) -> Result<Vec<FilledRect>, AuditError> {
|
||||
let resources = page.get(b"Resources").map(|o| doc.resolve(o)).and_then(|o| o.as_dict().cloned()).unwrap_or_default();
|
||||
let contents: Vec<Object> = match page.get(b"Contents") {
|
||||
@@ -314,15 +317,16 @@ pub fn filled_rects(doc: &Document, page: &Dict) -> Result<Vec<FilledRect>, Audi
|
||||
_ => vec![c.clone()],
|
||||
},
|
||||
};
|
||||
let mut budget = DecodeBudget::page();
|
||||
let mut streams = Vec::with_capacity(contents.len());
|
||||
for c in &contents {
|
||||
if let Object::Stream(s) = &*doc.resolve(c) {
|
||||
streams.push(s.decoded_strict()?);
|
||||
streams.push(budget.decode_strict(s)?);
|
||||
}
|
||||
}
|
||||
let pieces = Pieces::join(&streams);
|
||||
let parsed = pieces.parse();
|
||||
let mut w = Walker { doc, pieces: &pieces, out: Vec::new(), ops_seen: 0 };
|
||||
let mut w = Walker { doc, pieces: &pieces, out: Vec::new(), ops_seen: 0, budget };
|
||||
let mut gs = Gs::default();
|
||||
let mut stack = Vec::new();
|
||||
w.walk_ops(&parsed.ops, &mut gs, &mut stack, &resources, 0, None)?;
|
||||
|
||||
@@ -592,3 +592,22 @@ fn reading_blocks_respect_stream_comments_and_split_operands() {
|
||||
assert_eq!(text, "Replacement text", "{}", String::from_utf8_lossy(s1));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_stream_references_share_one_decode_budget() {
|
||||
use pdfcraft_cos::{Dict, Document, Object, Stream};
|
||||
// One 1 MiB stream of spaces (a kilobyte on disk), named 300 times: by the page's /Contents,
|
||||
// and by 300 draws of a form holding it.
|
||||
let mut doc = Document::new_empty();
|
||||
let spaces = doc.add(Stream::flate(Dict::new(), &vec![b' '; 1 << 20]));
|
||||
let contents: Dict = [(b"Contents".to_vec(), Object::Array(vec![Object::Ref(spaces); 300]))].into_iter().collect();
|
||||
assert!(crate::cover::filled_rects(&doc, &contents).is_err(), "repeated content streams");
|
||||
|
||||
let form_dict: Dict = [(b"Subtype".to_vec(), Object::Name(b"Form".to_vec()))].into_iter().collect();
|
||||
let form = doc.add(Stream::flate(form_dict, &vec![b' '; 1 << 20]));
|
||||
let draws = doc.add(Stream::from_raw(Dict::new(), "/X Do ".repeat(300).into_bytes()));
|
||||
let xobjects: Dict = [(b"X".to_vec(), Object::Ref(form))].into_iter().collect();
|
||||
let resources: Dict = [(b"XObject".to_vec(), Object::Dict(xobjects))].into_iter().collect();
|
||||
let page: Dict = [(b"Contents".to_vec(), Object::Ref(draws)), (b"Resources".to_vec(), Object::Dict(resources))].into_iter().collect();
|
||||
assert!(crate::cover::filled_rects(&doc, &page).is_err(), "repeated form draws");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user