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("{0}")]
45    Failed(String),
46}
47
48/// Due to the fact that metadata in MOV files is typically located at the end
49/// of the file, conventional parsing methods would require reading a
50/// significant amount of unnecessary data during the parsing process. This
51/// would impact the performance of the parsing program and consume more memory.
52///
53/// To address this issue, we have defined an `Error::Skip` enumeration type to
54/// inform the caller that certain bytes in the parsing process are not required
55/// and can be skipped directly. The specific method of skipping can be
56/// determined by the caller based on the situation. For example:
57///
58/// - For files, you can quickly skip using a `Seek` operation.
59///
60/// - For network byte streams, you may need to skip these bytes through read
61///   operations, or preferably, by designing an appropriate network protocol for
62///   skipping.
63///
64/// # [`ParsingError::ClearAndSkip`]
65///
66/// Please note that when the caller receives an `Error::Skip(n)` error, it
67/// should be understood as follows:
68///
69/// - The parsing program has already consumed all available data and needs to
70///   skip n bytes further.
71///
72/// - After skipping n bytes, it should continue to read subsequent data to fill
73///   the buffer and use it as input for the parsing function.
74///
75/// - The next time the parsing function is called (usually within a loop), the
76///   previously consumed data (including the skipped bytes) should be ignored,
77///   and only the newly read data should be passed in.
78///
79/// # [`ParsingError::Need`]
80///
81/// Additionally, to simplify error handling, we have integrated
82/// `nom::Err::Incomplete` error into `Error::Need`. This allows us to use the
83/// same error type to notify the caller that we require more bytes to continue
84/// parsing.
85#[derive(Debug, Error)]
86pub(crate) enum ParsingError {
87    #[error("need more bytes: {0}")]
88    Need(usize),
89
90    #[error("clear and skip bytes: {0:?}")]
91    ClearAndSkip(usize),
92
93    #[error("{0}")]
94    Failed(String),
95}
96
97#[derive(Debug, Error)]
98pub(crate) struct ParsingErrorState {
99    pub err: ParsingError,
100    pub state: Option<ParsingState>,
101}
102
103impl ParsingErrorState {
104    pub fn new(err: ParsingError, state: Option<ParsingState>) -> Self {
105        Self { err, state }
106    }
107}
108
109impl Display for ParsingErrorState {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        Display::fmt(
112            &format!(
113                "ParsingError(err: {}, state: {})",
114                self.err,
115                self.state
116                    .as_ref()
117                    .map(|x| x.to_string())
118                    .unwrap_or("None".to_string())
119            ),
120            f,
121        )
122    }
123}
124
125impl From<&str> for ParsingError {
126    fn from(value: &str) -> Self {
127        Self::Failed(value.to_string())
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            // Best-effort default: P3 will plumb the actual MalformedKind
145            // through ParsedError so this fallback can go away.
146            ParsedError::Failed(e) => Self::Malformed {
147                kind: MalformedKind::IsoBmffBox,
148                message: e,
149            },
150        }
151    }
152}
153
154use crate::parser::ParsingState;
155
156impl<T: Debug> From<nom::Err<nom::error::Error<T>>> for crate::Error {
157    fn from(e: nom::Err<nom::error::Error<T>>) -> Self {
158        convert_parse_error(e, "")
159    }
160}
161
162pub(crate) fn convert_parse_error<T: Debug>(
163    e: nom::Err<nom::error::Error<T>>,
164    message: &str,
165) -> Error {
166    let s = match e {
167        nom::Err::Incomplete(_) => format!("{e}; {message}"),
168        nom::Err::Error(e) => format!("{}; {message}", e.code.description()),
169        nom::Err::Failure(e) => format!("{}; {message}", e.code.description()),
170    };
171    Error::Malformed {
172        kind: MalformedKind::TiffHeader,
173        message: s,
174    }
175}
176
177impl From<nom::Err<nom::error::Error<&[u8]>>> for ParsingError {
178    fn from(e: nom::Err<nom::error::Error<&[u8]>>) -> Self {
179        match e {
180            nom::Err::Incomplete(needed) => match needed {
181                nom::Needed::Unknown => ParsingError::Need(1),
182                nom::Needed::Size(n) => ParsingError::Need(n.get()),
183            },
184            nom::Err::Failure(e) | nom::Err::Error(e) => {
185                ParsingError::Failed(e.code.description().to_string())
186            }
187        }
188    }
189}
190
191pub(crate) fn nom_error_to_parsing_error_with_state(
192    e: nom::Err<nom::error::Error<&[u8]>>,
193    state: Option<ParsingState>,
194) -> ParsingErrorState {
195    match e {
196        nom::Err::Incomplete(needed) => match needed {
197            nom::Needed::Unknown => ParsingErrorState::new(ParsingError::Need(1), state),
198            nom::Needed::Size(n) => ParsingErrorState::new(ParsingError::Need(n.get()), state),
199        },
200        nom::Err::Failure(e) | nom::Err::Error(e) => ParsingErrorState::new(
201            ParsingError::Failed(e.code.description().to_string()),
202            state,
203        ),
204    }
205}
206
207/// Categorizes the *structural unit* that produced a `Error::Malformed`.
208///
209/// Variants describe the kind of bytes that failed to parse (a JPEG segment,
210/// a TIFF header, an IFD entry, an ISO BMFF box, an EBML element), not the
211/// outer file format. Format-specific context — e.g. "cr3:", "heif idat:" —
212/// is conveyed in the accompanying `message` string.
213///
214/// This intentionally avoids a parallel format-level taxonomy (`Heif`,
215/// `Cr3Container`, `Raf`, …): those families are all built on top of one of
216/// the structural units listed here, so adding a row per format would create
217/// non-orthogonal categories that overlap with the structural ones.
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219#[non_exhaustive]
220pub enum MalformedKind {
221    JpegSegment,
222    TiffHeader,
223    IfdEntry,
224    IsoBmffBox,
225    EbmlElement,
226}
227
228impl std::fmt::Display for MalformedKind {
229    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230        let s = match self {
231            Self::JpegSegment => "jpeg segment",
232            Self::TiffHeader => "tiff header",
233            Self::IfdEntry => "ifd entry",
234            Self::IsoBmffBox => "iso-bmff box",
235            Self::EbmlElement => "ebml element",
236        };
237        f.write_str(s)
238    }
239}
240
241/// Errors from conversions that are *orthogonal* to file parsing: parsing a tag
242/// name from a string, narrowing an `IRational` into a `URational`, building a
243/// `LatLng` from decimal degrees, parsing an ISO 6709 coordinate string.
244///
245/// Deliberately a peer type of `Error` — there is **no** `From<ConvertError>
246/// for Error`. Downstream code that needs to combine file-level errors and
247/// conversion errors should define its own wrapper enum (the standard
248/// `thiserror` `#[from]` pattern). See spec §3.2.
249#[derive(Debug, Clone, thiserror::Error)]
250#[non_exhaustive]
251pub enum ConvertError {
252    #[error("unknown ExifTag name: {0}")]
253    UnknownTagName(String),
254
255    #[error("invalid ISO 6709 coordinate: {0}")]
256    InvalidIso6709(String),
257
258    #[error("rational has negative value")]
259    NegativeRational,
260
261    #[error("decimal degrees out of range or non-finite: {0}")]
262    InvalidDecimalDegrees(f64),
263}
264
265/// Errors that occur while decoding a single IFD entry.
266///
267/// Constructed internally during EXIF parsing; surfaces to downstream code
268/// as the `Err` arm of [`crate::ExifIterEntry::result`],
269/// or — when converted via `From<EntryError> for Error` — as
270/// [`Error::Malformed`] with [`MalformedKind::IfdEntry`].
271#[derive(Debug, Clone, PartialEq, thiserror::Error)]
272#[non_exhaustive]
273pub enum EntryError {
274    #[error("entry truncated: needed {needed} bytes, only {available} available")]
275    Truncated { needed: usize, available: usize },
276
277    #[error("invalid entry shape: format={format}, count={count}")]
278    InvalidShape { format: u16, count: u32 },
279
280    #[error("invalid value: {0}")]
281    InvalidValue(&'static str),
282}
283
284impl From<EntryError> for Error {
285    fn from(e: EntryError) -> Self {
286        Error::Malformed {
287            kind: MalformedKind::IfdEntry,
288            message: e.to_string(),
289        }
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn malformed_kind_is_copy_and_eq() {
299        let a = MalformedKind::JpegSegment;
300        let b = a;
301        assert_eq!(a, b);
302    }
303
304    #[test]
305    fn malformed_kind_covers_all_structural_units() {
306        for k in [
307            MalformedKind::JpegSegment,
308            MalformedKind::TiffHeader,
309            MalformedKind::IfdEntry,
310            MalformedKind::IsoBmffBox,
311            MalformedKind::EbmlElement,
312        ] {
313            let _ = format!("{k:?}");
314        }
315    }
316
317    #[test]
318    fn convert_error_displays_each_variant() {
319        let cases: &[(ConvertError, &str)] = &[
320            (
321                ConvertError::UnknownTagName("Foo".into()),
322                "unknown ExifTag name: Foo",
323            ),
324            (
325                ConvertError::InvalidIso6709("garbage".into()),
326                "invalid ISO 6709 coordinate: garbage",
327            ),
328            (
329                ConvertError::NegativeRational,
330                "rational has negative value",
331            ),
332            (
333                ConvertError::InvalidDecimalDegrees(f64::NAN),
334                "decimal degrees out of range or non-finite: NaN",
335            ),
336        ];
337        for (err, expected) in cases {
338            assert_eq!(err.to_string(), *expected);
339        }
340    }
341
342    #[test]
343    fn convert_error_does_not_convert_to_error() {
344        // Compile-time intent: ConvertError must NOT be convertible into Error.
345        // This is asserted documentally — there is no `impl From<ConvertError> for Error`.
346        // We just verify both types compile here.
347        let _ = ConvertError::NegativeRational;
348        let _ = Error::UnsupportedFormat;
349    }
350
351    #[test]
352    fn error_io_from_io_error() {
353        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "x");
354        let err: Error = io_err.into();
355        assert!(matches!(err, Error::Io(_)));
356    }
357
358    #[test]
359    fn error_unsupported_format_displays() {
360        assert_eq!(
361            Error::UnsupportedFormat.to_string(),
362            "unsupported media format"
363        );
364    }
365
366    #[test]
367    fn error_exif_not_found_displays() {
368        assert_eq!(
369            Error::ExifNotFound.to_string(),
370            "no exif data found in this file"
371        );
372    }
373
374    #[test]
375    fn error_track_not_found_displays() {
376        assert_eq!(
377            Error::TrackNotFound.to_string(),
378            "no track info found in this file"
379        );
380    }
381
382    #[test]
383    fn error_malformed_displays() {
384        let e = Error::Malformed {
385            kind: MalformedKind::JpegSegment,
386            message: "bad SOI".into(),
387        };
388        assert_eq!(e.to_string(), "malformed jpeg segment: bad SOI");
389    }
390
391    #[test]
392    fn error_unexpected_eof_displays() {
393        let e = Error::UnexpectedEof {
394            context: "tiff header",
395        };
396        assert_eq!(
397            e.to_string(),
398            "unexpected end of input while parsing tiff header"
399        );
400    }
401
402    #[test]
403    fn entry_error_truncated_displays() {
404        let e = EntryError::Truncated {
405            needed: 8,
406            available: 4,
407        };
408        assert_eq!(
409            e.to_string(),
410            "entry truncated: needed 8 bytes, only 4 available"
411        );
412    }
413
414    #[test]
415    fn entry_error_invalid_shape_displays() {
416        let e = EntryError::InvalidShape {
417            format: 7,
418            count: 1,
419        };
420        assert_eq!(e.to_string(), "invalid entry shape: format=7, count=1");
421    }
422
423    #[test]
424    fn entry_error_invalid_value_displays() {
425        let e = EntryError::InvalidValue("not utf-8");
426        assert_eq!(e.to_string(), "invalid value: not utf-8");
427    }
428
429    #[test]
430    fn entry_error_into_error_routes_to_malformed_ifd_entry() {
431        let e = EntryError::Truncated {
432            needed: 8,
433            available: 4,
434        };
435        let err: Error = e.into();
436        match err {
437            Error::Malformed { kind, message } => {
438                assert_eq!(kind, MalformedKind::IfdEntry);
439                assert!(message.contains("entry truncated"));
440            }
441            other => panic!("unexpected variant: {other:?}"),
442        }
443    }
444}