mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-07 11:14:14 +00: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>
157 lines
4.7 KiB
Rust
157 lines
4.7 KiB
Rust
use super::{Dictionary, Document, Object, ObjectId};
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Bookmark {
|
|
/// Children, Must be a Collection that allows for insertion of the same page ID.
|
|
pub children: Vec<u32>,
|
|
pub title: String,
|
|
/// 0, 1 for italic, 2 for bold, 3 for italic bold
|
|
pub format: u32,
|
|
/// R,G,B
|
|
pub color: [f32; 3],
|
|
pub page: ObjectId,
|
|
pub id: u32,
|
|
}
|
|
|
|
impl Bookmark {
|
|
pub fn new(title: String, color: [f32; 3], format: u32, page: ObjectId) -> Bookmark {
|
|
Bookmark {
|
|
children: Vec::new(),
|
|
title,
|
|
format,
|
|
color,
|
|
page,
|
|
id: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Document {
|
|
pub fn add_bookmark(&mut self, mut bookmark: Bookmark, parent: Option<u32>) -> u32 {
|
|
self.max_bookmark_id += 1;
|
|
let id = self.max_bookmark_id;
|
|
|
|
bookmark.id = id;
|
|
|
|
if let Some(p) = parent {
|
|
if let Some(b) = self.bookmark_table.get_mut(&p) {
|
|
b.children.push(id);
|
|
}
|
|
} else {
|
|
self.bookmarks.push(id);
|
|
}
|
|
|
|
self.bookmark_table.insert(id, bookmark);
|
|
id
|
|
}
|
|
|
|
fn outline_child(
|
|
&self, maxid: &mut u32, parent: (ObjectId, &[u32]), processed: &mut HashMap<ObjectId, Dictionary>,
|
|
) -> (Option<ObjectId>, Option<ObjectId>, i64) {
|
|
let mut first: Option<ObjectId> = None;
|
|
let mut last: Option<ObjectId> = None;
|
|
let count = parent.1.len();
|
|
for i in parent.1 {
|
|
let mut child = Dictionary::new();
|
|
*maxid += 1;
|
|
let id: ObjectId = (*maxid, 0);
|
|
*maxid += 1;
|
|
let info_id: ObjectId = (*maxid, 0);
|
|
let bookmark = self.bookmark_table.get(i).unwrap();
|
|
|
|
let info = dictionary! {
|
|
"D" => vec![bookmark.page.into(), Object::Name("Fit".into())],
|
|
"S" => "GoTo",
|
|
};
|
|
|
|
let title_bytes = if bookmark.title.is_ascii() {
|
|
bookmark.title.as_bytes().to_vec()
|
|
} else {
|
|
// If the title contains non-ASCII characters:
|
|
// Create a new vector with the UTF-16 Byte Order Mark (BOM) for UTF-16BE.
|
|
let mut bom = vec![0xFE, 0xFF];
|
|
let utf16_title = bookmark.title.encode_utf16();
|
|
// Append the UTF-16BE encoded bytes of the title to the BOM.
|
|
bom.extend(utf16_title.flat_map(u16::to_be_bytes));
|
|
bom
|
|
};
|
|
|
|
child.set("Parent", parent.0);
|
|
child.set("Title", Object::string_literal(title_bytes));
|
|
child.set("A", info_id);
|
|
child.set("F", Object::Integer(bookmark.format.into()));
|
|
child.set(
|
|
"C",
|
|
vec![
|
|
bookmark.color[0].into(),
|
|
bookmark.color[1].into(),
|
|
bookmark.color[2].into(),
|
|
],
|
|
);
|
|
|
|
if first.is_none() {
|
|
first = Some(id);
|
|
} else if let Some(x) = last {
|
|
let inner_object = processed.get_mut(&x).unwrap();
|
|
inner_object.set("Next", id);
|
|
child.set("Prev", x);
|
|
}
|
|
|
|
last = Some(id);
|
|
|
|
if !bookmark.children.is_empty() {
|
|
let (c_first, c_last, c_count) = self.outline_child(maxid, (id, &bookmark.children[..]), processed);
|
|
|
|
if let Some(n) = c_first {
|
|
child.set("First", n);
|
|
}
|
|
|
|
if let Some(n) = c_last {
|
|
child.set("Last", n);
|
|
}
|
|
|
|
child.set("Count", c_count);
|
|
}
|
|
|
|
processed.insert(id, child);
|
|
processed.insert(info_id, info);
|
|
}
|
|
|
|
(first, last, count as i64)
|
|
}
|
|
|
|
pub fn build_outline(&mut self) -> Option<ObjectId> {
|
|
let mut processed: HashMap<ObjectId, Dictionary> = HashMap::new();
|
|
|
|
if !self.bookmarks.is_empty() {
|
|
let mut outline = Dictionary::new();
|
|
let mut maxid = self.max_id;
|
|
maxid += 1;
|
|
let id: ObjectId = (maxid, 0);
|
|
|
|
let (first, last, count) = self.outline_child(&mut maxid, (id, &self.bookmarks[..]), &mut processed);
|
|
|
|
if let Some(n) = first {
|
|
outline.set("First", n);
|
|
}
|
|
|
|
if let Some(n) = last {
|
|
outline.set("Last", n);
|
|
}
|
|
|
|
outline.set("Count", Object::Integer(count));
|
|
|
|
for (obj_id, obj) in processed.drain() {
|
|
self.objects.insert(obj_id, obj.into());
|
|
}
|
|
|
|
self.objects.insert(id, outline.into());
|
|
self.max_id = maxid;
|
|
return Some(id);
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|