1use std::{error::Error, fmt};
2
3use libwebp_sys::WebPGetInfo;
4
5use crate::{
6 codec::decode::{AnimationDecoder, DecodeError, DecodeLimits},
7 model::{CanvasSize, StaticWebpInfo},
8};
9
10const RIFF_HEADER_LEN: usize = 12;
11const RIFF_CHUNK_HEADER_LEN: usize = 8;
12const WEBP_ANIMATION_FLAG: u8 = 0x02;
13
14#[derive(Clone, Debug)]
21pub struct InspectLimits {
22 pub max_input_bytes: usize,
24 pub max_canvas_pixels: u64,
26 pub max_frame_count: u32,
28 pub max_frame_rgba_bytes: usize,
30}
31
32impl Default for InspectLimits {
33 fn default() -> Self {
34 let decode = DecodeLimits::default();
35 Self {
36 max_input_bytes: decode.max_input_bytes,
37 max_canvas_pixels: decode.max_canvas_pixels,
38 max_frame_count: decode.max_frame_count,
39 max_frame_rgba_bytes: decode.max_frame_rgba_bytes,
40 }
41 }
42}
43
44impl InspectLimits {
45 pub const fn for_trusted_input() -> Self {
49 Self {
50 max_input_bytes: usize::MAX,
51 max_canvas_pixels: u64::MAX,
52 max_frame_count: u32::MAX,
53 max_frame_rgba_bytes: usize::MAX,
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub enum WebpKind {
61 Static(StaticWebpInfo),
63 Animated(crate::model::AnimationInfo),
65}
66
67#[derive(Clone, Debug, Eq, PartialEq)]
69pub enum InspectError {
70 InputTooLarge {
72 actual: usize,
74 maximum: usize,
76 },
77 InvalidContainer,
79 TruncatedContainer,
81 InvalidWebp,
83 LimitExceeded {
85 limit: &'static str,
87 actual: u64,
89 maximum: u64,
91 },
92 Animation(DecodeError),
94}
95
96impl fmt::Display for InspectError {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 match self {
99 Self::InputTooLarge { actual, maximum } => {
100 write!(
101 f,
102 "input is {actual} bytes, exceeding the {maximum}-byte limit"
103 )
104 }
105 Self::InvalidContainer => f.write_str("input is not a RIFF/WebP container"),
106 Self::TruncatedContainer => {
107 f.write_str("WebP RIFF container is truncated or malformed")
108 }
109 Self::InvalidWebp => f.write_str("input is not a valid WebP image"),
110 Self::LimitExceeded {
111 limit,
112 actual,
113 maximum,
114 } => {
115 write!(f, "{limit} is {actual}, exceeding the {maximum} limit")
116 }
117 Self::Animation(error) => write!(f, "failed to inspect animated WebP: {error}"),
118 }
119 }
120}
121
122impl Error for InspectError {}
123
124pub fn inspect(input: &[u8], limits: InspectLimits) -> Result<WebpKind, InspectError> {
126 if input.len() > limits.max_input_bytes {
127 return Err(InspectError::InputTooLarge {
128 actual: input.len(),
129 maximum: limits.max_input_bytes,
130 });
131 }
132 let animated = animation_flag(input)?;
133
134 let mut width = 0_i32;
135 let mut height = 0_i32;
136 if unsafe { WebPGetInfo(input.as_ptr(), input.len(), &mut width, &mut height) } == 0 {
138 return Err(InspectError::InvalidWebp);
139 }
140 let canvas = CanvasSize {
141 width: u32::try_from(width).map_err(|_| InspectError::InvalidWebp)?,
142 height: u32::try_from(height).map_err(|_| InspectError::InvalidWebp)?,
143 };
144 enforce_canvas_limit(canvas, limits.max_canvas_pixels)?;
145
146 if !animated {
147 return Ok(WebpKind::Static(StaticWebpInfo { canvas }));
148 }
149
150 let decode_limits = DecodeLimits {
151 max_input_bytes: limits.max_input_bytes,
152 max_canvas_pixels: limits.max_canvas_pixels,
153 max_frame_count: limits.max_frame_count,
154 max_frame_rgba_bytes: limits.max_frame_rgba_bytes,
155 ..DecodeLimits::default()
156 };
157 let decoder = AnimationDecoder::new(input, decode_limits).map_err(InspectError::Animation)?;
158 Ok(WebpKind::Animated(*decoder.info()))
159}
160
161pub fn is_animated_webp_fast(input: &[u8]) -> bool {
163 animation_flag(input).unwrap_or(false)
164}
165
166fn animation_flag(input: &[u8]) -> Result<bool, InspectError> {
167 if input.len() < RIFF_HEADER_LEN {
168 return Err(InspectError::TruncatedContainer);
169 }
170 if &input[..4] != b"RIFF" || &input[8..RIFF_HEADER_LEN] != b"WEBP" {
171 return Err(InspectError::InvalidContainer);
172 }
173 let riff_size =
174 usize::try_from(u32::from_le_bytes(input[4..8].try_into().unwrap())).unwrap_or(usize::MAX);
175 let container_end = riff_size
176 .checked_add(8)
177 .ok_or(InspectError::TruncatedContainer)?;
178 if container_end > input.len() {
179 return Err(InspectError::TruncatedContainer);
180 }
181
182 let mut offset = RIFF_HEADER_LEN;
183 while offset < container_end {
184 let header_end = offset
185 .checked_add(RIFF_CHUNK_HEADER_LEN)
186 .ok_or(InspectError::TruncatedContainer)?;
187 if header_end > container_end {
188 return Err(InspectError::TruncatedContainer);
189 }
190 let chunk = &input[offset..offset + 4];
191 let length = usize::try_from(u32::from_le_bytes(
192 input[offset + 4..header_end].try_into().unwrap(),
193 ))
194 .unwrap_or(usize::MAX);
195 let payload = header_end;
196 let padded_length = length
197 .checked_add(length & 1)
198 .ok_or(InspectError::TruncatedContainer)?;
199 let next = payload
200 .checked_add(padded_length)
201 .ok_or(InspectError::TruncatedContainer)?;
202 if next > container_end {
203 return Err(InspectError::TruncatedContainer);
204 }
205 if chunk == b"VP8X" {
206 if length < 10 {
207 return Err(InspectError::TruncatedContainer);
208 }
209 return Ok(input[payload] & WEBP_ANIMATION_FLAG != 0);
210 }
211 offset = next;
212 }
213 Ok(false)
214}
215
216fn enforce_canvas_limit(canvas: CanvasSize, maximum: u64) -> Result<(), InspectError> {
217 let actual = canvas.pixel_count().ok_or(InspectError::InvalidWebp)?;
218 if actual > maximum {
219 return Err(InspectError::LimitExceeded {
220 limit: "canvas pixels",
221 actual,
222 maximum,
223 });
224 }
225 Ok(())
226}