diff --git a/crates/doc/src/cmyk.rs b/crates/doc/src/cmyk.rs index 8cd3f85a4f..77454f64a1 100644 --- a/crates/doc/src/cmyk.rs +++ b/crates/doc/src/cmyk.rs @@ -56,19 +56,114 @@ fn is_tiff(bytes: &[u8]) -> bool { bytes.starts_with(b"II*\0") || bytes.starts_with(b"MM\0*") } -/// The width and height of the first image of TIFF `bytes` when it has more strips or tiles than -/// the `tiff` decoder can count: strips per plane times planes past `u32::MAX`, or tiles per plane -/// times planes past `usize::MAX`. A file that stores one plane per channel (PlanarConfiguration 2) -/// has a plane for each channel; other files have one. The decoder multiplies these counts without -/// a check, which panics where overflow checks are on. Such an image, when it is at least a pixel -/// wide, claims at least 2^32 samples. -/// Where a tag repeats, the value giving the most strips or tiles counts. `None` for other images +/// What [`jpeg_frames`] found in a JPEG's headers. +enum Frames { + /// Every frame header declares a size the check accepts, or there is none. + Fit, + /// The width and height of the first frame header the check refuses. + Misfit(u16, u16), + /// The steps ran past [`MAX_JPEG_HEADER_STEPS`]. + OverBudget, +} + +/// Checks the width and height each 8-bit baseline, extended or progressive frame header in the +/// headers of a JPEG declares with `fits`. The JPEG is the bytes after its start-of-image marker: +/// `parts[0]` followed by `parts[1]`. The headers are read as the decoder reads them, up to the +/// first start-of-scan or end-of-image marker: every other marker is followed by its segment's +/// length, and bytes between segments are passed over, as are fill bytes (0xFF, and 0x00 after +/// one) before a marker. `steps` counts each byte looked at and each segment passed over. +fn jpeg_frames(parts: [&[u8]; 2], steps: &mut u64, fits: impl Fn(u16, u16) -> bool) -> Frames { + let byte = |i: usize| match i.checked_sub(parts[0].len()) { + None => parts[0].get(i).copied(), + Some(j) => parts[1].get(j).copied(), + }; + let u16_at = |i: usize| Some(u16::from_be_bytes([byte(i)?, byte(i.checked_add(1)?)?])); + let mut p = 0usize; + loop { + *steps += 1; + if *steps > MAX_JPEG_HEADER_STEPS { + return Frames::OverBudget; + } + let Some(b) = byte(p) else { return Frames::Fit }; + p += 1; + if b != 0xFF { + continue; + } + while matches!(byte(p), Some(0xFF | 0)) { + p += 1; + *steps += 1; + if *steps > MAX_JPEG_HEADER_STEPS { + return Frames::OverBudget; + } + } + let Some(marker) = byte(p) else { return Frames::Fit }; + p += 1; + if matches!(marker, 0xDA | 0xD9) { + return Frames::Fit; + } + // Length (2 bytes), then for a frame header its precision, height and width. + let Some(len) = u16_at(p) else { return Frames::Fit }; + if matches!(marker, 0xC0..=0xC2) + && byte(p.saturating_add(2)) == Some(8) + && let (Some(h), Some(w)) = (u16_at(p.saturating_add(3)), u16_at(p.saturating_add(5))) + && !fits(w, h) + { + return Frames::Misfit(w, h); + } + if len < 2 { + return Frames::Fit; + } + p = p.saturating_add(usize::from(len)); + } +} + +/// Most strips or tiles the first image of a TIFF may have: VectorCraft's own limit, 1/1024 of +/// [`crate::MAX_RASTER_ALLOC`] (524,288 in the web app, 2,097,152 on the desktop). The decoder +/// reads an offset and a byte count of 8 bytes each for every one into memory before anything else. +pub const MAX_TIFF_CHUNKS: u64 = crate::MAX_RASTER_ALLOC / 1024; + +/// Most pixels the JPEG of a JPEG-compressed TIFF tile may declare when it declares more pixels +/// than the whole image has: VectorCraft's own limit (2048 × 2048, 16 MB as CMYK). +pub const MAX_JPEG_TILE_PIXELS: u64 = 1 << 22; + +/// Most steps reading the JPEG headers of a TIFF's strips or tiles may take (a byte looked at, a +/// segment passed over or a strip offset read): VectorCraft's own limit. +const MAX_JPEG_HEADER_STEPS: u64 = 1 << 24; + +/// The width and height of the first image of TIFF or BigTIFF `bytes` when it has more strips or +/// tiles than the `tiff` decoder can count: strips per plane times planes past `u32::MAX`, or tiles +/// per plane times planes past `usize::MAX`. A file that stores one plane per channel +/// (PlanarConfiguration 2) has a plane for each channel; other files have one. The decoder +/// multiplies these counts without a check, which panics where overflow checks are on. Such an +/// image, when it is at least a pixel wide, claims at least 2^32 samples. +/// It also gives them for an image with more than [`MAX_TIFF_CHUNKS`] strips or tiles, counted from +/// its size or from the number of values its StripOffsets and StripByteCounts tags hold, or its +/// TileOffsets and TileByteCounts tags for a tiled image. An image the decoder reads as strips (it +/// has both strip tables and neither tile table) counts strips, and one it reads as tiles counts +/// tiles; the decoder refuses an image with any other mix of these tags before it reads a table. +/// For a JPEG-compressed image (Compression 7) it gives the width and height a strip's or tile's +/// JPEG declares in a frame header (in the JPEGTables, or in the strip or tile before its first +/// scan) when they are wider or taller than the strip or tile, or, for a tile, when they make more +/// than [`MAX_JPEG_TILE_PIXELS`] pixels and more than the image has. The decoder allocates the +/// pixels the JPEG declares. It gives the image's width and height when reading those headers +/// takes more than 16,777,216 steps (a byte looked at, a segment passed over or a strip offset +/// read), when the strip or tile offsets are of a type other than BYTE, UNDEFINED, SHORT, LONG, +/// IFD, LONG8 or IFD8, and when the image's width and height, a striped image's RowsPerStrip or a +/// tiled image's tile size isn't a single value of one of those types. +/// Where a tag repeats, the value giving the most strips or tiles counts, and the JPEG check uses +/// the last entry of a field type the decoder knows, as the decoder does. `None` for other images /// and for bytes that aren't a TIFF. pub fn oversized_tiff(bytes: &[u8]) -> Option<(u32, u32)> { - let le = bytes.starts_with(b"II*\0"); - if !le && !bytes.starts_with(b"MM\0*") { + let le = bytes.starts_with(b"II"); + let head = bytes.get(..4)?; + // A BigTIFF counts and points with 8 bytes where a TIFF uses 4, so its entries are 20 bytes. + let big = if head == b"II*\0" || head == b"MM\0*" { + false + } else if head == b"II+\0" || head == b"MM\0+" { + true + } else { return None; - } + }; let u16_at = |at: usize| -> Option { let b: [u8; 2] = bytes.get(at..at.checked_add(2)?)?.try_into().ok()?; Some(if le { u16::from_le_bytes(b) } else { u16::from_be_bytes(b) }) @@ -77,32 +172,71 @@ pub fn oversized_tiff(bytes: &[u8]) -> Option<(u32, u32)> { let b: [u8; 4] = bytes.get(at..at.checked_add(4)?)?.try_into().ok()?; Some(if le { u32::from_le_bytes(b) } else { u32::from_be_bytes(b) }) }; - let ifd = usize::try_from(u32_at(4)?).ok()?; - // (tag, the entry's single BYTE, SHORT, LONG or IFD value, which sits in the entry, or its - // single LONG8 or IFD8 value that fits in 32 bits, which sits where the entry points) - let entry = |i: usize| -> Option<(u16, Option)> { - let e = ifd.checked_add(i.checked_mul(12)?.checked_add(2)?)?; - let (ty, count, at) = (u16_at(e.checked_add(2)?)?, u32_at(e.checked_add(4)?)?, e.checked_add(8)?); - let value = match (ty, count) { - (1, 1) => bytes.get(at).map(|&b| u32::from(b)), - (3, 1) => u16_at(at).map(u32::from), - (4 | 13, 1) => u32_at(at), - (16 | 18, 1) => { - let at = usize::try_from(u32_at(at)?).ok()?; - let (lo, hi) = (u32_at(at)?, u32_at(at.checked_add(4)?)?); - let wide = if le { u64::from(hi) << 32 | u64::from(lo) } else { u64::from(lo) << 32 | u64::from(hi) }; - u32::try_from(wide).ok() - } + let u64_at = |at: usize| -> Option { + let b: [u8; 8] = bytes.get(at..at.checked_add(8)?)?.try_into().ok()?; + Some(if le { u64::from_le_bytes(b) } else { u64::from_be_bytes(b) }) + }; + let word_len: usize = if big { 8 } else { 4 }; + let word = |at: usize| if big { u64_at(at) } else { u32_at(at).map(u64::from) }; + // The bytes one value of each field type takes; 0 for types the decoder doesn't know. + let width_of = |ty: u16| -> usize { + match ty { + 1 | 2 | 6 | 7 => 1, + 3 | 8 => 2, + 4 | 9 | 11 | 13 => 4, + 5 | 10 | 12 | 16..=18 => 8, + _ => 0, + } + }; + let ifd = usize::try_from(word(if big { 8 } else { 4 })?).ok()?; + let entries = if big { u64_at(ifd)? } else { u64::from(u16_at(ifd)?) }; + let first = ifd.checked_add(if big { 8 } else { 2 })?; + // (tag, field type, number of values, where the values are: in the entry when they fit in its + // last word, otherwise where that word points) + let entry = |i: usize| -> Option<(u16, u16, u64, Option)> { + let e = first.checked_add(i.checked_mul(4 + 2 * word_len)?)?; + let (tag, ty, count) = (u16_at(e)?, u16_at(e.checked_add(2)?)?, word(e.checked_add(4)?)?); + let at = e.checked_add(4 + word_len)?; + let fits = u64::try_from(width_of(ty)).ok()?.checked_mul(count).is_some_and(|n| n <= word_len as u64); + let values = if fits { Some(at) } else { word(at).and_then(|p| usize::try_from(p).ok()) }; + Some((tag, ty, count, values)) + }; + // Value `i` of an entry's BYTE, UNDEFINED, SHORT, LONG, IFD, LONG8 or IFD8 values at `at`. + let nth = |ty: u16, at: usize, i: u64| -> Option { + let p = at.checked_add(usize::try_from(i).ok()?.checked_mul(width_of(ty))?)?; + match ty { + 1 | 7 => bytes.get(p).map(|&b| u64::from(b)), + 3 => u16_at(p).map(u64::from), + 4 | 13 => u32_at(p).map(u64::from), + 16 | 18 => u64_at(p), _ => None, - }; - Some((u16_at(e)?, value)) + } }; let (mut width, mut height, mut planes, mut planar) = (0u32, 0u32, 1u32, false); let (mut rows, mut tile_w, mut tile_h) = (None::, None::, None::); + // The most values a strip table and a tile table hold. + let (mut strip_chunks, mut tile_chunks) = (0u64, 0u64); + // The decoder keeps the last entry of a tag whose field type it knows. These are the last such + // entries (type, count, where) of the tags `LAST` lists. + const LAST: [u16; 11] = [256, 257, 259, 278, 322, 323, 273, 279, 324, 325, 347]; + let mut last = [None::<(u16, u64, Option)>; LAST.len()]; let least = |was: Option, v: u32| Some(was.map_or(v, |w| w.min(v))); - for i in 0..usize::from(u16_at(ifd)?) { - let Some((tag, value)) = entry(i) else { break }; - let Some(v) = value else { continue }; + // The entry's single BYTE, UNDEFINED, SHORT, LONG or IFD value, or its single LONG8 or IFD8 + // value when that fits in 32 bits. + let single = |ty: u16, count: u64, at: Option| at.filter(|_| count == 1).and_then(|at| nth(ty, at, 0)).and_then(|v| u32::try_from(v).ok()); + for i in 0..usize::try_from(entries).unwrap_or(usize::MAX) { + let Some((tag, ty, count, at)) = entry(i) else { break }; + match tag { + 273 | 279 => strip_chunks = strip_chunks.max(count), + 324 | 325 => tile_chunks = tile_chunks.max(count), + _ => {} + } + if width_of(ty) > 0 + && let Some(slot) = LAST.iter().position(|&t| t == tag).and_then(|k| last.get_mut(k)) + { + *slot = Some((ty, count, at)); + } + let Some(v) = single(ty, count, at) else { continue }; match tag { 256 => width = width.max(v), 257 => height = height.max(v), @@ -115,13 +249,87 @@ pub fn oversized_tiff(bytes: &[u8]) -> Option<(u32, u32)> { } } let planes = if planar { u128::from(planes) } else { 1 }; - let tiles = match (tile_w, tile_h) { - (Some(tw @ 1..), Some(th @ 1..)) => u128::from(width.div_ceil(tw)) * u128::from(height.div_ceil(th)) * planes > usize::MAX as u128, - _ => false, + let tiled = match (tile_w, tile_h) { + (Some(tw @ 1..), Some(th @ 1..)) => u128::from(width.div_ceil(tw)) * u128::from(height.div_ceil(th)) * planes, + _ => 0, }; + let tiles = tiled > usize::MAX as u128; let rows = rows.unwrap_or(height); - let strips = rows > 0 && u128::from(height.saturating_sub(1) / rows + 1) * planes > u128::from(u32::MAX); - (tiles || strips).then_some((width, height)) + let striped = height.saturating_sub(1).checked_div(rows).map_or(0, |n| u128::from(n + 1) * planes); + let strips = striped > u128::from(u32::MAX); + if tiles || strips { + return Some((width, height)); + } + let last_of = |tag: u16| LAST.iter().position(|&t| t == tag).and_then(|k| last.get(k).copied().flatten()); + // The single value the decoder reads from the last entry of `tag`: `None` without one, and + // `Some(None)` when that entry doesn't hold a single value read here. + let value_of = |tag: u16| last_of(tag).map(|(ty, count, at)| single(ty, count, at)); + // The decoder reads strips when the image has both strip tables and neither tile table, tiles + // in the opposite case, and refuses every other image before it reads a table. + let layout = match [273, 279, 324, 325].map(|tag| last_of(tag).is_some()) { + [true, true, false, false] => Some((273, striped.max(u128::from(strip_chunks)))), + [false, false, true, true] => Some((324, tiled.max(u128::from(tile_chunks)))), + _ => None, + }; + let (offsets_tag, chunks) = layout?; + if chunks > u128::from(MAX_TIFF_CHUNKS) { + return Some((width, height)); + } + if value_of(259) != Some(Some(7)) { + return None; + } + // The JPEG check uses the sizes the decoder reads. A size it needs that isn't a single value + // read here refuses the image. + let (Some(Some(w_img)), Some(Some(h_img))) = (value_of(256), value_of(257)) else { return Some((width, height)) }; + let tile = offsets_tag == 324; + // RowsPerStrip, which the decoder reads only for strips. + let rows = if tile { Some(h_img) } else { value_of(278).unwrap_or(Some(h_img)) }; + let Some(rows) = rows else { return Some((width, height)) }; + let (tw, th) = match (value_of(322).flatten(), value_of(323).flatten()) { + (Some(tw), Some(th)) => (tw, th), + _ if tile => return Some((width, height)), + _ => (0, 0), + }; + // A strip is as wide as the image and RowsPerStrip tall (at most the image's height); a tile's + // JPEG may be as large as the tile, which the edge tiles of the image overhang. + let fits = |w: u16, h: u16| { + let (w, h) = (u32::from(w), u32::from(h)); + if tile { + let pixels = u64::from(w) * u64::from(h); + w <= tw && h <= th && (pixels <= MAX_JPEG_TILE_PIXELS || pixels <= u64::from(w_img) * u64::from(h_img)) + } else { + w <= w_img && h <= rows.min(h_img) + } + }; + // The decoder reads the JPEGTables after their start-of-image marker and without their last two + // bytes, followed by each strip or tile after its first two bytes; without JPEGTables, each + // strip or tile after its start-of-image marker. + let tables = last_of(347) + .and_then(|(_, count, at)| { + let at = at?; + let end = at.checked_add(usize::try_from(count).ok()?)?.min(bytes.len()); + bytes.get(at.checked_add(2)?..end.checked_sub(2)?) + }) + .unwrap_or_default(); + let (ty, count, at) = last_of(offsets_tag)?; + let at = at?; + // The decoder also takes offsets of other types (a RATIONAL or ASCII value), which aren't read + // here: such an image is refused. + if !matches!(ty, 1 | 3 | 4 | 7 | 13 | 16 | 18) { + return Some((width, height)); + } + let mut steps = 0u64; + for i in 0..count { + steps += 1; + let Some(at) = nth(ty, at, i).and_then(|o| usize::try_from(o).ok()) else { break }; + let part = bytes.get(at.saturating_add(2)..).unwrap_or_default(); + match jpeg_frames([tables, part], &mut steps, fits) { + Frames::Fit => {} + Frames::Misfit(w, h) => return Some((u32::from(w), u32::from(h))), + Frames::OverBudget => return Some((width, height)), + } + } + None } /// The color type of TIFF `bytes` and its decoder; `None` for [`oversized_tiff`] images too. @@ -891,7 +1099,7 @@ mod tests { f }; assert_eq!(oversized_tiff(&header(2)), Some((u32::MAX, u32::MAX))); - assert_eq!(oversized_tiff(&header(1)), None, "one plane: (2^32 − 1)² tiles can be counted in 64 bits"); + assert_eq!(oversized_tiff(&header(1)), Some((u32::MAX, u32::MAX)), "one plane: countable, but past MAX_TIFF_CHUNKS"); assert!(tiff(&header(2)).is_none(), "not decoded"); } @@ -933,6 +1141,227 @@ mod tests { } } + /// A little-endian TIFF, or BigTIFF when `big`, holding `data` right after its header (from + /// byte 8, or 16 in a BigTIFF), then one directory of `entries` (tag, field type, values). An + /// entry's values sit in the entry when they fit, otherwise after the directory. + fn tiff_file(big: bool, entries: &[(u16, u16, Vec)], data: &[u8]) -> Vec { + let (word, head) = if big { (8, 16) } else { (4, 8) }; + let put = |f: &mut Vec, v: u64, n: usize| f.extend(&v.to_le_bytes()[..n]); + let mut f = if big { b"II+\0\x08\0\0\0".to_vec() } else { b"II*\0".to_vec() }; + let ifd = head + data.len(); + put(&mut f, ifd as u64, word); + f.extend(data); + put(&mut f, entries.len() as u64, if big { 8 } else { 2 }); + let after = f.len() + entries.len() * (4 + 2 * word) + word; + let mut tail = vec![]; + for (tag, ty, values) in entries { + let width = match ty { + 1 | 7 => 1, + 3 => 2, + 4 => 4, + _ => 8, + }; + f.extend([tag.to_le_bytes(), ty.to_le_bytes()].concat()); + put(&mut f, values.len() as u64, word); + let mut v = vec![]; + for &x in values { + put(&mut v, x, width); + } + if v.len() <= word { + v.resize(word, 0); + f.extend(v); + } else { + put(&mut f, (after + tail.len()) as u64, word); + tail.extend(v); + } + } + put(&mut f, 0, word); + f.extend(tail); + f + } + + /// A JPEG of four components whose progressive frame header declares `w` × `h` pixels, with a + /// scan header and no scan data. + fn jpeg_header(w: u16, h: u16) -> Vec { + let mut j = vec![0xFF, 0xD8, 0xFF, 0xC2, 0, 20, 8]; + j.extend(h.to_be_bytes()); + j.extend(w.to_be_bytes()); + j.push(4); + for id in 1..=4 { + j.extend([id, 0x11, 0]); + } + j.extend([0xFF, 0xDA, 0, 14, 4, 1, 0, 2, 0, 3, 0, 4, 0, 0, 63, 0, 0xFF, 0xD9]); + j + } + + /// The directory of a 1 × 1 CMYK image with alpha, JPEG-compressed in one strip of `len` bytes + /// at byte `at`. + fn jpeg_strip_entries(at: u64, len: u64) -> Vec<(u16, u16, Vec)> { + vec![ + (256, 4, vec![1]), + (257, 4, vec![1]), + (258, 3, vec![8; 5]), + (259, 3, vec![7]), + (262, 3, vec![5]), + (273, 4, vec![at]), + (277, 3, vec![5]), + (278, 4, vec![1]), + (279, 4, vec![len]), + (338, 3, vec![2]), + ] + } + + /// A strip's JPEG that declares more pixels than the strip holds would have the decoder + /// allocate them: a 1 × 1 image whose JPEG claims 16384 × 16384 is refused before any decoder + /// opens it, in a TIFF or a BigTIFF, with fill bytes or other bytes before the frame header, + /// and with the frame header in the JPEGTables. + #[test] + fn a_jpeg_strip_declaring_more_than_its_strip_is_refused() { + let big = jpeg_header(16384, 16384); + let file = tiff_file(false, &jpeg_strip_entries(8, big.len() as u64), &big); + assert_eq!(oversized_tiff(&file), Some((16384, 16384))); + assert!(tiff(&file).is_none(), "not decoded"); + let bigtiff = tiff_file(true, &jpeg_strip_entries(16, big.len() as u64), &big); + assert_eq!(oversized_tiff(&bigtiff), Some((16384, 16384)), "BigTIFF"); + // The decoder reads the last ImageWidth and ImageLength: 1 × 1 after 16384 × 16384, in one + // strip without RowsPerStrip. + let mut entries = jpeg_strip_entries(8, big.len() as u64); + entries.retain(|e| e.0 != 278); + entries.splice(0..0, [(256, 4, vec![16384]), (257, 4, vec![16384])]); + assert_eq!(oversized_tiff(&tiff_file(false, &entries, &big)), Some((16384, 16384)), "repeated sizes"); + // StripOffsets as a BYTE, which the decoder reads, and as a RATIONAL, which it reads as two + // offsets. + for (ty, expected) in [(1, (16384, 16384)), (5, (1, 1))] { + let mut entries = jpeg_strip_entries(8, big.len() as u64); + for e in &mut entries { + if e.0 == 273 { + e.1 = ty; + } + } + assert_eq!(oversized_tiff(&tiff_file(false, &entries, &big)), Some(expected), "StripOffsets of type {ty}"); + } + let one = jpeg_header(1, 1); + assert_eq!(oversized_tiff(&tiff_file(false, &jpeg_strip_entries(8, one.len() as u64), &one)), None, "a JPEG as large as its strip"); + let wide = jpeg_header(2, 1); + assert_eq!(oversized_tiff(&tiff_file(false, &jpeg_strip_entries(8, wide.len() as u64), &wide)), Some((2, 1)), "one pixel too wide"); + for (what, prefix) in [("fill bytes", &[0xFF, 0x00, 0xFF][..]), ("other bytes", &[0x12, 0x34][..])] { + let mut j = big[..2].to_vec(); + j.extend(prefix); + j.extend(&big[2..]); + let file = tiff_file(false, &jpeg_strip_entries(8, j.len() as u64), &j); + assert_eq!(oversized_tiff(&file), Some((16384, 16384)), "{what} before the frame header"); + } + // The JPEGTables hold the frame header; the strip holds only the scan. + let tables = [&big[..24], &[0xFF, 0xD9][..]].concat(); + let scan = [&[0xFF, 0xD8][..], &big[24..]].concat(); + let mut entries = jpeg_strip_entries(8 + tables.len() as u64, scan.len() as u64); + entries.push((347, 7, tables.iter().map(|&b| u64::from(b)).collect())); + let file = tiff_file(false, &entries, &[tables, scan].concat()); + assert_eq!(oversized_tiff(&file), Some((16384, 16384)), "frame header in the JPEGTables"); + // An encoder's 8 × 8 RGB JPEG, with its own segments before the frame header, in an + // 8 × 8 image of one strip, and of strips 4 rows tall. + let mut real = Vec::new(); + image::codecs::jpeg::JpegEncoder::new(&mut real).encode(&[90; 8 * 8 * 3], 8, 8, image::ExtendedColorType::Rgb8).unwrap(); + let rgb = |rows: u64| { + let entries = [ + (256, 4, vec![8]), + (257, 4, vec![8]), + (258, 3, vec![8; 3]), + (259, 3, vec![7]), + (262, 3, vec![2]), + (273, 4, vec![8]), + (277, 3, vec![3]), + (278, 4, vec![rows]), + (279, 4, vec![real.len() as u64]), + ]; + oversized_tiff(&tiff_file(false, &entries, &real)) + }; + assert_eq!(rgb(8), None, "an encoder's JPEG as large as its strip"); + assert_eq!(rgb(4), Some((8, 8)), "an encoder's JPEG taller than its strip"); + } + + /// A tile's JPEG may be as large as the tile, which overhangs the image, and when it is more + /// than MAX_JPEG_TILE_PIXELS, no larger than the image. + #[test] + fn a_jpeg_tile_declaring_more_than_its_tile_is_refused() { + let tiled_with = |tile: u64, j: &[u8], rows: Vec| { + let mut entries: Vec<_> = jpeg_strip_entries(0, 0).into_iter().filter(|e| !matches!(e.0, 273 | 278 | 279)).collect(); + if !rows.is_empty() { + entries.push((278, 4, rows)); + } + entries.extend([(322, 4, vec![tile]), (323, 4, vec![tile]), (324, 4, vec![8]), (325, 4, vec![j.len() as u64])]); + oversized_tiff(&tiff_file(false, &entries, j)) + }; + let tiled = |tile: u64, j: &[u8]| tiled_with(tile, j, vec![]); + assert_eq!(tiled_with(16, &jpeg_header(16, 16), vec![1, 1]), None, "RowsPerStrip, unread in a tiled image"); + assert_eq!(tiled(16, &jpeg_header(16, 16)), None, "as large as the tile"); + assert_eq!(tiled(16, &jpeg_header(17, 16)), Some((17, 16)), "wider than the tile"); + assert_eq!(tiled(16, &jpeg_header(16, 17)), Some((16, 17)), "taller than the tile"); + assert_eq!(tiled(4096, &jpeg_header(2048, 2048)), None, "MAX_JPEG_TILE_PIXELS"); + assert_eq!(tiled(4096, &jpeg_header(2048, 2049)), Some((2048, 2049)), "past MAX_JPEG_TILE_PIXELS and the image"); + } + + /// The decoder reads every strip's offset and byte count before anything else: an image with + /// more than MAX_TIFF_CHUNKS strips, counted from its size or from its tables, is refused. + #[test] + fn tiffs_with_more_strips_than_their_tables_may_hold_are_refused() { + let file = |height: u64, count: u32| { + let entries = [ + (256, 4, vec![1]), + (257, 4, vec![height]), + (258, 3, vec![8]), + (262, 3, vec![1]), + (273, 4, vec![8]), + (278, 4, vec![1]), + (279, 4, vec![1]), + ]; + let mut f = tiff_file(false, &entries, &[0]); + // StripOffsets' count, its values left out of the file. + let at = 8 + 1 + 2 + 4 * 12 + 4; + f[at..at + 4].copy_from_slice(&count.to_le_bytes()); + f + }; + let most = MAX_TIFF_CHUNKS; + assert_eq!(oversized_tiff(&file(most, 1)), None, "MAX_TIFF_CHUNKS rows of a strip each"); + assert_eq!(oversized_tiff(&file(most + 1, 1)), Some((1, u32::try_from(most + 1).unwrap())), "one strip more"); + assert_eq!(oversized_tiff(&file(1, u32::try_from(most).unwrap())), None, "MAX_TIFF_CHUNKS offsets"); + assert_eq!(oversized_tiff(&file(1, u32::try_from(most + 1).unwrap())), Some((1, 1)), "one offset more"); + assert!(tiff(&file(1, 1 << 25)).is_none(), "not decoded"); + // Tile sizes without tile tables: the decoder reads the strips and never counts the + // 4,194,304 tiles of a pixel the sizes give. + let strips_with_tile_sizes = |offsets: Option| { + let mut entries = vec![ + (256, 4, vec![2048]), + (257, 4, vec![2048]), + (258, 3, vec![8]), + (262, 3, vec![1]), + (273, 4, vec![8]), + (279, 4, vec![2048 * 2048]), + (322, 4, vec![1]), + (323, 4, vec![1]), + ]; + entries.extend(offsets.map(|tag| (tag, 4, vec![8]))); + entries.sort_by_key(|e| e.0); + oversized_tiff(&tiff_file(false, &entries, &[0])) + }; + assert_eq!(strips_with_tile_sizes(None), None, "a strip image with tile sizes of one pixel"); + assert_eq!(strips_with_tile_sizes(Some(324)), None, "strip and tile tables: the decoder refuses it unread"); + } + + /// Strips whose JPEG headers take more than MAX_JPEG_HEADER_STEPS steps to read refuse the + /// file: here every strip points at the same 1 MB without a marker. + #[test] + fn jpeg_strips_that_take_too_long_to_read_are_refused() { + let file = |strips: u64| { + let mut entries = jpeg_strip_entries(0, 0); + entries.retain(|e| !matches!(e.0, 257 | 273 | 279)); + entries.extend([(257, 4, vec![strips]), (273, 4, vec![8; strips as usize]), (279, 4, vec![1 << 20; strips as usize])]); + tiff_file(false, &entries, &std::iter::repeat_n(0, 1 << 20).collect::>()) + }; + assert_eq!(oversized_tiff(&file(8)), None, "about 8 million steps"); + assert_eq!(oversized_tiff(&file(17)), Some((1, 17)), "about 17 million steps"); + } + fn inks() -> Inks { Inks::new(3, 2, vec![0, 40, 100, 0, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 12, 34, 56, 78]).unwrap() }