mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 02:29:31 -06:00
Fuzzed inputs drove one check-one process to 2-6 GB (the 16 GB CI runner running two of them was killed every night). Three causes, each with a test: - lopdf (bootstrap inspector) sized PNG predictor rows from /Columns before reading data: /Columns 4294967295 allocated two 4 GiB rows. Vendored lopdf 0.45.0 refuses rows longer than the data (and uses saturating row widths); its load-time stream decoding also gets a 256 MiB limit (it had none). - hayro sized a tiling pattern's cell pixmap from /XStep x scale, bounded only by u16: a step far beyond /BBox allocated 2 GB. The vendored patch keeps the cell within 3000 px a side, lowering the resolution instead. - cos decoded cross-reference streams up to the general 1 GiB limit; they now stop at what 8,388,607 objects' rows can hold (~200 MB at most), and the section is reconstructed (logged) instead. On the nightly's seed (41) the worst captured input fell from 6.2 GB to 6 MB, and a 300 s fuzz run had no process above 300 MB (before: many at 2-5 GB). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
3.6 KiB
Rust
101 lines
3.6 KiB
Rust
use lopdf::{Document, Object, Result, dictionary};
|
|
|
|
mod utils;
|
|
|
|
/// One page whose `/Annots` mixes the two legal entry forms: a reference to
|
|
/// an annotation dictionary, and the dictionary written directly into the
|
|
/// array. Returns the document and the page's id.
|
|
fn page_with_mixed_annots(annots_indirect: bool) -> (Document, lopdf::ObjectId) {
|
|
let mut doc = Document::with_version("1.7");
|
|
let referenced = doc.add_object(dictionary! {
|
|
"Type" => "Annot",
|
|
"Subtype" => "Text",
|
|
"Contents" => Object::string_literal("referenced"),
|
|
});
|
|
let entries = vec![
|
|
Object::Reference(referenced),
|
|
Object::Dictionary(dictionary! {
|
|
"Type" => "Annot",
|
|
"Subtype" => "Text",
|
|
"Contents" => Object::string_literal("direct"),
|
|
}),
|
|
];
|
|
// `/Annots` is itself allowed to be either an array or a reference to one.
|
|
let annots = if annots_indirect {
|
|
Object::Reference(doc.add_object(Object::Array(entries)))
|
|
} else {
|
|
Object::Array(entries)
|
|
};
|
|
let page_id = doc.add_object(dictionary! {
|
|
"Type" => "Page",
|
|
"Annots" => annots,
|
|
});
|
|
(doc, page_id)
|
|
}
|
|
|
|
fn contents_of(doc: &Document, page_id: lopdf::ObjectId) -> Vec<String> {
|
|
doc.get_page_annotations(page_id)
|
|
.unwrap()
|
|
.iter()
|
|
.map(|a| String::from_utf8_lossy(a.get(b"Contents").unwrap().as_str().unwrap()).into_owned())
|
|
.collect()
|
|
}
|
|
|
|
/// An entry of `/Annots` may be the annotation dictionary itself rather than
|
|
/// a reference to one — ISO 32000-1, 12.5.2 requires an indirect object only
|
|
/// for an annotation that carries a `/Popup` or is an `/IRT` target. Direct
|
|
/// dictionaries used to be dropped without a word, so a page could report
|
|
/// fewer annotations than it has.
|
|
#[test]
|
|
fn page_annotations_include_direct_dictionaries() {
|
|
let (doc, page_id) = page_with_mixed_annots(false);
|
|
assert_eq!(contents_of(&doc, page_id), vec!["referenced", "direct"]);
|
|
}
|
|
|
|
/// The same, with `/Annots` written as a reference to the array.
|
|
#[test]
|
|
fn page_annotations_include_direct_dictionaries_behind_an_indirect_annots() {
|
|
let (doc, page_id) = page_with_mixed_annots(true);
|
|
assert_eq!(contents_of(&doc, page_id), vec!["referenced", "direct"]);
|
|
}
|
|
|
|
/// A reference that does not resolve costs its own annotation, not the page:
|
|
/// the surviving entries still come back.
|
|
#[test]
|
|
fn page_annotations_skip_a_dangling_reference() {
|
|
let mut doc = Document::with_version("1.7");
|
|
let good = doc.add_object(dictionary! {
|
|
"Type" => "Annot",
|
|
"Subtype" => "Text",
|
|
"Contents" => Object::string_literal("kept"),
|
|
});
|
|
let page_id = doc.add_object(dictionary! {
|
|
"Type" => "Page",
|
|
"Annots" => vec![
|
|
Object::Reference((9999, 0)),
|
|
Object::Reference(good),
|
|
Object::Null,
|
|
],
|
|
});
|
|
assert_eq!(contents_of(&doc, page_id), vec!["kept"]);
|
|
}
|
|
|
|
/// A page without `/Annots` has no annotations, and that is not an error.
|
|
#[test]
|
|
fn page_annotations_without_annots_is_empty() {
|
|
let mut doc = Document::with_version("1.7");
|
|
let page_id = doc.add_object(dictionary! { "Type" => "Page" });
|
|
assert!(doc.get_page_annotations(page_id).unwrap().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn annotation_count() -> Result<()> {
|
|
// This test file from the pdfcpu repository,
|
|
// https://github.com/pdfcpu/pdfcpu/blob/master/pkg/samples/basic/AnnotationDemo.pdf
|
|
let doc = utils::load_document("assets/AnnotationDemo.pdf")?;
|
|
assert_eq!(doc.version, "1.7".to_string());
|
|
assert_eq!(doc.page_iter().count(), 1);
|
|
assert_eq!(doc.get_page_annotations(doc.page_iter().next().unwrap())?.len(), 33);
|
|
Ok(())
|
|
}
|