mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 11:37:59 -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>
167 lines
4.9 KiB
Rust
167 lines
4.9 KiB
Rust
use lopdf::{Document, Object, ObjectStream, SaveOptions, dictionary};
|
|
use std::time::Instant;
|
|
|
|
#[test]
|
|
fn test_can_be_compressed_performance() {
|
|
// Create a document with many objects
|
|
let mut doc = Document::with_version("1.5");
|
|
let mut object_ids = Vec::new();
|
|
|
|
// Create 1000 objects
|
|
for i in 0..1000 {
|
|
let obj_id = doc.add_object(dictionary! {
|
|
"Type" => format!("TestObject{}", i),
|
|
"Index" => i as i64,
|
|
"Data" => format!("This is test object number {}", i)
|
|
});
|
|
object_ids.push(obj_id);
|
|
}
|
|
|
|
// Add some to trailer
|
|
doc.trailer.set("Root", object_ids[0]);
|
|
doc.trailer.set("Info", object_ids[1]);
|
|
doc.trailer.set("Custom1", object_ids[2]);
|
|
doc.trailer.set("Custom2", object_ids[3]);
|
|
|
|
// Measure performance of can_be_compressed checks
|
|
let start = Instant::now();
|
|
let mut compressible_count = 0;
|
|
|
|
for &id in &object_ids {
|
|
if let Some(obj) = doc.objects.get(&id)
|
|
&& ObjectStream::can_be_compressed(id, obj, &doc)
|
|
{
|
|
compressible_count += 1;
|
|
}
|
|
}
|
|
|
|
let duration = start.elapsed();
|
|
|
|
println!("Checked {} objects in {:?}", object_ids.len(), duration);
|
|
println!("Compressible objects: {}", compressible_count);
|
|
|
|
// All objects should be compressible (none are encryption dicts)
|
|
assert_eq!(
|
|
compressible_count,
|
|
object_ids.len(),
|
|
"All non-encryption objects should be compressible"
|
|
);
|
|
|
|
// Performance check: should complete in reasonable time
|
|
assert!(
|
|
duration.as_millis() < 100,
|
|
"Performance check took too long: {:?}",
|
|
duration
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_save_performance_with_trailer_objects() {
|
|
let mut doc = Document::with_version("1.5");
|
|
let pages_id = doc.new_object_id();
|
|
|
|
// Create many pages
|
|
let mut page_ids = Vec::new();
|
|
for i in 0..100 {
|
|
let page_id = doc.add_object(dictionary! {
|
|
"Type" => "Page",
|
|
"Parent" => pages_id,
|
|
"MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
|
|
"Contents" => Object::Reference((1000 + i, 0))
|
|
});
|
|
page_ids.push(page_id);
|
|
}
|
|
|
|
doc.objects.insert(
|
|
pages_id,
|
|
Object::Dictionary(dictionary! {
|
|
"Type" => "Pages",
|
|
"Kids" => page_ids.iter().map(|&id| Object::Reference(id)).collect::<Vec<_>>(),
|
|
"Count" => page_ids.len() as i64
|
|
}),
|
|
);
|
|
|
|
let catalog_id = doc.add_object(dictionary! {
|
|
"Type" => "Catalog",
|
|
"Pages" => pages_id
|
|
});
|
|
|
|
let info_id = doc.add_object(dictionary! {
|
|
"Title" => "Performance Test PDF",
|
|
"PageCount" => page_ids.len() as i64
|
|
});
|
|
|
|
// Add many custom entries to trailer
|
|
doc.trailer.set("Root", catalog_id);
|
|
doc.trailer.set("Info", info_id);
|
|
for i in 0..10 {
|
|
doc.trailer.set(format!("Custom{}", i).as_bytes(), Object::Integer(i));
|
|
}
|
|
|
|
// Measure save performance
|
|
let options = SaveOptions::builder().use_object_streams(true).build();
|
|
|
|
let start = Instant::now();
|
|
let mut output = Vec::new();
|
|
doc.save_with_options(&mut output, options).unwrap();
|
|
let save_duration = start.elapsed();
|
|
|
|
println!("Saved {} page PDF in {:?}", page_ids.len(), save_duration);
|
|
println!("Output size: {} bytes", output.len());
|
|
|
|
// Should complete quickly even with many objects
|
|
assert!(
|
|
save_duration.as_millis() < 500,
|
|
"Save took too long: {:?}",
|
|
save_duration
|
|
);
|
|
|
|
// Verify compression worked
|
|
let content = String::from_utf8_lossy(&output);
|
|
assert!(content.contains("/ObjStm"), "Object streams should be created");
|
|
}
|
|
|
|
#[test]
|
|
fn test_encryption_check_performance() {
|
|
// Test the specific encryption dictionary check performance
|
|
let mut doc = Document::with_version("1.5");
|
|
|
|
// Create an encryption dictionary
|
|
let encrypt_id = doc.add_object(dictionary! {
|
|
"Filter" => "Standard",
|
|
"V" => 2
|
|
});
|
|
|
|
// Set it in trailer
|
|
doc.trailer.set("Encrypt", encrypt_id);
|
|
|
|
// Create many other objects
|
|
let mut other_ids = Vec::new();
|
|
for i in 0..1000 {
|
|
let id = doc.add_object(Object::Integer(i));
|
|
other_ids.push(id);
|
|
}
|
|
|
|
// Time the encryption check
|
|
let start = Instant::now();
|
|
|
|
// Check encryption dictionary
|
|
let encrypt_obj = doc.objects.get(&encrypt_id).unwrap();
|
|
let encrypt_compressible = ObjectStream::can_be_compressed(encrypt_id, encrypt_obj, &doc);
|
|
|
|
// Check many non-encryption objects
|
|
for &id in &other_ids[..100] {
|
|
// Check first 100
|
|
if let Some(obj) = doc.objects.get(&id) {
|
|
let _ = ObjectStream::can_be_compressed(id, obj, &doc);
|
|
}
|
|
}
|
|
|
|
let duration = start.elapsed();
|
|
|
|
println!("Encryption check performance: {:?} for 101 objects", duration);
|
|
|
|
assert!(!encrypt_compressible, "Encryption dict should not be compressible");
|
|
assert!(duration.as_micros() < 1000, "Check should be very fast");
|
|
}
|