From 991ce2c6356310dce02d48d752e8b916a9d3745c Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 14 Oct 2026 10:13:23 +0700 Subject: [PATCH] Importers: PDF streams the reader would fail on are refused (#1700, #1701) hayro (0.7.2 and 0.8.0 alike) inflates Flate and LZW data with no ceiling, so a few megabytes of zeros ask for gigabytes, and applies a predictor before checking its parameters: Columns 0 divides by zero (a panic, an abort on the web), a huge Columns allocates a row that large (an abort everywhere). Both happen inside the reader, before any of our limits. A file is refused ("a stream in it would decode to more than 256 MB, or its decode parameters can't be applied") when a stream's decode parameters ask for a predictor the reader can't apply, or when it decodes past 256 MB through Flate, LZW, run-length and the ASCII encodings before them, decoded here with a ceiling (only streams large enough to get there are decoded). Every object's stream is looked at once the file is open (decrypted, references followed), with the inline images of anything that can be content; object and cross-reference streams, which the reader decodes while it opens the file, are looked at first in the file's text. --- Cargo.lock | 1 + crates/engine/tests/import_fuzz.rs | 56 ++++- crates/pdf/Cargo.toml | 1 + crates/pdf/src/lib.rs | 1 + crates/pdf/src/pages.rs | 27 ++- crates/pdf/src/streams.rs | 367 +++++++++++++++++++++++++++++ 6 files changed, 447 insertions(+), 6 deletions(-) create mode 100644 crates/pdf/src/streams.rs diff --git a/Cargo.lock b/Cargo.lock index 1303758d..c5913b50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4800,6 +4800,7 @@ dependencies = [ "vectorcraft-geom", "vectorcraft-testkit", "vectorcraft-text", + "weezl", ] [[package]] diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index c3e2fb11..cd70250e 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -2552,10 +2552,14 @@ fn deep_pdf(content: &str, resources: &str) -> Vec { /// A stream object with the entries `dict`, its `data` compressed. fn packed(dict: &str, data: &str) -> Vec { - let z = deflated(data.as_bytes()); + packed_as_is(dict, &deflated(data.as_bytes())) +} + +/// A stream object with the entries `dict`, its data `z` compressed already. +fn packed_as_is(dict: &str, z: &[u8]) -> Vec { // (A length the test gives stands.) let length = if dict.contains("/Length") { String::new() } else { format!(" /Length {}", z.len()) }; - [format!("<< {dict} /Filter /FlateDecode{length} >>\nstream\n").as_bytes(), &z, b"\nendstream"].concat() + [format!("<< {dict} /Filter /FlateDecode{length} >>\nstream\n").as_bytes(), z, b"\nendstream"].concat() } /// A page whose content is `content`, with `resources` and the objects `more` (numbered from 5). @@ -2801,6 +2805,54 @@ fn pdf_color_spaces_that_hold_themselves() { ); } +/// Spaces past what a stream may decode to (256 MB), compressed (about 260 kB). +fn spaces_past_the_cap() -> Vec { + use std::io::Write as _; + let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best()); + for _ in 0..257 { + e.write_all(&[b' '; 1 << 20]).unwrap(); + } + e.finish().unwrap() +} + +// Streams the reader would fail on (#1700, #1701): it applies a predictor before checking its +// parameters (`Columns 0` divides by zero, a huge `Columns` allocates a row that large), and +// inflates without a ceiling. + +#[test] +fn pdf_streams_the_reader_would_fail_on_are_refused() { + let zero = "/DecodeParms << /Predictor 2 /Columns 0 >>"; + let huge = "/DecodeParms << /Predictor 12 /Colors 4 /Columns 99999999999 >>"; + // An inline image whose data is deflated, written in hexadecimal so the content stays text. + let inline = |dp: &str| { + let data: String = deflated(&[0; 8]).iter().map(|b| format!("{b:02x}")).collect(); + format!("q 10 0 0 10 0 0 cm BI /W 2 /H 2 /CS /G /BPC 8 /F [/AHx /Fl] /DP [null {dp}] ID {data}> EI Q") + }; + let form = |dict: &str, content: &str| pdf_with("/F Do", "<< /XObject << /F 5 0 R >> >>", &[packed(&format!("{FORM} {dict}"), content)]); + let files = [ + ("a form with Columns 0", form(zero, "0 0 5 5 re f")), + ("a form with a huge Columns", form(huge, "0 0 5 5 re f")), + ("an inline image with Columns 0", deep_pdf(&inline("<< /Predictor 2 /Columns 0 >>"), "<< >>")), + ("an inline image with a huge Columns", deep_pdf(&inline("<< /Predictor 15 /Columns 99999999999 >>"), "<< >>")), + ("an inline image in a compressed form", form("", &inline("<< /Predictor 2 /Columns 0 >>"))), + ("an object stream with Columns 0", pdf_with_object_stream("[0 0 100 100]", zero)), + ("a form of spaces past the cap", pdf_with("/F Do", "<< /XObject << /F 5 0 R >> >>", &[packed_as_is(FORM, &spaces_past_the_cap())])), + ]; + on_a_small_stack(move || { + for (what, bytes) in &files { + for name in ["hostile.pdf", "hostile.ai"] { + let e = vectorcraft_engine::cmd::fileio::load(name, bytes).err().map(|e| e.to_string()); + assert!(e.as_deref().is_some_and(|e| e.contains("would decode to more than 256 MB")), "{what} as {name}: {e:?}"); + } + } + }); + // Within range, the same files open. + let fine = "/DecodeParms << /Predictor 1 /Columns 0 >>"; + for bytes in [form(fine, "0 0 5 5 re f"), deep_pdf(&inline("<< /Predictor 1 >>"), "<< >>")] { + vectorcraft_engine::cmd::fileio::load("fine.pdf", &bytes).expect("opens"); + } +} + #[test] fn pdf_art_that_draws_itself() { let own = "<< /XObject << /F 5 0 R >> /Pattern << /P 5 0 R >> /Font << /F1 5 0 R >> /ExtGState << /G << /SMask << /S /Alpha /G 5 0 R >> >> >> >>"; diff --git a/crates/pdf/Cargo.toml b/crates/pdf/Cargo.toml index e53fc21b..ea9e7c67 100644 --- a/crates/pdf/Cargo.toml +++ b/crates/pdf/Cargo.toml @@ -27,6 +27,7 @@ hayro-syntax = "0.8.0" hayro-interpret = { version = "0.8.0", default-features = false, features = ["embed-fonts", "embed-cmaps"] } log = { workspace = true } smallvec = "1" +weezl = "0.1" serde = { workspace = true } # PDF security (encrypt.rs): pure Rust, MIT OR Apache-2.0, builds for wasm. aes = "0.8" diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 636b6712..c69d8030 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -64,6 +64,7 @@ mod post; mod presets; mod print; mod settings; +mod streams; mod syntax; pub use editing::{EDITING_FILE, Editing, LEGACY_EDITING_FILE, editing, editing_with}; diff --git a/crates/pdf/src/pages.rs b/crates/pdf/src/pages.rs index 9d38e43a..b56f2cd1 100644 --- a/crates/pdf/src/pages.rs +++ b/crates/pdf/src/pages.rs @@ -114,6 +114,21 @@ fn procedures_too_deep(o: &Object<'_>) -> bool { }) } +/// The error of a file with a stream the reader would fail on ([`crate::streams`]). +const STREAM_NOTE: &str = "a stream in it would decode to more than 256 MB, or its decode parameters can't be applied"; + +/// The error of a file [`nests_without_end`]. +const NESTED_NOTE: &str = "its functions or colour spaces are built on one another without end"; + +/// Why `pdf` is refused, if it is: a stream the reader would fail on (its own checks come first: +/// the calculator functions are decoded below), or [`nests_without_end`]. +fn hostile(pdf: &Pdf) -> Option<&'static str> { + if pdf.objects().into_iter().any(|o| matches!(&o, Object::Stream(s) if crate::streams::unsafe_stream(s))) { + return Some(STREAM_NOTE); + } + nests_without_end(pdf).then_some(NESTED_NOTE) +} + /// Does `pdf` hold functions or colour spaces built on one another past [`MAX_LINKS`] (or on /// themselves), or a calculator function nested past [`MAX_PROCEDURES`]? Every object is looked /// at, wherever it is used from: the interpreter builds them from resources, shadings, images, @@ -142,11 +157,15 @@ fn nests_without_end(pdf: &Pdf) -> bool { /// dictionaries 64 deep and no deeper, wherever they are: the file's text, an object stream, the /// content of a page, form, pattern or glyph; and follows a page tree or a chain of object streams /// once). What it reads beyond that is left out, as an object it can't read. What the interpreter -/// builds by recursion with no cap is refused here ([`nests_without_end`]). +/// builds by recursion with no cap, and streams it would decode without a ceiling or with a +/// predictor it can't apply, are refused here ([`hostile`], [`crate::streams`]). pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result { if is_postscript(bytes) { return Err(PdfError::PostScript); } + if crate::streams::unsafe_file_streams(bytes) { + return Err(PdfError::Parse(STREAM_NOTE.into())); + } let password = password.unwrap_or_default(); let read = |pw: &str| Pdf::new_with_password(bytes.to_vec(), pw); read(password) @@ -165,9 +184,9 @@ pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result Err(PdfError::Parse("its functions or colour spaces are built on one another without end".into())), - false => Ok(pdf), + .and_then(|pdf| match (0..2).find_map(|_| hostile(&pdf)) { + Some(why) => Err(PdfError::Parse(why.into())), + None => Ok(pdf), }) } diff --git a/crates/pdf/src/streams.rs b/crates/pdf/src/streams.rs new file mode 100644 index 00000000..61a6fcb9 --- /dev/null +++ b/crates/pdf/src/streams.rs @@ -0,0 +1,367 @@ +//! Streams looked at before the PDF reader (hayro) decodes them. It inflates Flate and LZW data +//! with no ceiling, so a few megabytes can ask for gigabytes (#1700), and it applies a predictor +//! before checking its parameters: `Columns 0` divides by zero, a huge `Columns` allocates a row +//! that large whatever the stream holds (#1701). A file with such a stream is refused instead. + +use std::borrow::Cow; +use std::io::Read; + +use hayro_syntax::content::TypedIter; +use hayro_syntax::content::ops::TypedInstruction; +use hayro_syntax::object::{Array, Dict, FromBytes, MaybeRef, Name, Object, Stream}; + +/// The most a stream may decode to: 256 MB, the pixels of the largest image the app reads (64 +/// million, 4 bytes each). +pub(crate) const MAX_DECODED: usize = 256 << 20; + +/// The longest row a predictor may have, in bytes. +const MAX_ROW: usize = 1 << 24; + +/// A stream that decodes to more than [`MAX_DECODED`]. +struct TooBig; + +/// The filters of the stream with dictionary `dict`, by name (`Filter` or `F`: a name or an array). +fn filters<'a>(dict: &Dict<'a>) -> Vec> { + [&b"Filter"[..], b"F"] + .into_iter() + .find_map(|key| dict.get::>(key).map(|n| vec![n]).or_else(|| dict.get::>(key).map(|a| a.iter::>().collect()))) + .unwrap_or_default() +} + +/// Every decode parameter dictionary of the stream with dictionary `dict`, as the reader may take +/// them (`DecodeParms` or `DP`: a dictionary, or an array with one for each filter). +fn params<'a>(dict: &Dict<'a>) -> Vec> { + let mut all = vec![]; + for key in [&b"DecodeParms"[..], b"DP"] { + all.extend(dict.get::>(key)); + all.extend(dict.get::>(key).into_iter().flat_map(|a| a.iter::>().filter_map(|o| o.into_dict()).collect::>())); + } + all +} + +/// Would the reader's predictor fail on `params`? It reads `Colors` and `BitsPerComponent` as +/// bytes and `Columns` as a size, multiplies the first two as bytes, divides by the length of a row +/// and allocates one before it checks anything. +fn bad_predictor(params: &Dict<'_>) -> bool { + if params.get::(b"Predictor").unwrap_or(1) == 1 { + return false; + } + let colors = usize::from(params.get::(b"Colors").unwrap_or(1)); + let bits = usize::from(params.get::(b"BitsPerComponent").unwrap_or(8)); + let columns = params.get::(b"Columns").unwrap_or(1); + let pixel = colors * bits; + let row = columns.checked_mul(pixel).map(|b| b.div_ceil(8)); + !(matches!(bits, 1 | 2 | 4 | 8 | 16) && colors > 0 && pixel <= 255 && columns > 0 && row.is_some_and(|r| r <= MAX_ROW)) +} + +/// `data` inflated, up to one byte past [`MAX_DECODED`] (zlib, or raw deflate as the reader also +/// takes); `None` when it is damaged before that. +fn inflate(data: &[u8]) -> Option> { + let read = |mut r: Box| { + let mut out = vec![]; + // (Damaged data: what came out before the damage still counts.) + let done = r.by_ref().take(MAX_DECODED as u64 + 1).read_to_end(&mut out).is_ok(); + (done || out.len() > MAX_DECODED).then_some(out) + }; + read(Box::new(flate2::read::ZlibDecoder::new(data))).or_else(|| read(Box::new(flate2::read::DeflateDecoder::new(data)))) +} + +/// `data` decoded from LZW, up to one byte past [`MAX_DECODED`]. `early`: the code length grows +/// one code early (`EarlyChange`, 1 by default). +fn lzw(data: &[u8], early: bool) -> Option> { + use weezl::{BitOrder, LzwStatus, decode::Decoder}; + let mut decoder = if early { Decoder::with_tiff_size_switch(BitOrder::Msb, 8) } else { Decoder::new(BitOrder::Msb, 8) }; + let (mut out, mut chunk, mut input) = (vec![], vec![0; 1 << 16], data); + while out.len() <= MAX_DECODED { + let r = decoder.decode_bytes(input, &mut chunk); + input = input.get(r.consumed_in..).unwrap_or_default(); + out.extend_from_slice(chunk.get(..r.consumed_out).unwrap_or_default()); + match r.status { + Ok(LzwStatus::Ok) => {} + Ok(LzwStatus::Done | LzwStatus::NoProgress) => break, + Err(_) => return (out.len() > MAX_DECODED).then_some(out), + } + } + Some(out) +} + +/// `data` decoded from run-length encoding, up to one byte past [`MAX_DECODED`]. +fn run_length(data: &[u8]) -> Vec { + let (mut out, mut at) = (vec![], 0); + while let Some(&n) = data.get(at) + && n != 128 + && out.len() <= MAX_DECODED + { + if n < 128 { + let take = usize::from(n) + 1; + out.extend_from_slice(data.get(at + 1..).unwrap_or_default().get(..take).unwrap_or_default()); + at += 1 + take; + } else { + out.extend(std::iter::repeat_n(data.get(at + 1).copied().unwrap_or(0), 257 - usize::from(n))); + at += 2; + } + } + out +} + +/// `data` decoded from hexadecimal digits (white space skipped, up to `>`). +fn hex(data: &[u8]) -> Vec { + let digits: Vec = data.iter().take_while(|c| **c != b'>').filter_map(|c| char::from(*c).to_digit(16)).map(|d| d as u8).collect(); + digits.chunks(2).map(|p| (p.first().copied().unwrap_or(0) << 4) | p.get(1).copied().unwrap_or(0)).collect() +} + +/// `data` decoded from base-85 (white space skipped, `z` for four zeros, up to `~>`). +fn base85(data: &[u8]) -> Vec { + let (mut out, mut group) = (vec![], vec![]); + let flush = |group: &mut Vec, out: &mut Vec| { + let n = group.len(); + if n > 1 { + group.resize(5, 84); + let v = group.iter().fold(0u32, |v, d| v.wrapping_mul(85).wrapping_add(*d)); + out.extend_from_slice(v.to_be_bytes().get(..n - 1).unwrap_or_default()); + } + group.clear(); + }; + for &c in data.iter().take_while(|c| **c != b'~') { + match c { + b'z' if group.is_empty() => out.extend([0; 4]), + b'!'..=b'u' => { + group.push(u32::from(c - b'!')); + if group.len() == 5 { + flush(&mut group, &mut out); + } + } + _ => {} + } + } + flush(&mut group, &mut out); + out +} + +/// The most data of `len` bytes can grow to through `filters`: 1,032 times through Flate (its +/// largest ratio), 3,641 through LZW (4,096 bytes from a 9-bit code), 64 through run-length. +fn most_after(len: usize, filters: &[Name<'_>]) -> u128 { + filters.iter().fold(len as u128, |n, f| { + n.saturating_mul(match f.as_ref() { + b"FlateDecode" | b"Fl" => 1032, + b"LZWDecode" | b"LZW" => 3641, + b"RunLengthDecode" | b"RL" => 64, + _ => 1, + }) + }) +} + +/// `data` (of the stream with dictionary `dict`) through its filters, as far as this reads them: +/// Flate, LZW, run-length and the ASCII encodings. Not decoded when it can't decode past +/// [`MAX_DECODED`] and `keep` doesn't ask for it, or when an image codec or damaged data comes +/// first (an image's own decoder knows its size). +fn decode(data: &[u8], dict: &Dict<'_>, keep: bool) -> Result>, TooBig> { + let filters = filters(dict); + if !keep && most_after(data.len(), &filters) <= MAX_DECODED as u128 { + return Ok(None); + } + let params = params(dict); + let mut data = Cow::Borrowed(data); + for (i, f) in filters.iter().enumerate() { + let early = params.get(i).and_then(|p| p.get::(b"EarlyChange")).is_none_or(|e| e != 0); + let out = match f.as_ref() { + b"FlateDecode" | b"Fl" => inflate(&data), + b"LZWDecode" | b"LZW" => lzw(&data, early), + b"RunLengthDecode" | b"RL" => Some(run_length(&data)), + b"ASCIIHexDecode" | b"AHx" => Some(hex(&data)), + b"ASCII85Decode" | b"A85" => Some(base85(&data)), + _ => None, + }; + match out { + Some(out) if out.len() > MAX_DECODED => return Err(TooBig), + Some(out) => data = Cow::Owned(out), + None => return Ok(None), + } + } + Ok(Some(data.into_owned())) +} + +/// May the stream with dictionary `dict` be content (of a page, form, pattern or glyph), which can +/// hold inline images? Not an image, font, profile, function, metadata or object stream. +fn maybe_content(dict: &Dict<'_>) -> bool { + let named = |key: &[u8], ok: &[&[u8]]| dict.get::>(key).is_none_or(|n| ok.contains(&n.as_ref())); + named(b"Subtype", &[b"Form"]) + && named(b"Type", &[b"XObject", b"Pattern"]) + && ![&b"Length1"[..], b"Length2", b"Length3", b"FunctionType", b"N"].iter().any(|k| dict.contains_key(k)) +} + +/// Would the reader fail on `stream` (or on an inline image in it, when it is content): a +/// predictor it can't apply, or more than [`MAX_DECODED`] decoded? +pub(crate) fn unsafe_stream(stream: &Stream<'_>) -> bool { + unsafe_data(stream, true) +} + +/// [`unsafe_stream`], looking for inline images in it when `inside` (not in an inline image's own +/// data: it would be read again for each inline image a hostile one nests). +fn unsafe_data(stream: &Stream<'_>, inside: bool) -> bool { + let dict = stream.dict(); + if params(dict).iter().any(bad_predictor) { + return true; + } + let content = inside && maybe_content(dict); + match decode(&stream.raw_data(), dict, content) { + Err(TooBig) => true, + Ok(Some(data)) if content && data.windows(2).any(|w| w == b"ID") => { + let mut ops = TypedIter::new(&data); + while let Some(op) = ops.next() { + if let TypedInstruction::InlineImage(image) = op + && unsafe_data(image.0, false) + { + return true; + } + } + false + } + Ok(_) => false, + } +} + +/// Would the reader fail on a stream it decodes while it opens `pdf`, before anything else can +/// look at it: an object stream or a cross-reference stream (read here from the file's text)? +/// Their decode parameters must be in their dictionary, as the reader can't be asked for others +/// yet. Encrypted object streams are looked at once the file is open (their data is ciphered +/// 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; + while let Some(at) = find(from, b"stream") { + from = at + 6; + if at >= 3 && pdf.get(at - 3..at) == Some(b"end") { + continue; + } + // The dictionary before it: back from its `>>` to the `<<` that opens it. + let Some(close) = pdf.get(..at).and_then(|head| head.iter().rposition(|c| !c.is_ascii_whitespace())).map(|i| i + 1) else { continue }; + if close < 2 || pdf.get(close - 2..close) != Some(b">>") { + continue; + } + let (mut depth, mut open, mut i) = (0usize, None, close); + while i >= 2 && close - i < 1 << 16 { + match pdf.get(i - 2..i) { + Some(b">>") => (depth, i) = (depth + 1, i - 2), + Some(b"<<") => { + depth -= 1; + i -= 2; + if depth == 0 { + open = Some(i); + break; + } + } + _ => i -= 1, + } + } + let Some(dict) = open.and_then(|o| pdf.get(o..close)).and_then(Dict::from_bytes) else { continue }; + if !dict.get::>(b"Type").is_some_and(|t| matches!(t.as_ref(), b"ObjStm" | b"XRef")) { + continue; + } + let indirect = [&b"DecodeParms"[..], b"DP"].iter().any(|k| matches!(dict.get_raw::>(k), Some(MaybeRef::Ref(_)))); + if indirect || params(&dict).iter().any(bad_predictor) { + return true; + } + 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::(b"Length") + .and_then(|n| start.checked_add(n)) + .filter(|e| *e <= pdf.len()) + .or_else(|| find(start, b"endstream")) + .unwrap_or(pdf.len()); + if matches!(decode(pdf.get(start..end).unwrap_or_default(), &dict, false), Err(TooBig)) { + return true; + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + fn dict(text: &str) -> Dict<'_> { + Dict::from_bytes(text.as_bytes()).unwrap() + } + + #[test] + fn predictors_the_reader_can_apply_pass() { + for ok in [ + "<< >>", + "<< /Predictor 1 /Columns 0 >>", + "<< /Predictor 12 /Columns 5 >>", + "<< /Predictor 2 /Colors 3 /BitsPerComponent 16 /Columns 1000 >>", + ] { + assert!(!bad_predictor(&dict(ok)), "{ok}"); + } + for bad in [ + "<< /Predictor 2 /Columns 0 >>", + "<< /Predictor 12 /Colors 0 >>", + "<< /Predictor 2 /BitsPerComponent 0 >>", + "<< /Predictor 2 /BitsPerComponent 3 >>", + "<< /Predictor 12 /Colors 32 /BitsPerComponent 16 >>", + "<< /Predictor 15 /Columns 99999999999 >>", + "<< /Predictor 10 /Columns 18446744073709551615 /Colors 4 >>", + ] { + assert!(bad_predictor(&dict(bad)), "{bad}"); + } + } + + #[test] + fn the_decoders_read_what_the_specification_writes() { + assert_eq!(hex(b"48 65 6c6C6\n>junk"), b"Hell`"); + assert_eq!(base85(b"87cURD]i,\"Ebo80~>"), b"Hello World!"); + assert_eq!(base85(b"z!!~>"), [0, 0, 0, 0, 0]); + assert_eq!(run_length(&[2, b'a', b'b', b'c', 254, b'x', 128, b'q']), b"abcxxx"); + let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default()); + std::io::Write::write_all(&mut e, &[7; 1000]).unwrap(); + assert_eq!(inflate(&e.finish().unwrap()).unwrap(), [7; 1000]); + // "-----A---B" in LZW, as the specification's example writes it (7.4.4.2). + let lzw_example = [0x80, 0x0B, 0x60, 0x50, 0x22, 0x0C, 0x0C, 0x85, 0x01]; + assert_eq!(lzw(&lzw_example, true).unwrap(), b"-----A---B"); + } + + /// Zeros past the cap, compressed. + fn zeros_past_the_cap() -> Vec { + let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best()); + for _ in 0..(MAX_DECODED >> 20) + 1 { + std::io::Write::write_all(&mut e, &[0; 1 << 20]).unwrap(); + } + e.finish().unwrap() + } + + #[test] + fn a_stream_that_decodes_past_the_cap_is_found_without_holding_it() { + // Inflated in pieces: never more than one byte past the cap is held. + let bomb = zeros_past_the_cap(); + assert!(bomb.len() < MAX_DECODED / 500); + assert_eq!(inflate(&bomb).map(|d| d.len()), Some(MAX_DECODED + 1)); + let d = dict("<< /Filter /FlateDecode >>"); + assert!(matches!(decode(&bomb, &d, false), Err(TooBig))); + // Small enough that it can't get there: not decoded at all. + assert!(matches!(decode(&[1, 2, 3], &d, false), Ok(None))); + } + + #[test] + fn object_and_cross_reference_streams_are_looked_at_before_the_file_is_read() { + let stream = |dict: &str, data: &[u8]| { + [format!("1 0 obj\n<< {dict} /Length {} >>\nstream\n", data.len()).as_bytes(), data, b"\nendstream\nendobj\n"].concat() + }; + for bad in [ + stream("/Type /ObjStm /N 1 /First 4 /Filter /FlateDecode /DecodeParms << /Predictor 2 /Columns 0 >>", b"x"), + stream("/Type /XRef /Filter /FlateDecode /DecodeParms 9 0 R", b"x"), + stream("/Type /ObjStm /N 1 /First 4 /Filter /FlateDecode", &zeros_past_the_cap()), + ] { + assert!(unsafe_file_streams(&bad)); + } + for fine in [ + stream("/Type /XRef /Filter /FlateDecode /DecodeParms << /Predictor 12 /Columns 5 >>", b"x"), + // Another kind of stream is looked at once the file is open, its references followed. + stream("/Filter /FlateDecode /DecodeParms << /Predictor 2 /Columns 0 >>", b"x"), + ] { + assert!(!unsafe_file_streams(&fine)); + } + } +}