mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 05:28:09 -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>
187 lines
5.8 KiB
Rust
187 lines
5.8 KiB
Rust
use lopdf::{Document, Object, ObjectStream, dictionary};
|
|
|
|
#[test]
|
|
fn test_empty_trailer() {
|
|
let doc = Document::with_version("1.5");
|
|
let obj_id = (1, 0);
|
|
let obj = Object::Dictionary(dictionary! { "Test" => "Value" });
|
|
|
|
// With empty trailer, all objects should be compressible
|
|
assert!(
|
|
ObjectStream::can_be_compressed(obj_id, &obj, &doc),
|
|
"Object should be compressible with empty trailer"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_malformed_encrypt_reference() {
|
|
let mut doc = Document::with_version("1.5");
|
|
|
|
let obj_id = doc.add_object(dictionary! {
|
|
"Type" => "Catalog"
|
|
});
|
|
|
|
// Set Encrypt to a malformed reference (wrong generation)
|
|
doc.trailer.set("Encrypt", Object::Reference((999, 99)));
|
|
|
|
// Object should still be compressible (not the encryption dict)
|
|
assert!(
|
|
ObjectStream::can_be_compressed(obj_id, doc.objects.get(&obj_id).unwrap(), &doc),
|
|
"Object should be compressible even with malformed Encrypt reference"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_self_referencing_object() {
|
|
let mut doc = Document::with_version("1.5");
|
|
|
|
let obj_id = (5, 0);
|
|
doc.objects.insert(
|
|
obj_id,
|
|
Object::Dictionary(dictionary! {
|
|
"Type" => "Test",
|
|
"Self" => Object::Reference(obj_id) // Self reference
|
|
}),
|
|
);
|
|
|
|
doc.trailer.set("Test", obj_id);
|
|
|
|
// Should be compressible (not encryption dict)
|
|
assert!(
|
|
ObjectStream::can_be_compressed(obj_id, doc.objects.get(&obj_id).unwrap(), &doc),
|
|
"Self-referencing object should be compressible"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_circular_references() {
|
|
let mut doc = Document::with_version("1.5");
|
|
|
|
let obj1_id = (1, 0);
|
|
let obj2_id = (2, 0);
|
|
|
|
doc.objects.insert(
|
|
obj1_id,
|
|
Object::Dictionary(dictionary! {
|
|
"Next" => Object::Reference(obj2_id)
|
|
}),
|
|
);
|
|
|
|
doc.objects.insert(
|
|
obj2_id,
|
|
Object::Dictionary(dictionary! {
|
|
"Next" => Object::Reference(obj1_id) // Circular reference
|
|
}),
|
|
);
|
|
|
|
doc.trailer.set("Start", obj1_id);
|
|
|
|
// Both should be compressible
|
|
assert!(
|
|
ObjectStream::can_be_compressed(obj1_id, doc.objects.get(&obj1_id).unwrap(), &doc),
|
|
"First object in circular reference should be compressible"
|
|
);
|
|
assert!(
|
|
ObjectStream::can_be_compressed(obj2_id, doc.objects.get(&obj2_id).unwrap(), &doc),
|
|
"Second object in circular reference should be compressible"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_trailer_with_all_pdf_types() {
|
|
let mut doc = Document::with_version("1.5");
|
|
|
|
let obj_id = doc.add_object(dictionary! { "Type" => "Test" });
|
|
|
|
// Add all possible PDF types to trailer
|
|
doc.trailer.set("Root", obj_id);
|
|
doc.trailer.set("Null", Object::Null);
|
|
doc.trailer.set("Bool", Object::Boolean(true));
|
|
doc.trailer.set("Int", Object::Integer(42));
|
|
doc.trailer.set("Real", Object::Real(1.25));
|
|
doc.trailer
|
|
.set("String", Object::String(b"test".to_vec(), lopdf::StringFormat::Literal));
|
|
doc.trailer.set("Name", Object::Name(b"Test".to_vec()));
|
|
doc.trailer
|
|
.set("Array", Object::Array(vec![Object::Integer(1), Object::Integer(2)]));
|
|
doc.trailer
|
|
.set("Dict", Object::Dictionary(dictionary! { "Key" => "Value" }));
|
|
|
|
// Object should still be compressible
|
|
assert!(
|
|
ObjectStream::can_be_compressed(obj_id, doc.objects.get(&obj_id).unwrap(), &doc),
|
|
"Object should be compressible with diverse trailer entries"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_unicode_trailer_keys() {
|
|
let mut doc = Document::with_version("1.5");
|
|
|
|
let obj_id = doc.add_object(dictionary! { "Test" => "Value" });
|
|
|
|
// Use Unicode/non-ASCII keys in trailer
|
|
doc.trailer.set("Root", obj_id);
|
|
doc.trailer.set(
|
|
"Ünïcödé",
|
|
Object::String(b"test".to_vec(), lopdf::StringFormat::Literal),
|
|
);
|
|
doc.trailer.set("日本語", Object::Integer(42));
|
|
doc.trailer.set("🎯", Object::Boolean(true));
|
|
|
|
// Should still work correctly
|
|
assert!(
|
|
ObjectStream::can_be_compressed(obj_id, doc.objects.get(&obj_id).unwrap(), &doc),
|
|
"Object should be compressible with Unicode trailer keys"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_very_large_trailer() {
|
|
let mut doc = Document::with_version("1.5");
|
|
|
|
let obj_id = doc.add_object(dictionary! { "Type" => "Catalog" });
|
|
|
|
// Add many entries to trailer
|
|
doc.trailer.set("Root", obj_id);
|
|
for i in 0..1000 {
|
|
doc.trailer
|
|
.set(format!("Custom{:04}", i).as_bytes(), Object::Integer(i));
|
|
}
|
|
|
|
// Should still be efficient
|
|
let start = std::time::Instant::now();
|
|
let compressible = ObjectStream::can_be_compressed(obj_id, doc.objects.get(&obj_id).unwrap(), &doc);
|
|
let duration = start.elapsed();
|
|
|
|
assert!(compressible, "Object should be compressible even with large trailer");
|
|
assert!(
|
|
duration.as_micros() < 100,
|
|
"Check should be fast even with large trailer"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_modification_safety() {
|
|
// This test verifies the function doesn't panic with concurrent-like access patterns
|
|
let mut doc = Document::with_version("1.5");
|
|
|
|
let obj1_id = doc.add_object(dictionary! { "Type" => "Test1" });
|
|
let obj2_id = doc.add_object(dictionary! { "Type" => "Test2" });
|
|
|
|
doc.trailer.set("Ref1", obj1_id);
|
|
|
|
// Check first object
|
|
let result1 = ObjectStream::can_be_compressed(obj1_id, doc.objects.get(&obj1_id).unwrap(), &doc);
|
|
|
|
// Modify trailer between checks
|
|
doc.trailer.set("Ref2", obj2_id);
|
|
doc.trailer.set("Encrypt", obj1_id); // Now obj1 is encryption dict
|
|
|
|
// Check again - result should change
|
|
let result2 = ObjectStream::can_be_compressed(obj1_id, doc.objects.get(&obj1_id).unwrap(), &doc);
|
|
|
|
assert!(result1, "Initially should be compressible");
|
|
assert!(!result2, "Should not be compressible after becoming encryption dict");
|
|
}
|