mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Fix #3586: the PDF pre-open scan reads a file dense with stream keywords in linear time
Each `stream` keyword walked back up to 64 KiB through every dictionary before it, so 1 MiB of `<</A <</B 1>> >>stream` took minutes. A walk back now stops where the previous keyword's walk began, and a stream without a Length shares the search for the next `endstream`.
This commit is contained in:
committed by
bflatastic
parent
cdd3b949ba
commit
ea55f89c24
@@ -271,7 +271,12 @@ fn unsafe_data(stream: &Stream<'_>, inside: bool) -> bool {
|
||||
/// here).
|
||||
pub(crate) fn unsafe_file_streams(pdf: &[u8]) -> bool {
|
||||
let find = |from: usize, what: &[u8]| pdf.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at);
|
||||
let mut from = 0;
|
||||
// A walk back through a dictionary stops at `dicts_from`, the end of the last keyword walked
|
||||
// back from, so the scan reads each byte a bounded number of times (#3586): before, every
|
||||
// keyword walked back up to 64 KiB through all the dictionaries before it.
|
||||
let (mut from, mut dicts_from) = (0, 0);
|
||||
// The first `endstream` at or after the last place it was looked for (`None`: there is none).
|
||||
let mut endstream: Option<Option<usize>> = None;
|
||||
while let Some(at) = find(from, b"stream") {
|
||||
from = at + 6;
|
||||
if at >= 3 && pdf.get(at - 3..at) == Some(b"end") {
|
||||
@@ -282,8 +287,10 @@ pub(crate) fn unsafe_file_streams(pdf: &[u8]) -> bool {
|
||||
if close < 2 || pdf.get(close - 2..close) != Some(b">>") {
|
||||
continue;
|
||||
}
|
||||
let floor = close.saturating_sub(1 << 16).max(dicts_from);
|
||||
dicts_from = from;
|
||||
let (mut depth, mut open, mut i) = (0usize, None, close);
|
||||
while i >= 2 && close - i < 1 << 16 {
|
||||
while i >= floor + 2 {
|
||||
match pdf.get(i - 2..i) {
|
||||
Some(b">>") => (depth, i) = (depth + 1, i - 2),
|
||||
Some(b"<<") => {
|
||||
@@ -308,12 +315,16 @@ pub(crate) fn unsafe_file_streams(pdf: &[u8]) -> bool {
|
||||
let start = from
|
||||
+ usize::from(pdf.get(from) == Some(&b'\r'))
|
||||
+ usize::from(pdf.get(from + usize::from(pdf.get(from) == Some(&b'\r'))) == Some(&b'\n'));
|
||||
let end = dict
|
||||
.get::<usize>(b"Length")
|
||||
.and_then(|n| start.checked_add(n))
|
||||
.filter(|e| *e <= pdf.len())
|
||||
.or_else(|| find(start, b"endstream"))
|
||||
.unwrap_or(pdf.len());
|
||||
let length = dict.get::<usize>(b"Length").and_then(|n| start.checked_add(n)).filter(|e| *e <= pdf.len());
|
||||
let end = match length {
|
||||
Some(end) => end,
|
||||
None => {
|
||||
if endstream.is_none_or(|e| e.is_some_and(|e| e < start)) {
|
||||
endstream = Some(find(start, b"endstream"));
|
||||
}
|
||||
endstream.flatten().unwrap_or(pdf.len())
|
||||
}
|
||||
};
|
||||
if matches!(decode(pdf.get(start..end).unwrap_or_default(), &dict, false), Err(TooBig)) {
|
||||
return true;
|
||||
}
|
||||
@@ -463,5 +474,22 @@ mod tests {
|
||||
] {
|
||||
assert!(!unsafe_file_streams(&fine));
|
||||
}
|
||||
// A bad stream after others is still found: a walk back stops at the previous keyword, not before its own `<<`.
|
||||
let fine = stream("/Type /XRef /Filter /FlateDecode /DecodeParms << /Predictor 12 /Columns 5 >>", b"x");
|
||||
let bad = stream("/Type /XRef /Filter /FlateDecode /DecodeParms 9 0 R", b"x");
|
||||
assert!(unsafe_file_streams(&[fine.as_slice(), b"<< >>stream x endstream", &bad].concat()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_dense_with_stream_keywords_is_scanned_in_linear_time() {
|
||||
// #3586: each `stream` keyword walked back up to 64 KiB, through every dictionary before
|
||||
// it, so 1 MiB of keywords took minutes. Now the whole scan takes milliseconds.
|
||||
for unit in [&b"<</A <</B 1>> >>stream"[..], b">>stream", b"<</Type/XRef>>stream"] {
|
||||
let pdf = unit.repeat((1 << 20) / unit.len());
|
||||
let t = std::time::Instant::now();
|
||||
assert!(!unsafe_file_streams(&pdf));
|
||||
let took = t.elapsed();
|
||||
assert!(took < std::time::Duration::from_secs(5), "{} took {took:?}", String::from_utf8_lossy(unit));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,6 +605,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv
|
||||
|---|---|---|
|
||||
| 2026-10-24 | minor | G4: images in OpenType SVG glyph documents draw, export and outline with the glyph; colour fonts are done but for CPAL palette variables; estimate unchanged. |
|
||||
| 2026-10-24 | minor | G4: bitmap glyphs (emoji) go with type that effects reshape, into Expand Appearance, envelope Expand and EMF/WMF export, as images that hold their own pixels until a document takes them in; estimate unchanged. |
|
||||
| 2026-10-24 | minor | G10: Fix #3586: the PDF pre-open scan reads a file dense with `stream` keywords in linear time; each keyword walked back up to 64 KiB through every dictionary before it, so 1 MiB of `<</A <</B 1>> >>stream` took minutes. No entry closed, estimates unchanged. |
|
||||
| 2026-10-24 | minor | Ranked table: one row each for G2 and G13 (doc merges had left G2 twice and G13 three times, at three hour estimates); G13 keeps the newest, 13–23 h; scores and hours unchanged |
|
||||
| 2026-10-23 | minor | G5: Select › Start Global Edit on paths, compound paths and symbol instances (shape match, Appearance and Size, artboard range and canvas, proportional edits as one undo step; #3521); Global Edit of similar groups and Shift-click exclusion remain. Estimate unchanged. |
|
||||
| 2026-10-23 | minor | G5: symbol libraries (Window › Symbol Libraries, the Symbols panel's Symbol Libraries Menu, Other Library…, Save Symbols, User Defined, adding and placing library symbols as one undo step; `crates/engine/src/tests_symbollib.rs`, `crates/ui-egui/src/panels/tests_symbol_libraries.rs`); brush libraries remain. Estimate unchanged. |
|
||||
|
||||
Reference in New Issue
Block a user