Never crash: decompression bombs can't exhaust the renderer's memory (#411)
* Never crash: decompression bombs can't exhaust the renderer's memory The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and RunLength streams with no output limit. A 2 MB page whose content stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli render` and `text` past 3 GB in under a second, and a few more MB of file scale it further. On the desktop that is the first-page render of an emailed PDF, and the allocation-failure abort takes every open document with it. Our own layers already bound single streams (lopdf's load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few MB; the renderer path didn't. Random mutation almost never makes a high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by hand while triaging the fuzz run. Vendored hayro-syntax patch (6): - `decode_limit` gives each filter an output limit, and Flate (both flate2 and the fallback inflater), LZW and RunLength never write past it (the buffer can still grow to under twice it): `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter of an image whose size, bits per component and components are known, twice its pixel bytes (a PNG predictor tag byte per byte at most), capped at 1 GiB. Earlier filters in a chain produce compressed data and get the general limit. - `Stream::decoded_within(max)` for callers that budget. - A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one bomb twenty times decoded 5 GB), reserving exactly what it appends. Vendored hayro-interpret patch (18): - The content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint budget. Forms nest fifty deep and each level kept its content while the next ran: a form painting itself around a 256 MiB stream would have held over 12 GB. Each level gives its bytes back when it is dropped, so content painted again and again keeps drawing, and a stream without filters (borrowed from the file) costs nothing. - Type 0 sampled functions read every sample into a 4-byte value and then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They now take at most 2^21 table points and 2^23 values (the largest real tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are 1.2 million and 3.6 million), counted with checked arithmetic, and read no further than `/Size` asks. `/Size 0` no longer underflows building the default `/Encode`. Three rounds of independent review found the aggregate cases, buffer doubling past the limit, a chain/predictor shape the first image limit would have truncated, the Type 0 amplifier, and a first, cumulative budget that would have dropped legitimately repeated content, and a first Type 0 cap low enough to refuse real tint transforms; all fixed here. Measured in a release build (render; OS peak commit): - content stream inflating to 2 GB: 389 MB, 0.3 s - 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s - /Contents naming it twenty times: 516 MB, 0.3 s - a form painting itself around it: 644 MB, 0.4 s Synthetic regression tests in render (flate2 is a new dev-dependency there, already in the workspace): - `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through flate2, the fallback inflater (a stored block with a wrong length check, which flate2 refuses and the fallback accepts), LZW and RunLength each decode exactly 256 MiB, and at most 128 bytes as an 8 x 8 image. - `page_contents_and_nested_forms_share_one_budget`: three /Contents references decode one stream's worth; a self-painting form finishes. - `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted 300 times draws the 300th time. - `sampled_functions_with_billions_of_samples_are_refused`. Ten mutations each turn them red: no limit at all; no limit in flate2, the fallback, LZW, RunLength or for images; an unshared /Contents limit; no page budget; no refund on drop; no Type 0 sample cap. (Taking a form out of the budget no longer compiles: its content is the budget guard.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M2: normalize JPEG2000 samples without overflowing Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions. Co-Authored-By: OpenAI Codex <codex@openai.com> * Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Streetblock <github@streetblock.de> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
a5202e11fc
commit
e88def3465
|
|
@ -3750,6 +3750,7 @@ name = "pdfcraft-render"
|
|||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"flate2",
|
||||
"getrandom 0.4.3",
|
||||
"hayro",
|
||||
"hayro-syntax",
|
||||
|
|
|
|||
|
|
@ -26,3 +26,5 @@ getrandom = { version = "0.4", features = ["wasm_js"] }
|
|||
[dev-dependencies]
|
||||
# Checks the Japanese substitute's character coverage; the version pdfcraft-fonts already uses.
|
||||
skrifa = "0.44"
|
||||
# Builds the decompression-bomb streams in the raster tests.
|
||||
flate2 = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1831,6 +1831,228 @@ trailer << /Root 1 0 R >>
|
|||
}
|
||||
}
|
||||
|
||||
/// Zlib data that inflates to `len` zero bytes.
|
||||
fn zlib_zeros(len: usize) -> Vec<u8> {
|
||||
use std::io::Write;
|
||||
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
|
||||
let chunk = vec![0u8; 1 << 20];
|
||||
let mut left = len;
|
||||
while left > 0 {
|
||||
let n = left.min(chunk.len());
|
||||
z.write_all(&chunk[..n]).unwrap();
|
||||
left -= n;
|
||||
}
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
/// A raw deflate stream that flate2 rejects at once and the fallback inflater expands to at
|
||||
/// least `len` zero bytes: a one-byte stored block whose length check is wrong (flate2 refuses
|
||||
/// it, the fallback carries on), then a fixed-Huffman block of 258-byte copies.
|
||||
fn deflate_for_the_fallback(len: usize) -> Vec<u8> {
|
||||
let mut out = vec![0b000]; // not final, stored; the rest of the byte is padding
|
||||
out.extend_from_slice(&[1, 0, 0, 0, 0]); // LEN 1, NLEN 0 (should be !1), the byte 0
|
||||
let (mut acc, mut bits) = (0u64, 0u32);
|
||||
// Deflate packs bits from the least significant end; Huffman codes go most significant
|
||||
// bit first.
|
||||
let mut put = |value: u32, n: u32, huffman: bool| {
|
||||
for i in 0..n {
|
||||
let bit = if huffman { (value >> (n - 1 - i)) & 1 } else { (value >> i) & 1 };
|
||||
acc |= u64::from(bit) << bits;
|
||||
bits += 1;
|
||||
if bits == 8 {
|
||||
out.push(acc as u8);
|
||||
(acc, bits) = (0, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
put(1, 1, false); // final block
|
||||
put(1, 2, false); // fixed Huffman codes
|
||||
for _ in 0..len / 258 + 1 {
|
||||
put(0b1100_0101, 8, true); // length code 285: 258 bytes
|
||||
put(0, 5, true); // distance code 0: 1 byte back
|
||||
}
|
||||
put(0, 7, true); // end of block
|
||||
put(0, 7, false); // flush
|
||||
out
|
||||
}
|
||||
|
||||
/// LZW codes (MSB first, early change) for `fills` table fills of zero runs: after a clear,
|
||||
/// code 0, then every code is the one being defined (the previous entry plus a zero), so
|
||||
/// entry n holds n - 256 zeros. One fill is about 7 MB of output from 5 KB of codes.
|
||||
fn lzw_zeros(fills: usize) -> Vec<u8> {
|
||||
let width = |size: usize| match size + 1 {
|
||||
2048.. => 12,
|
||||
1024.. => 11,
|
||||
512.. => 10,
|
||||
_ => 9,
|
||||
};
|
||||
let (mut out, mut acc, mut bits) = (Vec::new(), 0u64, 0u32);
|
||||
let mut emit = |code: usize, w: u32| {
|
||||
acc = (acc << w) | code as u64;
|
||||
bits += w;
|
||||
while bits >= 8 {
|
||||
bits -= 8;
|
||||
out.push((acc >> bits) as u8);
|
||||
}
|
||||
};
|
||||
let mut size = 258;
|
||||
for _ in 0..fills {
|
||||
emit(256, width(size)); // clear
|
||||
size = 258;
|
||||
emit(0, width(size));
|
||||
for code in 258..4096 {
|
||||
emit(code, width(size));
|
||||
size = code + 1;
|
||||
}
|
||||
}
|
||||
emit(257, width(size)); // end of data
|
||||
emit(0, 7); // flush the last partial byte
|
||||
out
|
||||
}
|
||||
|
||||
/// Found while triaging `cargo xtask fuzz` on Windows, by hand rather than by the fuzzer
|
||||
/// (random mutations almost never make a high-ratio stream): a 2 MB page whose content
|
||||
/// stream, or whose 8 × 8 image, inflated to 2 GB took the renderer past 3 GB in under a
|
||||
/// second. Flate (through flate2 and through the fallback inflater), LZW and RunLength expand
|
||||
/// without limit. Vendored hayro-syntax patch: each stops at `MAX_DECODED_STREAM`, and an image
|
||||
/// of known size at its own rows.
|
||||
#[test]
|
||||
fn decompression_bombs_stop_at_the_stream_limit() {
|
||||
use hayro::hayro_syntax::object::stream::ImageDecodeParams;
|
||||
use hayro::hayro_syntax::object::{Object, Stream};
|
||||
let limit = hayro::hayro_syntax::MAX_DECODED_STREAM;
|
||||
let big = limit + (32 << 20);
|
||||
let mut run_length = [0x81u8, 0].repeat(big / 128 + 1); // runs of 128 zeros
|
||||
run_length.push(128);
|
||||
let streams: [(&str, Vec<u8>); 4] = [
|
||||
("/FlateDecode", zlib_zeros(big)),
|
||||
("/FlateDecode", deflate_for_the_fallback(big)),
|
||||
("/LZWDecode", lzw_zeros(big / 7_000_000 + 1)),
|
||||
("/RunLengthDecode", run_length),
|
||||
];
|
||||
let mut pdf = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] >> endobj\n".to_vec();
|
||||
for (i, (filter, data)) in streams.iter().enumerate() {
|
||||
pdf.extend_from_slice(format!("{} 0 obj << /Filter {filter} /Length {} >> stream\n", i + 4, data.len()).as_bytes());
|
||||
pdf.extend_from_slice(data);
|
||||
pdf.extend_from_slice(b"\nendstream endobj\n");
|
||||
}
|
||||
pdf.extend_from_slice(b"trailer << /Root 1 0 R >>\n%%EOF\n");
|
||||
let doc = Pdf::new(pdf).expect("parses");
|
||||
let found: Vec<Stream<'_>> = doc.objects().into_iter().filter_map(|o| if let Object::Stream(s) = o { Some(s) } else { None }).collect();
|
||||
assert_eq!(found.len(), 4, "all four bomb streams are found");
|
||||
for stream in &found {
|
||||
let what = format!("{:?}", stream.filters());
|
||||
let decoded = stream.decoded().expect("a bomb still decodes, up to the limit");
|
||||
assert_eq!(decoded.len(), limit, "{what}: stops at MAX_DECODED_STREAM, not at {big} bytes");
|
||||
// An 8 × 8 one-byte grey image has 64 pixel bytes, plus at most a predictor tag byte
|
||||
// for each.
|
||||
let params = ImageDecodeParams { bpc: Some(8), num_components: Some(1), width: 8, height: 8, ..ImageDecodeParams::default() };
|
||||
let image = stream.decoded_image(¶ms).expect("decodes as an image");
|
||||
assert!(image.data.len() <= 128, "{what}: an 8 × 8 image decodes at most 128 bytes, not {}", image.data.len());
|
||||
}
|
||||
}
|
||||
|
||||
/// Review of the decompression-bomb fix: one stream stopping at `MAX_DECODED_STREAM` isn't
|
||||
/// enough when streams add up. A page's `/Contents` array can name the same bomb many times,
|
||||
/// and a form that paints itself nests fifty deep, each level keeping its content while the
|
||||
/// next runs. Vendored patches: a `/Contents` array shares one `MAX_DECODED_STREAM`
|
||||
/// (hayro-syntax), and page contents, forms, tiling patterns and Type 3 glyphs share a
|
||||
/// per-page budget (hayro-interpret `MAX_PAGE_CONTENT`).
|
||||
#[test]
|
||||
fn page_contents_and_nested_forms_share_one_budget() {
|
||||
let limit = hayro::hayro_syntax::MAX_DECODED_STREAM;
|
||||
let bomb = zlib_zeros(limit + (32 << 20));
|
||||
// Three references to one bomb in /Contents decode no more than one stream's worth.
|
||||
let mut pdf = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents [4 0 R 4 0 R 4 0 R] >> endobj\n".to_vec();
|
||||
pdf.extend_from_slice(format!("4 0 obj << /Filter /FlateDecode /Length {} >> stream\n", bomb.len()).as_bytes());
|
||||
pdf.extend_from_slice(&bomb);
|
||||
pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n");
|
||||
let doc = Pdf::new(pdf).expect("parses");
|
||||
let page = doc.pages().first().expect("one page");
|
||||
let contents = page.page_stream().map_or(0, <[u8]>::len);
|
||||
assert!(contents <= limit + 3, "the /Contents array decodes {contents} bytes, over one stream's {limit}");
|
||||
|
||||
// A form that paints itself before 256 MiB of spaces: every level would keep its content.
|
||||
let mut content = b"/Fm0 Do ".to_vec();
|
||||
content.resize(limit, b' ');
|
||||
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
|
||||
std::io::Write::write_all(&mut z, &content).unwrap();
|
||||
let form = z.finish().unwrap();
|
||||
drop(content);
|
||||
let page_content = b"/Fm0 Do 1 0 0 rg 0 0 4 4 re f";
|
||||
let mut pdf = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents 4 0 R /Resources << /XObject << /Fm0 5 0 R >> >> >> endobj\n".to_vec();
|
||||
pdf.extend_from_slice(format!("4 0 obj << /Length {} >> stream\n", page_content.len()).as_bytes());
|
||||
pdf.extend_from_slice(page_content);
|
||||
pdf.extend_from_slice(b"\nendstream endobj\n");
|
||||
pdf.extend_from_slice(
|
||||
format!("5 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 40 40] /Resources << /XObject << /Fm0 5 0 R >> >> /Filter /FlateDecode /Length {} >> stream\n", form.len()).as_bytes(),
|
||||
);
|
||||
pdf.extend_from_slice(&form);
|
||||
pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n");
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
|
||||
let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }));
|
||||
});
|
||||
let page = rx.recv_timeout(std::time::Duration::from_secs(60)).expect("a self-painting form must not stall the renderer");
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 0, 0, 255], "the rest of the page draws");
|
||||
}
|
||||
|
||||
/// The page content budget counts what is held at once, not everything decoded: a form
|
||||
/// with 2 MiB of content painted three hundred times (600 MiB in all) draws every time.
|
||||
#[test]
|
||||
fn content_painted_again_and_again_keeps_drawing() {
|
||||
let mut content = b"1 0 0 rg 0 0 4 4 re f ".to_vec();
|
||||
content.resize(2 << 20, b' ');
|
||||
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
|
||||
std::io::Write::write_all(&mut z, &content).unwrap();
|
||||
let form = z.finish().unwrap();
|
||||
let mut page_content = "/Fm0 Do ".repeat(299);
|
||||
page_content.push_str("q 1 0 0 1 30 30 cm /Fm0 Do Q");
|
||||
let mut pdf = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents 4 0 R /Resources << /XObject << /Fm0 5 0 R >> >> >> endobj\n".to_vec();
|
||||
pdf.extend_from_slice(format!("4 0 obj << /Length {} >> stream\n{page_content}\nendstream endobj\n", page_content.len()).as_bytes());
|
||||
pdf.extend_from_slice(
|
||||
format!("5 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 40 40] /Filter /FlateDecode /Length {} >> stream\n", form.len()).as_bytes(),
|
||||
);
|
||||
pdf.extend_from_slice(&form);
|
||||
pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n");
|
||||
let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
|
||||
let page = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 });
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
// The last paint is at user (30, 30)–(34, 34): device row 40 - 32 = 8.
|
||||
assert_eq!(&page.rgba[((8 * 40 + 32) * 4)..][..4], &[255, 0, 0, 255], "the 300th paint is drawn");
|
||||
}
|
||||
|
||||
/// Review of the decompression-bomb fix: a Type 0 sampled function read every sample of its
|
||||
/// stream into a 4-byte value, then a table entry each, so 256 MiB of 1-bit samples asked for
|
||||
/// over 8 GB. Vendored hayro-interpret patch: at most `MAX_SAMPLE_VALUES` samples, read no
|
||||
/// further than `/Size` asks.
|
||||
#[test]
|
||||
fn sampled_functions_with_billions_of_samples_are_refused() {
|
||||
let bomb = zlib_zeros(hayro::hayro_syntax::MAX_DECODED_STREAM + (32 << 20));
|
||||
let page_content = b"/Sh0 sh 1 0 0 rg 0 0 4 4 re f";
|
||||
let mut pdf = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents 4 0 R /Resources << /Shading << /Sh0 5 0 R >> >> >> endobj\n".to_vec();
|
||||
pdf.extend_from_slice(format!("4 0 obj << /Length {} >> stream\n", page_content.len()).as_bytes());
|
||||
pdf.extend_from_slice(page_content);
|
||||
pdf.extend_from_slice(
|
||||
b"\nendstream endobj\n5 0 obj << /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 40 0] /Function 6 0 R >> endobj\n",
|
||||
);
|
||||
pdf.extend_from_slice(
|
||||
format!("6 0 obj << /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [1073741824] /BitsPerSample 1 /Filter /FlateDecode /Length {} >> stream\n", bomb.len()).as_bytes(),
|
||||
);
|
||||
pdf.extend_from_slice(&bomb);
|
||||
pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n");
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
|
||||
let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }));
|
||||
});
|
||||
let page = rx.recv_timeout(std::time::Duration::from_secs(60)).expect("a sampled-function bomb must not stall the renderer");
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 0, 0, 255], "the rest of the page draws");
|
||||
}
|
||||
|
||||
/// From the nightly `cargo xtask fuzz`: an embedded Type 1 font program holding a long run of
|
||||
/// integers. read-fonts 0.39 (through skrifa 0.42) looked ahead after every integer by
|
||||
/// parsing the next token, which looked ahead again, recursing through the whole run: a long
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001).
|
|||
|
||||
| Crate | Version | Licence | Patches | Test |
|
||||
|---|---|---|---|---|
|
||||
| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on` |
|
||||
| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on`; (18) `raster::tests::page_contents_and_nested_forms_share_one_budget`, `raster::tests::content_painted_again_and_again_keeps_drawing`, `raster::tests::sampled_functions_with_billions_of_samples_are_refused` |
|
||||
|
||||
| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid`; (7) `raster::tests::pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over` |
|
||||
| hayro-jbig2 | 0.3.0 | Apache-2.0 OR MIT | (1) at most 2^28 pixels per bitmap, page bitmap, halftone grid and grayscale image (each side could be 65535: 4.3 billion pixels decoded one by one); (2) at most 2^26 symbol pixels per symbol dictionary (a fuzzed dictionary decoded for minutes); (3) a symbol dictionary's height classes are capped at one per new symbol and its export runs at `2 × symbols + 1`, each plus `EXTRA_EMPTY_STEPS` (1024): an empty height class or a zero-length export run doesn't advance, and past the end of its data the arithmetic decoder can decode them for ever (one changed byte hung the renderer) | (1) `raster::tests::jbig2_regions_of_billions_of_pixels_are_refused`; (2) the nightly `cargo xtask fuzz` finding, which now finishes in about a second; (3) `raster::tests::jbig2_symbol_dictionaries_that_never_advance_terminate` |
|
||||
| lopdf | 0.45.0 | MIT | (1) PNG predictor rows longer than the data are refused before their buffers are allocated (a fuzzed `/Columns 4294967295` allocated two 4 GiB rows), and empty frames return without row buffers; (2) predictor row widths use saturating arithmetic or reject overflow; (3) sub-byte TIFF Predictor 2 scratch is bounded by the samples present in the input instead of the declared `/Colors`, aligned with [lopdf#589](https://github.com/J-F-Liu/lopdf/pull/589) | (1) `inspect::tests::png_predictor_rows_longer_than_the_data_are_refused_before_allocating`, `inspect::tests::empty_png_predictor_frames_do_not_allocate_rows`; (2) `inspect::tests::tiff_subbyte_predictor_row_width_overflow_is_refused`; (3) `inspect::tests::tiff_subbyte_predictor_scratch_is_bounded_by_available_samples`, `inspect::tests::tiff_subbyte_predictors_keep_components_rows_and_padding` |
|
||||
| hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped; (6) JPX rescaling uses an exact integer sample range for 1–32-bit components (32 shifted past u32, and floating-point maxima at 25–31 bits wrapped to zero), and refuses zero-sized rows | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream`; (6) `raster::tests::jpx_stream_bit_depth_boundaries_do_not_panic`, `raster::tests::jpx_32_bit_image_does_not_prevent_other_page_content_from_rendering`, `raster::tests::jpx_gray_rgb_and_alpha_keep_eight_bit_samples`, `raster::tests::jpx_streams_reject_out_of_range_bit_depths` |
|
||||
| hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped; (6) JPX rescaling uses an exact integer sample range for 1–32-bit components (32 shifted past u32, and floating-point maxima at 25–31 bits wrapped to zero), and refuses zero-sized rows; (7) `decode_limit`: Flate (through flate2 and the fallback inflater), LZW and RunLength never write past `MAX_DECODED_STREAM` (256 MiB), or, for the last filter of an image of known size, past twice its pixel bytes (a predictor tag byte per byte), at most 1 GiB; `Stream::decoded_within` for budgeted callers; a page's `/Contents` array shares one `MAX_DECODED_STREAM` (a 2 MB page whose content stream or 8 × 8 image inflated to 2 GB took the renderer past 3 GB in under a second; naming the stream twenty times in `/Contents`, 5 GB) | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream`; (6) `raster::tests::jpx_stream_bit_depth_boundaries_do_not_panic`, `raster::tests::jpx_32_bit_image_does_not_prevent_other_page_content_from_rendering`, `raster::tests::jpx_gray_rgb_and_alpha_keep_eight_bit_samples`, `raster::tests::jpx_streams_reject_out_of_range_bit_depths`; (7) `raster::tests::decompression_bombs_stop_at_the_stream_limit`, `raster::tests::page_contents_and_nested_forms_share_one_budget` |
|
||||
| winit | 0.30.13 | Apache-2.0 | (1) on Windows 11 (build 22000 and later), `WM_DPICHANGED` applies the rect Windows suggests. It used to nudge the window onto the monitor `MonitorFromWindow` names, which during a drag is still the monitor being left; the nudge sent another `WM_DPICHANGED`, so a window dragged to a monitor with another scale factor grew and stayed on the wrong monitor at the wrong scale (#324). Backport of winit master 488c036a ([winit#4341](https://github.com/rust-windowing/winit/pull/4341)), which no 0.30 release has; Windows 10 keeps the old adjustment, as on winit master. The published crate's asset files (`docs/res/*.svg`, `examples/data/*.png`) are left out, so its `window` example doesn't build | (1) `pdfcraft` `tests::winit_carries_the_windows_11_monitor_scale_fix` keeps the patch in the build; the drag itself needs two monitors with different scale factors on Windows 11 |
|
||||
|
||||
Temporary: hayro is the bootstrap renderer and lopdf the bootstrap inspector (ADR-0004); M2.6 replaces them with our own devices and `cos`/`model`.
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ use crate::{ClipPath, Device, FillRule, InterpreterSettings, StrokeProps};
|
|||
use hayro_syntax::content::ops::Transform;
|
||||
use hayro_syntax::object::Dict;
|
||||
use hayro_syntax::object::Name;
|
||||
use hayro_syntax::object::Stream;
|
||||
use hayro_syntax::page::Resources;
|
||||
use hayro_syntax::xref::XRef;
|
||||
use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape};
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
|
@ -31,6 +33,58 @@ pub(crate) const MAX_NESTED_PAINTS: u32 = 50_000;
|
|||
|
||||
std::thread_local! {
|
||||
static NESTED_PAINTS: core::cell::Cell<u32> = const { core::cell::Cell::new(0) };
|
||||
static CONTENT_LEFT: core::cell::Cell<usize> = const { core::cell::Cell::new(MAX_PAGE_CONTENT) };
|
||||
}
|
||||
|
||||
/// PdfCraft patch: decoded content-stream bytes one page may hold at once: its contents, and
|
||||
/// the forms, tiling patterns and Type 3 glyph procedures being painted. Each stream already
|
||||
/// stops at `MAX_DECODED_STREAM`, but forms nest fifty deep and every level keeps its content
|
||||
/// while the next one runs: a form painting itself around a stream that inflates to 256 MiB
|
||||
/// would have held over 12 GB. A level gives its bytes back when it finishes, so content
|
||||
/// painted again and again doesn't use the budget up. Ordinary pages hold a few megabytes.
|
||||
pub(crate) const MAX_PAGE_CONTENT: usize = 512 << 20;
|
||||
|
||||
/// PdfCraft patch: a content stream decoded within the page's [`MAX_PAGE_CONTENT`]. The bytes
|
||||
/// it decoded count against the budget until it is dropped; a stream without filters is
|
||||
/// borrowed from the file and costs nothing.
|
||||
pub(crate) struct DecodedContent<'a> {
|
||||
data: Cow<'a, [u8]>,
|
||||
held: usize,
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for DecodedContent<'_> {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DecodedContent<'_> {
|
||||
fn drop(&mut self) {
|
||||
let held = self.held;
|
||||
CONTENT_LEFT.with(|c| c.set(c.get().saturating_add(held).min(MAX_PAGE_CONTENT)));
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch: decode a content stream within what is left of this page's
|
||||
/// [`MAX_PAGE_CONTENT`]. `None` once the budget is spent (the content is skipped).
|
||||
pub(crate) fn decode_content<'a>(stream: &Stream<'a>) -> Option<DecodedContent<'a>> {
|
||||
let left = CONTENT_LEFT.with(core::cell::Cell::get);
|
||||
if left == 0 {
|
||||
warn!("page content budget exceeded");
|
||||
return None;
|
||||
}
|
||||
let data = stream.decoded_within(left).ok()?;
|
||||
let held = match &data {
|
||||
Cow::Owned(v) => v.len(),
|
||||
Cow::Borrowed(_) => 0,
|
||||
};
|
||||
charge_content(held);
|
||||
Some(DecodedContent { data, held })
|
||||
}
|
||||
|
||||
/// PdfCraft patch: count `len` bytes held for the rest of the page (its own contents).
|
||||
pub(crate) fn charge_content(len: usize) {
|
||||
CONTENT_LEFT.with(|c| c.set(c.get().saturating_sub(len)));
|
||||
}
|
||||
|
||||
/// PdfCraft patch: count a paint at `depth` (top-level ones are free); `false` once this
|
||||
|
|
@ -101,8 +155,9 @@ impl<'a> Context<'a> {
|
|||
settings: InterpreterSettings,
|
||||
) -> Self {
|
||||
let state = State::new(initial_transform);
|
||||
// PdfCraft patch: a page (or other top-level content) starts with a fresh budget.
|
||||
// PdfCraft patch: a page (or other top-level content) starts with fresh budgets.
|
||||
NESTED_PAINTS.with(|n| n.set(0));
|
||||
CONTENT_LEFT.with(|c| c.set(MAX_PAGE_CONTENT));
|
||||
|
||||
Self::new_with(initial_transform, bbox, cache, xref, settings, state, 0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,8 @@ impl<'a> Type3<'a> {
|
|||
|
||||
let name = self.glyph_simulator.glyph_to_string(glyph.glyph_id)?;
|
||||
let program = self.char_procs.get(&name)?;
|
||||
let decoded = program.decoded().ok()?;
|
||||
// PdfCraft patch: see `context::MAX_PAGE_CONTENT`.
|
||||
let decoded = crate::context::decode_content(program)?;
|
||||
let iter = TypedIter::new(decoded.as_ref());
|
||||
|
||||
let is_shape_glyph = {
|
||||
|
|
|
|||
|
|
@ -44,9 +44,25 @@ impl Type0 {
|
|||
.iter::<u32>()
|
||||
.collect::<IntVec>();
|
||||
|
||||
let encode = dict
|
||||
.get::<TupleVec>(ENCODE)
|
||||
.unwrap_or(sizes.iter().map(|s| (0.0, (*s - 1) as f32)).collect());
|
||||
let encode = dict.get::<TupleVec>(ENCODE).unwrap_or(
|
||||
sizes
|
||||
.iter()
|
||||
.map(|s| (0.0, s.saturating_sub(1) as f32))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
// PdfCraft patch: see `MAX_SAMPLE_VALUES`.
|
||||
let Some(num_expected_entries) = sizes
|
||||
.iter()
|
||||
.try_fold(1usize, |n, s| n.checked_mul(*s as usize))
|
||||
.filter(|entries| *entries <= MAX_SAMPLE_ENTRIES)
|
||||
.and_then(|entries| entries.checked_mul(range.len()))
|
||||
.filter(|n| *n <= MAX_SAMPLE_VALUES)
|
||||
else {
|
||||
warn!("Type0 function has too many samples");
|
||||
|
||||
return None;
|
||||
};
|
||||
|
||||
let decode = dict.get::<TupleVec>(DECODE).unwrap_or(range.clone());
|
||||
|
||||
|
|
@ -55,15 +71,15 @@ impl Type0 {
|
|||
let mut buf = vec![];
|
||||
let mut reader = BitReader::new(&decoded);
|
||||
|
||||
while let Some(data) = reader.read(bits_per_sample) {
|
||||
while buf.len() < num_expected_entries
|
||||
&& let Some(data) = reader.read(bits_per_sample)
|
||||
{
|
||||
buf.push(data);
|
||||
}
|
||||
|
||||
buf
|
||||
};
|
||||
|
||||
let num_expected_entries = sizes.iter().fold(1, |i1, i2| i1 * *i2 as usize) * range.len();
|
||||
|
||||
if data.len() != num_expected_entries {
|
||||
warn!("Type0 function didn't have the expected number of sample entries.");
|
||||
data.truncate(num_expected_entries);
|
||||
|
|
@ -233,6 +249,15 @@ impl Interpolator {
|
|||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the most sample values a sampled function may have, and the most table
|
||||
/// entries (one per point of its `/Size` grid). The samples were read into 4-byte values and
|
||||
/// then into a table entry each, so a stream inflating to 256 MiB of 1-bit samples asked for
|
||||
/// 8 GB; `/Size` sets how many the table expects. The largest real tables are DeviceN tint
|
||||
/// transforms: 33 × 33 × 33 × 33 points with 3 outputs is 1.2 million entries and 3.6 million
|
||||
/// values, 17^5 points with 4 outputs 5.7 million values.
|
||||
const MAX_SAMPLE_VALUES: usize = 1 << 23;
|
||||
const MAX_SAMPLE_ENTRIES: usize = 1 << 21;
|
||||
|
||||
fn build_table(data: &[u32], sizes: &[u32], n: usize) -> Option<HashMap<Key, IntVec>> {
|
||||
let mut key = Key::new(sizes);
|
||||
let mut table = HashMap::new();
|
||||
|
|
|
|||
|
|
@ -157,6 +157,8 @@ pub fn interpret_page<'a>(
|
|||
device: &mut impl Device<'a>,
|
||||
) {
|
||||
let resources = page.resources();
|
||||
// PdfCraft patch: the page's own contents count against its content budget too.
|
||||
crate::context::charge_content(page.page_stream().map_or(0, <[u8]>::len));
|
||||
interpret(page.typed_operations(), resources, context, device);
|
||||
|
||||
if context.settings.render_annotations
|
||||
|
|
|
|||
|
|
@ -248,7 +248,8 @@ impl<'a> TilingPattern<'a> {
|
|||
self.nesting_depth,
|
||||
);
|
||||
|
||||
let decoded = self.stream.decoded().ok()?;
|
||||
// PdfCraft patch: see `context::MAX_PAGE_CONTENT`.
|
||||
let decoded = crate::context::decode_content(&self.stream)?;
|
||||
let resources = Resources::from_parent(
|
||||
self.stream.dict().get(RESOURCES).unwrap_or_default(),
|
||||
self.parent_resources.clone(),
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ impl<'a> XObject<'a> {
|
|||
}
|
||||
|
||||
pub(crate) struct FormXObject<'a> {
|
||||
pub(crate) decoded: Cow<'a, [u8]>,
|
||||
// PdfCraft patch: counted against the page's content budget while the form is alive.
|
||||
pub(crate) decoded: crate::context::DecodedContent<'a>,
|
||||
pub(crate) matrix: Affine,
|
||||
pub(crate) bbox: [f32; 4],
|
||||
is_transparency_group: bool,
|
||||
|
|
@ -65,7 +66,8 @@ impl<'a> FormXObject<'a> {
|
|||
pub(crate) fn new(stream: &Stream<'a>) -> Option<Self> {
|
||||
let dict = stream.dict();
|
||||
|
||||
let decoded = stream.decoded().ok()?;
|
||||
// PdfCraft patch: see `context::MAX_PAGE_CONTENT`.
|
||||
let decoded = crate::context::decode_content(stream)?;
|
||||
let resources = dict.get::<Dict<'_>>(RESOURCES).unwrap_or_default();
|
||||
|
||||
let matrix = Affine::new(
|
||||
|
|
|
|||
|
|
@ -9,37 +9,51 @@ pub(crate) mod flate {
|
|||
use crate::filter::lzw_flate::{PredictorParams, apply_predictor};
|
||||
use crate::object::Dict;
|
||||
|
||||
/// PdfCraft patch: `limit` is the most bytes inflated (see `filter::decode_limit`).
|
||||
#[cfg(feature = "unsafe")]
|
||||
pub(crate) fn decode(data: &[u8], params: &Dict<'_>) -> Option<Vec<u8>> {
|
||||
pub(crate) fn decode(data: &[u8], params: &Dict<'_>, limit: usize) -> Option<Vec<u8>> {
|
||||
use flate2::read::{DeflateDecoder, ZlibDecoder};
|
||||
use std::io::Read;
|
||||
|
||||
fn zlib_stream(data: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut decoder = ZlibDecoder::new(data);
|
||||
// Read in chunks and never past `limit`, so the buffer doesn't grow past it either.
|
||||
fn read_up_to(mut decoder: impl Read, limit: usize) -> Option<Vec<u8>> {
|
||||
let mut result = Vec::new();
|
||||
decoder.read_to_end(&mut result).ok().map(|_| result)
|
||||
}
|
||||
|
||||
fn deflate_stream(data: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut decoder = DeflateDecoder::new(data);
|
||||
let mut result = Vec::new();
|
||||
decoder.read_to_end(&mut result).ok().map(|_| result)
|
||||
let mut chunk = vec![0; 64 * 1024];
|
||||
while result.len() < limit {
|
||||
let n = decoder.read(&mut chunk).ok()?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
let take = n.min(limit - result.len());
|
||||
result.extend_from_slice(chunk.get(..take)?);
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
let zlib_stream = |data: &[u8]| read_up_to(ZlibDecoder::new(data), limit);
|
||||
let deflate_stream = |data: &[u8]| read_up_to(DeflateDecoder::new(data), limit);
|
||||
|
||||
let decoded = zlib_stream(data)
|
||||
.or_else(|| deflate_stream(data))
|
||||
.or_else(|| {
|
||||
warn!("flate stream is broken, decoding with fallback");
|
||||
|
||||
fallback::decode(data)
|
||||
fallback::decode(data, limit)
|
||||
})?;
|
||||
// PdfCraft patch: output cut at the limit is reported, never dropped silently.
|
||||
if decoded.len() >= limit {
|
||||
warn!("flate stream stopped at its decode limit of {limit} bytes");
|
||||
}
|
||||
let params = PredictorParams::from_params(params);
|
||||
apply_predictor(decoded, ¶ms)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unsafe"))]
|
||||
pub(crate) fn decode(data: &[u8], params: &Dict<'_>) -> Option<Vec<u8>> {
|
||||
let decoded = fallback::decode(data)?;
|
||||
pub(crate) fn decode(data: &[u8], params: &Dict<'_>, limit: usize) -> Option<Vec<u8>> {
|
||||
let decoded = fallback::decode(data, limit)?;
|
||||
// PdfCraft patch: output cut at the limit is reported, never dropped silently.
|
||||
if decoded.len() >= limit {
|
||||
warn!("flate stream stopped at its decode limit of {limit} bytes");
|
||||
}
|
||||
let params = PredictorParams::from_params(params);
|
||||
apply_predictor(decoded, ¶ms)
|
||||
}
|
||||
|
|
@ -50,11 +64,11 @@ pub(crate) mod flate {
|
|||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub(crate) fn decode(data: &[u8]) -> Option<Vec<u8>> {
|
||||
flate_decode(data)
|
||||
pub(crate) fn decode(data: &[u8], limit: usize) -> Option<Vec<u8>> {
|
||||
flate_decode(data, limit)
|
||||
}
|
||||
|
||||
fn flate_decode(data: &[u8]) -> Option<Vec<u8>> {
|
||||
fn flate_decode(data: &[u8], limit: usize) -> Option<Vec<u8>> {
|
||||
if data.len() >= 2 {
|
||||
let cmf = data[0];
|
||||
let flg = data[1];
|
||||
|
|
@ -63,12 +77,12 @@ pub(crate) mod flate {
|
|||
&& ((cmf as u16) << 8 | flg as u16).is_multiple_of(31)
|
||||
&& (flg & 0x20) == 0
|
||||
{
|
||||
let mut stream = FlateStream::new(&data[2..]);
|
||||
let mut stream = FlateStream::new(&data[2..], limit);
|
||||
return stream.decode();
|
||||
}
|
||||
}
|
||||
|
||||
let mut stream = FlateStream::new(data);
|
||||
let mut stream = FlateStream::new(data, limit);
|
||||
stream.decode()
|
||||
}
|
||||
|
||||
|
|
@ -79,10 +93,12 @@ pub(crate) mod flate {
|
|||
code_size: u8,
|
||||
output: Vec<u8>,
|
||||
eof: bool,
|
||||
/// PdfCraft patch: the most bytes inflated (see `filter::decode_limit`).
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
impl<'a> FlateStream<'a> {
|
||||
fn new(data: &'a [u8]) -> Self {
|
||||
fn new(data: &'a [u8], limit: usize) -> Self {
|
||||
FlateStream {
|
||||
data,
|
||||
pos: 0,
|
||||
|
|
@ -90,11 +106,12 @@ pub(crate) mod flate {
|
|||
code_size: 0,
|
||||
output: Vec::new(),
|
||||
eof: false,
|
||||
limit,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode(&mut self) -> Option<Vec<u8>> {
|
||||
while !self.eof && self.pos < self.data.len() {
|
||||
while !self.eof && self.pos < self.data.len() && self.output.len() < self.limit {
|
||||
self.read_block();
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +269,10 @@ pub(crate) mod flate {
|
|||
}
|
||||
} else {
|
||||
let block = self.get_bytes(block_len as usize);
|
||||
self.output.extend_from_slice(&block);
|
||||
// PdfCraft patch: never past `limit`.
|
||||
let left = self.limit.saturating_sub(self.output.len());
|
||||
self.output
|
||||
.extend_from_slice(block.get(..block.len().min(left)).unwrap_or(&[]));
|
||||
if block.len() < block_len as usize {
|
||||
self.eof = true;
|
||||
}
|
||||
|
|
@ -273,6 +293,10 @@ pub(crate) mod flate {
|
|||
};
|
||||
|
||||
loop {
|
||||
// PdfCraft patch: one code adds at most 258 bytes; stop at the limit.
|
||||
if self.output.len() >= self.limit {
|
||||
return;
|
||||
}
|
||||
let code1 = match self.get_code(&lit_code_table) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
|
|
@ -333,6 +357,10 @@ pub(crate) mod flate {
|
|||
// Copy from previous output
|
||||
let start = self.output.len().wrapping_sub(distance);
|
||||
for _ in 0..length {
|
||||
// PdfCraft patch: never past `limit`.
|
||||
if self.output.len() >= self.limit {
|
||||
return;
|
||||
}
|
||||
if start < self.output.len() {
|
||||
let byte = self.output[self.output.len() - distance];
|
||||
self.output.push(byte);
|
||||
|
|
@ -568,11 +596,17 @@ pub(crate) mod lzw {
|
|||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
/// Decode a LZW-encoded stream.
|
||||
pub(crate) fn decode(data: &[u8], params: &Dict<'_>) -> Option<Vec<u8>> {
|
||||
/// Decode a LZW-encoded stream. PdfCraft patch: `limit` is the most bytes produced (see
|
||||
/// `filter::decode_limit`); with a full table every 12-bit code can repeat an entry of
|
||||
/// thousands of bytes.
|
||||
pub(crate) fn decode(data: &[u8], params: &Dict<'_>, limit: usize) -> Option<Vec<u8>> {
|
||||
let params = PredictorParams::from_params(params);
|
||||
|
||||
let decoded = decode_impl(data, params.early_change)?;
|
||||
let decoded = decode_impl(data, params.early_change, limit)?;
|
||||
// PdfCraft patch: output cut at the limit is reported, never dropped silently.
|
||||
if decoded.len() >= limit {
|
||||
warn!("LZW stream stopped at its decode limit of {limit} bytes");
|
||||
}
|
||||
|
||||
apply_predictor(decoded, ¶ms)
|
||||
}
|
||||
|
|
@ -582,7 +616,7 @@ pub(crate) mod lzw {
|
|||
const MAX_ENTRIES: usize = 4096;
|
||||
const INITIAL_SIZE: u16 = 258;
|
||||
|
||||
fn decode_impl(data: &[u8], early_change: bool) -> Option<Vec<u8>> {
|
||||
fn decode_impl(data: &[u8], early_change: bool, limit: usize) -> Option<Vec<u8>> {
|
||||
let mut table = Table::new(early_change);
|
||||
let mut bit_size = table.code_length();
|
||||
let mut reader = BitReader::new(data);
|
||||
|
|
@ -590,6 +624,9 @@ pub(crate) mod lzw {
|
|||
let mut prev = None;
|
||||
|
||||
loop {
|
||||
if decoded.len() >= limit {
|
||||
return Some(decoded);
|
||||
}
|
||||
let next = match reader.read(bit_size) {
|
||||
Some(code) => code as usize,
|
||||
None => {
|
||||
|
|
@ -611,10 +648,12 @@ pub(crate) mod lzw {
|
|||
return None;
|
||||
}
|
||||
|
||||
// PdfCraft patch: never past `limit`.
|
||||
let left = limit.saturating_sub(decoded.len());
|
||||
if new < table.size() {
|
||||
let entry = table.get(new)?;
|
||||
let first_byte = entry[0];
|
||||
decoded.extend_from_slice(entry);
|
||||
decoded.extend_from_slice(entry.get(..entry.len().min(left))?);
|
||||
|
||||
if let Some(prev_code) = prev {
|
||||
table.register(prev_code, first_byte);
|
||||
|
|
@ -625,7 +664,7 @@ pub(crate) mod lzw {
|
|||
let first_byte = prev_entry[0];
|
||||
|
||||
let new_entry = table.register(prev_code, first_byte)?;
|
||||
decoded.extend_from_slice(new_entry);
|
||||
decoded.extend_from_slice(new_entry.get(..new_entry.len().min(left))?);
|
||||
} else {
|
||||
warn!("LZW decode error: code {new} not found and prev is None");
|
||||
return None;
|
||||
|
|
@ -973,7 +1012,7 @@ mod tests {
|
|||
#[test]
|
||||
fn decode_lzw() {
|
||||
let input = [0x80, 0x0B, 0x60, 0x50, 0x22, 0x0C, 0x0C, 0x85, 0x01];
|
||||
let decoded = lzw::decode(&input, &Dict::default()).unwrap();
|
||||
let decoded = lzw::decode(&input, &Dict::default(), usize::MAX).unwrap();
|
||||
|
||||
assert_eq!(decoded, vec![45, 45, 45, 45, 45, 65, 45, 45, 45, 66]);
|
||||
}
|
||||
|
|
@ -984,7 +1023,7 @@ mod tests {
|
|||
0x78, 0x9c, 0xf3, 0x48, 0xcd, 0xc9, 0xc9, 0x7, 0x0, 0x5, 0x8c, 0x1, 0xf5,
|
||||
];
|
||||
|
||||
let decoded = flate::decode(&input, &Dict::default()).unwrap();
|
||||
let decoded = flate::decode(&input, &Dict::default(), usize::MAX).unwrap();
|
||||
assert_eq!(decoded, b"Hello");
|
||||
}
|
||||
|
||||
|
|
@ -992,7 +1031,7 @@ mod tests {
|
|||
fn decode_flate() {
|
||||
let input = [0xf3, 0x48, 0xcd, 0xc9, 0xc9, 0x7, 0x0];
|
||||
|
||||
let decoded = flate::decode(&input, &Dict::default()).unwrap();
|
||||
let decoded = flate::decode(&input, &Dict::default(), usize::MAX).unwrap();
|
||||
assert_eq!(decoded, b"Hello");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,11 +80,13 @@ impl Filter {
|
|||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch: `limit` is the most bytes the filter may produce (see `decode_limit`).
|
||||
pub(crate) fn apply(
|
||||
&self,
|
||||
data: &[u8],
|
||||
params: &Dict<'_>,
|
||||
#[cfg_attr(not(feature = "images"), allow(unused))] image_params: &ImageDecodeParams,
|
||||
limit: usize,
|
||||
) -> Result<FilterResult<'static>, DecodeFailure> {
|
||||
let res = match self {
|
||||
Self::AsciiHexDecode => ascii_hex::decode(data)
|
||||
|
|
@ -93,13 +95,13 @@ impl Filter {
|
|||
Self::Ascii85Decode => ascii_85::decode(data)
|
||||
.map(FilterResult::from_data)
|
||||
.ok_or(DecodeFailure::StreamDecode),
|
||||
Self::RunLengthDecode => run_length::decode(data)
|
||||
Self::RunLengthDecode => run_length::decode(data, limit)
|
||||
.map(FilterResult::from_data)
|
||||
.ok_or(DecodeFailure::StreamDecode),
|
||||
Self::LzwDecode => lzw_flate::lzw::decode(data, params)
|
||||
Self::LzwDecode => lzw_flate::lzw::decode(data, params, limit)
|
||||
.map(FilterResult::from_data)
|
||||
.ok_or(DecodeFailure::StreamDecode),
|
||||
Self::FlateDecode => lzw_flate::flate::decode(data, params)
|
||||
Self::FlateDecode => lzw_flate::flate::decode(data, params, limit)
|
||||
.map(FilterResult::from_data)
|
||||
.ok_or(DecodeFailure::StreamDecode),
|
||||
#[cfg(feature = "images")]
|
||||
|
|
@ -132,6 +134,37 @@ impl Filter {
|
|||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the most bytes Flate, LZW or RunLength may produce for a stream that isn't an
|
||||
/// image of known size: content streams, fonts, forms, ICC profiles. These filters expand without
|
||||
/// limit (Flate about 1000:1), so a 2 MB page that inflated to 2 GB took the renderer past 3 GB in
|
||||
/// under a second. They never write past the limit (the buffer can still grow to under twice
|
||||
/// it). Real streams are far smaller; the same figure bounds lopdf's load-time decoding.
|
||||
pub const MAX_DECODED_STREAM: usize = 256 << 20;
|
||||
|
||||
/// PdfCraft patch: the most bytes they may produce for an image of known size, however large it
|
||||
/// says it is.
|
||||
const MAX_DECODED_IMAGE: u64 = 1 << 30;
|
||||
|
||||
/// PdfCraft patch: the output limit for one filter of a stream decoded with `params`; `last`
|
||||
/// says whether it is the last filter. The last filter of an image whose size, bits per
|
||||
/// component and components are known produces its pixels, plus at most one PNG predictor tag
|
||||
/// byte per pixel byte (a predictor row can be as short as one byte): bytes past that are never
|
||||
/// drawn. Earlier filters produce compressed data, which can be larger than the pixels, and
|
||||
/// everything else gets [`MAX_DECODED_STREAM`]. Decoding stops at the limit and keeps what it
|
||||
/// has.
|
||||
pub(crate) fn decode_limit(params: &ImageDecodeParams, last: bool) -> usize {
|
||||
let (true, Some(bpc), Some(components)) = (last, params.bpc, params.num_components) else {
|
||||
return MAX_DECODED_STREAM;
|
||||
};
|
||||
if params.width == 0 || params.height == 0 {
|
||||
return MAX_DECODED_STREAM;
|
||||
}
|
||||
let bits = u64::from(params.width) * u64::from(components) * u64::from(bpc);
|
||||
let pixels = bits.div_ceil(8).saturating_mul(u64::from(params.height));
|
||||
let image = pixels.saturating_mul(2).min(MAX_DECODED_IMAGE);
|
||||
usize::try_from(image).unwrap_or(MAX_DECODED_STREAM)
|
||||
}
|
||||
|
||||
/// PdfCraft patch: whether a CCITT image of `columns` × `rows` may be decoded. The decoder sizes
|
||||
/// its line buffers from `/Columns`, which a fuzzed file set to 4294967295 (a 4 GiB allocation);
|
||||
/// real fax lines are a few thousand pixels wide.
|
||||
|
|
|
|||
|
|
@ -2,11 +2,17 @@ use crate::reader::Reader;
|
|||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub(crate) fn decode(data: &[u8]) -> Option<Vec<u8>> {
|
||||
/// PdfCraft patch: `limit` is the most bytes produced (see `filter::decode_limit`); a run of 128
|
||||
/// copies costs two input bytes, so a few megabytes could ask for gigabytes.
|
||||
pub(crate) fn decode(data: &[u8], limit: usize) -> Option<Vec<u8>> {
|
||||
let mut reader = Reader::new(data);
|
||||
let mut decoded = vec![];
|
||||
|
||||
loop {
|
||||
let left = limit.saturating_sub(decoded.len());
|
||||
if left == 0 {
|
||||
break;
|
||||
}
|
||||
let length = reader.read_byte()?;
|
||||
|
||||
match length {
|
||||
|
|
@ -17,15 +23,20 @@ pub(crate) fn decode(data: &[u8]) -> Option<Vec<u8>> {
|
|||
break;
|
||||
};
|
||||
|
||||
decoded.extend(bytes);
|
||||
decoded.extend(bytes.iter().take(left));
|
||||
}
|
||||
_ => {
|
||||
let length = 257 - length as usize;
|
||||
decoded.extend([reader.read_byte()?].repeat(length));
|
||||
let length = (257 - length as usize).min(left);
|
||||
let byte = reader.read_byte()?;
|
||||
decoded.extend(core::iter::repeat_n(byte, length));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PdfCraft patch: output cut at the limit is reported, never dropped silently.
|
||||
if decoded.len() >= limit {
|
||||
warn!("run-length stream stopped at its decode limit of {limit} bytes");
|
||||
}
|
||||
Some(decoded)
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +48,7 @@ mod tests {
|
|||
fn run_length() {
|
||||
let input = vec![4, 10, 11, 12, 13, 14, 253, 3, 128];
|
||||
assert_eq!(
|
||||
decode(&input).unwrap(),
|
||||
decode(&input, usize::MAX).unwrap(),
|
||||
vec![10, 11, 12, 13, 14, 3, 3, 3, 3]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,21 +166,44 @@ impl<'a> Stream<'a> {
|
|||
pub fn decoded_image(
|
||||
&self,
|
||||
image_params: &ImageDecodeParams,
|
||||
) -> Result<FilterResult<'a>, DecodeFailure> {
|
||||
self.decode_within(image_params, usize::MAX)
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the decoded data of the stream, for callers that budget what a page
|
||||
/// holds. Flate, LZW and RunLength stop at `max` bytes (and at
|
||||
/// [`crate::filter::MAX_DECODED_STREAM`] in any case). A stream without filters is returned
|
||||
/// as it is; ASCIIHex and ASCII85 barely expand, and the image codecs stop at their own pixel
|
||||
/// limits.
|
||||
pub fn decoded_within(&self, max: usize) -> Result<Cow<'a, [u8]>, DecodeFailure> {
|
||||
self.decode_within(&ImageDecodeParams::default(), max)
|
||||
.map(|r| r.data)
|
||||
}
|
||||
|
||||
fn decode_within(
|
||||
&self,
|
||||
image_params: &ImageDecodeParams,
|
||||
max: usize,
|
||||
) -> Result<FilterResult<'a>, DecodeFailure> {
|
||||
let data = self.raw_data();
|
||||
let filters_and_params = self.filters_and_params();
|
||||
let count = filters_and_params.filters.len();
|
||||
|
||||
let mut current: Option<FilterResult<'a>> = None;
|
||||
|
||||
for (filter, params) in filters_and_params
|
||||
for (i, (filter, params)) in filters_and_params
|
||||
.filters
|
||||
.iter()
|
||||
.zip(filters_and_params.params.iter())
|
||||
.enumerate()
|
||||
{
|
||||
// PdfCraft patch: see `filter::decode_limit`.
|
||||
let limit = crate::filter::decode_limit(image_params, i + 1 == count).min(max);
|
||||
let new = filter.apply(
|
||||
current.as_ref().map(|c| c.data.as_ref()).unwrap_or(&data),
|
||||
params,
|
||||
image_params,
|
||||
limit,
|
||||
)?;
|
||||
current = Some(new);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,7 +251,9 @@ impl<'a> Page<'a> {
|
|||
pub fn page_stream(&self) -> Option<&[u8]> {
|
||||
let convert_single = |s: Stream<'_>| {
|
||||
let data = s.decoded().ok()?;
|
||||
Some(data.to_vec())
|
||||
// PdfCraft patch: take the decoded buffer instead of copying it (up to
|
||||
// `MAX_DECODED_STREAM` twice over).
|
||||
Some(data.into_owned())
|
||||
};
|
||||
|
||||
self.page_streams
|
||||
|
|
@ -259,12 +261,23 @@ impl<'a> Page<'a> {
|
|||
if let Some(stream) = self.inner.get::<Stream<'_>>(CONTENTS) {
|
||||
convert_single(stream)
|
||||
} else if let Some(array) = self.inner.get::<Array<'_>>(CONTENTS) {
|
||||
let streams = array.iter::<Stream<'_>>().flat_map(convert_single);
|
||||
|
||||
let mut collected = vec![];
|
||||
|
||||
for stream in streams {
|
||||
collected.extend(stream);
|
||||
for stream in array.iter::<Stream<'_>>() {
|
||||
// PdfCraft patch: the streams share one `MAX_DECODED_STREAM` (an array
|
||||
// naming one inflating stream twenty times decoded 5 GB).
|
||||
let left =
|
||||
crate::filter::MAX_DECODED_STREAM.saturating_sub(collected.len());
|
||||
if left == 0 {
|
||||
break;
|
||||
}
|
||||
let Ok(data) = stream.decoded_within(left) else {
|
||||
continue;
|
||||
};
|
||||
// Exactly what this stream and its separator need: growing by doubling
|
||||
// here held 1 GB for a 256 MiB stream.
|
||||
collected.reserve_exact(data.len() + 1);
|
||||
collected.extend_from_slice(&data);
|
||||
// Streams must have at least one whitespace in-between.
|
||||
collected.push(b' ');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue