mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 22:41:47 -06:00
Merge fix/jbig2-retained-bitmaps: the run's allowance spends the decoder's work count
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
@@ -3527,6 +3527,788 @@ fn jbig2_regions_of_billions_of_pixels_are_refused() {
|
||||
assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 0, 0, 255], "the rest of the page draws");
|
||||
}
|
||||
|
||||
/// One embedded JBIG2 segment (ISO 14492 §7.2) of page 1. More than four references take the long
|
||||
/// form, whose retention flags are left clear.
|
||||
fn jbig2_segment(out: &mut Vec<u8>, number: u32, kind: u8, refs: &[u8], data: &[u8]) {
|
||||
out.extend_from_slice(&number.to_be_bytes());
|
||||
out.push(kind);
|
||||
if refs.len() <= 4 {
|
||||
out.push(u8::try_from(refs.len()).unwrap() << 5);
|
||||
} else {
|
||||
out.extend_from_slice(&(0xE000_0000u32 | u32::try_from(refs.len()).unwrap()).to_be_bytes());
|
||||
out.extend(std::iter::repeat_n(0u8, (refs.len() + 1).div_ceil(8)));
|
||||
}
|
||||
out.extend_from_slice(refs);
|
||||
out.push(1);
|
||||
out.extend_from_slice(&u32::try_from(data.len()).unwrap().to_be_bytes());
|
||||
out.extend_from_slice(data);
|
||||
}
|
||||
|
||||
/// The page information of a 16 × 2 JBIG2 page whose default pixel is black, so that the image, if
|
||||
/// it decodes, draws black whatever its other segments hold.
|
||||
fn black_jbig2_page() -> Vec<u8> {
|
||||
let mut page = Vec::new();
|
||||
page.extend_from_slice(&16u32.to_be_bytes());
|
||||
page.extend_from_slice(&2u32.to_be_bytes());
|
||||
page.extend_from_slice(&[0; 8]); // resolution
|
||||
page.extend_from_slice(&[4, 0, 0]); // flags (default pixel black), striping
|
||||
let mut stream = Vec::new();
|
||||
jbig2_segment(&mut stream, 0, 48, &[], &page);
|
||||
stream
|
||||
}
|
||||
|
||||
/// A region's information field, at the page's corner.
|
||||
fn jbig2_region_info(width: u32, height: u32) -> Vec<u8> {
|
||||
let mut region = Vec::new();
|
||||
region.extend_from_slice(&width.to_be_bytes());
|
||||
region.extend_from_slice(&height.to_be_bytes());
|
||||
region.extend_from_slice(&[0; 9]); // x, y, combination operator
|
||||
region
|
||||
}
|
||||
|
||||
/// Whether a 16 × 2 JBIG2 image decodes: drawn at 5..25 both ways, it draws black, and refused it
|
||||
/// leaves the page white. The red square beside it shows the rest of the page drew either way.
|
||||
fn jbig2_image_draws(stream: &[u8]) -> bool {
|
||||
jbig2_image_draws_in(stream, "/DeviceGray")
|
||||
}
|
||||
|
||||
/// [`jbig2_image_draws`], for an image in colour space `space`.
|
||||
fn jbig2_image_draws_in(stream: &[u8], space: &str) -> bool {
|
||||
jbig2_image_draws_with(stream, space, "", b"")
|
||||
}
|
||||
|
||||
/// [`jbig2_image_draws_in`], with entries for the image's dictionary and more objects, from 6 up.
|
||||
fn jbig2_image_draws_with(stream: &[u8], space: &str, entries: &str, objects: &[u8]) -> bool {
|
||||
let content = b"q 20 0 0 20 5 5 cm /Im1 Do Q 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 << /Im1 5 0 R >> >> >> endobj\n".to_vec();
|
||||
pdf.extend_from_slice(format!("4 0 obj << /Length {} >> stream\n", content.len()).as_bytes());
|
||||
pdf.extend_from_slice(content);
|
||||
pdf.extend_from_slice(b"\nendstream endobj\n");
|
||||
pdf.extend_from_slice(
|
||||
format!("5 0 obj << /Type /XObject /Subtype /Image /Width 16 /Height 2 /BitsPerComponent 1 /ColorSpace {space} {entries} /Filter /JBIG2Decode /Length {} >> stream\n", stream.len())
|
||||
.as_bytes(),
|
||||
);
|
||||
pdf.extend_from_slice(stream);
|
||||
pdf.extend_from_slice(b"\nendstream endobj\n");
|
||||
pdf.extend_from_slice(objects);
|
||||
pdf.extend_from_slice(b"trailer << /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, ..Default::default() });
|
||||
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");
|
||||
// Inside the image: top-down rows 15..35.
|
||||
match &page.rgba[((25 * 40 + 15) * 4)..][..4] {
|
||||
[0, 0, 0, 255] => true,
|
||||
[255, 255, 255, 255] => false,
|
||||
other => panic!("neither the image nor the page: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Bits written from the top of each byte, as JBIG2's Huffman-coded data is.
|
||||
#[derive(Default)]
|
||||
struct Bits {
|
||||
bytes: Vec<u8>,
|
||||
used: u32,
|
||||
}
|
||||
|
||||
impl Bits {
|
||||
fn put(&mut self, value: u32, count: u32) {
|
||||
for i in (0..count).rev() {
|
||||
if self.used.is_multiple_of(8) {
|
||||
self.bytes.push(0);
|
||||
}
|
||||
if (value >> i) & 1 != 0 {
|
||||
*self.bytes.last_mut().unwrap() |= 0x80 >> (self.used % 8);
|
||||
}
|
||||
self.used += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn align(&mut self) {
|
||||
self.used = self.used.div_ceil(8) * 8;
|
||||
}
|
||||
|
||||
/// `value` in table B.1 (ISO 14492 Annex B).
|
||||
fn table_b1(&mut self, value: u32) {
|
||||
match value {
|
||||
0..=15 => {
|
||||
self.put(0, 1);
|
||||
self.put(value, 4);
|
||||
}
|
||||
16..=271 => {
|
||||
self.put(0b10, 2);
|
||||
self.put(value - 16, 8);
|
||||
}
|
||||
272..=65807 => {
|
||||
self.put(0b110, 3);
|
||||
self.put(value - 272, 16);
|
||||
}
|
||||
_ => {
|
||||
self.put(0b111, 3);
|
||||
self.put(value - 65808, 32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Huffman-coded symbol dictionary of `symbols` symbols of width 0 and height 1, exported or not:
|
||||
/// each holds no pixels, and costs a bitmap's header all the same.
|
||||
fn zero_width_jbig2_dictionary(symbols: u32, exported: bool) -> Vec<u8> {
|
||||
let mut dictionary = vec![0x00, 0x01]; // Huffman, tables B.4, B.2, B.1, B.1
|
||||
dictionary.extend_from_slice(&(if exported { symbols } else { 0 }).to_be_bytes());
|
||||
dictionary.extend_from_slice(&symbols.to_be_bytes()); // new
|
||||
let mut bits = Bits::default();
|
||||
bits.put(0, 1); // height class delta 1 (B.4)
|
||||
for _ in 0..symbols {
|
||||
bits.put(0, 1); // width delta 0 (B.2)
|
||||
}
|
||||
bits.put(0b111111, 6); // end of the class (B.2)
|
||||
bits.put(0, 5); // BMSIZE 0, uncompressed (B.1)
|
||||
bits.align();
|
||||
if exported {
|
||||
bits.table_b1(0); // the first export run isn't exported
|
||||
}
|
||||
bits.table_b1(symbols);
|
||||
dictionary.extend_from_slice(&bits.bytes);
|
||||
dictionary
|
||||
}
|
||||
|
||||
/// A Huffman-coded symbol dictionary of `symbols` white symbols of width 1 and `height` rows, none
|
||||
/// exported, for `height` from 76 up.
|
||||
fn narrow_jbig2_dictionary(symbols: u32, height: u32) -> Vec<u8> {
|
||||
let mut bits = Bits::default();
|
||||
bits.put(0b11111, 5); // height class delta, the upper line of table B.4
|
||||
bits.put(height - 76, 32);
|
||||
bits.put(0b10, 2); // width delta 1 (B.2)
|
||||
for _ in 1..symbols {
|
||||
bits.put(0, 1); // width delta 0 (B.2)
|
||||
}
|
||||
bits.put(0b111111, 6); // end of the class (B.2)
|
||||
let rows = height.div_ceil(8) + 1; // an MMR-coded white bitmap: one bit a row
|
||||
bits.table_b1(rows); // BMSIZE (B.1)
|
||||
bits.align();
|
||||
for _ in 0..rows {
|
||||
bits.put(0xFF, 8);
|
||||
}
|
||||
bits.table_b1(symbols); // the first export run, not exported, is all of them
|
||||
let mut dictionary = vec![0x00, 0x01]; // Huffman, tables B.4, B.2, B.1, B.1
|
||||
dictionary.extend_from_slice(&0u32.to_be_bytes()); // exported
|
||||
dictionary.extend_from_slice(&symbols.to_be_bytes()); // new
|
||||
dictionary.extend_from_slice(&bits.bytes);
|
||||
dictionary
|
||||
}
|
||||
|
||||
/// An intermediate generic region, white, every row typical: it decodes in a few bytes however
|
||||
/// large it is.
|
||||
fn jbig2_white_region(width: u32, height: u32) -> Vec<u8> {
|
||||
let mut region = jbig2_region_info(width, height);
|
||||
region.push(0x08); // arithmetic coding, template 0, typical prediction
|
||||
region.extend_from_slice(&[3, 0xff, 0xfd, 0xff, 2, 0xfe, 0xfe, 0xfe]); // AT pixels
|
||||
// Every row typical, the first white: pdfcraft-filters' coding of a white 16,384 square.
|
||||
region.extend_from_slice(&[0xb6, 0xc2, 0x1f, 0xff, 0x7f, 0xff, 0xac]);
|
||||
region
|
||||
}
|
||||
|
||||
/// A pattern dictionary of `patterns` patterns of width 1 and `height` rows, with nothing coded.
|
||||
fn jbig2_pattern_dictionary(height: u8, patterns: u32) -> Vec<u8> {
|
||||
let mut dictionary = vec![0, 1, height];
|
||||
dictionary.extend_from_slice(&(patterns - 1).to_be_bytes());
|
||||
dictionary
|
||||
}
|
||||
|
||||
/// A 1 × 1 halftone region over a grid of `width` × `height` cells, all at the region's corner,
|
||||
/// coded by MMR or arithmetically.
|
||||
fn jbig2_halftone(width: u32, height: u32, mmr: bool) -> Vec<u8> {
|
||||
jbig2_halftone_of(1, width, height, mmr)
|
||||
}
|
||||
|
||||
/// [`jbig2_halftone`] for a square region of `side` pixels.
|
||||
fn jbig2_halftone_of(side: u32, width: u32, height: u32, mmr: bool) -> Vec<u8> {
|
||||
let mut halftone = jbig2_region_info(side, side);
|
||||
halftone.push(u8::from(mmr)); // template 0
|
||||
halftone.extend_from_slice(&width.to_be_bytes());
|
||||
halftone.extend_from_slice(&height.to_be_bytes());
|
||||
halftone.extend_from_slice(&[0; 12]); // grid offset and vector
|
||||
halftone
|
||||
}
|
||||
|
||||
/// Each JBIG2 segment's bitmaps are bounded, but nothing bounded what a page's segments hold for
|
||||
/// later ones: a stream of a few dozen bytes an intermediate region (16,384 samples a side, every
|
||||
/// row typical) held 33 MB each, so a few kilobytes could exhaust memory. Vendored hayro-jbig2
|
||||
/// patch: what regions, dictionaries and tables hold is at most 64 MiB (`MAX_RETAINED_WORDS`), two
|
||||
/// such squares, which are exactly the bound.
|
||||
#[test]
|
||||
fn jbig2_regions_held_past_a_pages_bound_are_refused() {
|
||||
let stream = |regions: u32| {
|
||||
let mut stream = black_jbig2_page();
|
||||
for number in 1..=regions {
|
||||
jbig2_segment(&mut stream, number, 36, &[], &jbig2_white_region(16384, 16384));
|
||||
}
|
||||
stream
|
||||
};
|
||||
assert!(jbig2_image_draws(&stream(2)), "two such squares fit");
|
||||
assert!(!jbig2_image_draws(&stream(3)), "a third doesn't");
|
||||
}
|
||||
|
||||
/// A symbol of width 0 holds no pixels, and still costs a bitmap's header: 5 KB of dictionaries of
|
||||
/// them held 781 MB, with the bound counting pixels only.
|
||||
#[test]
|
||||
fn jbig2_symbols_without_pixels_count_towards_a_pages_bound() {
|
||||
let stream = |dictionaries: u32| {
|
||||
let mut stream = black_jbig2_page();
|
||||
let dictionary = zero_width_jbig2_dictionary(32767, true);
|
||||
for number in 1..=dictionaries {
|
||||
jbig2_segment(&mut stream, number, 0, &[], &dictionary);
|
||||
}
|
||||
stream
|
||||
};
|
||||
assert!(jbig2_image_draws(&stream(1)), "one dictionary fits");
|
||||
assert!(!jbig2_image_draws(&stream(44)), "44 × 32,767 headers are past the bound");
|
||||
}
|
||||
|
||||
/// A symbol dictionary may retain its coding contexts for a later one, 73,728 bytes each time, and
|
||||
/// 1.1 MB of dictionaries with no symbols, 103 KB behind Flate, held 2.89 GB.
|
||||
#[test]
|
||||
fn jbig2_retained_contexts_count_towards_a_pages_bound() {
|
||||
let stream = |dictionaries: u32| {
|
||||
let mut dictionary = vec![0x02, 0x00]; // arithmetic coding, contexts retained
|
||||
dictionary.extend_from_slice(&[3, 0xff, 0xfd, 0xff, 2, 0xfe, 0xfe, 0xfe]); // AT pixels
|
||||
dictionary.extend_from_slice(&[0; 8]); // exported, new
|
||||
let mut stream = black_jbig2_page();
|
||||
for number in 1..=dictionaries {
|
||||
jbig2_segment(&mut stream, number, 0, &[], &dictionary);
|
||||
}
|
||||
stream
|
||||
};
|
||||
assert!(jbig2_image_draws(&stream(1)), "one dictionary fits");
|
||||
assert!(!jbig2_image_draws(&stream(1000)), "a thousand don't");
|
||||
}
|
||||
|
||||
/// Huffman tables are held for later segments too, each taking some 586 bytes however small it
|
||||
/// is, so 200,000 of them are past the bound, and they were held to it at 4 times that.
|
||||
#[test]
|
||||
fn jbig2_huffman_tables_count_towards_a_pages_bound() {
|
||||
let stream = |tables: u32| {
|
||||
// One-bit prefix and range lengths, values 0 to 1: a table line, then the lower range.
|
||||
let mut table = vec![0];
|
||||
table.extend_from_slice(&0i32.to_be_bytes());
|
||||
table.extend_from_slice(&1i32.to_be_bytes());
|
||||
table.push(0b1010_0000);
|
||||
let mut stream = black_jbig2_page();
|
||||
for number in 1..=tables {
|
||||
jbig2_segment(&mut stream, number, 53, &[], &table);
|
||||
}
|
||||
stream
|
||||
};
|
||||
assert!(jbig2_image_draws(&stream(1)), "one table fits");
|
||||
assert!(!jbig2_image_draws(&stream(200_000)), "200,000 don't");
|
||||
}
|
||||
|
||||
/// New symbols count as they are decoded, whatever the dictionary exports: rows take a word at
|
||||
/// least, so symbols of one column held 270 MiB within the bound on a dictionary's pixels, and
|
||||
/// exporting none of them they were never counted at all.
|
||||
#[test]
|
||||
fn jbig2_new_symbols_count_towards_a_pages_bound() {
|
||||
let stream = |height: u32| {
|
||||
let mut stream = black_jbig2_page();
|
||||
jbig2_segment(&mut stream, 1, 0, &[], &narrow_jbig2_dictionary(65534, height));
|
||||
stream
|
||||
};
|
||||
assert!(jbig2_image_draws(&stream(100)), "65,534 symbols of 100 rows fit");
|
||||
assert!(!jbig2_image_draws(&stream(300)), "of 300 rows they don't");
|
||||
}
|
||||
|
||||
/// New symbols are held to what the page has left, headers included, not to the whole bound. These
|
||||
/// two regions leave it 512 words, and the dictionary after them exports nothing, so only that
|
||||
/// check can refuse it.
|
||||
#[test]
|
||||
fn jbig2_new_symbols_are_held_to_what_a_page_has_left() {
|
||||
let stream = |dictionary: &[u8]| {
|
||||
let mut stream = black_jbig2_page();
|
||||
jbig2_segment(&mut stream, 1, 36, &[], &jbig2_white_region(16384, 16384));
|
||||
jbig2_segment(&mut stream, 2, 36, &[], &jbig2_white_region(16384, 16383));
|
||||
jbig2_segment(&mut stream, 3, 0, &[], dictionary);
|
||||
stream
|
||||
};
|
||||
assert!(jbig2_image_draws(&stream(&narrow_jbig2_dictionary(1, 100))), "100 rows fit");
|
||||
assert!(!jbig2_image_draws(&stream(&narrow_jbig2_dictionary(1, 1000))), "1,000 rows don't");
|
||||
assert!(jbig2_image_draws(&stream(&zero_width_jbig2_dictionary(10, false))), "10 headers fit");
|
||||
assert!(!jbig2_image_draws(&stream(&zero_width_jbig2_dictionary(100, false))), "100 headers don't");
|
||||
}
|
||||
|
||||
/// Pattern dictionaries count towards the bound, with no shifted copies to do it for them: at 44
|
||||
/// words a pattern, 6 of these are past it.
|
||||
#[test]
|
||||
fn jbig2_pattern_dictionaries_count_towards_a_pages_bound() {
|
||||
let stream = |dictionaries: u32| {
|
||||
let mut stream = black_jbig2_page();
|
||||
for number in 1..=dictionaries {
|
||||
jbig2_segment(&mut stream, number, 16, &[], &jbig2_pattern_dictionary(32, 65535));
|
||||
}
|
||||
stream
|
||||
};
|
||||
assert!(jbig2_image_draws(&stream(5)), "5 fit");
|
||||
assert!(!jbig2_image_draws(&stream(6)), "6 don't");
|
||||
}
|
||||
|
||||
/// Shifted copies are only a speed-up, so they are never what gets an image refused: each of these
|
||||
/// tables is within its own limit, and the fourth won't fit what the page has left, so it is left
|
||||
/// out, where counting it refused the image.
|
||||
#[test]
|
||||
fn jbig2_shifted_patterns_never_refuse_an_image() {
|
||||
let mut stream = black_jbig2_page();
|
||||
for number in 1..=4 {
|
||||
jbig2_segment(&mut stream, number, 16, &[], &jbig2_pattern_dictionary(32, 2048));
|
||||
}
|
||||
assert!(jbig2_image_draws(&stream));
|
||||
}
|
||||
|
||||
/// Ordinary globals still reach the decoder, with a filter on them or without: here they hold the
|
||||
/// page information, so the image draws only if they are read.
|
||||
#[test]
|
||||
fn jbig2_globals_are_read() {
|
||||
let globals = black_jbig2_page();
|
||||
let hex: String = globals.iter().map(|b| format!("{b:02x}")).collect();
|
||||
let mut stream = Vec::new();
|
||||
jbig2_segment(&mut stream, 1, 62, &[], &[]); // an extension, which decoders skip
|
||||
for (filter, data) in [("", globals.clone()), ("/Filter /ASCIIHexDecode", hex.into_bytes())] {
|
||||
let mut objects = format!("6 0 obj << {filter} /Length {} >> stream\n", data.len()).into_bytes();
|
||||
objects.extend_from_slice(&data);
|
||||
objects.extend_from_slice(b"\nendstream endobj\n");
|
||||
assert!(jbig2_image_draws_with(&stream, "/DeviceGray", "/DecodeParms << /JBIG2Globals 6 0 R >>", &objects), "{filter}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Globals are raw segments, but a `/JBIG2Globals` stream could itself be JBIG2, naming the next
|
||||
/// such stream, and each read decoded the next: 4,000 of them, here on a test thread's 2 MiB stack,
|
||||
/// overflowed it, which aborts the process. Vendored hayro-syntax patch: such a stream reads as
|
||||
/// globals that can't be decoded, so the image decodes without them.
|
||||
#[test]
|
||||
fn jbig2_globals_that_are_jbig2_themselves_are_not_decoded() {
|
||||
let links = 4000;
|
||||
let mut objects = Vec::new();
|
||||
for number in 0..links {
|
||||
let link = 6 + number;
|
||||
let next = link + 1;
|
||||
let parms = if number + 1 < links { format!("/DecodeParms << /JBIG2Globals {next} 0 R >>") } else { String::new() };
|
||||
objects.extend_from_slice(format!("{link} 0 obj << /Filter /JBIG2Decode {parms} /Length 0 >> stream\n\nendstream endobj\n").as_bytes());
|
||||
}
|
||||
assert!(jbig2_image_draws_with(&black_jbig2_page(), "/DeviceGray", "/DecodeParms << /JBIG2Globals 6 0 R >>", &objects));
|
||||
}
|
||||
|
||||
/// A Huffman-coded symbol dictionary of one white symbol, `side` from 76 up, exported or not.
|
||||
fn square_jbig2_dictionary(side: u32, exported: bool) -> Vec<u8> {
|
||||
let mut bits = Bits::default();
|
||||
bits.put(0b11111, 5); // height class delta, the upper line of table B.4
|
||||
bits.put(side - 76, 32);
|
||||
bits.put(0b111110, 6); // width delta, the upper line of table B.2
|
||||
bits.put(side - 75, 32);
|
||||
bits.put(0b111111, 6); // end of the class (B.2)
|
||||
let bytes = side.div_ceil(8) + 1; // an MMR-coded white bitmap: one bit a row
|
||||
bits.table_b1(bytes); // BMSIZE (B.1)
|
||||
bits.align();
|
||||
for _ in 0..bytes {
|
||||
bits.put(0xFF, 8);
|
||||
}
|
||||
if exported {
|
||||
bits.table_b1(0);
|
||||
}
|
||||
bits.table_b1(1);
|
||||
let mut dictionary = vec![0x00, 0x01]; // Huffman, tables B.4, B.2, B.1, B.1
|
||||
dictionary.extend_from_slice(&u32::from(exported).to_be_bytes());
|
||||
dictionary.extend_from_slice(&1u32.to_be_bytes()); // new
|
||||
dictionary.extend_from_slice(&bits.bytes);
|
||||
dictionary
|
||||
}
|
||||
|
||||
/// A text region of `width` × `height` placing symbol 0 of one, Huffman-coded, `instances` of 0 or 1.
|
||||
fn jbig2_text_region(width: u32, height: u32, instances: u32) -> Vec<u8> {
|
||||
let mut text = jbig2_region_info(width, height);
|
||||
text.extend_from_slice(&[0x00, 0x11, 0, 0]); // Huffman, top left; tables B.6, B.8, B.11
|
||||
text.extend_from_slice(&instances.to_be_bytes());
|
||||
let mut bits = Bits::default();
|
||||
for code in 0..35 {
|
||||
bits.put(u32::from(code == 1), 4); // only run code 1, one bit long
|
||||
}
|
||||
bits.put(0, 1); // symbol 0 is one bit long
|
||||
bits.align();
|
||||
bits.put(0, 1); // the first strip's T (B.11)
|
||||
if instances > 0 {
|
||||
bits.put(0, 1); // delta T 1 (B.11)
|
||||
bits.put(0, 9); // first S 0 (B.6)
|
||||
bits.put(0, 1); // symbol 0
|
||||
bits.put(0b01, 2); // end of the strip (B.8)
|
||||
}
|
||||
text.extend_from_slice(&bits.bytes);
|
||||
text
|
||||
}
|
||||
|
||||
/// A 1 × 1 text region placing one instance of symbol 0 of one, refined, Huffman-coded.
|
||||
fn jbig2_refined_text_region() -> Vec<u8> {
|
||||
let mut text = jbig2_region_info(1, 1);
|
||||
text.extend_from_slice(&[0x80, 0x13, 0, 0]); // Huffman, refined by template 1, top left
|
||||
text.extend_from_slice(&1u32.to_be_bytes());
|
||||
let mut bits = Bits::default();
|
||||
for code in 0..35 {
|
||||
bits.put(u32::from(code == 1), 4); // only run code 1, one bit long
|
||||
}
|
||||
bits.put(0, 1); // symbol 0 is one bit long
|
||||
bits.align();
|
||||
bits.put(0, 1); // the first strip's T (B.11)
|
||||
bits.put(0, 1); // delta T 1 (B.11)
|
||||
bits.put(0, 9); // first S 0 (B.6)
|
||||
bits.put(0, 1); // symbol 0
|
||||
bits.put(1, 1); // refined
|
||||
bits.put(0, 4); // no change of width, height or offset (B.14)
|
||||
bits.table_b1(0); // no refinement data (B.1)
|
||||
bits.align();
|
||||
bits.put(0b01, 2); // end of the strip (B.8)
|
||||
text.extend_from_slice(&bits.bytes);
|
||||
text
|
||||
}
|
||||
|
||||
/// A generic refinement region of 1 × 1, with whatever its data decodes to.
|
||||
fn jbig2_refinement_region() -> Vec<u8> {
|
||||
let mut region = jbig2_region_info(1, 1);
|
||||
region.push(1); // template 1, no typical prediction
|
||||
region.extend_from_slice(&[0; 4]);
|
||||
region
|
||||
}
|
||||
|
||||
/// Each item a JBIG2 stream decodes costs its work, and the stream may ask for 2^30: four of the
|
||||
/// largest pages, some 6 s to 17 s. Each bitmap is bounded on its own, but nothing bounded how many
|
||||
/// there were, so 1,140 bytes of generic regions took over 40 s, and a 795-byte PDF's one halftone
|
||||
/// 265 s. Vendored hayro-jbig2 patch: `MAX_WORK`. A row costs 4 pixels, even in a bitmap of no
|
||||
/// width, and a copy goes by the word and the row, so a tall thin one costs its rows. Here regions
|
||||
/// of typical rows, cheap to decode though charged like any, spend all but exactly what the item
|
||||
/// costs, so it draws with that left and is refused with one less, for each item at its own cost.
|
||||
#[test]
|
||||
fn jbig2_decoding_work_is_counted_item_by_item() {
|
||||
struct Item {
|
||||
what: &'static str,
|
||||
/// What comes first, and what it costs.
|
||||
before: Vec<(u8, Vec<u8>)>,
|
||||
spent: u64,
|
||||
kind: u8,
|
||||
refs: &'static [u8],
|
||||
data: Vec<u8>,
|
||||
cost: u64,
|
||||
}
|
||||
let stream = |item: &Item, left: u64| {
|
||||
let mut stream = black_jbig2_page();
|
||||
let mut number = 0;
|
||||
for (kind, data) in &item.before {
|
||||
number += 1;
|
||||
jbig2_segment(&mut stream, number, *kind, &[], data);
|
||||
}
|
||||
// A square costs 16,384 rows of 16,388, and a single row what it has and 4 more.
|
||||
let work = (1u64 << 30) - item.spent - left;
|
||||
let (row, square) = (16388u64, 16384 * 16388u64);
|
||||
let mut squares = work / square;
|
||||
let mut rest = work % square;
|
||||
if rest > 0 && rest < 5 {
|
||||
squares -= 1;
|
||||
rest += square;
|
||||
}
|
||||
let mut rows = rest / row;
|
||||
let mut last = rest % row;
|
||||
if last > 0 && last < 5 {
|
||||
rows -= 1;
|
||||
last += row;
|
||||
}
|
||||
let regions = std::iter::repeat_n((16384, 16384), usize::try_from(squares).unwrap())
|
||||
.chain((rows > 0).then_some((16384, rows)))
|
||||
.chain((last > 0).then_some((last - 4, 1)));
|
||||
for (width, height) in regions {
|
||||
number += 1;
|
||||
let region = jbig2_white_region(u32::try_from(width).unwrap(), u32::try_from(height).unwrap());
|
||||
jbig2_segment(&mut stream, number, 38, &[], ®ion); // immediate generic region
|
||||
}
|
||||
jbig2_segment(&mut stream, number + 1, item.kind, item.refs, &item.data);
|
||||
stream
|
||||
};
|
||||
let symbol = || vec![(0, zero_width_jbig2_dictionary(1, true))];
|
||||
let patterns = || vec![(16, jbig2_pattern_dictionary(1, 2))];
|
||||
let exporting_none = {
|
||||
let mut dictionary = vec![0x00, 0x01];
|
||||
dictionary.extend_from_slice(&[0; 8]); // exported, new
|
||||
let mut bits = Bits::default();
|
||||
bits.table_b1(1); // the first export run, not exported, is the one it refers to
|
||||
dictionary.extend_from_slice(&bits.bytes);
|
||||
dictionary
|
||||
};
|
||||
let zero = |height: u32| {
|
||||
let mut bits = Bits::default();
|
||||
bits.put(0b11111, 5); // height class delta, the upper line of table B.4
|
||||
bits.put(height - 76, 32);
|
||||
bits.put(0, 1); // width delta 0 (B.2)
|
||||
bits.put(0b111111, 6); // end of the class (B.2)
|
||||
bits.table_b1(0); // BMSIZE 0, stored (B.1)
|
||||
bits.align();
|
||||
bits.table_b1(1); // the first export run, not exported, is the symbol
|
||||
let mut dictionary = vec![0x00, 0x01];
|
||||
dictionary.extend_from_slice(&0u32.to_be_bytes()); // exported
|
||||
dictionary.extend_from_slice(&1u32.to_be_bytes()); // new
|
||||
dictionary.extend_from_slice(&bits.bytes);
|
||||
dictionary
|
||||
};
|
||||
// The cost of a pattern dictionary of two patterns: rows of them side by side, then each taken out.
|
||||
let two_patterns = |width: u64, height: u64| height * (2 * width + 4) + 2 * height * (width.div_ceil(32) + 1);
|
||||
let thin_region = |side: u32| {
|
||||
let mut region = jbig2_white_region(1, 1);
|
||||
region.splice(0..8, [0u32.to_be_bytes(), side.to_be_bytes()].concat());
|
||||
region
|
||||
};
|
||||
let items = vec![
|
||||
Item { what: "immediate generic", before: vec![], spent: 0, kind: 38, refs: &[], data: jbig2_white_region(1, 1), cost: 5 },
|
||||
Item { what: "intermediate generic", before: vec![], spent: 0, kind: 36, refs: &[], data: jbig2_white_region(1, 1), cost: 5 },
|
||||
Item { what: "a region of no width", before: vec![], spent: 0, kind: 38, refs: &[], data: thin_region(1000), cost: 4000 },
|
||||
Item { what: "immediate refinement", before: vec![], spent: 0, kind: 42, refs: &[], data: jbig2_refinement_region(), cost: 5 },
|
||||
Item { what: "intermediate refinement", before: vec![], spent: 0, kind: 40, refs: &[], data: jbig2_refinement_region(), cost: 5 },
|
||||
Item {
|
||||
what: "pattern dictionary",
|
||||
before: vec![],
|
||||
spent: 0,
|
||||
kind: 16,
|
||||
refs: &[],
|
||||
data: jbig2_pattern_dictionary(1, 2),
|
||||
cost: two_patterns(1, 1),
|
||||
},
|
||||
Item {
|
||||
what: "tall patterns",
|
||||
before: vec![],
|
||||
spent: 0,
|
||||
kind: 16,
|
||||
refs: &[],
|
||||
data: jbig2_pattern_dictionary(32, 2),
|
||||
cost: two_patterns(1, 32),
|
||||
},
|
||||
// Rows cost in a symbol of no pixels too.
|
||||
Item { what: "symbols without pixels", before: vec![], spent: 0, kind: 0, refs: &[], data: zero_width_jbig2_dictionary(3, false), cost: 12 },
|
||||
Item { what: "a tall symbol of no width", before: vec![], spent: 0, kind: 0, refs: &[], data: zero(1000), cost: 4000 },
|
||||
Item { what: "new symbols", before: vec![], spent: 0, kind: 0, refs: &[], data: square_jbig2_dictionary(80, false), cost: 6720 },
|
||||
Item { what: "referred symbols", before: symbol(), spent: 4, kind: 0, refs: &[1], data: exporting_none, cost: 1 },
|
||||
// Its symbol, and its one row and word; Huffman-coded, a table line for the symbol.
|
||||
Item { what: "immediate text", before: symbol(), spent: 4, kind: 6, refs: &[1], data: jbig2_text_region(1, 1, 0), cost: 13 },
|
||||
Item { what: "intermediate text", before: symbol(), spent: 4, kind: 4, refs: &[1], data: jbig2_text_region(1, 1, 0), cost: 13 },
|
||||
Item { what: "a text region's pixels", before: symbol(), spent: 4, kind: 6, refs: &[1], data: jbig2_text_region(32, 32, 0), cost: 75 },
|
||||
Item {
|
||||
what: "an intermediate text region's pixels",
|
||||
before: symbol(),
|
||||
spent: 4,
|
||||
kind: 4,
|
||||
refs: &[1],
|
||||
data: jbig2_text_region(32, 32, 0),
|
||||
cost: 75,
|
||||
},
|
||||
// The region's 13, a copy of the symbol's row and the round of the loop.
|
||||
Item { what: "an instance", before: symbol(), spent: 4, kind: 6, refs: &[1], data: jbig2_text_region(1, 1, 1), cost: 15 },
|
||||
// The region's 13, 80 rows of 4 words copied, the round of the loop.
|
||||
Item {
|
||||
what: "an instance of a large symbol",
|
||||
before: vec![(0, square_jbig2_dictionary(80, true))],
|
||||
spent: 6720,
|
||||
kind: 6,
|
||||
refs: &[1],
|
||||
data: jbig2_text_region(1, 1, 1),
|
||||
cost: 334,
|
||||
},
|
||||
// And decoded as well as copied.
|
||||
Item {
|
||||
what: "a refined instance",
|
||||
before: vec![(0, square_jbig2_dictionary(80, true))],
|
||||
spent: 6720,
|
||||
kind: 6,
|
||||
refs: &[1],
|
||||
data: jbig2_refined_text_region(),
|
||||
cost: 7054,
|
||||
},
|
||||
// A bit plane, a cell's copy and the region's.
|
||||
Item {
|
||||
what: "immediate halftone",
|
||||
before: patterns(),
|
||||
spent: two_patterns(1, 1),
|
||||
kind: 22,
|
||||
refs: &[1],
|
||||
data: jbig2_halftone(1, 1, false),
|
||||
cost: 9,
|
||||
},
|
||||
Item {
|
||||
what: "intermediate halftone",
|
||||
before: patterns(),
|
||||
spent: two_patterns(1, 1),
|
||||
kind: 20,
|
||||
refs: &[1],
|
||||
data: jbig2_halftone(1, 1, false),
|
||||
cost: 9,
|
||||
},
|
||||
// A plane of 2 cells, each a copy of 32 rows of 2 words' cost, and the region's.
|
||||
Item {
|
||||
what: "a halftone's cells",
|
||||
before: vec![(16, jbig2_pattern_dictionary(32, 2))],
|
||||
spent: two_patterns(1, 32),
|
||||
kind: 22,
|
||||
refs: &[1],
|
||||
data: jbig2_halftone(2, 1, false),
|
||||
cost: 136,
|
||||
},
|
||||
Item {
|
||||
what: "a grid of no width",
|
||||
before: patterns(),
|
||||
spent: two_patterns(1, 1),
|
||||
kind: 22,
|
||||
refs: &[1],
|
||||
data: jbig2_halftone(0, 1000, false),
|
||||
cost: 4002,
|
||||
},
|
||||
Item {
|
||||
what: "a halftone's pixels",
|
||||
before: patterns(),
|
||||
spent: two_patterns(1, 1),
|
||||
kind: 22,
|
||||
refs: &[1],
|
||||
data: jbig2_halftone_of(32, 1, 1, false),
|
||||
cost: 71,
|
||||
},
|
||||
];
|
||||
for item in &items {
|
||||
assert!(jbig2_image_draws(&stream(item, item.cost)), "{}: with its cost left it draws", item.what);
|
||||
assert!(!jbig2_image_draws(&stream(item, item.cost - 1)), "{}: with one less it's refused", item.what);
|
||||
}
|
||||
}
|
||||
|
||||
/// An image's page header can claim any size, and the Indexed path reserves its output from it
|
||||
/// before the page bitmap is sized: a 743-byte PDF asked for 137 GB, which aborts the process.
|
||||
/// Vendored hayro-jbig2 patch: a page past the bounds is refused when the stream is opened.
|
||||
#[test]
|
||||
fn jbig2_pages_past_the_bounds_are_refused_before_anything_is_sized_from_them() {
|
||||
let indexed = "[/Indexed /DeviceGray 1 <00FF>]";
|
||||
assert!(jbig2_image_draws_in(&black_jbig2_page(), indexed), "a page within the bounds");
|
||||
let mut page = Vec::new();
|
||||
page.extend_from_slice(&(1u32 << 20).to_be_bytes());
|
||||
page.extend_from_slice(&(1u32 << 20).to_be_bytes());
|
||||
page.extend_from_slice(&[0; 11]); // resolution, flags, striping
|
||||
let mut stream = Vec::new();
|
||||
jbig2_segment(&mut stream, 0, 48, &[], &page);
|
||||
assert!(!jbig2_image_draws_in(&stream, indexed), "the page draws without it");
|
||||
}
|
||||
|
||||
/// One custom Huffman table could grow without bound as it was read, before anything counted it:
|
||||
/// a line takes 2 bits of input and 8 bytes here, so 250 MiB behind nested Flate, in a 1,297-byte
|
||||
/// PDF, was killed at 8 GiB. Vendored hayro-jbig2 patch: at most 2^16 lines.
|
||||
#[test]
|
||||
fn jbig2_huffman_tables_of_too_many_lines_are_refused() {
|
||||
let stream = |lines: i32| {
|
||||
// One-bit prefix and range lengths, all zero: a line per value.
|
||||
let mut table = vec![0];
|
||||
table.extend_from_slice(&0i32.to_be_bytes());
|
||||
table.extend_from_slice(&lines.to_be_bytes());
|
||||
table.resize(table.len() + usize::try_from(lines).unwrap().div_ceil(4) + 1, 0);
|
||||
let mut stream = black_jbig2_page();
|
||||
jbig2_segment(&mut stream, 1, 53, &[], &table);
|
||||
stream
|
||||
};
|
||||
assert!(jbig2_image_draws(&stream(1 << 16)), "2^16 lines are allowed");
|
||||
assert!(!jbig2_image_draws(&stream((1 << 16) + 1)), "one more isn't");
|
||||
}
|
||||
|
||||
/// A stream's segments are all parsed and sorted before the first is decoded: 11 bytes of input took
|
||||
/// 64 here, so behind nested Flate a 4,765-byte PDF reached 5 GB, and failed only afterwards.
|
||||
/// Vendored hayro-jbig2 patch: at most 2^20 segments.
|
||||
#[test]
|
||||
fn jbig2_streams_of_too_many_segments_are_refused() {
|
||||
let stream = |segments: u32| {
|
||||
let mut stream = black_jbig2_page();
|
||||
for number in 1..segments {
|
||||
jbig2_segment(&mut stream, number, 62, &[], &[]); // an extension, which decoders skip
|
||||
}
|
||||
stream
|
||||
};
|
||||
assert!(jbig2_image_draws(&stream(1 << 20)), "2^20 segments are allowed");
|
||||
assert!(!jbig2_image_draws(&stream((1 << 20) + 1)), "one more isn't");
|
||||
}
|
||||
|
||||
/// References take 4 bytes each for 1 of input, a stream's total as much as a segment's.
|
||||
/// Vendored hayro-jbig2 patch: at most 2^22 in a stream, which is 64 × 65,535 + 64.
|
||||
#[test]
|
||||
fn jbig2_streams_of_too_many_references_are_refused() {
|
||||
let stream = |last: usize| {
|
||||
let mut stream = black_jbig2_page();
|
||||
for number in 1..=64 {
|
||||
jbig2_segment(&mut stream, number, 62, &vec![0u8; 65535], &[]);
|
||||
}
|
||||
jbig2_segment(&mut stream, 65, 62, &vec![0u8; last], &[]);
|
||||
stream
|
||||
};
|
||||
assert!(jbig2_image_draws(&stream(64)), "2^22 references are allowed");
|
||||
assert!(!jbig2_image_draws(&stream(65)), "one more isn't");
|
||||
}
|
||||
|
||||
/// One segment may refer to the same dictionary again and again, and collecting its symbols took a
|
||||
/// pointer each: 65,535 references to a dictionary of 32,767 symbols, in a 78 KB stream, asked for
|
||||
/// 17 GB. Vendored hayro-jbig2 patch: at most 2^20 symbols per segment, and 32 × 32,767 + 32 is
|
||||
/// exactly that. All three segment kinds that take symbols are held to it: the text regions, whether
|
||||
/// immediate or intermediate, and a dictionary that refers to others.
|
||||
#[test]
|
||||
fn jbig2_segments_referring_to_too_many_symbols_are_refused() {
|
||||
let mut text = jbig2_region_info(1, 1);
|
||||
text.extend_from_slice(&[0, 0]); // arithmetic coding, no refinement
|
||||
text.extend_from_slice(&[0; 4]); // no instances
|
||||
// A dictionary of no symbols of its own, exporting none of those it refers to.
|
||||
let dictionary = |symbols: u32| {
|
||||
let mut dictionary = vec![0x00, 0x01]; // Huffman, tables B.4, B.2, B.1, B.1
|
||||
dictionary.extend_from_slice(&[0; 8]); // exported, new
|
||||
let mut bits = Bits::default();
|
||||
bits.table_b1(symbols); // the first export run, not exported, is all of them
|
||||
dictionary.extend_from_slice(&bits.bytes);
|
||||
dictionary
|
||||
};
|
||||
let stream = |kind: u8, small: usize, data: &[u8]| {
|
||||
let mut stream = black_jbig2_page();
|
||||
jbig2_segment(&mut stream, 1, 0, &[], &zero_width_jbig2_dictionary(32767, true));
|
||||
jbig2_segment(&mut stream, 2, 0, &[], &zero_width_jbig2_dictionary(32, true));
|
||||
let mut refs = vec![1u8; 32];
|
||||
refs.extend(std::iter::repeat_n(2u8, small));
|
||||
jbig2_segment(&mut stream, 3, kind, &refs, data);
|
||||
stream
|
||||
};
|
||||
let limit = 1 << 20;
|
||||
for (what, kind, allowed, past) in [
|
||||
("immediate text region", 6, text.clone(), text.clone()),
|
||||
("intermediate text region", 4, text.clone(), text.clone()),
|
||||
("symbol dictionary", 0, dictionary(limit), dictionary(limit + 32)),
|
||||
] {
|
||||
assert!(jbig2_image_draws(&stream(kind, 1, &allowed)), "{what}: 2^20 symbols are allowed");
|
||||
assert!(!jbig2_image_draws(&stream(kind, 2, &past)), "{what}: 32 more aren't");
|
||||
}
|
||||
}
|
||||
|
||||
/// A halftone grid takes a 4-byte value per cell while it decodes, so the bound of 2^28 cells let a
|
||||
/// 98-byte segment take 1 GiB. Vendored hayro-jbig2 patch: at most 2^25 cells, however it's coded.
|
||||
#[test]
|
||||
fn jbig2_halftone_grids_past_their_bound_are_refused() {
|
||||
let stream = |width: u32, height: u32, mmr: bool| {
|
||||
let mut stream = black_jbig2_page();
|
||||
jbig2_segment(&mut stream, 1, 16, &[], &jbig2_pattern_dictionary(1, 2));
|
||||
jbig2_segment(&mut stream, 2, 22, &[1], &jbig2_halftone(width, height, mmr));
|
||||
stream
|
||||
};
|
||||
assert!(jbig2_image_draws(&stream(8192, 4096, false)), "2^25 cells are allowed");
|
||||
assert!(!jbig2_image_draws(&stream(8193, 4096, false)), "one more column isn't");
|
||||
assert!(!jbig2_image_draws(&stream(8193, 4096, true)), "nor when it is MMR-coded");
|
||||
}
|
||||
|
||||
/// A pattern dictionary's shifted copies only speed up drawing, and 48 bytes could ask for 539 MB
|
||||
/// of them, past what a page may hold. Vendored hayro-jbig2 patch: past 16 MiB they are left out,
|
||||
/// so such a dictionary is drawn the slow way rather than refused.
|
||||
#[test]
|
||||
fn jbig2_pattern_dictionaries_too_large_to_shift_still_draw() {
|
||||
let mut stream = black_jbig2_page();
|
||||
jbig2_segment(&mut stream, 1, 16, &[], &jbig2_pattern_dictionary(32, 65535));
|
||||
jbig2_segment(&mut stream, 2, 22, &[1], &jbig2_halftone(1, 1, false));
|
||||
assert!(jbig2_image_draws(&stream));
|
||||
}
|
||||
|
||||
/// From `cargo xtask fuzz`: one changed byte in a symbol dictionary's arithmetic-coded data
|
||||
/// hung the renderer. Two of its loops can stop advancing: an export run of length 0, and a
|
||||
/// height class whose first width is already the end-of-class marker. Past the end of its
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+52
-7
@@ -19,6 +19,35 @@ pub(crate) const MAX_DIMENSION: u32 = u16::MAX as u32;
|
||||
/// pixels, decoded one by one.
|
||||
pub(crate) const MAX_PIXELS: u64 = 1 << 28;
|
||||
|
||||
/// PdfCraft patch: the most cells a halftone grid may have, which still gives the largest page
|
||||
/// (see [`MAX_PIXELS`]) cells of 3 × 3, unrotated. Each cell takes a 4-byte value while the grid decodes, so
|
||||
/// the pixel bound alone let a 98-byte segment take 1 GiB; this one leaves 128 MiB.
|
||||
pub(crate) const MAX_GRID_CELLS: u64 = 1 << 25;
|
||||
|
||||
/// PdfCraft patch: the decoding work a stream may ask for, in pixels decoded from their code: four
|
||||
/// of the largest pages, about 6 s on data the decoder predicts and about 17 s on data it can't, so
|
||||
/// a render finishes inside its 20 s watchdog. Each bitmap is bounded on its own, but nothing
|
||||
/// bounded how many there are: 1,140 bytes of generic regions took over 40 s, a 795-byte PDF's one
|
||||
/// halftone 265 s, and a 923-byte PDF's regions would have taken days.
|
||||
pub(crate) const MAX_WORK: u64 = 4 * MAX_PIXELS;
|
||||
|
||||
/// PdfCraft patch: the work of a line of a Huffman table built for symbols, about 50 ns (see
|
||||
/// [`MAX_WORK`]).
|
||||
pub(crate) const HUFFMAN_LINE_WORK: u64 = 10;
|
||||
|
||||
/// PdfCraft patch: the work of decoding a bitmap pixel by pixel (see [`MAX_WORK`]). A row costs up
|
||||
/// to 21 ns however few pixels it has, about 4 pixels' worth, so one of no width isn't free: 16,384
|
||||
/// regions of 0 × 65535 took 9 s at a cost of nothing.
|
||||
pub(crate) fn decode_work(width: u32, height: u32) -> u64 {
|
||||
u64::from(height) * (u64::from(width) + 4)
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the work of copying a bitmap (see [`MAX_WORK`]): a word at a time, and a row is
|
||||
/// a copy of its own, so a tall thin bitmap costs its rows, not its pixels.
|
||||
pub(crate) fn copy_work(width: u32, height: u32) -> u64 {
|
||||
u64::from(height) * (u64::from(width.div_ceil(WORD_BITS)) + 1)
|
||||
}
|
||||
|
||||
/// The underlying word type for packed pixel storage.
|
||||
/// Change this to `u32` to switch back to 32-bit words.
|
||||
pub(crate) type Word = u32;
|
||||
@@ -26,6 +55,23 @@ pub(crate) type Word = u32;
|
||||
pub(crate) const WORD_BITS: u32 = Word::BITS;
|
||||
pub(crate) const WORD_SHIFT: u32 = WORD_BITS - 1;
|
||||
|
||||
/// PdfCraft patch: the words a [`Bitmap`] takes before its pixels. A symbol of width 0 has no
|
||||
/// pixels and still costs this: 5 KB of zero-width dictionaries held 781 MB.
|
||||
pub(crate) const BITMAP_WORDS: usize = size_of::<Bitmap>() / size_of::<Word>();
|
||||
|
||||
/// PdfCraft patch: refuse a page past the bounds on a bitmap (see `MAX_PIXELS`). A stream is held
|
||||
/// to this when it is opened as well as when its page bitmap is sized: a caller sizes its own
|
||||
/// output from the page header in between, and a 743-byte PDF's Indexed image asked for 137 GB.
|
||||
pub(crate) fn check_page_size(width: u32, height: u32) -> Result<()> {
|
||||
if width > MAX_DIMENSION || height > MAX_DIMENSION {
|
||||
return Err(OverflowError::BitmapDimension.into());
|
||||
}
|
||||
if u64::from(width) * u64::from(height) > MAX_PIXELS {
|
||||
return Err(OverflowError::BitmapDimension.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A decoded bitmap with position information.
|
||||
///
|
||||
/// "Pixels decoded by the MMR decoder having the value 'black' shall be treated
|
||||
@@ -51,6 +97,11 @@ pub(crate) struct Bitmap {
|
||||
}
|
||||
|
||||
impl Bitmap {
|
||||
/// PdfCraft patch: the words this bitmap holds, its header included.
|
||||
pub(crate) fn words(&self) -> usize {
|
||||
self.data.len().saturating_add(BITMAP_WORDS)
|
||||
}
|
||||
|
||||
/// Create a new bitmap filled with white pixels (false).
|
||||
///
|
||||
/// The bitmap is positioned at (0, 0).
|
||||
@@ -93,13 +144,7 @@ impl Bitmap {
|
||||
height: u32,
|
||||
default_pixel: bool,
|
||||
) -> Result<()> {
|
||||
if width > MAX_DIMENSION || height > MAX_DIMENSION {
|
||||
return Err(OverflowError::BitmapDimension.into());
|
||||
}
|
||||
// PdfCraft patch: see `MAX_PIXELS` (the page bitmap is sized here).
|
||||
if u64::from(width) * u64::from(height) > MAX_PIXELS {
|
||||
return Err(OverflowError::BitmapDimension.into());
|
||||
}
|
||||
check_page_size(width, height)?;
|
||||
|
||||
let stride = width.div_ceil(WORD_BITS);
|
||||
let default_word: Word = if default_pixel { !0 } else { 0 };
|
||||
|
||||
+26
-8
@@ -7,7 +7,7 @@ use super::RegionBitmap;
|
||||
use super::pattern::PatternDictionary;
|
||||
use super::{CombinationOperator, RegionSegmentInfo, Template, parse_region_segment_info};
|
||||
use crate::ScratchBuffers;
|
||||
use crate::bitmap::{Bitmap, MAX_DIMENSION, WORD_BITS};
|
||||
use crate::bitmap::{Bitmap, MAX_DIMENSION, WORD_BITS, copy_work, decode_work};
|
||||
use crate::error::{OverflowError, ParseError, RegionError, Result, bail};
|
||||
use crate::gray_scale::{GrayScaleParams, decode_gray_scale_image};
|
||||
use crate::reader::Reader;
|
||||
@@ -48,11 +48,7 @@ pub(crate) fn decode_into(
|
||||
None
|
||||
};
|
||||
|
||||
// "3) Set HBPP to ⌈log₂(HNUMPATS)⌉." (6.6.5)
|
||||
let bits_per_pixel = (pattern_dict.patterns.len() as u32)
|
||||
.saturating_sub(1)
|
||||
.checked_ilog2()
|
||||
.map_or(1, |n| n + 1);
|
||||
let bits_per_pixel = bits_per_pixel(pattern_dict);
|
||||
|
||||
// "4) Decode an image GI of size HGW by HGH with HBPP bits per pixel using
|
||||
// the gray-scale image decoding procedure as described in Annex C." (6.6.5)
|
||||
@@ -73,6 +69,28 @@ pub(crate) fn decode_into(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the work of a halftone region (see `bitmap::MAX_WORK`): a bit plane of the grid
|
||||
/// for each bit of a pattern's index, a pattern copied for each cell, and the region itself. One
|
||||
/// such segment in a 795-byte PDF took 265 s.
|
||||
pub(crate) fn work(header: &HalftoneRegionHeader<'_>, patterns: &PatternDictionary) -> u64 {
|
||||
let grid = &header.grid_position_and_size;
|
||||
let cells = u64::from(grid.width) * u64::from(grid.height);
|
||||
let planes = u64::from(bits_per_pixel(patterns));
|
||||
let copied = copy_work(patterns.pattern_width, patterns.pattern_height);
|
||||
planes
|
||||
.saturating_mul(decode_work(grid.width, grid.height))
|
||||
.saturating_add(cells.saturating_mul(copied))
|
||||
.saturating_add(copy_work(header.region_info.width, header.region_info.height))
|
||||
}
|
||||
|
||||
/// "3) Set HBPP to ⌈log₂(HNUMPATS)⌉." (6.6.5)
|
||||
fn bits_per_pixel(patterns: &PatternDictionary) -> u32 {
|
||||
(patterns.patterns.len() as u32)
|
||||
.saturating_sub(1)
|
||||
.checked_ilog2()
|
||||
.map_or(1, |n| n + 1)
|
||||
}
|
||||
|
||||
/// Parse a halftone region segment header (7.4.5.1).
|
||||
pub(crate) fn parse<'a>(reader: &mut Reader<'a>) -> Result<HalftoneRegionHeader<'a>> {
|
||||
let region_info = parse_region_segment_info(reader)?;
|
||||
@@ -97,8 +115,8 @@ pub(crate) fn parse<'a>(reader: &mut Reader<'a>) -> Result<HalftoneRegionHeader<
|
||||
if grid_width > MAX_DIMENSION || grid_height > MAX_DIMENSION {
|
||||
bail!(OverflowError::GridDimension);
|
||||
}
|
||||
// PdfCraft patch: the product is bounded too (see `bitmap::MAX_PIXELS`).
|
||||
if u64::from(grid_width) * u64::from(grid_height) > crate::bitmap::MAX_PIXELS {
|
||||
// PdfCraft patch: the product is bounded too (see `bitmap::MAX_GRID_CELLS`).
|
||||
if u64::from(grid_width) * u64::from(grid_height) > crate::bitmap::MAX_GRID_CELLS {
|
||||
bail!(OverflowError::GridDimension);
|
||||
}
|
||||
|
||||
|
||||
+35
-2
@@ -3,15 +3,20 @@
|
||||
use super::{AdaptiveTemplatePixel, Template, generic};
|
||||
use crate::ScratchBuffers;
|
||||
use crate::arithmetic_decoder::{ArithmeticDecoder, ArithmeticDecoderContext};
|
||||
use crate::bitmap::{Bitmap, WORD_BITS, Word};
|
||||
use crate::bitmap::{Bitmap, WORD_BITS, Word, copy_work};
|
||||
use crate::error::{OverflowError, ParseError, Result};
|
||||
use crate::reader::Reader;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
/// PdfCraft patch: the most words of shifted copies a dictionary keeps, 16 MiB, where 256 patterns
|
||||
/// of 32 rows take 2 MiB. A 48-byte dictionary of 65,535 such patterns asked for 539 MB.
|
||||
const MAX_SHIFTED_WORDS: u64 = 1 << 22;
|
||||
|
||||
/// Decode a pattern dictionary segment (7.4.4.2, 6.7).
|
||||
pub(crate) fn decode(
|
||||
header: &PatternDictionaryHeader<'_>,
|
||||
scratch: &mut ScratchBuffers,
|
||||
room: u64,
|
||||
) -> Result<PatternDictionary> {
|
||||
let pattern_width = header.pattern_width as u32;
|
||||
let pattern_height = header.pattern_height as u32;
|
||||
@@ -133,7 +138,19 @@ pub(crate) fn decode(
|
||||
// for all possible 32-bit alignments, which later on allows us to
|
||||
// very easily blit it into the backdrop, significantly speeding up
|
||||
// the whole process.
|
||||
let shifted_patterns = if pattern_width <= WORD_BITS && pattern_height <= WORD_BITS {
|
||||
//
|
||||
// PdfCraft patch: only when the table is small, and fits what the page may still hold
|
||||
// (`room`), so that a speed-up never gets an image refused. It can be left out:
|
||||
// `shifted_pattern` then returns `None` and the patterns are combined the slow way.
|
||||
let shifted_words = (patterns.len() as u64)
|
||||
.saturating_mul(32 * 2)
|
||||
.saturating_mul(u64::from(pattern_height));
|
||||
let pattern_words = patterns.iter().map(|p| p.words() as u64).fold(0, u64::saturating_add);
|
||||
let shifted_patterns = if pattern_width <= WORD_BITS
|
||||
&& pattern_height <= WORD_BITS
|
||||
&& shifted_words <= MAX_SHIFTED_WORDS
|
||||
&& shifted_words.saturating_add(pattern_words) <= room
|
||||
{
|
||||
let h = pattern_height as usize;
|
||||
let mut data = Vec::with_capacity(patterns.len() * 32 * h * 2);
|
||||
for pattern in &patterns {
|
||||
@@ -180,6 +197,11 @@ impl PatternDictionary {
|
||||
let start = (pattern_idx * 32 + bit_offset as usize) * len;
|
||||
self.shifted_patterns.get(start..start + len)
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the words the dictionary holds, its patterns and their shifted copies.
|
||||
pub(crate) fn words(&self) -> usize {
|
||||
self.patterns.iter().map(Bitmap::words).fold(self.shifted_patterns.len(), usize::saturating_add)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed pattern dictionary segment header (7.4.4.1).
|
||||
@@ -196,6 +218,17 @@ pub(crate) struct PatternDictionaryHeader<'a> {
|
||||
pub(crate) data: &'a [u8],
|
||||
}
|
||||
|
||||
impl PatternDictionaryHeader<'_> {
|
||||
/// PdfCraft patch: the work of decoding the patterns, side by side in one bitmap, and of
|
||||
/// taking each out of it (see `bitmap::MAX_WORK`).
|
||||
pub(crate) fn work(&self) -> u64 {
|
||||
let (width, height) = (u32::from(self.pattern_width), u32::from(self.pattern_height));
|
||||
let patterns = u64::from(self.num_patterns) + 1;
|
||||
let side_by_side = u64::from(height) * (patterns.saturating_mul(u64::from(width)) + 4);
|
||||
side_by_side.saturating_add(patterns.saturating_mul(copy_work(width, height)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a pattern dictionary segment header (7.4.4.1).
|
||||
pub(crate) fn parse<'a>(reader: &mut Reader<'a>) -> Result<PatternDictionaryHeader<'a>> {
|
||||
let flags_byte = reader.read_byte().ok_or(ParseError::UnexpectedEof)?;
|
||||
|
||||
+72
-5
@@ -4,7 +4,7 @@ use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::arithmetic_decoder::{ArithmeticDecoder, ArithmeticDecoderContext};
|
||||
use crate::bitmap::{Bitmap, WORD_BITS};
|
||||
use crate::bitmap::{BITMAP_WORDS, Bitmap, HUFFMAN_LINE_WORK, WORD_BITS, Word, decode_work};
|
||||
use crate::decode::generic::{decode_bitmap_mmr, parse_adaptive_template_pixels};
|
||||
use crate::decode::text::{
|
||||
DecodeContext, ReferenceCorner, TextRegionContexts, TextRegionFlags, TextRegionHeader,
|
||||
@@ -45,6 +45,8 @@ pub(crate) fn decode(
|
||||
referred_tables: &[HuffmanTable],
|
||||
standard_tables: &StandardHuffmanTables,
|
||||
input_contexts: Option<&RetainedContexts>,
|
||||
room: u64,
|
||||
work_left: &mut u64,
|
||||
) -> Result<SymbolDictionary> {
|
||||
let num_new_symbols = header.num_new_symbols;
|
||||
|
||||
@@ -58,6 +60,9 @@ pub(crate) fn decode(
|
||||
total_width: 0,
|
||||
height_class_height: 0,
|
||||
pixels: 0,
|
||||
new_words: 0,
|
||||
room,
|
||||
work_left,
|
||||
header,
|
||||
standard_tables,
|
||||
};
|
||||
@@ -130,6 +135,16 @@ pub(crate) fn decode(
|
||||
if ctx.pixels > MAX_DICTIONARY_PIXELS {
|
||||
bail!(OverflowError::BitmapDimension);
|
||||
}
|
||||
// PdfCraft patch: see `bitmap::MAX_WORK`.
|
||||
let work = decode_work(symbol_width, ctx.height_class_height);
|
||||
*ctx.work_left = ctx.work_left.checked_sub(work).ok_or(OverflowError::DecodingWork)?;
|
||||
// PdfCraft patch: see `room`. Rows take a word at least, so symbols of one column
|
||||
// held 270 MiB within the pixel bound, however few the dictionary exported.
|
||||
let rows = u64::from(symbol_width.div_ceil(WORD_BITS)) * u64::from(ctx.height_class_height);
|
||||
ctx.new_words = ctx.new_words.saturating_add(rows).saturating_add(BITMAP_WORDS as u64);
|
||||
if ctx.new_words > ctx.room {
|
||||
bail!(OverflowError::RetainedBitmaps);
|
||||
}
|
||||
|
||||
match (ctx.header.flags.use_huffman, ctx.header.flags.use_refagg) {
|
||||
(false, false) => {
|
||||
@@ -202,6 +217,18 @@ pub(crate) struct SymbolDictionary {
|
||||
pub(crate) retained_contexts: Option<RetainedContexts>,
|
||||
}
|
||||
|
||||
impl SymbolDictionary {
|
||||
/// PdfCraft patch: the words the dictionary holds, its retained contexts included. Those are
|
||||
/// a byte each, and 1.1 MB of dictionaries that retain them, 103 KB behind Flate, held 2.89 GB.
|
||||
pub(crate) fn words(&self) -> usize {
|
||||
let contexts = self.retained_contexts.as_ref().map_or(0, |contexts| {
|
||||
let bytes = contexts.generic_region.len().saturating_add(contexts.refinement_region.len());
|
||||
bytes.saturating_mul(size_of::<ArithmeticDecoderContext>()) / size_of::<Word>()
|
||||
});
|
||||
self.exported_symbols.iter().map(Bitmap::words).fold(contexts, usize::saturating_add)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a symbol bitmap using refinement/aggregate coding (6.5.8.2).
|
||||
fn decode_refinement_aggregation_bitmap(
|
||||
ctx: &mut SymbolDecodeContext<'_>,
|
||||
@@ -337,6 +364,11 @@ fn decode_aggregation_bitmap(
|
||||
) -> Result<Bitmap> {
|
||||
let use_huffman = ctx.header.flags.use_huffman;
|
||||
|
||||
// PdfCraft patch: see `bitmap::MAX_WORK`. What follows is sized by all the symbols, for each
|
||||
// aggregated symbol, at about 50 ns a symbol: a 713-byte PDF asked for an hour.
|
||||
let work = u64::from(ctx.total_symbols()).saturating_mul(HUFFMAN_LINE_WORK);
|
||||
*ctx.work_left = ctx.work_left.checked_sub(work).ok_or(OverflowError::DecodingWork)?;
|
||||
|
||||
// Concatenate input and new symbols.
|
||||
let mut all_symbols: Vec<&Bitmap> =
|
||||
Vec::with_capacity(ctx.symbols.input.len() + ctx.symbols.new.len());
|
||||
@@ -422,7 +454,7 @@ fn decode_aggregation_bitmap(
|
||||
header.region_info.y_location,
|
||||
header.flags.default_pixel,
|
||||
)?;
|
||||
decode_with(decode_ctx, &all_symbols, &header, &mut bitmap)?;
|
||||
decode_with(decode_ctx, &all_symbols, &header, &mut bitmap, ctx.work_left)?;
|
||||
Ok(bitmap)
|
||||
}
|
||||
|
||||
@@ -463,6 +495,14 @@ struct SymbolDecodeContext<'a> {
|
||||
symbols_decoded_count: u32,
|
||||
total_width: u32,
|
||||
height_class_height: u32,
|
||||
/// PdfCraft patch: the words the page may still hold, which neither the new symbols nor the
|
||||
/// exported ones may pass. They are counted as they are made: counted only once the
|
||||
/// dictionary was stored, 8 MiB symbols exported again and again killed the process.
|
||||
room: u64,
|
||||
/// PdfCraft patch: the words of the new symbols so far (see `room`).
|
||||
new_words: u64,
|
||||
/// PdfCraft patch: the decoding work the stream may still ask for (see `bitmap::MAX_WORK`).
|
||||
work_left: &'a mut u64,
|
||||
/// PdfCraft patch: symbol pixels decoded so far (see `MAX_DICTIONARY_PIXELS`).
|
||||
pixels: u64,
|
||||
}
|
||||
@@ -708,6 +748,7 @@ fn export_symbols(ctx: &mut SymbolDecodeContext<'_>) -> Result<Vec<Bitmap>> {
|
||||
u32::try_from(value).map_err(|_| HuffmanError::InvalidCode.into())
|
||||
};
|
||||
let mut exported = Vec::with_capacity(ctx.header.num_exported_symbols as usize);
|
||||
let mut exported_words: u64 = 0;
|
||||
let mut index: u32 = 0;
|
||||
let mut should_export = false;
|
||||
// PdfCraft patch: see `EXTRA_EMPTY_STEPS`.
|
||||
@@ -730,9 +771,13 @@ fn export_symbols(ctx: &mut SymbolDecodeContext<'_>) -> Result<Vec<Bitmap>> {
|
||||
let symbol = ctx
|
||||
.symbols
|
||||
.get(symbol_idx as usize)
|
||||
.ok_or(SymbolError::OutOfRange)?
|
||||
.clone();
|
||||
exported.push(symbol);
|
||||
.ok_or(SymbolError::OutOfRange)?;
|
||||
// PdfCraft patch: see `room`.
|
||||
exported_words = exported_words.saturating_add(symbol.words() as u64);
|
||||
if exported_words > ctx.room {
|
||||
bail!(OverflowError::RetainedBitmaps);
|
||||
}
|
||||
exported.push(symbol.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -864,3 +909,25 @@ pub(crate) fn parse<'a>(reader: &mut Reader<'a>) -> Result<SymbolDictionaryHeade
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A dictionary may export what it refers to, and may refer to one symbol again and again, so
|
||||
/// its exports are counted against what the page may still hold before each is copied.
|
||||
/// Refused only once stored, the copies are already made, which a render test can't see.
|
||||
#[test]
|
||||
fn exported_symbols_are_counted_before_they_are_copied() {
|
||||
// Huffman coding, 1,000 exported and no new symbols; export runs of 0 and 1,000 (B.1).
|
||||
let data = [0x00, 0x01, 0, 0, 0x03, 0xE8, 0, 0, 0, 0, 0x06, 0x02, 0xD8];
|
||||
let header = parse(&mut Reader::new(&data)).unwrap();
|
||||
let symbol = Bitmap::new(32, 32).unwrap();
|
||||
let input = vec![&symbol; 1000];
|
||||
let tables = StandardHuffmanTables::new();
|
||||
let words = symbol.words() as u64 * 1000;
|
||||
let decoded = decode(&header, &input, &[], &tables, None, words, &mut u64::MAX.clone()).unwrap();
|
||||
assert_eq!(decoded.exported_symbols.len(), 1000);
|
||||
assert!(decode(&header, &input, &[], &tables, None, words - 1, &mut u64::MAX.clone()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+18
-3
@@ -11,7 +11,7 @@ use super::{
|
||||
use super::{RegionBitmap, generic_refinement};
|
||||
use crate::ScratchBuffers;
|
||||
use crate::arithmetic_decoder::{ArithmeticDecoder, ArithmeticDecoderContext};
|
||||
use crate::bitmap::Bitmap;
|
||||
use crate::bitmap::{Bitmap, copy_work, decode_work};
|
||||
use crate::error::{HuffmanError, OverflowError, ParseError, Result, SymbolError, bail};
|
||||
use crate::huffman_table::{HuffmanTable, StandardHuffmanTables, TableLine};
|
||||
use crate::integer_decoder::IntegerDecoder;
|
||||
@@ -25,6 +25,7 @@ pub(crate) fn decode(
|
||||
referred_tables: &[HuffmanTable],
|
||||
standard_tables: &StandardHuffmanTables,
|
||||
scratch: &mut ScratchBuffers,
|
||||
work_left: &mut u64,
|
||||
) -> Result<RegionBitmap> {
|
||||
let mut bitmap = Bitmap::new_with(
|
||||
header.region_info.width,
|
||||
@@ -41,6 +42,7 @@ pub(crate) fn decode(
|
||||
standard_tables,
|
||||
&mut bitmap,
|
||||
scratch,
|
||||
work_left,
|
||||
)?;
|
||||
|
||||
Ok(RegionBitmap {
|
||||
@@ -56,12 +58,13 @@ pub(crate) fn decode_into(
|
||||
standard_tables: &StandardHuffmanTables,
|
||||
bitmap: &mut Bitmap,
|
||||
scratch: &mut ScratchBuffers,
|
||||
work_left: &mut u64,
|
||||
) -> Result<()> {
|
||||
if header.flags.use_huffman {
|
||||
let mut reader = Reader::new(header.data);
|
||||
let ctx =
|
||||
DecodeContext::new_huffman(&mut reader, header, referred_tables, standard_tables)?;
|
||||
decode_with(ctx, symbols, header, bitmap)?;
|
||||
decode_with(ctx, symbols, header, bitmap, work_left)?;
|
||||
} else {
|
||||
let mut decoder = ArithmeticDecoder::new(header.data);
|
||||
|
||||
@@ -76,7 +79,7 @@ pub(crate) fn decode_into(
|
||||
.resize(num_gr_contexts, ArithmeticDecoderContext::default());
|
||||
|
||||
let ctx = DecodeContext::new_arithmetic(&mut decoder, &mut contexts, &mut scratch.contexts);
|
||||
decode_with(ctx, symbols, header, bitmap)?;
|
||||
decode_with(ctx, symbols, header, bitmap, work_left)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -88,6 +91,7 @@ pub(crate) fn decode_with(
|
||||
symbols: &[&Bitmap],
|
||||
header: &TextRegionHeader<'_>,
|
||||
region: &mut Bitmap,
|
||||
work_left: &mut u64,
|
||||
) -> Result<()> {
|
||||
let strip_size = header.strip_size();
|
||||
|
||||
@@ -156,6 +160,17 @@ pub(crate) fn decode_with(
|
||||
let symbol_width = symbol_bitmap_ref.width as i32;
|
||||
let symbol_height = symbol_bitmap_ref.height as i32;
|
||||
|
||||
// PdfCraft patch: see `bitmap::MAX_WORK`. An instance of a large symbol costs its copy,
|
||||
// and 10,000 of one took 64 s. A refined one was decoded as well. Even one with no
|
||||
// pixels goes round this loop.
|
||||
let (width, height) = (symbol_bitmap_ref.width, symbol_bitmap_ref.height);
|
||||
let copied = copy_work(width, height).saturating_add(1);
|
||||
let work = match &symbol_bitmap {
|
||||
SymbolBitmap::Reference(_) => copied,
|
||||
SymbolBitmap::Owned(_) => copied.saturating_add(decode_work(width, height)),
|
||||
};
|
||||
*work_left = work_left.checked_sub(work).ok_or(OverflowError::DecodingWork)?;
|
||||
|
||||
if !header.flags.transposed
|
||||
&& (header.flags.reference_corner == ReferenceCorner::TopRight
|
||||
|| header.flags.reference_corner == ReferenceCorner::BottomRight)
|
||||
|
||||
Vendored
+11
-2
@@ -133,8 +133,14 @@ pub enum OverflowError {
|
||||
ReferenceOffset,
|
||||
/// Index computation overflowed.
|
||||
Index,
|
||||
/// A symbol dictionary has too many symbols.
|
||||
/// A symbol dictionary has too many symbols, or a segment refers to too many.
|
||||
SymbolCount,
|
||||
/// PdfCraft patch: the segments retain more bitmaps than a page may hold.
|
||||
RetainedBitmaps,
|
||||
/// PdfCraft patch: a stream has more segments, or references, than it may.
|
||||
SegmentCount,
|
||||
/// PdfCraft patch: a stream asks for more decoding work than it may.
|
||||
DecodingWork,
|
||||
}
|
||||
|
||||
impl fmt::Display for DecodeError {
|
||||
@@ -161,7 +167,10 @@ impl fmt::Display for OverflowError {
|
||||
Self::ReferenceOffset => write!(f, "overflow in reference offset computation"),
|
||||
Self::Index => write!(f, "overflow in index computation"),
|
||||
Self::GridDimension => write!(f, "overflow in grid dimension computation"),
|
||||
Self::SymbolCount => write!(f, "a symbol dictionary has too many symbols"),
|
||||
Self::SymbolCount => write!(f, "too many symbols in a dictionary or referred to"),
|
||||
Self::RetainedBitmaps => write!(f, "the segments retain too many bitmaps"),
|
||||
Self::SegmentCount => write!(f, "too many segments or references in a stream"),
|
||||
Self::DecodingWork => write!(f, "a stream asks for too much decoding work"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+35
-2
@@ -5,12 +5,39 @@
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::error::{FormatError, ParseError, Result, bail};
|
||||
use crate::error::{FormatError, OverflowError, ParseError, Result, bail};
|
||||
use crate::reader::Reader;
|
||||
use crate::segment::{
|
||||
Segment, SegmentType, parse_segment, parse_segment_data, parse_segment_header,
|
||||
};
|
||||
|
||||
/// PdfCraft patch: the most segments a stream, with its globals, may have. They are all parsed and
|
||||
/// sorted before the first is decoded, at about 64 bytes for 11 of input, so behind nested Flate a
|
||||
/// 4,765-byte PDF reached 5 GB and failed only afterwards.
|
||||
const MAX_SEGMENTS: usize = 1 << 20;
|
||||
|
||||
/// PdfCraft patch: the most references those segments may make, 4 bytes each for 1 of input.
|
||||
const MAX_REFERENCES: usize = 1 << 22;
|
||||
|
||||
/// PdfCraft patch: the segments of a stream so far (see [`MAX_SEGMENTS`], [`MAX_REFERENCES`]).
|
||||
#[derive(Default)]
|
||||
pub(crate) struct SegmentCount {
|
||||
segments: usize,
|
||||
references: usize,
|
||||
}
|
||||
|
||||
impl SegmentCount {
|
||||
/// Count one more segment, making `references` references.
|
||||
fn add(&mut self, references: usize) -> Result<()> {
|
||||
self.segments = self.segments.saturating_add(1);
|
||||
self.references = self.references.saturating_add(references);
|
||||
if self.segments > MAX_SEGMENTS || self.references > MAX_REFERENCES {
|
||||
bail!(OverflowError::SegmentCount);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// "There are two standalone file organizations possible for a JBIG2 bitstream."
|
||||
/// (Annex D)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -140,7 +167,9 @@ fn parse_segments<'a>(
|
||||
let mut segments = Vec::new();
|
||||
|
||||
match organization {
|
||||
FileOrganization::Sequential => parse_segments_sequential(reader, &mut segments)?,
|
||||
FileOrganization::Sequential => {
|
||||
parse_segments_sequential(reader, &mut segments, &mut SegmentCount::default())?;
|
||||
}
|
||||
FileOrganization::RandomAccess => parse_segments_random_access(reader, &mut segments)?,
|
||||
}
|
||||
|
||||
@@ -155,6 +184,7 @@ fn parse_segments<'a>(
|
||||
pub(crate) fn parse_segments_sequential<'a>(
|
||||
reader: &mut Reader<'a>,
|
||||
segments: &mut Vec<Segment<'a>>,
|
||||
count: &mut SegmentCount,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
if reader.at_end() {
|
||||
@@ -162,6 +192,7 @@ pub(crate) fn parse_segments_sequential<'a>(
|
||||
}
|
||||
|
||||
let segment = parse_segment(reader)?;
|
||||
count.add(segment.header.referred_to_segments.len())?;
|
||||
|
||||
// "If a file contains an end of file segment, it must be the last segment."
|
||||
// (7.4.11)
|
||||
@@ -187,6 +218,7 @@ fn parse_segments_random_access<'a>(
|
||||
segments: &mut Vec<Segment<'a>>,
|
||||
) -> Result<()> {
|
||||
let mut headers = Vec::new();
|
||||
let mut count = SegmentCount::default();
|
||||
|
||||
loop {
|
||||
if reader.at_end() {
|
||||
@@ -194,6 +226,7 @@ fn parse_segments_random_access<'a>(
|
||||
}
|
||||
|
||||
let header = parse_segment_header(reader)?;
|
||||
count.add(header.referred_to_segments.len())?;
|
||||
|
||||
// "If a file contains an end of file segment, it must be the last segment."
|
||||
// (7.4.11)
|
||||
|
||||
Vendored
+2
-2
@@ -83,8 +83,8 @@ fn decode_arithmetic(
|
||||
if width > MAX_DIMENSION || height > MAX_DIMENSION {
|
||||
bail!(OverflowError::BitmapDimension);
|
||||
}
|
||||
// PdfCraft patch: the product is bounded too (see `bitmap::MAX_PIXELS`).
|
||||
if u64::from(width) * u64::from(height) > crate::bitmap::MAX_PIXELS {
|
||||
// PdfCraft patch: the product is bounded too (see `bitmap::MAX_GRID_CELLS`).
|
||||
if u64::from(width) * u64::from(height) > crate::bitmap::MAX_GRID_CELLS {
|
||||
bail!(OverflowError::BitmapDimension);
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -14,11 +14,27 @@ include!("huffman_tables_generated.rs");
|
||||
/// Maximum number of nodes in an inline Huffman table.
|
||||
const INLINE_TABLE_SIZE: usize = 43;
|
||||
|
||||
/// PdfCraft patch: the most lines a custom table may have. A line takes 2 bits of input at least
|
||||
/// and 8 bytes here, so a table reading 250 MiB behind nested Flate, a 1,297-byte PDF, was killed
|
||||
/// at 8 GiB before it could be counted.
|
||||
const MAX_TABLE_LINES: usize = 1 << 16;
|
||||
|
||||
/// A queryable Huffman table.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct HuffmanTable(Rc<InnerHuffmanTable>);
|
||||
|
||||
impl HuffmanTable {
|
||||
/// PdfCraft patch: the words this table holds. Every table takes the room of the inline
|
||||
/// variant, however few nodes it has, and a dynamic one its nodes besides.
|
||||
pub(crate) fn words(&self) -> usize {
|
||||
let nodes = match &*self.0 {
|
||||
InnerHuffmanTable::Inline { .. } => 0,
|
||||
InnerHuffmanTable::Dynamic { nodes } => nodes.len(),
|
||||
};
|
||||
let bytes = nodes.saturating_mul(size_of::<HuffmanNode>());
|
||||
bytes.saturating_add(size_of::<InnerHuffmanTable>()) / size_of::<u32>()
|
||||
}
|
||||
|
||||
/// Create a new inline Huffman table from a fixed-size node array.
|
||||
fn from_inline(nodes: [HuffmanNode; INLINE_TABLE_SIZE]) -> Self {
|
||||
Self(Rc::new(InnerHuffmanTable::Inline { nodes }))
|
||||
@@ -219,6 +235,9 @@ impl HuffmanTable {
|
||||
// 5) "Decode each table line as follows:"
|
||||
// d) "If CURRANGELOW ≥ HTHIGH then proceed to step 6."
|
||||
while current_range_low < maximum_value {
|
||||
if lines.len() >= MAX_TABLE_LINES {
|
||||
bail!(HuffmanError::InvalidCode);
|
||||
}
|
||||
// a) "Read HTPS bits. Set PREFLEN[NTEMP] to the value decoded."
|
||||
let prefix_length = reader
|
||||
.read_bits(prefix_length_bits)
|
||||
|
||||
Vendored
+163
-36
@@ -75,7 +75,7 @@ pub use error::{
|
||||
SegmentError, SymbolError, TemplateError,
|
||||
};
|
||||
|
||||
use crate::file::parse_segments_sequential;
|
||||
use crate::file::{SegmentCount, parse_segments_sequential};
|
||||
use bitmap::Bitmap;
|
||||
use decode::CombinationOperator;
|
||||
use decode::generic;
|
||||
@@ -120,13 +120,14 @@ impl<'a> Image<'a> {
|
||||
/// Annex D.3.
|
||||
pub fn new_embedded(data: &'a [u8], globals: Option<&'a [u8]>) -> Result<Self> {
|
||||
let mut segments = Vec::new();
|
||||
let mut count = SegmentCount::default();
|
||||
if let Some(globals_data) = globals {
|
||||
let mut reader = Reader::new(globals_data);
|
||||
parse_segments_sequential(&mut reader, &mut segments)?;
|
||||
parse_segments_sequential(&mut reader, &mut segments, &mut count)?;
|
||||
};
|
||||
|
||||
let mut reader = Reader::new(data);
|
||||
parse_segments_sequential(&mut reader, &mut segments)?;
|
||||
parse_segments_sequential(&mut reader, &mut segments, &mut count)?;
|
||||
|
||||
segments.sort_by_key(|seg| seg.header.segment_number);
|
||||
|
||||
@@ -162,6 +163,7 @@ impl<'a> Image<'a> {
|
||||
if page_info.width == 0 || height == 0 {
|
||||
bail!(FormatError::EmptyPage);
|
||||
}
|
||||
bitmap::check_page_size(page_info.width, height)?;
|
||||
|
||||
Ok(Self {
|
||||
segments,
|
||||
@@ -257,6 +259,18 @@ fn emit_bitmap<D: Decoder>(bitmap: &Bitmap, decoder: &mut D) {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch: count `work` more decoding work, refusing past [`bitmap::MAX_WORK`] before what
|
||||
/// asks for it is decoded.
|
||||
fn charge_work(left: &mut u64, work: u64) -> Result<()> {
|
||||
*left = left.checked_sub(work).ok_or(OverflowError::DecodingWork)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the work of a region decoded pixel by pixel.
|
||||
fn region_work(region: &decode::RegionSegmentInfo) -> u64 {
|
||||
bitmap::decode_work(region.width, region.height)
|
||||
}
|
||||
|
||||
fn decode_segments(
|
||||
segments: &[segment::Segment<'_>],
|
||||
height_from_stripes: Option<u32>,
|
||||
@@ -281,6 +295,7 @@ fn decode_segments(
|
||||
let page_bitmap = &mut decoder_ctx.page_bitmap;
|
||||
let page_state = &mut decoder_ctx.page_state;
|
||||
let scratch_buffers = &mut decoder_ctx.scratch_buffers;
|
||||
let mut work_left = bitmap::MAX_WORK;
|
||||
|
||||
// Process all segments.
|
||||
for seg in segments {
|
||||
@@ -293,6 +308,7 @@ fn decode_segments(
|
||||
SegmentType::ImmediateGenericRegion | SegmentType::ImmediateLosslessGenericRegion => {
|
||||
let had_unknown_length = seg.header.data_length.is_none();
|
||||
let header = generic::parse(&mut reader, had_unknown_length)?;
|
||||
charge_work(&mut work_left, region_work(&header.region_info))?;
|
||||
|
||||
if page_state.can_decode_directly(page_bitmap, &header.region_info, false) {
|
||||
generic::decode_into(&header, page_bitmap, scratch_buffers)?;
|
||||
@@ -310,25 +326,22 @@ fn decode_segments(
|
||||
SegmentType::IntermediateGenericRegion => {
|
||||
// Intermediate segments cannot have unknown length.
|
||||
let header = generic::parse(&mut reader, false)?;
|
||||
charge_work(&mut work_left, region_work(&header.region_info))?;
|
||||
let region = generic::decode(&header, scratch_buffers)?;
|
||||
page_state.store_region(seg.header.segment_number, region.bitmap);
|
||||
page_state.store_region(seg.header.segment_number, region.bitmap)?;
|
||||
}
|
||||
SegmentType::PatternDictionary => {
|
||||
let header = pattern::parse(&mut reader)?;
|
||||
let dictionary = pattern::decode(&header, scratch_buffers)?;
|
||||
page_state.store_pattern_dictionary(seg.header.segment_number, dictionary);
|
||||
charge_work(&mut work_left, header.work())?;
|
||||
let dictionary = pattern::decode(&header, scratch_buffers, page_state.retained_room())?;
|
||||
page_state.store_pattern_dictionary(seg.header.segment_number, dictionary)?;
|
||||
}
|
||||
SegmentType::SymbolDictionary => {
|
||||
// "1) Concatenate all the input symbol dictionaries to form SDINSYMS."
|
||||
// (6.5.5, step 1)
|
||||
// Collect references to avoid cloning; symbols are only cloned if re-exported.
|
||||
let input_symbols: Vec<&Bitmap> = seg
|
||||
.header
|
||||
.referred_to_segments
|
||||
.iter()
|
||||
.filter_map(|&num| page_state.get_symbol_dictionary(num))
|
||||
.flat_map(|dict| dict.exported_symbols.iter())
|
||||
.collect();
|
||||
let input_symbols = page_state.referred_symbols(&seg.header.referred_to_segments)?;
|
||||
charge_work(&mut work_left, input_symbols.len() as u64)?;
|
||||
|
||||
// Collect Huffman tables from referred table segments.
|
||||
let referred_tables: Vec<HuffmanTable> = seg
|
||||
@@ -354,18 +367,15 @@ fn decode_segments(
|
||||
&referred_tables,
|
||||
&page_state.standard_tables,
|
||||
retained_contexts,
|
||||
page_state.retained_room(),
|
||||
&mut work_left,
|
||||
)?;
|
||||
page_state.store_symbol_dictionary(seg.header.segment_number, dictionary);
|
||||
page_state.store_symbol_dictionary(seg.header.segment_number, dictionary)?;
|
||||
}
|
||||
SegmentType::ImmediateTextRegion | SegmentType::ImmediateLosslessTextRegion => {
|
||||
// Collect symbols from referred symbol dictionaries (SBSYMS).
|
||||
let symbols: Vec<&Bitmap> = seg
|
||||
.header
|
||||
.referred_to_segments
|
||||
.iter()
|
||||
.filter_map(|&num| page_state.get_symbol_dictionary(num))
|
||||
.flat_map(|dict| dict.exported_symbols.iter())
|
||||
.collect();
|
||||
let symbols = page_state.referred_symbols(&seg.header.referred_to_segments)?;
|
||||
charge_work(&mut work_left, symbols.len() as u64)?;
|
||||
|
||||
// Collect Huffman tables from referred table segments.
|
||||
// "These user-supplied Huffman decoding tables may be supplied either
|
||||
@@ -379,6 +389,11 @@ fn decode_segments(
|
||||
.collect();
|
||||
|
||||
let header = text::parse(&mut reader, symbols.len() as u32)?;
|
||||
charge_work(&mut work_left, bitmap::copy_work(header.region_info.width, header.region_info.height))?;
|
||||
if header.flags.use_huffman {
|
||||
// Its symbol ID table has a line for each symbol.
|
||||
charge_work(&mut work_left, (symbols.len() as u64).saturating_mul(bitmap::HUFFMAN_LINE_WORK))?;
|
||||
}
|
||||
|
||||
if page_state.can_decode_directly(
|
||||
page_bitmap,
|
||||
@@ -392,6 +407,7 @@ fn decode_segments(
|
||||
&page_state.standard_tables,
|
||||
page_bitmap,
|
||||
scratch_buffers,
|
||||
&mut work_left,
|
||||
)?;
|
||||
} else {
|
||||
let region = text::decode(
|
||||
@@ -400,6 +416,7 @@ fn decode_segments(
|
||||
&referred_tables,
|
||||
&page_state.standard_tables,
|
||||
scratch_buffers,
|
||||
&mut work_left,
|
||||
)?;
|
||||
page_bitmap.combine(
|
||||
®ion.bitmap,
|
||||
@@ -412,13 +429,8 @@ fn decode_segments(
|
||||
}
|
||||
SegmentType::IntermediateTextRegion => {
|
||||
// Collect symbols from referred symbol dictionaries (SBSYMS).
|
||||
let symbols: Vec<&Bitmap> = seg
|
||||
.header
|
||||
.referred_to_segments
|
||||
.iter()
|
||||
.filter_map(|&num| page_state.get_symbol_dictionary(num))
|
||||
.flat_map(|dict| dict.exported_symbols.iter())
|
||||
.collect();
|
||||
let symbols = page_state.referred_symbols(&seg.header.referred_to_segments)?;
|
||||
charge_work(&mut work_left, symbols.len() as u64)?;
|
||||
|
||||
// Collect Huffman tables from referred table segments.
|
||||
let referred_tables: Vec<HuffmanTable> = seg
|
||||
@@ -430,14 +442,20 @@ fn decode_segments(
|
||||
.collect();
|
||||
|
||||
let header = text::parse(&mut reader, symbols.len() as u32)?;
|
||||
charge_work(&mut work_left, bitmap::copy_work(header.region_info.width, header.region_info.height))?;
|
||||
if header.flags.use_huffman {
|
||||
// Its symbol ID table has a line for each symbol.
|
||||
charge_work(&mut work_left, (symbols.len() as u64).saturating_mul(bitmap::HUFFMAN_LINE_WORK))?;
|
||||
}
|
||||
let region = text::decode(
|
||||
&header,
|
||||
&symbols,
|
||||
&referred_tables,
|
||||
&page_state.standard_tables,
|
||||
scratch_buffers,
|
||||
&mut work_left,
|
||||
)?;
|
||||
page_state.store_region(seg.header.segment_number, region.bitmap);
|
||||
page_state.store_region(seg.header.segment_number, region.bitmap)?;
|
||||
}
|
||||
SegmentType::ImmediateHalftoneRegion | SegmentType::ImmediateLosslessHalftoneRegion => {
|
||||
let pattern_dict = seg
|
||||
@@ -448,6 +466,7 @@ fn decode_segments(
|
||||
.ok_or(SegmentError::MissingPatternDictionary)?;
|
||||
|
||||
let header = halftone::parse(&mut reader)?;
|
||||
charge_work(&mut work_left, halftone::work(&header, pattern_dict))?;
|
||||
|
||||
if page_state.can_decode_directly(
|
||||
page_bitmap,
|
||||
@@ -475,8 +494,9 @@ fn decode_segments(
|
||||
.ok_or(SegmentError::MissingPatternDictionary)?;
|
||||
|
||||
let header = halftone::parse(&mut reader)?;
|
||||
charge_work(&mut work_left, halftone::work(&header, pattern_dict))?;
|
||||
let region = halftone::decode(&header, pattern_dict, scratch_buffers)?;
|
||||
page_state.store_region(seg.header.segment_number, region.bitmap);
|
||||
page_state.store_region(seg.header.segment_number, region.bitmap)?;
|
||||
}
|
||||
SegmentType::IntermediateGenericRefinementRegion => {
|
||||
// Same logic as immediate refinement, but store result instead of combining.
|
||||
@@ -488,8 +508,9 @@ fn decode_segments(
|
||||
.unwrap_or(page_bitmap);
|
||||
|
||||
let header = generic_refinement::parse(&mut reader)?;
|
||||
charge_work(&mut work_left, region_work(&header.region_info))?;
|
||||
let region = generic_refinement::decode(&header, reference, scratch_buffers)?;
|
||||
page_state.store_region(seg.header.segment_number, region.bitmap);
|
||||
page_state.store_region(seg.header.segment_number, region.bitmap)?;
|
||||
}
|
||||
SegmentType::ImmediateGenericRefinementRegion
|
||||
| SegmentType::ImmediateLosslessGenericRefinementRegion => {
|
||||
@@ -505,6 +526,7 @@ fn decode_segments(
|
||||
.and_then(|&num| page_state.get_referred_segment(num));
|
||||
|
||||
let header = generic_refinement::parse(&mut reader)?;
|
||||
charge_work(&mut work_left, region_work(&header.region_info))?;
|
||||
|
||||
if let Some(referred_segment) = referred_segment
|
||||
&& page_state.can_decode_directly(page_bitmap, &header.region_info, false)
|
||||
@@ -532,7 +554,7 @@ fn decode_segments(
|
||||
// "This segment contains data which defines one or more user-supplied
|
||||
// Huffman coding tables." (7.4.13)
|
||||
let table = HuffmanTable::read_custom(&mut reader)?;
|
||||
page_state.store_huffman_table(seg.header.segment_number, table);
|
||||
page_state.store_huffman_table(seg.header.segment_number, table)?;
|
||||
}
|
||||
SegmentType::EndOfPage | SegmentType::EndOfFile => {
|
||||
break;
|
||||
@@ -545,6 +567,23 @@ fn decode_segments(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the words a stored item costs in its list, at least, apart from what it holds.
|
||||
const ENTRY_WORDS: u64 = 32;
|
||||
|
||||
/// PdfCraft patch: the most words a page's segments may hold for later ones, 64 MiB: two square
|
||||
/// bitmaps of 16,384 a side (`bitmap::MAX_PIXELS`). Each stored item is bounded on its own, but without a
|
||||
/// bound on all of them a stream of a few dozen bytes a segment held 33 MB each (a 16,384-sample
|
||||
/// square region of typical rows), and a few kilobytes could exhaust memory. A segment being
|
||||
/// decoded takes more for a while, within its own bounds: the page bitmap isn't counted, nor is
|
||||
/// what a region or a grid uses until it is stored or freed.
|
||||
const MAX_RETAINED_WORDS: u64 =
|
||||
2 * (bitmap::MAX_PIXELS / bitmap::WORD_BITS as u64 + bitmap::BITMAP_WORDS as u64 + ENTRY_WORDS);
|
||||
|
||||
/// PdfCraft patch: the most symbols one segment may refer to, 2^20, far beyond the 65,535 a
|
||||
/// dictionary may export. References may repeat, so each names the same symbols again: without
|
||||
/// this, 65,535 references to one dictionary in a 78 KB stream collected 17 GB of pointers.
|
||||
const MAX_REFERRED_SYMBOLS: u64 = 1 << 20;
|
||||
|
||||
/// Page-level decoding state for a JBIG2 page.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct PageState {
|
||||
@@ -561,6 +600,9 @@ pub(crate) struct PageState {
|
||||
/// Decoded Huffman tables from table segments, stored as (`segment_number`, table) pairs.
|
||||
/// "Tables – see 7.4.13." (type 53)
|
||||
pub(crate) huffman_tables: Vec<(u32, HuffmanTable)>,
|
||||
/// PdfCraft patch: the words the regions, dictionaries and tables above hold, at most
|
||||
/// [`MAX_RETAINED_WORDS`].
|
||||
pub(crate) retained_words: u64,
|
||||
/// Standard Huffman tables (`TABLE_A` through `TABLE_O`).
|
||||
pub(crate) standard_tables: StandardHuffmanTables,
|
||||
}
|
||||
@@ -573,6 +615,7 @@ impl PageState {
|
||||
self.pattern_dictionaries.clear();
|
||||
self.symbol_dictionaries.clear();
|
||||
self.huffman_tables.clear();
|
||||
self.retained_words = 0;
|
||||
// Standard tables are lazily built and reused across images.
|
||||
}
|
||||
|
||||
@@ -610,9 +653,26 @@ impl PageState {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the words the page may still hold for later segments.
|
||||
fn retained_room(&self) -> u64 {
|
||||
MAX_RETAINED_WORDS.saturating_sub(self.retained_words)
|
||||
}
|
||||
|
||||
/// PdfCraft patch: count a stored item holding `words` for later segments, refusing past
|
||||
/// [`MAX_RETAINED_WORDS`].
|
||||
fn retain(&mut self, words: usize) -> Result<()> {
|
||||
self.retained_words = self.retained_words.saturating_add(words as u64).saturating_add(ENTRY_WORDS);
|
||||
if self.retained_words > MAX_RETAINED_WORDS {
|
||||
bail!(OverflowError::RetainedBitmaps);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store a decoded region for later reference.
|
||||
fn store_region(&mut self, segment_number: u32, region: Bitmap) {
|
||||
fn store_region(&mut self, segment_number: u32, region: Bitmap) -> Result<()> {
|
||||
self.retain(region.words())?;
|
||||
self.referred_segments.push((segment_number, region));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Look up a referred segment by number.
|
||||
@@ -624,8 +684,10 @@ impl PageState {
|
||||
}
|
||||
|
||||
/// Store a decoded pattern dictionary for later reference.
|
||||
fn store_pattern_dictionary(&mut self, segment_number: u32, dictionary: PatternDictionary) {
|
||||
fn store_pattern_dictionary(&mut self, segment_number: u32, dictionary: PatternDictionary) -> Result<()> {
|
||||
self.retain(dictionary.words())?;
|
||||
self.pattern_dictionaries.push((segment_number, dictionary));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Look up a pattern dictionary by segment number.
|
||||
@@ -637,8 +699,23 @@ impl PageState {
|
||||
}
|
||||
|
||||
/// Store a decoded symbol dictionary for later reference.
|
||||
fn store_symbol_dictionary(&mut self, segment_number: u32, dictionary: SymbolDictionary) {
|
||||
fn store_symbol_dictionary(&mut self, segment_number: u32, dictionary: SymbolDictionary) -> Result<()> {
|
||||
self.retain(dictionary.words())?;
|
||||
self.symbol_dictionaries.push((segment_number, dictionary));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the symbols of the dictionaries in `segments`, in order, refusing past
|
||||
/// [`MAX_REFERRED_SYMBOLS`] before anything is collected.
|
||||
fn referred_symbols(&self, segments: &[u32]) -> Result<Vec<&Bitmap>> {
|
||||
let dictionaries = || segments.iter().filter_map(|&num| self.get_symbol_dictionary(num));
|
||||
let count = dictionaries()
|
||||
.map(|dictionary| dictionary.exported_symbols.len() as u64)
|
||||
.fold(0, u64::saturating_add);
|
||||
if count > MAX_REFERRED_SYMBOLS {
|
||||
bail!(OverflowError::SymbolCount);
|
||||
}
|
||||
Ok(dictionaries().flat_map(|dictionary| dictionary.exported_symbols.iter()).collect())
|
||||
}
|
||||
|
||||
/// Look up a symbol dictionary by segment number.
|
||||
@@ -650,8 +727,10 @@ impl PageState {
|
||||
}
|
||||
|
||||
/// Store a decoded Huffman table for later reference.
|
||||
fn store_huffman_table(&mut self, segment_number: u32, table: HuffmanTable) {
|
||||
fn store_huffman_table(&mut self, segment_number: u32, table: HuffmanTable) -> Result<()> {
|
||||
self.retain(table.words())?;
|
||||
self.huffman_tables.push((segment_number, table));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Look up a Huffman table by segment number.
|
||||
@@ -690,3 +769,51 @@ fn init_page(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A page past the bounds is refused when the stream is opened, since callers size their
|
||||
/// output from it before decoding starts. 16,384 squared is exactly the bound on pixels.
|
||||
#[test]
|
||||
fn a_page_past_the_bounds_is_refused_when_opened() {
|
||||
let stream = |width: u32, height: u32| {
|
||||
let mut stream = vec![0, 0, 0, 0, 48, 0, 1, 0, 0, 0, 19];
|
||||
stream.extend_from_slice(&width.to_be_bytes());
|
||||
stream.extend_from_slice(&height.to_be_bytes());
|
||||
stream.extend_from_slice(&[0; 11]); // resolution, flags, striping
|
||||
stream
|
||||
};
|
||||
assert!(Image::new_embedded(&stream(16384, 16384), None).is_ok());
|
||||
assert!(Image::new_embedded(&stream(16385, 16384), None).is_err());
|
||||
assert!(Image::new_embedded(&stream(65536, 1), None).is_err(), "too wide, though few pixels");
|
||||
assert!(Image::new_embedded(&stream(1 << 20, 1 << 20), None).is_err());
|
||||
}
|
||||
|
||||
/// A stream and its globals are one stream of segments, so they have one count.
|
||||
#[test]
|
||||
fn the_segments_of_a_stream_and_its_globals_count_together() {
|
||||
let segment: &[u8] = &[0, 0, 0, 0, 62, 0, 1, 0, 0, 0, 0]; // an empty extension
|
||||
let half = segment.repeat(1 << 19);
|
||||
let more = segment.repeat((1 << 19) + 1);
|
||||
let refused = |data: &[u8]| {
|
||||
let error = Image::new_embedded(data, Some(&half)).err().map(|e| e.to_string());
|
||||
error.as_deref() == Some("too many segments or references in a stream")
|
||||
};
|
||||
assert!(!refused(&half), "2^20 segments are allowed");
|
||||
assert!(refused(&more), "one more isn't");
|
||||
}
|
||||
|
||||
/// A dictionary being decoded has what the page may still hold, not all of it: refused only
|
||||
/// once it is stored, its copies would already have been made.
|
||||
#[test]
|
||||
fn the_room_left_shrinks_as_segments_are_stored() {
|
||||
let mut page = PageState::default();
|
||||
assert_eq!(page.retained_room(), MAX_RETAINED_WORDS);
|
||||
page.retain(1000).unwrap();
|
||||
assert_eq!(page.retained_room(), MAX_RETAINED_WORDS - 1000 - ENTRY_WORDS);
|
||||
page.retained_words = MAX_RETAINED_WORDS;
|
||||
assert_eq!(page.retained_room(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -1,4 +1,5 @@
|
||||
use crate::bit_reader::BitWriter;
|
||||
use crate::filter::Filter;
|
||||
use crate::object::Dict;
|
||||
use crate::object::Stream;
|
||||
use crate::object::dict::keys::JBIG2_GLOBALS;
|
||||
@@ -17,8 +18,12 @@ pub(crate) fn decode(
|
||||
params: &Dict<'_>,
|
||||
image_params: &ImageDecodeParams,
|
||||
) -> Option<FilterResult<'static>> {
|
||||
// PdfCraft patch: globals are raw segments, so JBIG2 inside them means nothing, and decoding
|
||||
// it reads its own globals in turn: a chain of 16,000 such streams overflowed the stack, which
|
||||
// aborts the process. Such a stream reads as globals that can't be decoded.
|
||||
let globals = params
|
||||
.get::<Stream<'_>>(JBIG2_GLOBALS)
|
||||
.filter(|g| !g.filters().iter().any(|f| matches!(f, Filter::Jbig2Decode)))
|
||||
.and_then(|g| g.decoded().ok());
|
||||
|
||||
let image = hayro_jbig2::Image::new_embedded(data, globals.as_deref()).ok()?;
|
||||
|
||||
Reference in New Issue
Block a user