mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 06:06:49 -06:00
Images: check the RGBA budget before GIF tone and CMYK TIFF decodes (#1586)
tone() decoded a GIF to RGBA and decode_cmyk_tiff() called read_image() without the header check decode_rgba() runs. Both now call too_large_to_decode() first. The decoders' own limits (image 512 MiB, tiff 256 MiB) already refused these files; the guard refuses them from the header instead.
This commit is contained in:
@@ -532,6 +532,9 @@ pub fn is_cmyk_tiff(bytes: &[u8]) -> bool {
|
||||
/// over the decoder's 256 MiB limit; [`is_cmyk_tiff`] tells those apart from RGB files.
|
||||
pub fn decode_cmyk_tiff(bytes: &[u8]) -> Option<CmykRaster> {
|
||||
use tiff::tags::Tag;
|
||||
if too_large_to_decode(bytes) {
|
||||
return None;
|
||||
}
|
||||
let mut d = tiff_decoder(bytes)?;
|
||||
let has_alpha = match d.colortype().ok()? {
|
||||
tiff::ColorType::CMYK(8 | 16) => false,
|
||||
|
||||
@@ -441,3 +441,57 @@ fn rasters_past_the_rgba_budget_are_refused_before_decoding() {
|
||||
let small = decode_rgba(&blank_png(9)).expect("decodes");
|
||||
assert_eq!(small.get_pixel(8, 8).0, [0, 0, 0, 255]);
|
||||
}
|
||||
|
||||
/// A GIF whose logical screen is `side` x `side`, holding one black 1x1 frame.
|
||||
fn gif_screen(side: u16) -> Vec<u8> {
|
||||
let mut gif = b"GIF89a".to_vec();
|
||||
gif.extend(side.to_le_bytes());
|
||||
gif.extend(side.to_le_bytes());
|
||||
// Two-colour global table: black, white.
|
||||
gif.extend([0x80, 0, 0, 0, 0, 0, 255, 255, 255]);
|
||||
// Frame at the origin; LZW minimum code size 2, codes clear, 0, end; trailer.
|
||||
gif.extend([0x2C, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 0x44, 0x01, 0, 0x3B]);
|
||||
gif
|
||||
}
|
||||
|
||||
/// The header of an uncompressed 8-bit CMYK TIFF of `side` x `side` pixels, without the pixels.
|
||||
fn cmyk_tiff_header(side: u32) -> Vec<u8> {
|
||||
let entries: [(u16, u16, u32, u32); 9] = [
|
||||
(256, 4, 1, side), // width
|
||||
(257, 4, 1, side), // height
|
||||
(258, 3, 4, 122), // bits per sample, stored after the IFD
|
||||
(259, 3, 1, 1), // no compression
|
||||
(262, 3, 1, 5), // separated (CMYK)
|
||||
(273, 4, 1, 130), // strip offset
|
||||
(277, 3, 1, 4), // samples per pixel
|
||||
(278, 4, 1, side), // rows per strip
|
||||
(279, 4, 1, side * side * 4), // strip byte count
|
||||
];
|
||||
let mut t = b"II*\0".to_vec();
|
||||
t.extend(8u32.to_le_bytes());
|
||||
t.extend(9u16.to_le_bytes());
|
||||
for (tag, kind, count, value) in entries {
|
||||
t.extend(tag.to_le_bytes());
|
||||
t.extend(kind.to_le_bytes());
|
||||
t.extend(count.to_le_bytes());
|
||||
t.extend(value.to_le_bytes());
|
||||
}
|
||||
t.extend(0u32.to_le_bytes());
|
||||
assert_eq!(t.len(), 122);
|
||||
t.extend([8u16; 4].iter().flat_map(|b| b.to_le_bytes()));
|
||||
t
|
||||
}
|
||||
|
||||
/// GIF tones and CMYK inks check the RGBA budget too (#1586).
|
||||
#[test]
|
||||
fn gif_tone_and_cmyk_inks_check_the_rgba_budget() {
|
||||
let gif = gif_screen(16_385);
|
||||
assert!(too_large_to_decode(&gif));
|
||||
assert_eq!(tone(&gif), None);
|
||||
assert_eq!(tone(&gif_screen(2)), Some(Tone::Bitmap));
|
||||
let tiff = cmyk_tiff_header(16_385);
|
||||
assert!(is_cmyk_tiff(&tiff));
|
||||
assert_eq!(pixel_size(&tiff), Some((16_385, 16_385)));
|
||||
assert!(too_large_to_decode(&tiff));
|
||||
assert_eq!(decode_cmyk_tiff(&tiff), None);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ pub fn tone(bytes: &[u8]) -> Option<Tone> {
|
||||
},
|
||||
// Indexed: what the palette holds decides. Transparent pixels are left out of a bitmap.
|
||||
F::Gif => {
|
||||
if crate::too_large_to_decode(bytes) {
|
||||
return None;
|
||||
}
|
||||
let img = image::load_from_memory_with_format(bytes, F::Gif).ok()?.into_rgba8();
|
||||
let mut opaque = true;
|
||||
let mut two_level = true;
|
||||
|
||||
Reference in New Issue
Block a user