1use std::fmt::{Debug, Display};
2use thiserror::Error;
3
4#[derive(Debug, Error)]
10#[non_exhaustive]
11pub enum Error {
12 #[error("io error: {0}")]
13 Io(#[from] std::io::Error),
14
15 #[error("unsupported media format")]
16 UnsupportedFormat,
17
18 #[error("no exif data found in this file")]
19 ExifNotFound,
20
21 #[error("no track info found in this file")]
22 TrackNotFound,
23
24 #[error("malformed {kind}: {message}")]
26 Malformed {
27 kind: MalformedKind,
28 message: String,
29 },
30
31 #[error("unexpected end of input while parsing {context}")]
33 UnexpectedEof { context: &'static str },
34}
35
36#[derive(Debug, Error)]
37pub(crate) enum ParsedError {
38 #[error("no enough bytes")]
39 NoEnoughBytes,
40
41 #[error("io error: {0}")]
42 IOError(std::io::Error),
43
44 #[error("malformed {kind}: {message}")]
45 Failed {
46 kind: MalformedKind,
47 message: String,
48 },
49}
50
51#[derive(Debug, Error)]
89pub(crate) enum ParsingError {
90 #[error("need more bytes: {0}")]
91 Need(usize),
92
93 #[error("clear and skip bytes: {0:?}")]
94 ClearAndSkip(usize),
95
96 #[error("malformed {kind}: {message}")]
97 Failed {
98 kind: MalformedKind,
99 message: String,
100 },
101}
102
103#[derive(Debug, Error)]
104pub(crate) struct ParsingErrorState {
105 pub err: ParsingError,
106 pub state: Option<ParsingState>,
107}
108
109impl ParsingErrorState {
110 pub fn new(err: ParsingError, state: Option<ParsingState>) -> Self {
111 Self { err, state }
112 }
113}
114
115impl Display for ParsingErrorState {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 Display::fmt(
118 &format!(
119 "ParsingError(err: {}, state: {})",
120 self.err,
121 self.state
122 .as_ref()
123 .map(|x| x.to_string())
124 .unwrap_or("None".to_string())
125 ),
126 f,
127 )
128 }
129}
130
131impl From<std::io::Error> for ParsedError {
132 fn from(value: std::io::Error) -> Self {
133 Self::IOError(value)
134 }
135}
136
137impl From<ParsedError> for crate::Error {
138 fn from(value: ParsedError) -> Self {
139 match value {
140 ParsedError::NoEnoughBytes => Self::UnexpectedEof {
141 context: "media stream",
142 },
143 ParsedError::IOError(e) => Self::Io(e),
144 ParsedError::Failed { kind, message } => Self::Malformed { kind, message },
145 }
146 }
147}
148
149use crate::parser::ParsingState;
150
151pub(crate) fn nom_err_to_malformed<T: Debug>(
157 e: nom::Err<nom::error::Error<T>>,
158 kind: MalformedKind,
159) -> crate::Error {
160 let message = match e {
161 nom::Err::Incomplete(_) => format!("{e}"),
162 nom::Err::Error(e) | nom::Err::Failure(e) => e.code.description().to_string(),
163 };
164 crate::Error::Malformed { kind, message }
165}
166
167pub(crate) fn nom_error_to_parsing_error_with_state(
168 e: nom::Err<nom::error::Error<&[u8]>>,
169 kind: MalformedKind,
170 state: Option<ParsingState>,
171) -> ParsingErrorState {
172 match e {
173 nom::Err::Incomplete(needed) => match needed {
174 nom::Needed::Unknown => ParsingErrorState::new(ParsingError::Need(1), state),
175 nom::Needed::Size(n) => ParsingErrorState::new(ParsingError::Need(n.get()), state),
176 },
177 nom::Err::Failure(e) | nom::Err::Error(e) => ParsingErrorState::new(
178 ParsingError::Failed {
179 kind,
180 message: e.code.description().to_string(),
181 },
182 state,
183 ),
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199#[non_exhaustive]
200pub enum MalformedKind {
201 JpegSegment,
202 TiffHeader,
203 IfdEntry,
204 IsoBmffBox,
205 EbmlElement,
206 PngChunk,
207 WebpChunk,
208}
209
210impl std::fmt::Display for MalformedKind {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 let s = match self {
213 Self::JpegSegment => "jpeg segment",
214 Self::TiffHeader => "tiff header",
215 Self::IfdEntry => "ifd entry",
216 Self::IsoBmffBox => "iso-bmff box",
217 Self::EbmlElement => "ebml element",
218 Self::PngChunk => "png chunk",
219 Self::WebpChunk => "webp chunk",
220 };
221 f.write_str(s)
222 }
223}
224
225#[derive(Debug, Clone, thiserror::Error)]
234#[non_exhaustive]
235pub enum ConvertError {
236 #[error("unknown ExifTag name: {0}")]
237 UnknownTagName(String),
238
239 #[error("invalid ISO 6709 coordinate: {0}")]
240 InvalidIso6709(String),
241
242 #[error("rational has negative value")]
243 NegativeRational,
244
245 #[error("decimal degrees out of range or non-finite: {0}")]
246 InvalidDecimalDegrees(f64),
247}
248
249#[derive(Debug, Clone, PartialEq, thiserror::Error)]
256#[non_exhaustive]
257pub enum EntryError {
258 #[error("entry truncated: needed {needed} bytes, only {available} available")]
259 Truncated { needed: usize, available: usize },
260
261 #[error("invalid entry shape: format={format}, count={count}")]
262 InvalidShape { format: u16, count: u32 },
263
264 #[error("invalid value: {0}")]
265 InvalidValue(&'static str),
266}
267
268impl From<EntryError> for Error {
269 fn from(e: EntryError) -> Self {
270 Error::Malformed {
271 kind: MalformedKind::IfdEntry,
272 message: e.to_string(),
273 }
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn malformed_kind_is_copy_and_eq() {
283 let a = MalformedKind::JpegSegment;
284 let b = a;
285 assert_eq!(a, b);
286 }
287
288 #[test]
289 fn malformed_kind_covers_all_structural_units() {
290 for k in [
291 MalformedKind::JpegSegment,
292 MalformedKind::TiffHeader,
293 MalformedKind::IfdEntry,
294 MalformedKind::IsoBmffBox,
295 MalformedKind::EbmlElement,
296 MalformedKind::PngChunk,
297 MalformedKind::WebpChunk,
298 ] {
299 let _ = format!("{k:?}");
300 }
301 }
302
303 #[test]
304 fn parsed_error_failed_propagates_kind_to_top_level_error() {
305 let pe = ParsedError::Failed {
312 kind: MalformedKind::PngChunk,
313 message: "PNG: bad signature".into(),
314 };
315 let top: Error = pe.into();
316 match top {
317 Error::Malformed { kind, message } => {
318 assert_eq!(kind, MalformedKind::PngChunk);
319 assert_eq!(message, "PNG: bad signature");
320 }
321 other => panic!("expected Malformed, got {other:?}"),
322 }
323 }
324
325 #[test]
326 fn convert_error_displays_each_variant() {
327 let cases: &[(ConvertError, &str)] = &[
328 (
329 ConvertError::UnknownTagName("Foo".into()),
330 "unknown ExifTag name: Foo",
331 ),
332 (
333 ConvertError::InvalidIso6709("garbage".into()),
334 "invalid ISO 6709 coordinate: garbage",
335 ),
336 (
337 ConvertError::NegativeRational,
338 "rational has negative value",
339 ),
340 (
341 ConvertError::InvalidDecimalDegrees(f64::NAN),
342 "decimal degrees out of range or non-finite: NaN",
343 ),
344 ];
345 for (err, expected) in cases {
346 assert_eq!(err.to_string(), *expected);
347 }
348 }
349
350 #[test]
351 fn convert_error_does_not_convert_to_error() {
352 let _ = ConvertError::NegativeRational;
356 let _ = Error::UnsupportedFormat;
357 }
358
359 #[test]
360 fn error_io_from_io_error() {
361 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "x");
362 let err: Error = io_err.into();
363 assert!(matches!(err, Error::Io(_)));
364 }
365
366 #[test]
367 fn error_unsupported_format_displays() {
368 assert_eq!(
369 Error::UnsupportedFormat.to_string(),
370 "unsupported media format"
371 );
372 }
373
374 #[test]
375 fn error_exif_not_found_displays() {
376 assert_eq!(
377 Error::ExifNotFound.to_string(),
378 "no exif data found in this file"
379 );
380 }
381
382 #[test]
383 fn error_track_not_found_displays() {
384 assert_eq!(
385 Error::TrackNotFound.to_string(),
386 "no track info found in this file"
387 );
388 }
389
390 #[test]
391 fn error_malformed_displays() {
392 let e = Error::Malformed {
393 kind: MalformedKind::JpegSegment,
394 message: "bad SOI".into(),
395 };
396 assert_eq!(e.to_string(), "malformed jpeg segment: bad SOI");
397 }
398
399 #[test]
400 fn error_unexpected_eof_displays() {
401 let e = Error::UnexpectedEof {
402 context: "tiff header",
403 };
404 assert_eq!(
405 e.to_string(),
406 "unexpected end of input while parsing tiff header"
407 );
408 }
409
410 #[test]
411 fn entry_error_truncated_displays() {
412 let e = EntryError::Truncated {
413 needed: 8,
414 available: 4,
415 };
416 assert_eq!(
417 e.to_string(),
418 "entry truncated: needed 8 bytes, only 4 available"
419 );
420 }
421
422 #[test]
423 fn entry_error_invalid_shape_displays() {
424 let e = EntryError::InvalidShape {
425 format: 7,
426 count: 1,
427 };
428 assert_eq!(e.to_string(), "invalid entry shape: format=7, count=1");
429 }
430
431 #[test]
432 fn entry_error_invalid_value_displays() {
433 let e = EntryError::InvalidValue("not utf-8");
434 assert_eq!(e.to_string(), "invalid value: not utf-8");
435 }
436
437 #[test]
438 fn entry_error_into_error_routes_to_malformed_ifd_entry() {
439 let e = EntryError::Truncated {
440 needed: 8,
441 available: 4,
442 };
443 let err: Error = e.into();
444 match err {
445 Error::Malformed { kind, message } => {
446 assert_eq!(kind, MalformedKind::IfdEntry);
447 assert!(message.contains("entry truncated"));
448 }
449 other => panic!("unexpected variant: {other:?}"),
450 }
451 }
452}