use std::{error::Error, fmt, time::Duration};
use libwebp_sys::{
WEBP_CSP_MODE, WebPAnimDecoder, WebPAnimDecoderDelete, WebPAnimDecoderGetInfo,
WebPAnimDecoderGetNext, WebPAnimDecoderHasMoreFrames, WebPAnimDecoderNewInternal,
WebPAnimDecoderOptions, WebPAnimDecoderOptionsInitInternal, WebPAnimInfo, WebPData,
WebPGetDemuxABIVersion,
};
use crate::{
inspect::is_animated_webp_fast,
model::{AnimationFrame, AnimationInfo, BackgroundColor, CanvasSize, LoopCount},
};
#[derive(Clone, Debug)]
pub struct DecodeLimits {
pub max_input_bytes: usize,
pub max_canvas_pixels: u64,
pub max_frame_count: u32,
pub max_total_duration: Duration,
pub max_frame_rgba_bytes: usize,
}
impl Default for DecodeLimits {
fn default() -> Self {
Self {
max_input_bytes: 256 * 1024 * 1024,
max_canvas_pixels: 100_000_000,
max_frame_count: 10_000,
max_total_duration: Duration::from_secs(60 * 60),
max_frame_rgba_bytes: 400 * 1024 * 1024,
}
}
}
impl DecodeLimits {
pub const fn for_trusted_input() -> Self {
Self {
max_input_bytes: usize::MAX,
max_canvas_pixels: u64::MAX,
max_frame_count: u32::MAX,
max_total_duration: Duration::MAX,
max_frame_rgba_bytes: usize::MAX,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DecodeError {
InputTooLarge {
actual: usize,
maximum: usize,
},
NotAnimatedWebp,
DecoderOptionsInitialization,
DecoderCreation,
DecoderInfo,
InvalidAnimationInfo,
FrameSizeOverflow,
LimitExceeded {
limit: &'static str,
actual: u64,
maximum: u64,
},
FrameDecode,
InvalidTimestamp,
}
impl fmt::Display for DecodeError {
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::NotAnimatedWebp => f.write_str("input is not an animated WebP"),
Self::DecoderOptionsInitialization => {
f.write_str("failed to initialize WebP decoder options")
}
Self::DecoderCreation => f.write_str("failed to create WebP animation decoder"),
Self::DecoderInfo => f.write_str("failed to read WebP animation information"),
Self::InvalidAnimationInfo => f.write_str("WebP animation information is invalid"),
Self::FrameSizeOverflow => {
f.write_str("WebP animation frame size overflows the host address space")
}
Self::LimitExceeded {
limit,
actual,
maximum,
} => {
write!(f, "{limit} is {actual}, exceeding the {maximum} limit")
}
Self::FrameDecode => f.write_str("failed to decode WebP animation frame"),
Self::InvalidTimestamp => f.write_str("WebP animation timestamps are invalid"),
}
}
}
impl Error for DecodeError {}
pub struct AnimationDecoder {
_input: Vec<u8>,
decoder: *mut WebPAnimDecoder,
info: AnimationInfo,
frame_rgba_bytes: usize,
previous_timestamp_ms: i32,
total_duration: Duration,
max_total_duration: Duration,
}
impl Drop for AnimationDecoder {
fn drop(&mut self) {
unsafe {
if !self.decoder.is_null() {
WebPAnimDecoderDelete(self.decoder);
}
}
}
}
impl AnimationDecoder {
pub fn new(input: &[u8], limits: DecodeLimits) -> Result<Self, DecodeError> {
if input.len() > limits.max_input_bytes {
return Err(DecodeError::InputTooLarge {
actual: input.len(),
maximum: limits.max_input_bytes,
});
}
if !is_animated_webp_fast(input) {
return Err(DecodeError::NotAnimatedWebp);
}
let input = input.to_vec();
let mut options: WebPAnimDecoderOptions = unsafe { std::mem::zeroed() };
let demux_abi = WebPGetDemuxABIVersion();
if unsafe { WebPAnimDecoderOptionsInitInternal(&mut options, demux_abi) } == 0 {
return Err(DecodeError::DecoderOptionsInitialization);
}
options.color_mode = WEBP_CSP_MODE::MODE_RGBA;
options.use_threads = 1;
let data = WebPData {
bytes: input.as_ptr(),
size: input.len(),
};
let decoder = unsafe { WebPAnimDecoderNewInternal(&data, &options, demux_abi) };
if decoder.is_null() {
return Err(DecodeError::DecoderCreation);
}
let mut raw_info: WebPAnimInfo = unsafe { std::mem::zeroed() };
if unsafe { WebPAnimDecoderGetInfo(decoder, &mut raw_info) } == 0 {
unsafe { WebPAnimDecoderDelete(decoder) };
return Err(DecodeError::DecoderInfo);
}
let canvas = CanvasSize {
width: raw_info.canvas_width,
height: raw_info.canvas_height,
};
let pixel_count = canvas.pixel_count().ok_or(DecodeError::FrameSizeOverflow)?;
enforce_limit("canvas pixels", pixel_count, limits.max_canvas_pixels)?;
enforce_limit(
"frame count",
u64::from(raw_info.frame_count),
u64::from(limits.max_frame_count),
)?;
let frame_rgba_bytes = canvas.rgba_bytes().ok_or(DecodeError::FrameSizeOverflow)?;
enforce_limit(
"RGBA bytes per frame",
u64::try_from(frame_rgba_bytes).unwrap_or(u64::MAX),
u64::try_from(limits.max_frame_rgba_bytes).unwrap_or(u64::MAX),
)?;
let loop_count = match raw_info.loop_count {
0 => LoopCount::Infinite,
value => LoopCount::Finite(
std::num::NonZeroU16::new(
u16::try_from(value).map_err(|_| DecodeError::InvalidAnimationInfo)?,
)
.expect("non-zero loop count"),
),
};
Ok(Self {
_input: input,
decoder,
info: AnimationInfo {
canvas,
frame_count: raw_info.frame_count,
loop_count,
background_color: BackgroundColor {
raw: raw_info.bgcolor,
},
},
frame_rgba_bytes,
previous_timestamp_ms: 0,
total_duration: Duration::ZERO,
max_total_duration: limits.max_total_duration,
})
}
pub fn info(&self) -> &AnimationInfo {
&self.info
}
pub fn has_more_frames(&self) -> bool {
unsafe { WebPAnimDecoderHasMoreFrames(self.decoder) != 0 }
}
pub fn next_frame(&mut self) -> Result<Option<AnimationFrame>, DecodeError> {
if !self.has_more_frames() {
return Ok(None);
}
let mut rgba = std::ptr::null_mut();
let mut timestamp_ms = 0_i32;
let ok = unsafe { WebPAnimDecoderGetNext(self.decoder, &mut rgba, &mut timestamp_ms) };
if ok == 0 || rgba.is_null() {
return Err(DecodeError::FrameDecode);
}
let duration_ms = timestamp_ms
.checked_sub(self.previous_timestamp_ms)
.ok_or(DecodeError::InvalidTimestamp)?;
let duration = Duration::from_millis(
u64::try_from(duration_ms).map_err(|_| DecodeError::InvalidTimestamp)?,
);
let total_duration = self
.total_duration
.checked_add(duration)
.ok_or(DecodeError::InvalidTimestamp)?;
enforce_limit(
"total duration in milliseconds",
total_duration.as_millis().try_into().unwrap_or(u64::MAX),
self.max_total_duration
.as_millis()
.try_into()
.unwrap_or(u64::MAX),
)?;
let rgba = unsafe { std::slice::from_raw_parts(rgba, self.frame_rgba_bytes) }.to_vec();
self.previous_timestamp_ms = timestamp_ms;
self.total_duration = total_duration;
Ok(Some(AnimationFrame {
rgba,
canvas: self.info.canvas,
duration,
}))
}
}
fn enforce_limit(limit: &'static str, actual: u64, maximum: u64) -> Result<(), DecodeError> {
if actual > maximum {
return Err(DecodeError::LimitExceeded {
limit,
actual,
maximum,
});
}
Ok(())
}