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    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/// Errors from conversions that are *orthogonal* to file parsing: parsing a tag
226/// name from a string, narrowing an `IRational` into a `URational`, building a
227/// `LatLng` from decimal degrees, parsing an ISO 6709 coordinate string.
228///
229/// Deliberately a peer type of `Error` — there is **no** `From<ConvertError>
230/// for Error`. Downstream code that needs to combine file-level errors and
231/// conversion errors should define its own wrapper enum (the standard
232/// `thiserror` `#[from]` pattern). See spec §3.2.
233#[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/// Errors that occur while decoding a single IFD entry.
250///
251/// Constructed internally during EXIF parsing; surfaces to downstream code
252/// as the `Err` arm of [`crate::ExifIterEntry::result`],
253/// or — when converted via `From<EntryError> for Error` — as
254/// [`Error::Malformed`] with [`MalformedKind::IfdEntry`].
255#[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        // Previously `ParsedError::Failed` was string-only and the
306        // `From<ParsedError> for Error` impl always labelled the
307        // resulting `Error::Malformed` as `IsoBmffBox`. That mislabel
308        // is what `parse_image_metadata` on a streaming PNG used to
309        // surface ("malformed iso-bmff box: PNG: bad signature").
310        // Verify the conversion now preserves the structural unit.
311        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        // Compile-time intent: ConvertError must NOT be convertible into Error.
353        // This is asserted documentally — there is no `impl From<ConvertError> for Error`.
354        // We just verify both types compile here.
355        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}