mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 11:08:48 -06:00
fix(content): skip a long unreadable number whole
When an operand failed to parse, the content parser went back to the byte after its start and tried again, so a run of n bytes that never reads as a number (a long run of '+', or digits ending in a letter) was read again from every one of its bytes: n^2/2 bytes from a tiny compressed stream. A failed number token longer than 64 bytes is now skipped whole; shorter ones still recover byte by byte.
This commit is contained in:
@@ -74,6 +74,9 @@ fn is_delim(b: u8) -> bool {
|
||||
matches!(b, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%')
|
||||
}
|
||||
|
||||
/// Longest unreadable number token retried from its next byte. Real numbers are far shorter.
|
||||
const MAX_RETRIED_TOKEN: usize = 64;
|
||||
|
||||
/// Parse a (decoded) content stream.
|
||||
pub fn parse(data: &[u8]) -> Parsed {
|
||||
parse_pieces(data, &[])
|
||||
@@ -110,9 +113,19 @@ fn parse_pieces(data: &[u8], ends: &[usize]) -> Parsed {
|
||||
lx.pos = data.len();
|
||||
}
|
||||
Err(_) => {
|
||||
// Skip the offending byte and carry on.
|
||||
lx.pos = at + 1;
|
||||
out.skipped += 1;
|
||||
let read = lx.pos.saturating_sub(at);
|
||||
#[cfg(test)]
|
||||
tests::RESCANNED.with(|c| c.set(c.get() + read));
|
||||
let number = matches!(b, b'+' | b'-' | b'.') || b.is_ascii_digit();
|
||||
if number && read > MAX_RETRIED_TOKEN {
|
||||
// A long unreadable number is skipped whole: retrying from each of its
|
||||
// bytes would read the rest of it again every time.
|
||||
out.skipped += read;
|
||||
} else {
|
||||
// Skip the offending byte and carry on.
|
||||
lx.pos = at + 1;
|
||||
out.skipped += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
use super::*;
|
||||
|
||||
thread_local! {
|
||||
/// Bytes the lexer read before an operand failed, on this thread.
|
||||
pub(crate) static RESCANNED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_unreadable_number_is_read_once() {
|
||||
for junk in ["+", "5"] {
|
||||
let n = 20_000;
|
||||
let src = format!("{}{} 1 0 0 1 0 0 cm", junk.repeat(n), if junk == "5" { "x" } else { "" });
|
||||
RESCANNED.with(|c| c.set(0));
|
||||
let p = parse(src.as_bytes());
|
||||
let rescanned = RESCANNED.with(|c| c.get());
|
||||
assert!(rescanned <= 2 * n, "{rescanned} bytes read for a {n}-byte token");
|
||||
assert_eq!(p.ops.last().and_then(|o| o.nums::<6>()), Some([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]));
|
||||
}
|
||||
// A short damaged number still recovers byte by byte.
|
||||
let p = parse(b"-+5 0 Td");
|
||||
assert_eq!((p.ops.len(), p.skipped), (1, 1));
|
||||
assert_eq!(p.ops[0].nums::<2>(), Some([5.0, 0.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operators_keep_operands_and_spans() {
|
||||
let src = b"q 1 0 0 1 72 720 cm BT /F1 12 Tf (Hello \\(world\\)) Tj [(A) -120 (B)] TJ ET Q";
|
||||
|
||||
Reference in New Issue
Block a user