diff --git a/crates/images/src/jpeg.rs b/crates/images/src/jpeg.rs index fea0a1e64..6188a53fc 100644 --- a/crates/images/src/jpeg.rs +++ b/crates/images/src/jpeg.rs @@ -67,21 +67,38 @@ pub(crate) const MAX_SHRINK: u8 = 3; /// A YCbCr, RGB or grayscale JPEG decoded at 1/2^`shrink` of its size, each side rounded up, for /// `shrink` 1 to [`MAX_SHRINK`]: straight RGBA and whether the image has colour. `None` for CMYK, -/// 12-bit and lossless JPEGs, other formats and anything that doesn't decode. +/// 12-bit and lossless JPEGs, other formats and anything that doesn't decode. `None` as well for a +/// JPEG whose full size is over the full decode's limit, and for a progressive JPEG whose +/// full-size coefficients take more than [`MAX_BYTES`]; the full decode applies its own limits to +/// those. pub(crate) fn decode_reduced(bytes: &[u8], shrink: u8) -> Option<(image::RgbaImage, bool)> { - use jpeg_decoder::PixelFormat; + use jpeg_decoder::{CodingProcess, PixelFormat}; if !bytes.starts_with(&[0xFF, 0xD8]) || shrink == 0 || shrink > MAX_SHRINK { return None; } let mut d = jpeg_decoder::Decoder::new(bytes); + // This limits the output only, which is the reduced size. d.set_max_decoding_buffer_size(MAX_BYTES); d.read_info().ok()?; let info = d.info()?; + if crate::over_rgba_budget(u32::from(info.width), u32::from(info.height)) { + return None; + } let colour = match info.pixel_format { PixelFormat::RGB24 => true, PixelFormat::L8 => false, PixelFormat::L16 | PixelFormat::CMYK32 => return None, }; + if info.coding_process == CodingProcess::DctProgressive { + // A progressive JPEG holds every coefficient of the full-size image (two bytes each) until + // its last scan, whatever the output size. Each component is counted at the full size and + // each side is rounded up to the largest MCU (32 pixels): an upper bound. + let side = |n: u16| u64::from(n).next_multiple_of(32); + let components: u64 = if colour { 3 } else { 1 }; + if side(info.width) * side(info.height) * components * 2 > MAX_BYTES as u64 { + return None; + } + } let reduced = |n: u16| u16::try_from(u32::from(n).div_ceil(1 << shrink)).ok(); let (w, h) = d.scale(reduced(info.width)?, reduced(info.height)?).ok()?; let px = d.decode().ok()?; diff --git a/crates/images/src/lib.rs b/crates/images/src/lib.rs index 530ca8097..5754e8788 100644 --- a/crates/images/src/lib.rs +++ b/crates/images/src/lib.rs @@ -407,7 +407,12 @@ const MAX_RGBA_BYTES: u64 = 1 << 30; /// Whether a raster's header gives it more pixels than [`decode_rgba`] decodes. pub fn too_large_to_decode(bytes: &[u8]) -> bool { // `image` limits only its own output, which can be a quarter of the RGBA copy or less. - pixel_size(bytes).is_some_and(|(w, h)| u64::from(w).saturating_mul(u64::from(h)).saturating_mul(4) > MAX_RGBA_BYTES) + pixel_size(bytes).is_some_and(|(w, h)| over_rgba_budget(w, h)) +} + +/// Whether a `w` × `h` raster takes more than [`MAX_RGBA_BYTES`] as RGBA. +fn over_rgba_budget(w: u32, h: u32) -> bool { + u64::from(w).saturating_mul(u64::from(h)).saturating_mul(4) > MAX_RGBA_BYTES } /// Straight (non-premultiplied) RGBA8 of a raster in display sRGB: an embedded ICC profile is @@ -448,9 +453,12 @@ pub const MAX_REDUCED_SHRINK: u8 = jpeg::MAX_SHRINK; /// [`decode_rgba`] of a JPEG at 1/2^`shrink` of its size (`shrink` 1 to [`MAX_REDUCED_SHRINK`], each /// side rounded up), read from its compressed blocks without a full-size decode. The embedded ICC /// profile is applied as [`decode_rgba`] applies it. `None` where only a full decode applies: other -/// formats, CMYK, 12-bit and lossless JPEGs, and files [`decode_rgba`] refuses as too large. +/// formats, CMYK, 12-bit and lossless JPEGs, files whose size [`pixel_size`] cannot read, and files +/// [`decode_rgba`] refuses as too large. pub fn decode_rgba_reduced(bytes: &[u8], shrink: u8) -> Option { use colour::{DisplayColour, Pixels, to_display_rgba}; + // The size check must have a size to check: a header only the JPEG decoder reads is refused. + pixel_size(bytes)?; if too_large_to_decode(bytes) { return None; } diff --git a/crates/images/src/tests.rs b/crates/images/src/tests.rs index ae5435a62..184e3c9c9 100644 --- a/crates/images/src/tests.rs +++ b/crates/images/src/tests.rs @@ -611,3 +611,61 @@ fn reduced_decoding_is_for_jpegs_and_survives_damaged_ones() { } } } + +#[test] +fn reduced_decoding_refuses_a_jpeg_whose_size_the_size_check_cannot_read() { + let jpeg = gradient_jpeg(64, 48, false); + assert_eq!(decode_rgba_reduced(&jpeg, 1).unwrap().dimensions(), (32, 24)); + // A stray byte after the start marker: the JPEG decoder skips it, but `pixel_size` doesn't + // recognize the file, so `too_large_to_decode` has no size to check. + let mut stray = jpeg.clone(); + stray.insert(2, 0); + assert_eq!(pixel_size(&stray), None); + assert!(decode_rgba_reduced(&stray, 1).is_none()); + assert!(decode_rgba(&stray).is_none()); +} + +/// A progressive JPEG with only its DC scan, `w` × `h` with `components` 1×1-sampled components: +/// every coefficient is zero (a flat mid-gray image). +fn progressive_jpeg(w: u16, h: u16, components: u8) -> Vec { + let mut out = vec![0xFF, 0xD8]; + // One quantization table of ones. + out.extend([0xFF, 0xDB, 0, 67, 0]); + out.extend([1; 64]); + // Start of frame, progressive DCT. + out.extend([0xFF, 0xC2, 0, 8 + 3 * components, 8]); + out.extend(h.to_be_bytes()); + out.extend(w.to_be_bytes()); + out.push(components); + for id in 1..=components { + out.extend([id, 0x11, 0]); + } + // A DC table with one code, "0", for a difference of 0. + out.extend([0xFF, 0xC4, 0, 20, 0x00, 1]); + out.extend([0; 15]); + out.push(0); + // The DC scan of every component (Ss, Se, Ah/Al all 0), then zero bits. + out.extend([0xFF, 0xDA, 0, 6 + 2 * components, components]); + for id in 1..=components { + out.extend([id, 0x00]); + } + out.extend([0, 0, 0]); + out.extend([0; 16]); + out.extend([0xFF, 0xD9]); + out +} + +#[test] +fn reduced_decoding_refuses_progressive_jpegs_whose_full_size_coefficients_are_over_the_limit() { + let small = decode_rgba_reduced(&progressive_jpeg(64, 48, 3), 3).unwrap(); + assert_eq!(small.dimensions(), (8, 6)); + // 10,240 × 8,800 is within the RGBA limit, but its three components' coefficients take + // 10,240 × 8,800 × 3 × 2 bytes, over 512 MiB, whatever the output size. + let large = progressive_jpeg(10_240, 8_800, 3); + assert_eq!(pixel_size(&large), Some((10_240, 8_800))); + assert!(!too_large_to_decode(&large)); + assert!(decode_rgba_reduced(&large, 3).is_none()); + // One component of the same size stays under the limit. + let gray = decode_rgba_reduced(&progressive_jpeg(10_240, 8_800, 1), 3).unwrap(); + assert_eq!(gray.dimensions(), (1_280, 1_100)); +}