mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 18:47:22 -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>
179 lines
5.7 KiB
Rust
179 lines
5.7 KiB
Rust
use lopdf::{Document, Object, ObjectId};
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
#[cfg(feature = "async")]
|
|
use tokio::runtime::Builder;
|
|
|
|
#[cfg(not(feature = "async"))]
|
|
fn load_document(path: &str) -> Result<Document, Box<dyn std::error::Error>> {
|
|
Ok(Document::load(path)?)
|
|
}
|
|
|
|
#[cfg(feature = "async")]
|
|
fn load_document(path: &str) -> Result<Document, Box<dyn std::error::Error>> {
|
|
Ok(Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async move { Document::load(path).await })?)
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let pdf_path = "/Users/nicolasdao/Downloads/pdfs/RFQ - SDS WebApp.docx.pdf";
|
|
println!("Analyzing references in: {}", pdf_path);
|
|
|
|
let doc = load_document(pdf_path)?;
|
|
|
|
// Build reference graph
|
|
let mut references: HashMap<ObjectId, HashSet<ObjectId>> = HashMap::new();
|
|
let mut referenced_by: HashMap<ObjectId, HashSet<ObjectId>> = HashMap::new();
|
|
|
|
// Collect all references
|
|
for (&id, obj) in &doc.objects {
|
|
let refs = collect_references_from_object(obj);
|
|
references.insert(id, refs.clone());
|
|
|
|
// Build reverse mapping
|
|
for ref_id in refs {
|
|
referenced_by.entry(ref_id).or_default().insert(id);
|
|
}
|
|
}
|
|
|
|
// Also check trailer references
|
|
let trailer_refs = collect_references_from_dict(&doc.trailer);
|
|
for ref_id in &trailer_refs {
|
|
referenced_by.entry(*ref_id).or_default().insert((0, 0));
|
|
}
|
|
|
|
// Analyze specific problematic objects
|
|
println!("\nAnalyzing problematic objects:");
|
|
|
|
// Object 427 (referenced by catalog 431)
|
|
if let Some(refs_to_427) = referenced_by.get(&(427, 0)) {
|
|
println!("\nObject 427 0 R is referenced by:");
|
|
for &ref_id in refs_to_427 {
|
|
println!(" {} {} R", ref_id.0, ref_id.1);
|
|
}
|
|
}
|
|
|
|
// Check what object 427 is
|
|
if let Ok(obj_427) = doc.get_object((427, 0)) {
|
|
println!("Object 427 is: {:?}", describe_object(obj_427));
|
|
}
|
|
|
|
// Check fonts that reference orphaned objects
|
|
println!("\nChecking font references:");
|
|
for &font_id in &[(8, 0), (9, 0), (10, 0), (11, 0), (12, 0)] {
|
|
if let Ok(Object::Dictionary(font_dict)) = doc.get_object(font_id) {
|
|
println!("\nFont {} {} R:", font_id.0, font_id.1);
|
|
for (key, value) in font_dict.iter() {
|
|
if let Object::Reference(ref_id) = value {
|
|
let exists = doc.objects.contains_key(ref_id);
|
|
println!(
|
|
" {} -> {} {} R (exists: {})",
|
|
String::from_utf8_lossy(key),
|
|
ref_id.0,
|
|
ref_id.1,
|
|
exists
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Analyze which objects would be compressed
|
|
println!("\n\nAnalyzing compression eligibility:");
|
|
let mut would_compress = Vec::new();
|
|
let mut would_not_compress = Vec::new();
|
|
|
|
for (&id, obj) in &doc.objects {
|
|
if lopdf::ObjectStream::can_be_compressed(id, obj, &doc) {
|
|
would_compress.push(id);
|
|
} else {
|
|
would_not_compress.push(id);
|
|
}
|
|
}
|
|
|
|
// Check if any compressed objects are referenced by non-compressed objects
|
|
println!("\nChecking for problematic compressions:");
|
|
let mut problems = Vec::new();
|
|
|
|
for &compressed_id in &would_compress {
|
|
if let Some(referencers) = referenced_by.get(&compressed_id) {
|
|
for &referencer_id in referencers {
|
|
if referencer_id == (0, 0) {
|
|
// Referenced from trailer
|
|
problems.push((compressed_id, referencer_id, "trailer"));
|
|
} else if would_not_compress.contains(&referencer_id) {
|
|
// Referenced by a non-compressed object
|
|
problems.push((compressed_id, referencer_id, "non-compressed object"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !problems.is_empty() {
|
|
println!("\nFOUND {} PROBLEMATIC COMPRESSIONS:", problems.len());
|
|
for (compressed_id, referencer_id, reason) in problems.iter().take(20) {
|
|
println!(
|
|
" {} {} R would be compressed but is referenced by {} {} R ({})",
|
|
compressed_id.0, compressed_id.1, referencer_id.0, referencer_id.1, reason
|
|
);
|
|
}
|
|
if problems.len() > 20 {
|
|
println!(" ... and {} more", problems.len() - 20);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn collect_references_from_object(obj: &Object) -> HashSet<ObjectId> {
|
|
let mut refs = HashSet::new();
|
|
|
|
match obj {
|
|
Object::Reference(id) => {
|
|
refs.insert(*id);
|
|
}
|
|
Object::Array(array) => {
|
|
for item in array {
|
|
refs.extend(collect_references_from_object(item));
|
|
}
|
|
}
|
|
Object::Dictionary(dict) => {
|
|
refs.extend(collect_references_from_dict(dict));
|
|
}
|
|
Object::Stream(stream) => {
|
|
refs.extend(collect_references_from_dict(&stream.dict));
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
refs
|
|
}
|
|
|
|
fn collect_references_from_dict(dict: &lopdf::Dictionary) -> HashSet<ObjectId> {
|
|
let mut refs = HashSet::new();
|
|
|
|
for (_key, value) in dict.iter() {
|
|
refs.extend(collect_references_from_object(value));
|
|
}
|
|
|
|
refs
|
|
}
|
|
|
|
fn describe_object(obj: &Object) -> String {
|
|
match obj {
|
|
Object::Dictionary(d) => {
|
|
let type_info = d
|
|
.get(b"Type")
|
|
.ok()
|
|
.and_then(|t| t.as_name().ok())
|
|
.map(|n| String::from_utf8_lossy(n).to_string())
|
|
.unwrap_or_else(|| "Unknown".to_string());
|
|
format!("Dictionary (Type: {})", type_info)
|
|
}
|
|
_ => format!("{:?}", obj),
|
|
}
|
|
}
|