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, WebPAnimDecoderGetDemuxer,
5    WebPAnimDecoderGetInfo, WebPAnimDecoderGetNext, WebPAnimDecoderHasMoreFrames,
6    WebPAnimDecoderNewInternal, WebPAnimDecoderOptions, WebPAnimDecoderOptionsInitInternal,
7    WebPAnimDecoderReset, WebPAnimInfo, WebPData, WebPDemuxGetFrame, WebPDemuxNextFrame,
8    WebPDemuxReleaseIterator, WebPGetDemuxABIVersion, WebPIterator,
9};
10
11use crate::{
12    inspect::is_animated_webp_fast,
13    model::{AnimationFrame, AnimationInfo, BackgroundColor, CanvasSize, LoopCount},
14};
15
16/// Per-animation limits applied before and while decoding.
17///
18/// The [`Default`] values are intended for ordinary untrusted inputs. Use
19/// [`Self::for_trusted_input`] only when the caller has already established an
20/// appropriate process-wide memory and workload policy.
21#[derive(Clone, Debug)]
22pub struct DecodeLimits {
23    /// Maximum number of input bytes accepted by [`AnimationDecoder::new`].
24    pub max_input_bytes: usize,
25    /// Maximum number of pixels in the decoded animation canvas.
26    pub max_canvas_pixels: u64,
27    /// Maximum number of frames in the stored animation sequence.
28    pub max_frame_count: u32,
29    /// Maximum sum of source frame durations observed while decoding.
30    pub max_total_duration: Duration,
31    /// Maximum number of bytes in one full-canvas RGBA frame.
32    pub max_frame_rgba_bytes: usize,
33}
34
35impl Default for DecodeLimits {
36    fn default() -> Self {
37        Self {
38            max_input_bytes: 256 * 1024 * 1024,
39            max_canvas_pixels: 100_000_000,
40            max_frame_count: 10_000,
41            max_total_duration: Duration::from_secs(60 * 60),
42            max_frame_rgba_bytes: 400 * 1024 * 1024,
43        }
44    }
45}
46
47impl DecodeLimits {
48    /// Relaxes crate-level resource limits for trusted input.
49    ///
50    /// This does not bypass libwebp or platform allocation limits.
51    pub const fn for_trusted_input() -> Self {
52        Self {
53            max_input_bytes: usize::MAX,
54            max_canvas_pixels: u64::MAX,
55            max_frame_count: u32::MAX,
56            max_total_duration: Duration::MAX,
57            max_frame_rgba_bytes: usize::MAX,
58        }
59    }
60}
61
62/// Failure to create or read an animation decoder.
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub enum DecodeError {
65    /// The input is larger than the configured byte limit.
66    InputTooLarge {
67        /// Actual input length in bytes.
68        actual: usize,
69        /// Configured maximum input length in bytes.
70        maximum: usize,
71    },
72    /// The input is a valid WebP image but does not contain animation data.
73    NotAnimatedWebp,
74    /// libwebp could not initialize its decoder options.
75    DecoderOptionsInitialization,
76    /// libwebp could not create an animation decoder.
77    DecoderCreation,
78    /// libwebp could not provide animation metadata.
79    DecoderInfo,
80    /// The animation metadata could not be represented by this crate's types.
81    InvalidAnimationInfo,
82    /// A canvas or frame size overflowed the host address space.
83    FrameSizeOverflow,
84    /// A configured resource limit was exceeded.
85    LimitExceeded {
86        /// Name of the exceeded limit.
87        limit: &'static str,
88        /// Observed value.
89        actual: u64,
90        /// Configured maximum value.
91        maximum: u64,
92    },
93    /// libwebp failed to decode the next frame.
94    FrameDecode,
95    /// Frame timestamps were not monotonic or could not be accumulated.
96    InvalidTimestamp,
97}
98
99impl fmt::Display for DecodeError {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            Self::InputTooLarge { actual, maximum } => {
103                write!(
104                    f,
105                    "input is {actual} bytes, exceeding the {maximum}-byte limit"
106                )
107            }
108            Self::NotAnimatedWebp => f.write_str("input is not an animated WebP"),
109            Self::DecoderOptionsInitialization => {
110                f.write_str("failed to initialize WebP decoder options")
111            }
112            Self::DecoderCreation => f.write_str("failed to create WebP animation decoder"),
113            Self::DecoderInfo => f.write_str("failed to read WebP animation information"),
114            Self::InvalidAnimationInfo => f.write_str("WebP animation information is invalid"),
115            Self::FrameSizeOverflow => {
116                f.write_str("WebP animation frame size overflows the host address space")
117            }
118            Self::LimitExceeded {
119                limit,
120                actual,
121                maximum,
122            } => {
123                write!(f, "{limit} is {actual}, exceeding the {maximum} limit")
124            }
125            Self::FrameDecode => f.write_str("failed to decode WebP animation frame"),
126            Self::InvalidTimestamp => f.write_str("WebP animation timestamps are invalid"),
127        }
128    }
129}
130
131impl Error for DecodeError {}
132
133/// Stateful decoder for exactly one stored animation sequence.
134pub struct AnimationDecoder {
135    // The C decoder borrows this allocation for its complete lifetime.
136    _input: Vec<u8>,
137    decoder: *mut WebPAnimDecoder,
138    info: AnimationInfo,
139    frame_rgba_bytes: usize,
140    previous_timestamp_ms: i32,
141    total_duration: Duration,
142    max_total_duration: Duration,
143}
144
145impl Drop for AnimationDecoder {
146    fn drop(&mut self) {
147        // SAFETY: `decoder` is created only by libwebp and released exactly once here.
148        unsafe {
149            if !self.decoder.is_null() {
150                WebPAnimDecoderDelete(self.decoder);
151            }
152        }
153    }
154}
155
156struct RawDecoderGuard(*mut WebPAnimDecoder);
157
158impl RawDecoderGuard {
159    fn into_raw(mut self) -> *mut WebPAnimDecoder {
160        let decoder = self.0;
161        self.0 = std::ptr::null_mut();
162        decoder
163    }
164}
165
166impl Drop for RawDecoderGuard {
167    fn drop(&mut self) {
168        // SAFETY: the pointer is returned by libwebp and is released at most once;
169        // `into_raw` nulls it before ownership is transferred to `AnimationDecoder`.
170        unsafe {
171            if !self.0.is_null() {
172                WebPAnimDecoderDelete(self.0);
173            }
174        }
175    }
176}
177
178struct DemuxIteratorGuard {
179    iterator: WebPIterator,
180    initialized: bool,
181}
182
183impl DemuxIteratorGuard {
184    fn new() -> Self {
185        // SAFETY: `WebPIterator` is a C struct whose fields are initialized by
186        // `WebPDemuxGetFrame`; zeroed storage is valid for that writable input.
187        Self {
188            iterator: unsafe { std::mem::zeroed() },
189            initialized: false,
190        }
191    }
192
193    fn mark_initialized(&mut self) {
194        self.initialized = true;
195    }
196}
197
198impl Drop for DemuxIteratorGuard {
199    fn drop(&mut self) {
200        // SAFETY: the iterator storage remains valid for the guard lifetime;
201        // libwebp requires releasing the iterator after the demux attempt and
202        // before the borrowed demuxer can be used or destroyed.
203        if self.initialized {
204            // SAFETY: `initialized` is set only after WebPDemuxGetFrame succeeds.
205            unsafe { WebPDemuxReleaseIterator(&mut self.iterator) };
206        }
207    }
208}
209
210impl AnimationDecoder {
211    /// Creates a decoder for one stored animated WebP sequence.
212    ///
213    /// The input bytes are copied so the returned decoder owns the data needed
214    /// by libwebp. Frames are returned as composited, full-canvas RGBA buffers.
215    /// The decoder does not replay the sequence according to its loop count.
216    pub fn new(input: &[u8], limits: DecodeLimits) -> Result<Self, DecodeError> {
217        if input.len() > limits.max_input_bytes {
218            return Err(DecodeError::InputTooLarge {
219                actual: input.len(),
220                maximum: limits.max_input_bytes,
221            });
222        }
223        if !is_animated_webp_fast(input) {
224            return Err(DecodeError::NotAnimatedWebp);
225        }
226
227        let input = input.to_vec();
228        // SAFETY: libwebp initializes every field before the options are read.
229        let mut options: WebPAnimDecoderOptions = unsafe { std::mem::zeroed() };
230        let demux_abi = WebPGetDemuxABIVersion();
231        // SAFETY: `options` is a valid writable pointer and the ABI is supplied by libwebp.
232        if unsafe { WebPAnimDecoderOptionsInitInternal(&mut options, demux_abi) } == 0 {
233            return Err(DecodeError::DecoderOptionsInitialization);
234        }
235        options.color_mode = WEBP_CSP_MODE::MODE_RGBA;
236        options.use_threads = 1;
237
238        let data = WebPData {
239            bytes: input.as_ptr(),
240            size: input.len(),
241        };
242        // SAFETY: `input` is moved into `Self`, keeping `data.bytes` valid until the decoder drops.
243        let raw_decoder = unsafe { WebPAnimDecoderNewInternal(&data, &options, demux_abi) };
244        if raw_decoder.is_null() {
245            return Err(DecodeError::DecoderCreation);
246        }
247        let decoder_guard = RawDecoderGuard(raw_decoder);
248
249        // SAFETY: libwebp writes `raw_info` when given a valid decoder.
250        let mut raw_info: WebPAnimInfo = unsafe { std::mem::zeroed() };
251        // SAFETY: `decoder_guard.0` is non-null and `raw_info` is valid writable storage.
252        if unsafe { WebPAnimDecoderGetInfo(decoder_guard.0, &mut raw_info) } == 0 {
253            return Err(DecodeError::DecoderInfo);
254        }
255
256        let canvas = CanvasSize {
257            width: raw_info.canvas_width,
258            height: raw_info.canvas_height,
259        };
260        let pixel_count = canvas.pixel_count().ok_or(DecodeError::FrameSizeOverflow)?;
261        enforce_limit("canvas pixels", pixel_count, limits.max_canvas_pixels)?;
262        enforce_limit(
263            "frame count",
264            u64::from(raw_info.frame_count),
265            u64::from(limits.max_frame_count),
266        )?;
267        let frame_rgba_bytes = canvas.rgba_bytes().ok_or(DecodeError::FrameSizeOverflow)?;
268        enforce_limit(
269            "RGBA bytes per frame",
270            u64::try_from(frame_rgba_bytes).unwrap_or(u64::MAX),
271            u64::try_from(limits.max_frame_rgba_bytes).unwrap_or(u64::MAX),
272        )?;
273
274        let loop_count = match raw_info.loop_count {
275            0 => LoopCount::Infinite,
276            value => LoopCount::Finite(
277                std::num::NonZeroU16::new(
278                    u16::try_from(value).map_err(|_| DecodeError::InvalidAnimationInfo)?,
279                )
280                .expect("non-zero loop count"),
281            ),
282        };
283        Ok(Self {
284            _input: input,
285            decoder: decoder_guard.into_raw(),
286            info: AnimationInfo {
287                canvas,
288                frame_count: raw_info.frame_count,
289                loop_count,
290                background_color: BackgroundColor {
291                    raw: raw_info.bgcolor,
292                },
293            },
294            frame_rgba_bytes,
295            previous_timestamp_ms: 0,
296            total_duration: Duration::ZERO,
297            max_total_duration: limits.max_total_duration,
298        })
299    }
300
301    /// Returns metadata for the stored animation sequence.
302    pub fn info(&self) -> &AnimationInfo {
303        &self.info
304    }
305
306    /// Returns one source display duration per stored frame, in frame order.
307    ///
308    /// This reads animation-container metadata only: it does not decode RGBA
309    /// pixels or advance the sequential decoder. The configured total-duration
310    /// limit is applied and may cause this method to return an error.
311    pub fn frame_durations(&self) -> Result<Vec<Duration>, DecodeError> {
312        // SAFETY: `decoder` is non-null and remains valid for this value's lifetime;
313        // libwebp returns a borrowed demuxer owned by that decoder.
314        let demuxer = unsafe { WebPAnimDecoderGetDemuxer(self.decoder) };
315        if demuxer.is_null() {
316            return Err(DecodeError::DecoderInfo);
317        }
318
319        let mut iterator = DemuxIteratorGuard::new();
320        // SAFETY: `demuxer` is non-null and borrowed for the guard lifetime;
321        // `iterator` is valid writable storage for libwebp to initialize.
322        if unsafe { WebPDemuxGetFrame(demuxer, 1, &mut iterator.iterator) } == 0 {
323            return Err(DecodeError::DecoderInfo);
324        }
325        iterator.mark_initialized();
326
327        let frame_count = usize::try_from(self.info.frame_count)
328            .map_err(|_| DecodeError::InvalidAnimationInfo)?;
329        if u32::try_from(iterator.iterator.num_frames).ok() != Some(self.info.frame_count)
330            || iterator.iterator.frame_num != 1
331        {
332            return Err(DecodeError::InvalidAnimationInfo);
333        }
334
335        let mut durations = Vec::with_capacity(frame_count);
336        let mut total_duration = Duration::ZERO;
337        loop {
338            let actual_count = durations.len().saturating_add(1);
339            if actual_count > frame_count {
340                return Err(DecodeError::InvalidAnimationInfo);
341            }
342            if i32::try_from(actual_count).ok() != Some(iterator.iterator.frame_num) {
343                return Err(DecodeError::InvalidAnimationInfo);
344            }
345
346            let duration_ms = iterator.iterator.duration;
347            if duration_ms < 0 {
348                return Err(DecodeError::InvalidTimestamp);
349            }
350            let duration = Duration::from_millis(
351                u64::try_from(duration_ms).map_err(|_| DecodeError::InvalidTimestamp)?,
352            );
353            total_duration = total_duration
354                .checked_add(duration)
355                .ok_or(DecodeError::InvalidTimestamp)?;
356            let total_duration_ms = u64::try_from(total_duration.as_millis())
357                .map_err(|_| DecodeError::InvalidTimestamp)?;
358            enforce_limit(
359                "total duration in milliseconds",
360                total_duration_ms,
361                self.max_total_duration
362                    .as_millis()
363                    .try_into()
364                    .unwrap_or(u64::MAX),
365            )?;
366            durations.push(duration);
367
368            // SAFETY: the iterator was initialized successfully and its
369            // borrowed demuxer remains alive; libwebp advances only its
370            // iterator state and reports the end through the return value.
371            if unsafe { WebPDemuxNextFrame(&mut iterator.iterator) } == 0 {
372                break;
373            }
374        }
375
376        if durations.len() != frame_count {
377            return Err(DecodeError::InvalidAnimationInfo);
378        }
379        Ok(durations)
380    }
381
382    /// Resets this decoder to the first frame of its stored animation sequence.
383    ///
384    /// The decoder, input allocation, immutable metadata, and configured limits
385    /// are preserved; only libwebp's sequence state and this wrapper's timing
386    /// accumulator are reset.
387    /// This is a sequence reset, not a random-seek operation; the next
388    /// [`Self::next_frame`] call starts at the first stored frame.
389    pub fn reset(&mut self) {
390        // SAFETY: `decoder` is non-null and remains valid for this value's lifetime;
391        // libwebp resets only the native decoder sequence state.
392        unsafe { WebPAnimDecoderReset(self.decoder) };
393        self.previous_timestamp_ms = 0;
394        self.total_duration = Duration::ZERO;
395    }
396
397    /// Returns whether unread frames remain in the stored sequence.
398    pub fn has_more_frames(&self) -> bool {
399        // SAFETY: the decoder is valid until `Drop`; this query does not advance it.
400        unsafe { WebPAnimDecoderHasMoreFrames(self.decoder) != 0 }
401    }
402
403    /// Returns `None` only after every frame in the stored sequence was read.
404    pub fn next_frame(&mut self) -> Result<Option<AnimationFrame>, DecodeError> {
405        if !self.has_more_frames() {
406            return Ok(None);
407        }
408
409        let mut rgba = std::ptr::null_mut();
410        let mut timestamp_ms = 0_i32;
411        // SAFETY: libwebp writes both output pointers for a valid decoder state.
412        let ok = unsafe { WebPAnimDecoderGetNext(self.decoder, &mut rgba, &mut timestamp_ms) };
413        if ok == 0 || rgba.is_null() {
414            return Err(DecodeError::FrameDecode);
415        }
416        let duration_ms = timestamp_ms
417            .checked_sub(self.previous_timestamp_ms)
418            .ok_or(DecodeError::InvalidTimestamp)?;
419        let duration = Duration::from_millis(
420            u64::try_from(duration_ms).map_err(|_| DecodeError::InvalidTimestamp)?,
421        );
422        let total_duration = self
423            .total_duration
424            .checked_add(duration)
425            .ok_or(DecodeError::InvalidTimestamp)?;
426        enforce_limit(
427            "total duration in milliseconds",
428            total_duration.as_millis().try_into().unwrap_or(u64::MAX),
429            self.max_total_duration
430                .as_millis()
431                .try_into()
432                .unwrap_or(u64::MAX),
433        )?;
434
435        // SAFETY: libwebp returns a full-canvas RGBA buffer with the checked size above.
436        let rgba = unsafe { std::slice::from_raw_parts(rgba, self.frame_rgba_bytes) }.to_vec();
437        self.previous_timestamp_ms = timestamp_ms;
438        self.total_duration = total_duration;
439
440        Ok(Some(AnimationFrame {
441            rgba,
442            canvas: self.info.canvas,
443            duration,
444        }))
445    }
446}
447
448fn enforce_limit(limit: &'static str, actual: u64, maximum: u64) -> Result<(), DecodeError> {
449    if actual > maximum {
450        return Err(DecodeError::LimitExceeded {
451            limit,
452            actual,
453            maximum,
454        });
455    }
456    Ok(())
457}