fix: skip page extraction when operation count exceeds 1M (#12)
* fix: skip page extraction when operation count exceeds 1M Vector-heavy architectural PDFs can have 10-26M path operators per page, causing ~60 GB allocation during per-op processing. The decode itself is tolerable but the subsequent loop amplifies memory 2-5x with text items, rects, paths, and state tracking. Check operation count after Content::decode() and return empty extraction for pages exceeding the limit, with a warning log. * test: add unit test for excessive operations guard Constructs a synthetic PDF with 1.1M path operators to verify that pages exceeding the operation limit return empty extraction.
This commit is contained in:
@@ -115,6 +115,17 @@ pub(crate) fn extract_page_text_items(
|
|||||||
|
|
||||||
let content = Content::decode(&content_data).map_err(|e| PdfError::Parse(e.to_string()))?;
|
let content = Content::decode(&content_data).map_err(|e| PdfError::Parse(e.to_string()))?;
|
||||||
|
|
||||||
|
const MAX_OPERATIONS: usize = 1_000_000;
|
||||||
|
if content.operations.len() > MAX_OPERATIONS {
|
||||||
|
log::warn!(
|
||||||
|
"page {}: skipping extraction — {} operations exceeds limit ({})",
|
||||||
|
page_num,
|
||||||
|
content.operations.len(),
|
||||||
|
MAX_OPERATIONS
|
||||||
|
);
|
||||||
|
return Ok(((Vec::new(), Vec::new(), Vec::new()), false));
|
||||||
|
}
|
||||||
|
|
||||||
// Graphics state tracking
|
// Graphics state tracking
|
||||||
let mut ctm = [1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0]; // Current Transformation Matrix
|
let mut ctm = [1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0]; // Current Transformation Matrix
|
||||||
let mut text_rendering_mode: i32 = 0; // 0=fill, 1=stroke, 2=fill+stroke, 3=invisible
|
let mut text_rendering_mode: i32 = 0; // 0=fill, 1=stroke, 2=fill+stroke, 3=invisible
|
||||||
@@ -1122,4 +1133,45 @@ mod tests {
|
|||||||
dedup_rects(&mut single);
|
dedup_rects(&mut single);
|
||||||
assert_eq!(single.len(), 1);
|
assert_eq!(single.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_skip_excessive_operations() {
|
||||||
|
use crate::tounicode::FontCMaps;
|
||||||
|
use lopdf::{dictionary, Object, Stream};
|
||||||
|
|
||||||
|
let mut doc = lopdf::Document::new();
|
||||||
|
|
||||||
|
// "0 0 m\n" = 6 bytes per op, 1_100_000 ops → ~6.6 MB content stream
|
||||||
|
let ops_bytes = "0 0 m\n".repeat(1_100_000).into_bytes();
|
||||||
|
let stream = Stream::new(dictionary! {}, ops_bytes);
|
||||||
|
let content_id = doc.add_object(Object::Stream(stream));
|
||||||
|
|
||||||
|
let page_dict = dictionary! {
|
||||||
|
"Type" => "Page",
|
||||||
|
"Contents" => Object::Reference(content_id),
|
||||||
|
"Resources" => dictionary! {},
|
||||||
|
"MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
|
||||||
|
};
|
||||||
|
let page_id = doc.add_object(page_dict);
|
||||||
|
|
||||||
|
// Register the page so get_page_content can find it
|
||||||
|
let pages_dict = dictionary! {
|
||||||
|
"Type" => "Pages",
|
||||||
|
"Count" => Object::Integer(1),
|
||||||
|
"Kids" => vec![Object::Reference(page_id)],
|
||||||
|
};
|
||||||
|
let pages_id = doc.add_object(pages_dict);
|
||||||
|
let catalog = dictionary! {
|
||||||
|
"Type" => "Catalog",
|
||||||
|
"Pages" => Object::Reference(pages_id),
|
||||||
|
};
|
||||||
|
doc.add_object(catalog);
|
||||||
|
|
||||||
|
let font_cmaps = FontCMaps::from_doc(&doc);
|
||||||
|
let result = extract_page_text_items(&doc, page_id, 1, &font_cmaps, false).unwrap();
|
||||||
|
let ((items, rects, lines), _has_gid) = result;
|
||||||
|
assert!(items.is_empty());
|
||||||
|
assert!(rects.is_empty());
|
||||||
|
assert!(lines.is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user