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}
208
209impl std::fmt::Display for MalformedKind {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 let s = match self {
212 Self::JpegSegment => "jpeg segment",
213 Self::TiffHeader => "tiff header",
214 Self::IfdEntry => "ifd entry",
215 Self::IsoBmffBox => "iso-bmff box",
216 Self::EbmlElement => "ebml element",
217 Self::PngChunk => "png chunk",
218 };
219 f.write_str(s)
220 }
221}
222
223#[derive(Debug, Clone, thiserror::Error)]
232#[non_exhaustive]
233pub enum ConvertError {
234 #[error("unknown ExifTag name: {0}")]
235 UnknownTagName(String),
236
237 #[error("invalid ISO 6709 coordinate: {0}")]
238 InvalidIso6709(String),
239
240 #[error("rational has negative value")]
241 NegativeRational,
242
243 #[error("decimal degrees out of range or non-finite: {0}")]
244 InvalidDecimalDegrees(f64),
245}
246
247#[derive(Debug, Clone, PartialEq, thiserror::Error)]
254#[non_exhaustive]
255pub enum EntryError {
256 #[error("entry truncated: needed {needed} bytes, only {available} available")]
257 Truncated { needed: usize, available: usize },
258
259 #[error("invalid entry shape: format={format}, count={count}")]
260 InvalidShape { format: u16, count: u32 },
261
262 #[error("invalid value: {0}")]
263 InvalidValue(&'static str),
264}
265
266impl From<EntryError> for Error {
267 fn from(e: EntryError) -> Self {
268 Error::Malformed {
269 kind: MalformedKind::IfdEntry,
270 message: e.to_string(),
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn malformed_kind_is_copy_and_eq() {
281 let a = MalformedKind::JpegSegment;
282 let b = a;
283 assert_eq!(a, b);
284 }
285
286 #[test]
287 fn malformed_kind_covers_all_structural_units() {
288 for k in [
289 MalformedKind::JpegSegment,
290 MalformedKind::TiffHeader,
291 MalformedKind::IfdEntry,
292 MalformedKind::IsoBmffBox,
293 MalformedKind::EbmlElement,
294 MalformedKind::PngChunk,
295 ] {
296 let _ = format!("{k:?}");
297 }
298 }
299
300 #[test]
301 fn parsed_error_failed_propagates_kind_to_top_level_error() {
302 let pe = ParsedError::Failed {
309 kind: MalformedKind::PngChunk,
310 message: "PNG: bad signature".into(),
311 };
312 let top: Error = pe.into();
313 match top {
314 Error::Malformed { kind, message } => {
315 assert_eq!(kind, MalformedKind::PngChunk);
316 assert_eq!(message, "PNG: bad signature");
317 }
318 other => panic!("expected Malformed, got {other:?}"),
319 }
320 }
321
322 #[test]
323 fn convert_error_displays_each_variant() {
324 let cases: &[(ConvertError, &str)] = &[
325 (
326 ConvertError::UnknownTagName("Foo".into()),
327 "unknown ExifTag name: Foo",
328 ),
329 (
330 ConvertError::InvalidIso6709("garbage".into()),
331 "invalid ISO 6709 coordinate: garbage",
332 ),
333 (
334 ConvertError::NegativeRational,
335 "rational has negative value",
336 ),
337 (
338 ConvertError::InvalidDecimalDegrees(f64::NAN),
339 "decimal degrees out of range or non-finite: NaN",
340 ),
341 ];
342 for (err, expected) in cases {
343 assert_eq!(err.to_string(), *expected);
344 }
345 }
346
347 #[test]
348 fn convert_error_does_not_convert_to_error() {
349 let _ = ConvertError::NegativeRational;
353 let _ = Error::UnsupportedFormat;
354 }
355
356 #[test]
357 fn error_io_from_io_error() {
358 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "x");
359 let err: Error = io_err.into();
360 assert!(matches!(err, Error::Io(_)));
361 }
362
363 #[test]
364 fn error_unsupported_format_displays() {
365 assert_eq!(
366 Error::UnsupportedFormat.to_string(),
367 "unsupported media format"
368 );
369 }
370
371 #[test]
372 fn error_exif_not_found_displays() {
373 assert_eq!(
374 Error::ExifNotFound.to_string(),
375 "no exif data found in this file"
376 );
377 }
378
379 #[test]
380 fn error_track_not_found_displays() {
381 assert_eq!(
382 Error::TrackNotFound.to_string(),
383 "no track info found in this file"
384 );
385 }
386
387 #[test]
388 fn error_malformed_displays() {
389 let e = Error::Malformed {
390 kind: MalformedKind::JpegSegment,
391 message: "bad SOI".into(),
392 };
393 assert_eq!(e.to_string(), "malformed jpeg segment: bad SOI");
394 }
395
396 #[test]
397 fn error_unexpected_eof_displays() {
398 let e = Error::UnexpectedEof {
399 context: "tiff header",
400 };
401 assert_eq!(
402 e.to_string(),
403 "unexpected end of input while parsing tiff header"
404 );
405 }
406
407 #[test]
408 fn entry_error_truncated_displays() {
409 let e = EntryError::Truncated {
410 needed: 8,
411 available: 4,
412 };
413 assert_eq!(
414 e.to_string(),
415 "entry truncated: needed 8 bytes, only 4 available"
416 );
417 }
418
419 #[test]
420 fn entry_error_invalid_shape_displays() {
421 let e = EntryError::InvalidShape {
422 format: 7,
423 count: 1,
424 };
425 assert_eq!(e.to_string(), "invalid entry shape: format=7, count=1");
426 }
427
428 #[test]
429 fn entry_error_invalid_value_displays() {
430 let e = EntryError::InvalidValue("not utf-8");
431 assert_eq!(e.to_string(), "invalid value: not utf-8");
432 }
433
434 #[test]
435 fn entry_error_into_error_routes_to_malformed_ifd_entry() {
436 let e = EntryError::Truncated {
437 needed: 8,
438 available: 4,
439 };
440 let err: Error = e.into();
441 match err {
442 Error::Malformed { kind, message } => {
443 assert_eq!(kind, MalformedKind::IfdEntry);
444 assert!(message.contains("entry truncated"));
445 }
446 other => panic!("unexpected variant: {other:?}"),
447 }
448 }
449}