Probe maximum raster frame allocation bounds

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 01:51:42 -05:00
1 parent 8076290854
commit afe51ed11e
2 files changed
+196

No files matched your search

+194
View File
@@ -0,0 +1,194 @@
//! Predecode bounds for raster containers whose frames may exceed their declared canvas.
use crate::codecs::webp;
use crate::error::CodecError;
use crate::format::Format;
use crate::image::{ChannelLayout, SampleType};
use crate::options::Limits;
/// The largest width and height any decoded frame can declare, without allocating pixel data.
/// Animated GIF and WebP frames are inspected individually, including frames larger than the
/// animation canvas. Other recognized raster formats use their container's image dimensions.
pub fn probe_allocation_size(bytes: &[u8]) -> Result<(u32, u32), CodecError> {
let format = crate::detect(bytes).ok_or(CodecError::UnknownFormat)?;
let (width, height) = match format {
Format::Gif => gif_bounds(bytes)?,
Format::WebP => {
let (width, height, _) = webp::probe(bytes, &Limits::default())?;
let (frame_width, frame_height) = webp_bounds(bytes)?;
(width.max(frame_width), height.max(frame_height))
}
_ => {
if let Some(info) = crate::inspect_raster(bytes)? {
(info.width, info.height)
} else {
crate::probe_size(bytes)?
}
}
};
Limits::default().check(width, height, ChannelLayout::Rgba, SampleType::U8)?;
Ok((width, height))
}
fn malformed(format: Format, message: &'static str) -> CodecError {
CodecError::malformed(format, message)
}
fn u16_le(bytes: &[u8], at: usize) -> Option<u16> {
Some(u16::from_le_bytes(bytes.get(at..at.checked_add(2)?)?.try_into().ok()?))
}
fn gif_bounds(bytes: &[u8]) -> Result<(u32, u32), CodecError> {
let bad = || malformed(Format::Gif, "truncated or malformed GIF frame table");
if !bytes.starts_with(b"GIF87a") && !bytes.starts_with(b"GIF89a") {
return Err(bad());
}
let packed = *bytes.get(10).ok_or_else(bad)?;
let (mut width, mut height) = (u32::from(u16_le(bytes, 6).ok_or_else(bad)?), u32::from(u16_le(bytes, 8).ok_or_else(bad)?));
let table_len =
if packed & 0x80 != 0 { 3usize.checked_mul(1usize.checked_shl(u32::from((packed & 7) + 1)).ok_or_else(bad)?).ok_or_else(bad)? } else { 0 };
let mut at = 13usize.checked_add(table_len).ok_or_else(bad)?;
if bytes.get(..at).is_none() || width == 0 || height == 0 {
return Err(bad());
}
let mut frames = 0usize;
loop {
match bytes.get(at).copied().ok_or_else(bad)? {
0x3b => break,
0x21 => {
at = at.checked_add(2).ok_or_else(bad)?;
loop {
let length = usize::from(*bytes.get(at).ok_or_else(bad)?);
at = at.checked_add(1).ok_or_else(bad)?;
if length == 0 {
break;
}
at = at.checked_add(length).ok_or_else(bad)?;
if bytes.get(..at).is_none() {
return Err(bad());
}
}
}
0x2c => {
let left = u32::from(u16_le(bytes, at + 1).ok_or_else(bad)?);
let top = u32::from(u16_le(bytes, at + 3).ok_or_else(bad)?);
let frame_width = u32::from(u16_le(bytes, at + 5).ok_or_else(bad)?);
let frame_height = u32::from(u16_le(bytes, at + 7).ok_or_else(bad)?);
let descriptor = bytes.get(at..at.checked_add(10).ok_or_else(bad)?).ok_or_else(bad)?;
let flags = descriptor[9];
let mut body = at.checked_add(10).ok_or_else(bad)?;
if flags & 0x80 != 0 {
let table = 3usize.checked_mul(1usize.checked_shl(u32::from((flags & 7) + 1)).ok_or_else(bad)?).ok_or_else(bad)?;
body = body.checked_add(table).ok_or_else(bad)?;
}
body = body.checked_add(1).ok_or_else(bad)?; // LZW minimum code size.
if bytes.get(..body).is_none() || frame_width == 0 || frame_height == 0 {
return Err(bad());
}
width = width.max(left.saturating_add(frame_width));
height = height.max(top.saturating_add(frame_height));
at = body;
loop {
let length = usize::from(*bytes.get(at).ok_or_else(bad)?);
at = at.checked_add(1).ok_or_else(bad)?;
if length == 0 {
break;
}
at = at.checked_add(length).ok_or_else(bad)?;
if bytes.get(..at).is_none() {
return Err(bad());
}
}
frames = frames.saturating_add(1);
}
_ => return Err(bad()),
}
}
if frames == 0 {
return Err(bad());
}
Ok((width, height))
}
fn webp_bounds(bytes: &[u8]) -> Result<(u32, u32), CodecError> {
let bad = || malformed(Format::WebP, "truncated or malformed WebP frame table");
if bytes.get(0..4) != Some(b"RIFF") || bytes.get(8..12) != Some(b"WEBP") {
return Err(bad());
}
let riff_len = usize::try_from(u32::from_le_bytes(bytes.get(4..8).ok_or_else(bad)?.try_into().map_err(|_| bad())?)).map_err(|_| bad())?;
let end = 8usize.checked_add(riff_len).ok_or_else(bad)?;
if end > bytes.len() || end < 12 {
return Err(bad());
}
let mut max_width = 0u32;
let mut max_height = 0u32;
webp_chunks(bytes, 12, end, false, &mut max_width, &mut max_height)?;
if max_width == 0 || max_height == 0 {
return Err(bad());
}
Ok((max_width, max_height))
}
fn webp_chunks(bytes: &[u8], mut at: usize, end: usize, nested: bool, max_width: &mut u32, max_height: &mut u32) -> Result<(), CodecError> {
let bad = || malformed(Format::WebP, "truncated or malformed WebP frame table");
while at < end {
let header_end = at.checked_add(8).ok_or_else(bad)?;
let header = bytes.get(at..header_end).filter(|_| header_end <= end).ok_or_else(bad)?;
let size = usize::try_from(u32::from_le_bytes(header.get(4..8).ok_or_else(bad)?.try_into().map_err(|_| bad())?)).map_err(|_| bad())?;
let from = header_end;
let to = from.checked_add(size).ok_or_else(bad)?;
let payload = bytes.get(from..to).filter(|_| to <= end).ok_or_else(bad)?;
match header.get(..4).ok_or_else(bad)? {
b"VP8 " => {
if payload.get(3..6) != Some(&[0x9d, 0x01, 0x2a]) || payload.len() < 10 {
return Err(bad());
}
let w = u32::from(u16::from_le_bytes([payload[6], payload[7]]) & 0x3fff);
let h = u32::from(u16::from_le_bytes([payload[8], payload[9]]) & 0x3fff);
*max_width = (*max_width).max(w);
*max_height = (*max_height).max(h);
}
b"VP8L" => {
if payload.len() < 5 || payload[0] != 0x2f {
return Err(bad());
}
let bits = u32::from_le_bytes([payload[1], payload[2], payload[3], payload[4]]);
*max_width = (*max_width).max((bits & 0x3fff) + 1);
*max_height = (*max_height).max(((bits >> 14) & 0x3fff) + 1);
}
b"VP8X" => {
if nested || payload.len() < 10 {
return Err(bad());
}
*max_width = (*max_width).max(read_le24(payload, 4).ok_or_else(bad)?.saturating_add(1));
*max_height = (*max_height).max(read_le24(payload, 7).ok_or_else(bad)?.saturating_add(1));
}
b"ANMF" => {
if nested || payload.len() < 16 {
return Err(bad());
}
let x = read_le24(payload, 0).ok_or_else(bad)?.saturating_mul(2);
let y = read_le24(payload, 3).ok_or_else(bad)?.saturating_mul(2);
let w = read_le24(payload, 6).ok_or_else(bad)?.saturating_add(1);
let h = read_le24(payload, 9).ok_or_else(bad)?.saturating_add(1);
*max_width = (*max_width).max(x.saturating_add(w));
*max_height = (*max_height).max(y.saturating_add(h));
webp_chunks(payload, 16, payload.len(), true, max_width, max_height)?;
}
_ => {}
}
at = to.checked_add(size & 1).ok_or_else(bad)?;
if at > end {
return Err(bad());
}
}
if at != end {
return Err(bad());
}
Ok(())
}
fn read_le24(bytes: &[u8], at: usize) -> Option<u32> {
let b = bytes.get(at..at.checked_add(3)?)?;
Some(u32::from_le_bytes([b[0], b[1], b[2], 0]))
}
+2
View File
@@ -14,6 +14,7 @@
#![forbid(unsafe_code)]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod allocation;
pub mod cicp;
mod codecs;
mod error;
@@ -29,6 +30,7 @@ pub mod orientation;
pub mod resolution;
pub mod source_settings;
pub mod web;
pub use crate::allocation::probe_allocation_size;
pub use crate::cicp::Cicp;
pub use crate::codecs::exr_cryptomatte::{cryptomatte_id, cryptomatte_key, cryptomatte_preview_color};
pub use crate::codecs::gif_frames::{AnimationFrame, MAX_GIF_FRAMES};