Skip to main content

webp_anim/codec/
decode.rs

1use std::{error::Error, fmt, time::Duration};
2
3use libwebp_sys::{
4    WEBP_CSP_MODE, WebPAnimDecoder, WebPAnimDecoderDelete, WebPAnimDecoderGetInfo,
5    WebPAnimDecoderGetNext, WebPAnimDecoderHasMoreFrames, WebPAnimDecoderNewInternal,
6    WebPAnimDecoderOptions, WebPAnimDecoderOptionsInitInternal, WebPAnimInfo, WebPData,
7    WebPGetDemuxABIVersion,
8};
9
10use crate::{
11    inspect::is_animated_webp_fast,
12    model::{AnimationFrame, AnimationInfo, BackgroundColor, CanvasSize, LoopCount},
13};
14
15/// Per-animation limits applied before and while decoding.
16///
17/// The [`Default`] values are intended for ordinary untrusted inputs. Use
18/// [`Self::for_trusted_input`] only when the caller has already established an
19/// appropriate process-wide memory and workload policy.
20#[derive(Clone, Debug)]
21pub struct DecodeLimits {
22    /// Maximum number of input bytes accepted by [`AnimationDecoder::new`].
23    pub max_input_bytes: usize,
24    /// Maximum number of pixels in the decoded animation canvas.
25    pub max_canvas_pixels: u64,
26    /// Maximum number of frames in the stored animation sequence.
27    pub max_frame_count: u32,
28    /// Maximum sum of source frame durations observed while decoding.
29    pub max_total_duration: Duration,
30    /// Maximum number of bytes in one full-canvas RGBA frame.
31    pub max_frame_rgba_bytes: usize,
32}
33
34impl Default for DecodeLimits {
35    fn default() -> Self {
36        Self {
37            max_input_bytes: 256 * 1024 * 1024,
38            max_canvas_pixels: 100_000_000,
39            max_frame_count: 10_000,
40            max_total_duration: Duration::from_secs(60 * 60),
41            max_frame_rgba_bytes: 400 * 1024 * 1024,
42        }
43    }
44}
45
46impl DecodeLimits {
47    /// Relaxes crate-level resource limits for trusted input.
48    ///
49    /// This does not bypass libwebp or platform allocation limits.
50    pub const fn for_trusted_input() -> Self {
51        Self {
52            max_input_bytes: usize::MAX,
53            max_canvas_pixels: u64::MAX,
54            max_frame_count: u32::MAX,
55            max_total_duration: Duration::MAX,
56            max_frame_rgba_bytes: usize::MAX,
57        }
58    }
59}
60
61/// Failure to create or read an animation decoder.
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub enum DecodeError {
64    /// The input is larger than the configured byte limit.
65    InputTooLarge {
66        /// Actual input length in bytes.
67        actual: usize,
68        /// Configured maximum input length in bytes.
69        maximum: usize,
70    },
71    /// The input is a valid WebP image but does not contain animation data.
72    NotAnimatedWebp,
73    /// libwebp could not initialize its decoder options.
74    DecoderOptionsInitialization,
75    /// libwebp could not create an animation decoder.
76    DecoderCreation,
77    /// libwebp could not provide animation metadata.
78    DecoderInfo,
79    /// The animation metadata could not be represented by this crate's types.
80    InvalidAnimationInfo,
81    /// A canvas or frame size overflowed the host address space.
82    FrameSizeOverflow,
83    /// A configured resource limit was exceeded.
84    LimitExceeded {
85        /// Name of the exceeded limit.
86        limit: &'static str,
87        /// Observed value.
88        actual: u64,
89        /// Configured maximum value.
90        maximum: u64,
91    },
92    /// libwebp failed to decode the next frame.
93    FrameDecode,
94    /// Frame timestamps were not monotonic or could not be accumulated.
95    InvalidTimestamp,
96}
97
98impl fmt::Display for DecodeError {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        match self {
101            Self::InputTooLarge { actual, maximum } => {
102                write!(
103                    f,
104                    "input is {actual} bytes, exceeding the {maximum}-byte limit"
105                )
106            }
107            Self::NotAnimatedWebp => f.write_str("input is not an animated WebP"),
108            Self::DecoderOptionsInitialization => {
109                f.write_str("failed to initialize WebP decoder options")
110            }
111            Self::DecoderCreation => f.write_str("failed to create WebP animation decoder"),
112            Self::DecoderInfo => f.write_str("failed to read WebP animation information"),
113            Self::InvalidAnimationInfo => f.write_str("WebP animation information is invalid"),
114            Self::FrameSizeOverflow => {
115                f.write_str("WebP animation frame size overflows the host address space")
116            }
117            Self::LimitExceeded {
118                limit,
119                actual,
120                maximum,
121            } => {
122                write!(f, "{limit} is {actual}, exceeding the {maximum} limit")
123            }
124            Self::FrameDecode => f.write_str("failed to decode WebP animation frame"),
125            Self::InvalidTimestamp => f.write_str("WebP animation timestamps are invalid"),
126        }
127    }
128}
129
130impl Error for DecodeError {}
131
132/// Stateful decoder for exactly one stored animation sequence.
133pub struct AnimationDecoder {
134    // The C decoder borrows this allocation for its complete lifetime.
135    _input: Vec<u8>,
136    decoder: *mut WebPAnimDecoder,
137    info: AnimationInfo,
138    frame_rgba_bytes: usize,
139    previous_timestamp_ms: i32,
140    total_duration: Duration,
141    max_total_duration: Duration,
142}
143
144impl Drop for AnimationDecoder {
145    fn drop(&mut self) {
146        // SAFETY: `decoder` is created only by libwebp and released exactly once here.
147        unsafe {
148            if !self.decoder.is_null() {
149                WebPAnimDecoderDelete(self.decoder);
150            }
151        }
152    }
153}
154
155impl AnimationDecoder {
156    /// Creates a decoder for one stored animated WebP sequence.
157    ///
158    /// The input bytes are copied so the returned decoder owns the data needed
159    /// by libwebp. Frames are returned as composited, full-canvas RGBA buffers.
160    /// The decoder does not replay the sequence according to its loop count.
161    pub fn new(input: &[u8], limits: DecodeLimits) -> Result<Self, DecodeError> {
162        if input.len() > limits.max_input_bytes {
163            return Err(DecodeError::InputTooLarge {
164                actual: input.len(),
165                maximum: limits.max_input_bytes,
166            });
167        }
168        if !is_animated_webp_fast(input) {
169            return Err(DecodeError::NotAnimatedWebp);
170        }
171
172        let input = input.to_vec();
173        // SAFETY: libwebp initializes every field before the options are read.
174        let mut options: WebPAnimDecoderOptions = unsafe { std::mem::zeroed() };
175        let demux_abi = WebPGetDemuxABIVersion();
176        // SAFETY: `options` is a valid writable pointer and the ABI is supplied by libwebp.
177        if unsafe { WebPAnimDecoderOptionsInitInternal(&mut options, demux_abi) } == 0 {
178            return Err(DecodeError::DecoderOptionsInitialization);
179        }
180        options.color_mode = WEBP_CSP_MODE::MODE_RGBA;
181        options.use_threads = 1;
182
183        let data = WebPData {
184            bytes: input.as_ptr(),
185            size: input.len(),
186        };
187        // SAFETY: `input` is moved into `Self`, keeping `data.bytes` valid until the decoder drops.
188        let decoder = unsafe { WebPAnimDecoderNewInternal(&data, &options, demux_abi) };
189        if decoder.is_null() {
190            return Err(DecodeError::DecoderCreation);
191        }
192
193        // SAFETY: libwebp writes `raw_info` when given a valid decoder.
194        let mut raw_info: WebPAnimInfo = unsafe { std::mem::zeroed() };
195        // SAFETY: `decoder` is non-null and `raw_info` is valid writable storage.
196        if unsafe { WebPAnimDecoderGetInfo(decoder, &mut raw_info) } == 0 {
197            // SAFETY: creation succeeded, so the decoder must be released on this early return.
198            unsafe { WebPAnimDecoderDelete(decoder) };
199            return Err(DecodeError::DecoderInfo);
200        }
201
202        let canvas = CanvasSize {
203            width: raw_info.canvas_width,
204            height: raw_info.canvas_height,
205        };
206        let pixel_count = canvas.pixel_count().ok_or(DecodeError::FrameSizeOverflow)?;
207        enforce_limit("canvas pixels", pixel_count, limits.max_canvas_pixels)?;
208        enforce_limit(
209            "frame count",
210            u64::from(raw_info.frame_count),
211            u64::from(limits.max_frame_count),
212        )?;
213        let frame_rgba_bytes = canvas.rgba_bytes().ok_or(DecodeError::FrameSizeOverflow)?;
214        enforce_limit(
215            "RGBA bytes per frame",
216            u64::try_from(frame_rgba_bytes).unwrap_or(u64::MAX),
217            u64::try_from(limits.max_frame_rgba_bytes).unwrap_or(u64::MAX),
218        )?;
219
220        let loop_count = match raw_info.loop_count {
221            0 => LoopCount::Infinite,
222            value => LoopCount::Finite(
223                std::num::NonZeroU16::new(
224                    u16::try_from(value).map_err(|_| DecodeError::InvalidAnimationInfo)?,
225                )
226                .expect("non-zero loop count"),
227            ),
228        };
229        Ok(Self {
230            _input: input,
231            decoder,
232            info: AnimationInfo {
233                canvas,
234                frame_count: raw_info.frame_count,
235                loop_count,
236                background_color: BackgroundColor {
237                    raw: raw_info.bgcolor,
238                },
239            },
240            frame_rgba_bytes,
241            previous_timestamp_ms: 0,
242            total_duration: Duration::ZERO,
243            max_total_duration: limits.max_total_duration,
244        })
245    }
246
247    /// Returns metadata for the stored animation sequence.
248    pub fn info(&self) -> &AnimationInfo {
249        &self.info
250    }
251
252    /// Returns whether unread frames remain in the stored sequence.
253    pub fn has_more_frames(&self) -> bool {
254        // SAFETY: the decoder is valid until `Drop`; this query does not advance it.
255        unsafe { WebPAnimDecoderHasMoreFrames(self.decoder) != 0 }
256    }
257
258    /// Returns `None` only after every frame in the stored sequence was read.
259    pub fn next_frame(&mut self) -> Result<Option<AnimationFrame>, DecodeError> {
260        if !self.has_more_frames() {
261            return Ok(None);
262        }
263
264        let mut rgba = std::ptr::null_mut();
265        let mut timestamp_ms = 0_i32;
266        // SAFETY: libwebp writes both output pointers for a valid decoder state.
267        let ok = unsafe { WebPAnimDecoderGetNext(self.decoder, &mut rgba, &mut timestamp_ms) };
268        if ok == 0 || rgba.is_null() {
269            return Err(DecodeError::FrameDecode);
270        }
271        let duration_ms = timestamp_ms
272            .checked_sub(self.previous_timestamp_ms)
273            .ok_or(DecodeError::InvalidTimestamp)?;
274        let duration = Duration::from_millis(
275            u64::try_from(duration_ms).map_err(|_| DecodeError::InvalidTimestamp)?,
276        );
277        let total_duration = self
278            .total_duration
279            .checked_add(duration)
280            .ok_or(DecodeError::InvalidTimestamp)?;
281        enforce_limit(
282            "total duration in milliseconds",
283            total_duration.as_millis().try_into().unwrap_or(u64::MAX),
284            self.max_total_duration
285                .as_millis()
286                .try_into()
287                .unwrap_or(u64::MAX),
288        )?;
289
290        // SAFETY: libwebp returns a full-canvas RGBA buffer with the checked size above.
291        let rgba = unsafe { std::slice::from_raw_parts(rgba, self.frame_rgba_bytes) }.to_vec();
292        self.previous_timestamp_ms = timestamp_ms;
293        self.total_duration = total_duration;
294
295        Ok(Some(AnimationFrame {
296            rgba,
297            canvas: self.info.canvas,
298            duration,
299        }))
300    }
301}
302
303fn enforce_limit(limit: &'static str, actual: u64, maximum: u64) -> Result<(), DecodeError> {
304    if actual > maximum {
305        return Err(DecodeError::LimitExceeded {
306            limit,
307            actual,
308            maximum,
309        });
310    }
311    Ok(())
312}