Skip to main content

nom_exif/
error.rs

1use std::fmt::{Debug, Display};
2use thiserror::Error;
3
4/// Top-level error returned by `read_exif`, `MediaParser::parse_*`,
5/// `MediaSource::open`, and any other public function that touches a file.
6///
7/// `#[non_exhaustive]` — downstream code MUST use a `_ =>` fallback in `match`
8/// to remain compatible with future variants.
9#[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    /// Data was recognized as the target format but its inner structure is broken.
25    #[error("malformed {kind}: {message}")]
26    Malformed {
27        kind: MalformedKind,
28        message: String,
29    },
30
31    /// Parsing needed more bytes but the stream ended.
32    #[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/// Due to the fact that metadata in MOV files is typically located at the end
52/// of the file, conventional parsing methods would require reading a
53/// significant amount of unnecessary data during the parsing process. This
54/// would impact the performance of the parsing program and consume more memory.
55///
56/// To address this issue, we have defined an `Error::Skip` enumeration type to
57/// inform the caller that certain bytes in the parsing process are not required
58/// and can be skipped directly. The specific method of skipping can be
59/// determined by the caller based on the situation. For example:
60///
61/// - For files, you can quickly skip using a `Seek` operation.
62///
63/// - For network byte streams, you may need to skip these bytes through read
64///   operations, or preferably, by designing an appropriate network protocol for
65///   skipping.
66///
67/// # [`ParsingError::ClearAndSkip`]
68///
69/// Please note that when the caller receives an `Error::Skip(n)` error, it
70/// should be understood as follows:
71///
72/// - The parsing program has already consumed all available data and needs to
73///   skip n bytes further.
74///
75/// - After skipping n bytes, it should continue to read subsequent data to fill
76///   the buffer and use it as input for the parsing function.
77///
78/// - The next time the parsing function is called (usually within a loop), the
79///   previously consumed data (including the skipped bytes) should be ignored,
80///   and only the newly read data should be passed in.
81///
82/// # [`ParsingError::Need`]
83///
84/// Additionally, to simplify error handling, we have integrated
85/// `nom::Err::Incomplete` error into `Error::Need`. This allows us to use the
86/// same error type to notify the caller that we require more bytes to continue
87/// parsing.
88#[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
151/// Convert a nom error into `crate::Error` with the supplied `kind`.
152/// Replaces the old blanket `From<nom::Err<...>> for crate::Error` impl,
153/// which hard-coded `MalformedKind::TiffHeader` for every caller
154/// regardless of context. Use this with `.map_err(|e| ...)` at sites
155/// that previously relied on `?` doing the implicit conversion.
156pub(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/// Categorizes the *structural unit* that produced a `Error::Malformed`.
188///
189/// Variants describe the kind of bytes that failed to parse (a JPEG segment,
190/// a TIFF header, an IFD entry, an ISO BMFF box, an EBML element, a PNG
191/// chunk), not the outer file format. Format-specific context — e.g. "cr3:",
192/// "heif idat:" — is conveyed in the accompanying `message` string.
193///
194/// This intentionally avoids a parallel format-level taxonomy (`Heif`,
195/// `Cr3Container`, `Raf`, …): those families are all built on top of one of
196/// the structural units listed here, so adding a row per format would create
197/// non-orthogonal categories that overlap with the structural ones.
198#[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/// Errors from conversions that are *orthogonal* to file parsing: parsing a tag
224/// name from a string, narrowing an `IRational` into a `URational`, building a
225/// `LatLng` from decimal degrees, parsing an ISO 6709 coordinate string.
226///
227/// Deliberately a peer type of `Error` — there is **no** `From<ConvertError>
228/// for Error`. Downstream code that needs to combine file-level errors and
229/// conversion errors should define its own wrapper enum (the standard
230/// `thiserror` `#[from]` pattern). See spec §3.2.
231#[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/// Errors that occur while decoding a single IFD entry.
248///
249/// Constructed internally during EXIF parsing; surfaces to downstream code
250/// as the `Err` arm of [`crate::ExifIterEntry::result`],
251/// or — when converted via `From<EntryError> for Error` — as
252/// [`Error::Malformed`] with [`MalformedKind::IfdEntry`].
253#[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        // Previously `ParsedError::Failed` was string-only and the
303        // `From<ParsedError> for Error` impl always labelled the
304        // resulting `Error::Malformed` as `IsoBmffBox`. That mislabel
305        // is what `parse_image_metadata` on a streaming PNG used to
306        // surface ("malformed iso-bmff box: PNG: bad signature").
307        // Verify the conversion now preserves the structural unit.
308        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        // Compile-time intent: ConvertError must NOT be convertible into Error.
350        // This is asserted documentally — there is no `impl From<ConvertError> for Error`.
351        // We just verify both types compile here.
352        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}