Files
pdfcraft/vendor/lopdf/src/incremental_document.rs
T
Brandon ThomasandClaude Opus 5.5 1449b71a19 Never crash: bound the allocations behind the nightly fuzz runner's out-of-memory deaths
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>
2026-10-04 17:59:25 -04:00

133 lines
4.7 KiB
Rust

use crate::{Dictionary, Document, Object, ObjectId, Result};
#[derive(Debug, Clone)]
pub struct IncrementalDocument {
/// The raw data for the files read from input.
bytes_documents: Vec<u8>,
/// The combined result of `bytes_documents`.
/// Do not edit this document as it will not be saved.
prev_documents: Document,
/// A new document appended to the previously loaded file.
pub new_document: Document,
}
impl IncrementalDocument {
/// Create new PDF document.
pub fn new() -> Self {
Self {
bytes_documents: Vec::new(),
prev_documents: Document::new(),
new_document: Document::new(),
}
}
/// Create new `IncrementalDocument` from the bytes and document.
///
/// The function expects the bytes and previous document to match.
/// If they do not match exactly this might result in broken PDFs.
pub fn create_from(prev_bytes: Vec<u8>, prev_documents: Document) -> Self {
Self {
bytes_documents: prev_bytes,
new_document: Document::new_from_prev(&prev_documents),
prev_documents,
}
}
/// Get the structure of the previous documents (all prev incremental updates combined.)
pub fn get_prev_documents(&self) -> &Document {
&self.prev_documents
}
/// Get the bytes of the previous documents.
pub fn get_prev_documents_bytes(&self) -> &[u8] {
&self.bytes_documents
}
/// Clone Object from previous document to new document.
/// If the object already exists nothing is done.
///
/// This function can be used to clone an object so it can be changed in the incremental updates.
pub fn opt_clone_object_to_new_document(&mut self, object_id: ObjectId) -> Result<()> {
if !self.new_document.has_object(object_id) {
let old_object = self.prev_documents.get_object(object_id)?;
self.new_document.set_object(object_id, old_object.clone());
}
Ok(())
}
/// Get the page's resource dictionary (only in new document).
///
/// Get Object that has the key `Resources`.
pub fn get_or_create_resources(&mut self, page_id: ObjectId) -> Result<&mut Object> {
self.opt_clone_object_to_new_document(page_id)?;
let resources_id = {
let page = self.new_document.get_object(page_id).and_then(Object::as_dict)?;
if page.has(b"Resources") {
page.get(b"Resources").and_then(Object::as_reference).ok()
} else {
None
}
};
if let Some(res_id) = resources_id {
self.opt_clone_object_to_new_document(res_id)?;
return self.new_document.get_object_mut(res_id);
}
let page = self
.new_document
.get_object_mut(page_id)
.and_then(Object::as_dict_mut)?;
if !page.has(b"Resources") {
page.set(b"Resources", Dictionary::new());
}
page.get_mut(b"Resources")
}
/// Add XObject to a page.
///
/// Get Object that has the key `Resources -> XObject`.
pub fn add_xobject<N: Into<Vec<u8>>>(
&mut self, page_id: ObjectId, xobject_name: N, xobject_id: ObjectId,
) -> Result<()> {
if let Ok(resources) = self.get_or_create_resources(page_id).and_then(Object::as_dict_mut) {
if !resources.has(b"XObject") {
resources.set("XObject", Dictionary::new());
}
let mut xobjects = resources.get_mut(b"XObject")?;
if let Object::Reference(xobjects_ref_id) = xobjects {
let mut xobjects_id = *xobjects_ref_id;
while let Object::Reference(id) = self.new_document.get_object(xobjects_id)? {
xobjects_id = *id;
}
xobjects = self.new_document.get_object_mut(xobjects_id)?;
}
let xobjects = Object::as_dict_mut(xobjects)?;
xobjects.set(xobject_name, Object::Reference(xobject_id));
}
Ok(())
}
/// Add Graphics State to a page.
///
/// Get Object that has the key `Resources -> ExtGState`.
pub fn add_graphics_state<N: Into<Vec<u8>>>(
&mut self, page_id: ObjectId, gs_name: N, gs_id: ObjectId,
) -> Result<()> {
if let Ok(resources) = self.get_or_create_resources(page_id).and_then(Object::as_dict_mut) {
if !resources.has(b"ExtGState") {
resources.set("ExtGState", Dictionary::new());
}
let states = resources.get_mut(b"ExtGState").and_then(Object::as_dict_mut)?;
states.set(gs_name, Object::Reference(gs_id));
}
Ok(())
}
}
impl Default for IncrementalDocument {
fn default() -> Self {
Self::new()
}
}