use std::{error::Error, fmt};
use libwebp_sys::WebPGetInfo;
use crate::{
codec::decode::{AnimationDecoder, DecodeError, DecodeLimits},
model::{CanvasSize, StaticWebpInfo},
};
const RIFF_HEADER_LEN: usize = 12;
const RIFF_CHUNK_HEADER_LEN: usize = 8;
const WEBP_ANIMATION_FLAG: u8 = 0x02;
#[derive(Clone, Debug)]
pub struct InspectLimits {
pub max_input_bytes: usize,
pub max_canvas_pixels: u64,
pub max_frame_count: u32,
pub max_frame_rgba_bytes: usize,
}
impl Default for InspectLimits {
fn default() -> Self {
let decode = DecodeLimits::default();
Self {
max_input_bytes: decode.max_input_bytes,
max_canvas_pixels: decode.max_canvas_pixels,
max_frame_count: decode.max_frame_count,
max_frame_rgba_bytes: decode.max_frame_rgba_bytes,
}
}
}
impl InspectLimits {
pub const fn for_trusted_input() -> Self {
Self {
max_input_bytes: usize::MAX,
max_canvas_pixels: u64::MAX,
max_frame_count: u32::MAX,
max_frame_rgba_bytes: usize::MAX,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WebpKind {
Static(StaticWebpInfo),
Animated(crate::model::AnimationInfo),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InspectError {
InputTooLarge {
actual: usize,
maximum: usize,
},
InvalidContainer,
TruncatedContainer,
InvalidWebp,
LimitExceeded {
limit: &'static str,
actual: u64,
maximum: u64,
},
Animation(DecodeError),
}
impl fmt::Display for InspectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InputTooLarge { actual, maximum } => {
write!(
f,
"input is {actual} bytes, exceeding the {maximum}-byte limit"
)
}
Self::InvalidContainer => f.write_str("input is not a RIFF/WebP container"),
Self::TruncatedContainer => {
f.write_str("WebP RIFF container is truncated or malformed")
}
Self::InvalidWebp => f.write_str("input is not a valid WebP image"),
Self::LimitExceeded {
limit,
actual,
maximum,
} => {
write!(f, "{limit} is {actual}, exceeding the {maximum} limit")
}
Self::Animation(error) => write!(f, "failed to inspect animated WebP: {error}"),
}
}
}
impl Error for InspectError {}
pub fn inspect(input: &[u8], limits: InspectLimits) -> Result<WebpKind, InspectError> {
if input.len() > limits.max_input_bytes {
return Err(InspectError::InputTooLarge {
actual: input.len(),
maximum: limits.max_input_bytes,
});
}
let animated = animation_flag(input)?;
let mut width = 0_i32;
let mut height = 0_i32;
if unsafe { WebPGetInfo(input.as_ptr(), input.len(), &mut width, &mut height) } == 0 {
return Err(InspectError::InvalidWebp);
}
let canvas = CanvasSize {
width: u32::try_from(width).map_err(|_| InspectError::InvalidWebp)?,
height: u32::try_from(height).map_err(|_| InspectError::InvalidWebp)?,
};
enforce_canvas_limit(canvas, limits.max_canvas_pixels)?;
if !animated {
return Ok(WebpKind::Static(StaticWebpInfo { canvas }));
}
let decode_limits = DecodeLimits {
max_input_bytes: limits.max_input_bytes,
max_canvas_pixels: limits.max_canvas_pixels,
max_frame_count: limits.max_frame_count,
max_frame_rgba_bytes: limits.max_frame_rgba_bytes,
..DecodeLimits::default()
};
let decoder = AnimationDecoder::new(input, decode_limits).map_err(InspectError::Animation)?;
Ok(WebpKind::Animated(*decoder.info()))
}
pub fn is_animated_webp_fast(input: &[u8]) -> bool {
animation_flag(input).unwrap_or(false)
}
fn animation_flag(input: &[u8]) -> Result<bool, InspectError> {
if input.len() < RIFF_HEADER_LEN {
return Err(InspectError::TruncatedContainer);
}
if &input[..4] != b"RIFF" || &input[8..RIFF_HEADER_LEN] != b"WEBP" {
return Err(InspectError::InvalidContainer);
}
let riff_size =
usize::try_from(u32::from_le_bytes(input[4..8].try_into().unwrap())).unwrap_or(usize::MAX);
let container_end = riff_size
.checked_add(8)
.ok_or(InspectError::TruncatedContainer)?;
if container_end > input.len() {
return Err(InspectError::TruncatedContainer);
}
let mut offset = RIFF_HEADER_LEN;
while offset < container_end {
let header_end = offset
.checked_add(RIFF_CHUNK_HEADER_LEN)
.ok_or(InspectError::TruncatedContainer)?;
if header_end > container_end {
return Err(InspectError::TruncatedContainer);
}
let chunk = &input[offset..offset + 4];
let length = usize::try_from(u32::from_le_bytes(
input[offset + 4..header_end].try_into().unwrap(),
))
.unwrap_or(usize::MAX);
let payload = header_end;
let padded_length = length
.checked_add(length & 1)
.ok_or(InspectError::TruncatedContainer)?;
let next = payload
.checked_add(padded_length)
.ok_or(InspectError::TruncatedContainer)?;
if next > container_end {
return Err(InspectError::TruncatedContainer);
}
if chunk == b"VP8X" {
if length < 10 {
return Err(InspectError::TruncatedContainer);
}
return Ok(input[payload] & WEBP_ANIMATION_FLAG != 0);
}
offset = next;
}
Ok(false)
}
fn enforce_canvas_limit(canvas: CanvasSize, maximum: u64) -> Result<(), InspectError> {
let actual = canvas.pixel_count().ok_or(InspectError::InvalidWebp)?;
if actual > maximum {
return Err(InspectError::LimitExceeded {
limit: "canvas pixels",
actual,
maximum,
});
}
Ok(())
}