Skip to main content

nom_exif/
parser.rs

1use std::{
2    cmp::{max, min},
3    fmt::{Debug, Display},
4    fs::File,
5    io::{self, Read, Seek},
6    path::Path,
7};
8
9use crate::{
10    error::{ParsedError, ParsingError, ParsingErrorState},
11    exif::TiffHeader,
12    file::MediaMime,
13    ExifIter, TrackInfo,
14};
15
16/// A function that tries to skip `n` bytes of `reader` by seeking. Returns
17/// `Ok(true)` on success, `Ok(false)` if the reader does not support seek
18/// (so the caller should fall back to reading-and-discarding), or
19/// `Err(io::Error)` if seek itself failed (e.g. truncated file handle).
20///
21/// This is captured at construction time by `MediaSource::seekable` /
22/// `unseekable`, replacing the v2 `S: Skip<R>` phantom parameter with a
23/// runtime fn pointer.
24pub(crate) type SkipBySeekFn<R> = fn(&mut R, u64) -> io::Result<bool>;
25
26/// `MediaSource` represents a media data source that can be parsed by
27/// [`MediaParser`].
28///
29/// - Use [`MediaSource::open`] to create a MediaSource from a file path.
30///
31/// - Use [`MediaSource::from_bytes`] for zero-copy in-memory input
32///   (`Vec<u8>`, `&'static [u8]`, [`bytes::Bytes`], …). Pair with
33///   [`MediaParser::parse_exif_from_bytes`] / [`MediaParser::parse_track_from_bytes`].
34///
35/// - In other cases:
36///
37///   - Use [`MediaSource::seekable`] to create a MediaSource from a `Read + Seek`
38///     (an already-open `File` goes here).
39///
40///   - Use [`MediaSource::unseekable`] to create a MediaSource from a
41///     reader that only impl `Read`
42///
43/// *Note*: Please use [`MediaSource::seekable`] in preference to [`MediaSource::unseekable`],
44/// since the former is more efficient when the parser needs to skip a large number of bytes.
45///
46/// Passing in a `BufRead` should be avoided because [`MediaParser`] comes with
47/// its own buffer management and the buffers can be shared between multiple
48/// parsing tasks, thus avoiding frequent memory allocations.
49pub struct MediaSource<R> {
50    pub(crate) reader: R,
51    pub(crate) buf: Vec<u8>,
52    pub(crate) mime: MediaMime,
53    pub(crate) skip_by_seek: SkipBySeekFn<R>,
54    /// P7: zero-copy memory-mode payload. `Some` only when the source was
55    /// built via [`MediaSource::<()>::from_bytes`]; `reader`, `buf`, and
56    /// `skip_by_seek` are placeholders (and never consulted) in that mode.
57    pub(crate) memory: Option<bytes::Bytes>,
58}
59
60/// Top-level classification of a media source.
61///
62/// `Image` files carry EXIF metadata (parse with `MediaParser::parse_exif`);
63/// `Track` files are time-based containers — video, audio, or both — and
64/// carry track-info metadata (parse with `MediaParser::parse_track`). Pure
65/// audio containers like `.mka` are classified as `Track`.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum MediaKind {
68    Image,
69    Track,
70}
71
72impl<R> Debug for MediaSource<R> {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.debug_struct("MediaSource")
75            .field("mime", &self.mime)
76            .finish_non_exhaustive()
77    }
78}
79
80// Should be enough for parsing header
81const HEADER_PARSE_BUF_SIZE: usize = 128;
82
83impl<R> MediaSource<R> {
84    /// Top-level classification of this media source.
85    pub fn kind(&self) -> MediaKind {
86        match self.mime {
87            MediaMime::Image(_) => MediaKind::Image,
88            MediaMime::Track(_) => MediaKind::Track,
89        }
90    }
91}
92
93impl<R: Read> MediaSource<R> {
94    fn build(mut reader: R, skip_by_seek: SkipBySeekFn<R>) -> crate::Result<Self> {
95        let mut buf = Vec::with_capacity(HEADER_PARSE_BUF_SIZE);
96        reader
97            .by_ref()
98            .take(HEADER_PARSE_BUF_SIZE as u64)
99            .read_to_end(&mut buf)?;
100        let mime: MediaMime = buf.as_slice().try_into()?;
101        Ok(Self {
102            reader,
103            buf,
104            mime,
105            skip_by_seek,
106            memory: None,
107        })
108    }
109
110    /// Use [`MediaSource::unseekable`] to create a MediaSource from a
111    /// reader that only impl `Read`
112    ///
113    /// *Note*: Please use [`MediaSource::seekable`] in preference to [`MediaSource::unseekable`],
114    /// since the former is more efficient when the parser needs to skip a large number of bytes.
115    pub fn unseekable(reader: R) -> crate::Result<Self> {
116        Self::build(reader, |_, _| Ok(false))
117    }
118}
119
120impl<R: Read + Seek> MediaSource<R> {
121    /// Use [`MediaSource::seekable`] to create a MediaSource from a `Read + Seek`
122    ///
123    /// *Note*: Please use [`MediaSource::seekable`] in preference to [`MediaSource::unseekable`],
124    /// since the former is more efficient when the parser needs to skip a large number of bytes.
125    pub fn seekable(reader: R) -> crate::Result<Self> {
126        Self::build(reader, |r, n| {
127            let signed: i64 = n
128                .try_into()
129                .map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
130            r.seek_relative(signed)?;
131            Ok(true)
132        })
133    }
134}
135
136impl MediaSource<File> {
137    /// Open a file at `path` and parse its header to detect the media format.
138    ///
139    /// This is the v3-preferred entry point for the common case of "I have a
140    /// path on disk". For an already-open `File` use [`Self::seekable`].
141    pub fn open<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
142        Self::seekable(File::open(path)?)
143    }
144}
145
146impl MediaSource<()> {
147    /// Build a [`MediaSource`] from an in-memory byte payload.
148    ///
149    /// Accepts any type convertible into [`bytes::Bytes`] — `Bytes`,
150    /// `Vec<u8>`, `&'static [u8]`, [`bytes::Bytes::from_owner`] outputs, and
151    /// HTTP-stack body types that implement `Into<Bytes>` directly.
152    ///
153    /// The header (first up to 128 bytes) is sniffed for media kind, the
154    /// same way [`MediaSource::open`] does it for files. The full payload is
155    /// stored zero-copy: subsequent parsing through
156    /// [`MediaParser::parse_exif_from_bytes`] / [`MediaParser::parse_track_from_bytes`]
157    /// shares this `Bytes` directly with the returned `ExifIter` / sub-IFDs
158    /// via reference counting.
159    ///
160    /// The returned source is parsed by the dedicated
161    /// [`MediaParser::parse_exif_from_bytes`] / [`MediaParser::parse_track_from_bytes`]
162    /// methods. The streaming `parse_exif` / `parse_track` methods do not
163    /// accept `MediaSource<()>` (their `R: Read` bound is unsatisfiable).
164    ///
165    /// # Example
166    ///
167    /// ```rust
168    /// use nom_exif::{MediaSource, MediaParser, MediaKind};
169    ///
170    /// let bytes = std::fs::read("./testdata/exif.jpg")?;
171    /// let ms = MediaSource::from_bytes(bytes)?;
172    /// assert_eq!(ms.kind(), MediaKind::Image);
173    ///
174    /// let mut parser = MediaParser::new();
175    /// let _iter = parser.parse_exif_from_bytes(ms)?;
176    /// # Ok::<(), nom_exif::Error>(())
177    /// ```
178    #[deprecated(
179        since = "3.3.0",
180        note = "Use `MediaSource::from_memory` and the unified `parse_*` \
181                methods (which now accept memory-mode sources directly). \
182                The `MediaSource<()>` shape will be removed in v4."
183    )]
184    pub fn from_bytes(bytes: impl Into<bytes::Bytes>) -> crate::Result<Self> {
185        let bytes = bytes.into();
186        let head_end = bytes.len().min(HEADER_PARSE_BUF_SIZE);
187        let mime: MediaMime = bytes[..head_end].try_into()?;
188        Ok(Self {
189            reader: (),
190            buf: Vec::new(),
191            mime,
192            // Placeholder: never invoked in memory mode (clear_and_skip's
193            // AdvanceOnly path is the only one taken).
194            skip_by_seek: |_, _| Ok(false),
195            memory: Some(bytes),
196        })
197    }
198
199    /// Internal adapter: convert a v3.0-style `MediaSource<()>` (built via
200    /// the deprecated `from_bytes`) into the unified `MediaSource<Empty>`
201    /// shape so the deprecated `parse_*_from_bytes` methods can delegate to
202    /// the unified `parse_*` methods. Memory contents are moved over
203    /// verbatim, preserving zero-copy.
204    pub(crate) fn into_empty(self) -> MediaSource<std::io::Empty> {
205        MediaSource {
206            reader: std::io::empty(),
207            buf: self.buf,
208            mime: self.mime,
209            // Placeholder: never invoked in memory mode (clear_and_skip's
210            // AdvanceOnly path is the only one taken).
211            skip_by_seek: |_, _| Ok(false),
212            memory: self.memory,
213        }
214    }
215}
216
217impl MediaSource<std::io::Empty> {
218    /// Build a [`MediaSource`] from an in-memory byte payload.
219    ///
220    /// This is the v3.3 replacement for [`MediaSource::<()>::from_bytes`]
221    /// (which is now `#[deprecated]`). Functionally identical — same
222    /// zero-copy semantics, same accepted input types — but produces a
223    /// `MediaSource<std::io::Empty>` so that the unified `parse_*<R: Read>`
224    /// methods can accept it directly without a separate `_from_bytes`
225    /// sibling.
226    ///
227    /// Accepts any type convertible into [`bytes::Bytes`] — `Bytes`,
228    /// `Vec<u8>`, `&'static [u8]`, `String`, `Box<[u8]>`, and HTTP-stack
229    /// body types that implement `Into<Bytes>` directly.
230    ///
231    /// The header (first up to 128 bytes) is sniffed for media kind, the
232    /// same way [`MediaSource::open`] does it for files. The full payload
233    /// is stored zero-copy: subsequent parsing through
234    /// [`MediaParser::parse_exif`] / [`MediaParser::parse_track`] shares
235    /// this `Bytes` directly with the returned `ExifIter` / sub-IFDs via
236    /// reference counting.
237    ///
238    /// # Example
239    ///
240    /// ```rust
241    /// use nom_exif::{MediaSource, MediaParser, MediaKind};
242    ///
243    /// let bytes = std::fs::read("./testdata/exif.jpg")?;
244    /// let ms = MediaSource::from_memory(bytes)?;
245    /// assert_eq!(ms.kind(), MediaKind::Image);
246    ///
247    /// let mut parser = MediaParser::new();
248    /// let _iter = parser.parse_exif(ms)?;  // unified entry point
249    /// # Ok::<(), nom_exif::Error>(())
250    /// ```
251    pub fn from_memory(bytes: impl Into<bytes::Bytes>) -> crate::Result<Self> {
252        let bytes = bytes.into();
253        let head_end = bytes.len().min(HEADER_PARSE_BUF_SIZE);
254        let mime: MediaMime = bytes[..head_end].try_into()?;
255        Ok(Self {
256            reader: std::io::empty(),
257            buf: Vec::new(),
258            mime,
259            // Placeholder: never invoked in memory mode (AdvanceOnly path).
260            skip_by_seek: |_, _| Ok(false),
261            memory: Some(bytes),
262        })
263    }
264}
265
266// ----- Parse-time buffer policy -----
267//
268// Layered by lifecycle:
269//
270// - `INIT_BUF_SIZE` — first fill into the parse loop and the initial
271//   `Vec::with_capacity` for fresh allocations. Modest so cold one-shot
272//   helpers don't over-commit.
273// - `MIN_GROW_SIZE` — floor for every subsequent fill once we're in deep
274//   parse. Larger than `INIT_BUF_SIZE` to amortize syscalls / async
275//   blocking-pool dispatches.
276// - `MAX_PARSE_BUF_SIZE` — hard cap on cumulative buffer growth during a
277//   single parse. Anything that would push past this is rejected as
278//   `io::ErrorKind::Unsupported`; defense against crafted box/IFD headers
279//   that declare absurd sizes.
280// - `MAX_REUSE_BUF_SIZE` — soft cap on the buffer kept between parses for
281//   recycling. After a parse whose buffer ended above this, `shrink_to`
282//   gives the excess back to the allocator. Tuned for typical metadata
283//   sizes (HEIC Live Photo / large CR3 / IIQ all fit under 4 MB) so the
284//   recycle path stays warm for batch workloads.
285pub(crate) const INIT_BUF_SIZE: usize = 8 * 1024;
286pub(crate) const MIN_GROW_SIZE: usize = 16 * 1024;
287pub(crate) const MAX_PARSE_BUF_SIZE: usize = 1024 * 1024 * 1024;
288const MAX_REUSE_BUF_SIZE: usize = 4 * 1024 * 1024;
289
290pub(crate) trait Buf {
291    fn buffer(&self) -> &[u8];
292    fn clear(&mut self);
293
294    fn set_position(&mut self, pos: usize);
295    #[allow(unused)]
296    fn position(&self) -> usize;
297}
298
299/// Buffer-management state used by `MediaParser` (sync and async paths share it).
300///
301/// Holds at most one *active* `Vec<u8>` (being filled by the current parse) and
302/// one *cached* `Bytes` clone of the most recently shared buffer. When the
303/// next parse starts, the cache is consulted: if `Bytes::try_into_mut`
304/// succeeds the underlying allocation is reused (the previous `ExifIter`
305/// has been dropped); otherwise the clone is discarded and a fresh
306/// `Vec<u8>` is allocated.
307///
308/// This replaces the v2 multi-slot `Buffers` pool — `MediaParser` methods
309/// are `&mut self`, so a single slot is sufficient.
310#[derive(Debug, Default)]
311pub(crate) struct BufferedParserState {
312    cached: Option<bytes::Bytes>,
313    buf: Option<Vec<u8>>,
314    /// P7: memory-mode storage. When `Some`, the parser is feeding from a
315    /// caller-owned `Bytes` instead of streaming via a reader. `buf` and
316    /// `cached` are unused in this mode — the user owns the allocation,
317    /// so there is nothing to recycle.
318    memory: Option<bytes::Bytes>,
319    position: usize,
320}
321
322impl BufferedParserState {
323    pub(crate) fn new() -> Self {
324        Self::default()
325    }
326
327    pub(crate) fn reset(&mut self) {
328        // If a parse failed mid-way the buf may still be present; drop it.
329        // Cache stays — recycle on next acquire if eligible.
330        self.buf = None;
331        self.memory = None;
332        self.position = 0;
333    }
334
335    /// Switch the parser state into memory mode, owning `bytes` directly.
336    /// Caller must have already called `reset()` (asserted in debug). Subsequent
337    /// `share_buf` returns a clone of `bytes` (zero-copy: `Bytes::clone` is a
338    /// refcount bump). Subsequent `Buf::buffer()` returns `&bytes[position..]`.
339    pub(crate) fn set_memory(&mut self, bytes: bytes::Bytes) {
340        debug_assert!(
341            self.buf.is_none() && self.memory.is_none(),
342            "set_memory called on non-clean state"
343        );
344        self.memory = Some(bytes);
345        self.position = 0;
346    }
347
348    pub(crate) fn is_memory_mode(&self) -> bool {
349        self.memory.is_some()
350    }
351
352    pub(crate) fn acquire_buf(&mut self) {
353        if self.memory.is_some() {
354            // Memory mode: nothing to acquire — `buffer()` reads from `memory`.
355            return;
356        }
357        debug_assert!(self.buf.is_none());
358        let buf = match self.cached.take() {
359            Some(b) => match b.try_into_mut() {
360                Ok(bm) => {
361                    let mut v = Vec::<u8>::from(bm);
362                    v.clear();
363                    if v.capacity() > MAX_REUSE_BUF_SIZE {
364                        v.shrink_to(MAX_REUSE_BUF_SIZE);
365                    }
366                    v
367                }
368                Err(_still_shared) => Vec::with_capacity(INIT_BUF_SIZE),
369            },
370            None => Vec::with_capacity(INIT_BUF_SIZE),
371        };
372        self.buf = Some(buf);
373    }
374
375    pub(crate) fn buf(&self) -> &Vec<u8> {
376        self.buf.as_ref().expect("no buf here")
377    }
378
379    pub(crate) fn buf_mut(&mut self) -> &mut Vec<u8> {
380        self.buf.as_mut().expect("no buf here")
381    }
382
383    #[cfg(test)]
384    pub(crate) fn cached_ptr_for_test(&self) -> Option<*const u8> {
385        self.cached.as_ref().map(|b| b.as_ptr())
386    }
387
388    #[cfg(test)]
389    pub(crate) fn buf_is_none_for_test(&self) -> bool {
390        self.buf.is_none()
391    }
392}
393
394impl Buf for BufferedParserState {
395    fn buffer(&self) -> &[u8] {
396        if let Some(m) = &self.memory {
397            return &m[self.position..];
398        }
399        &self.buf()[self.position..]
400    }
401    fn clear(&mut self) {
402        // In memory mode `clear` is a no-op: there is no scratch buffer to
403        // truncate, and the caller's bytes must remain available for further
404        // parse_loop_step iterations. clear_and_skip's AdvanceOnly path is
405        // what advances `position` in memory mode.
406        if self.memory.is_some() {
407            return;
408        }
409        self.buf_mut().clear();
410    }
411    fn set_position(&mut self, pos: usize) {
412        self.position = pos;
413    }
414    fn position(&self) -> usize {
415        self.position
416    }
417}
418
419impl ShareBuf for BufferedParserState {
420    fn share_buf(&mut self) -> (bytes::Bytes, usize) {
421        if let Some(m) = self.memory.take() {
422            // Zero-copy share: caller already owns the allocation. No cache
423            // write — recycle is irrelevant when the user holds the alloc.
424            let position = self.position;
425            return (m, position);
426        }
427        let vec = self.buf.take().expect("no buf to share");
428        let bytes = bytes::Bytes::from(vec);
429        let position = self.position;
430        self.cached = Some(bytes.clone());
431        (bytes, position)
432    }
433}
434
435/// What `clear_and_skip` should do, given the current buffer state and
436/// the requested skip count.
437pub(crate) enum SkipPlan {
438    /// Skip is fully within the current buffer; just advance position.
439    AdvanceOnly,
440    /// Buffer must be cleared and `extra` bytes skipped from the reader.
441    ClearAndSkip { extra: usize },
442}
443
444pub(crate) fn clear_and_skip_decide(buffer_len: usize, n: usize) -> SkipPlan {
445    if n <= buffer_len {
446        SkipPlan::AdvanceOnly
447    } else {
448        SkipPlan::ClearAndSkip {
449            extra: n - buffer_len,
450        }
451    }
452}
453
454pub(crate) fn check_fill_size(existing_len: usize, requested: usize) -> io::Result<()> {
455    if requested.saturating_add(existing_len) > MAX_PARSE_BUF_SIZE {
456        tracing::error!(?requested, "the requested buffer size is too big");
457        return Err(io::ErrorKind::Unsupported.into());
458    }
459    Ok(())
460}
461
462pub(crate) enum LoopAction<O> {
463    /// Parse succeeded; return this value to the caller.
464    Done(O),
465    /// Need more bytes — call `fill_buf(reader, n)` then re-step.
466    NeedFill(usize),
467    /// Need to skip bytes — call `clear_and_skip(reader, n)` then re-step.
468    Skip(usize),
469    /// Parse failed permanently. Carries the structural-unit kind so
470    /// the eventual `Error::Malformed` is labelled correctly.
471    Failed {
472        kind: crate::error::MalformedKind,
473        message: String,
474    },
475}
476
477/// Closure type passed to [`parse_loop_step`].
478pub(crate) type ParseFn<'a, O> =
479    dyn FnMut(&[u8], usize, Option<ParsingState>) -> Result<O, ParsingErrorState> + 'a;
480
481/// Drives one iteration of the parse-loop algorithm. Pure (no I/O).
482pub(crate) fn parse_loop_step<O>(
483    buffer: &[u8],
484    offset: usize,
485    parsing_state: &mut Option<ParsingState>,
486    parse: &mut ParseFn<'_, O>,
487) -> LoopAction<O> {
488    match parse(buffer, offset, parsing_state.take()) {
489        Ok(o) => LoopAction::Done(o),
490        Err(es) => {
491            *parsing_state = es.state;
492            match es.err {
493                ParsingError::Need(n) => LoopAction::NeedFill(n),
494                ParsingError::ClearAndSkip(n) => LoopAction::Skip(n),
495                ParsingError::Failed { kind, message } => LoopAction::Failed { kind, message },
496            }
497        }
498    }
499}
500
501#[derive(Debug, Clone)]
502pub(crate) enum ParsingState {
503    TiffHeader(TiffHeader),
504    HeifExifSize(usize),
505    Cr3ExifSize(usize),
506    /// PNG chunk walker has already validated the 8-byte signature.
507    /// Carried across `Need` / `ClearAndSkip` retries so the resumed
508    /// call doesn't re-check signature against a mid-stream slice.
509    PngPastSignature,
510    /// WebP RIFF walker has validated the 12-byte header and skipped some
511    /// chunks. Carries the number of chunk-stream bytes still remaining
512    /// (from the resumed buffer's start) so the walker can stop cleanly at
513    /// the end of the RIFF chunk region — WebP has no terminator chunk.
514    WebpPastHeader(usize),
515}
516
517impl Display for ParsingState {
518    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519        match self {
520            ParsingState::TiffHeader(h) => Display::fmt(&format!("ParsingState: {h:?})"), f),
521            ParsingState::HeifExifSize(n) => Display::fmt(&format!("ParsingState: {n}"), f),
522            ParsingState::Cr3ExifSize(n) => Display::fmt(&format!("ParsingState: {n}"), f),
523            ParsingState::PngPastSignature => f.write_str("ParsingState: PngPastSignature"),
524            ParsingState::WebpPastHeader(n) => {
525                Display::fmt(&format!("ParsingState: WebpPastHeader({n})"), f)
526            }
527        }
528    }
529}
530
531// Modern replacement for the `Load` trait in loader.rs. Adds offset-aware
532// parsing and `ParsingState` threading for format-specific state machines.
533pub(crate) trait BufParser: Buf + Debug {
534    fn fill_buf<R: Read>(&mut self, reader: &mut R, size: usize) -> io::Result<usize>;
535
536    fn load_and_parse<R: Read, P, O>(
537        &mut self,
538        reader: &mut R,
539        skip_by_seek: SkipBySeekFn<R>,
540        mut parse: P,
541    ) -> Result<O, ParsedError>
542    where
543        P: FnMut(&[u8], Option<ParsingState>) -> Result<O, ParsingErrorState>,
544    {
545        self.load_and_parse_with_offset(
546            reader,
547            skip_by_seek,
548            |data, _, state| parse(data, state),
549            0,
550        )
551    }
552
553    #[tracing::instrument(skip_all)]
554    fn load_and_parse_with_offset<R: Read, P, O>(
555        &mut self,
556        reader: &mut R,
557        skip_by_seek: SkipBySeekFn<R>,
558        mut parse: P,
559        offset: usize,
560    ) -> Result<O, ParsedError>
561    where
562        P: FnMut(&[u8], usize, Option<ParsingState>) -> Result<O, ParsingErrorState>,
563    {
564        if offset >= self.buffer().len() {
565            self.fill_buf(reader, MIN_GROW_SIZE)?;
566        }
567        let mut parsing_state: Option<ParsingState> = None;
568        loop {
569            match parse_loop_step(self.buffer(), offset, &mut parsing_state, &mut parse) {
570                LoopAction::Done(o) => return Ok(o),
571                LoopAction::NeedFill(needed) => {
572                    let to_read = max(needed, MIN_GROW_SIZE);
573                    let n = self.fill_buf(reader, to_read)?;
574                    if n == 0 {
575                        return Err(ParsedError::NoEnoughBytes);
576                    }
577                }
578                LoopAction::Skip(n) => {
579                    self.clear_and_skip(reader, skip_by_seek, n)?;
580                }
581                LoopAction::Failed { kind, message } => {
582                    return Err(ParsedError::Failed { kind, message })
583                }
584            }
585        }
586    }
587
588    #[tracing::instrument(skip(reader, skip_by_seek))]
589    fn clear_and_skip<R: Read>(
590        &mut self,
591        reader: &mut R,
592        skip_by_seek: SkipBySeekFn<R>,
593        n: usize,
594    ) -> Result<(), ParsedError> {
595        match clear_and_skip_decide(self.buffer().len(), n) {
596            SkipPlan::AdvanceOnly => {
597                self.set_position(self.position() + n);
598                Ok(())
599            }
600            SkipPlan::ClearAndSkip { extra: skip_n } => {
601                self.clear();
602                let done = (skip_by_seek)(
603                    reader,
604                    skip_n.try_into().map_err(|_| ParsedError::Failed {
605                        // No format context available here: the parser
606                        // hit an internal limit honoring a caller's skip.
607                        // Pick a sensible default — see #55 follow-up.
608                        kind: crate::error::MalformedKind::IsoBmffBox,
609                        message: "skip too many bytes".into(),
610                    })?,
611                )?;
612                if !done {
613                    let mut skipped = 0;
614                    while skipped < skip_n {
615                        let mut to_skip = skip_n - skipped;
616                        to_skip = min(to_skip, MAX_PARSE_BUF_SIZE);
617                        let n = self.fill_buf(reader, to_skip)?;
618                        skipped += n;
619                        if skipped <= skip_n {
620                            self.clear();
621                        } else {
622                            let remain = skipped - skip_n;
623                            self.set_position(self.buffer().len() - remain);
624                            break;
625                        }
626                    }
627                }
628
629                if self.buffer().is_empty() {
630                    self.fill_buf(reader, MIN_GROW_SIZE)?;
631                }
632                Ok(())
633            }
634        }
635    }
636}
637
638impl BufParser for MediaParser {
639    #[tracing::instrument(skip(self, reader), fields(buf_len=self.state.buffer().len()))]
640    fn fill_buf<R: Read>(&mut self, reader: &mut R, size: usize) -> io::Result<usize> {
641        if self.state.is_memory_mode() {
642            // Memory mode owns every byte it will ever have. A request for
643            // more is "the parser walked off the end of the input"; surface
644            // it the same way the streaming path surfaces a 0-byte read.
645            return Err(std::io::ErrorKind::UnexpectedEof.into());
646        }
647        check_fill_size(self.state.buf().len(), size)?;
648
649        // Do not pre-allocate `size` bytes: a crafted box header can declare a
650        // huge extended size (up to MAX_PARSE_BUF_SIZE) that far exceeds the actual
651        // stream length. reserve_exact would allocate that memory immediately
652        // even when the reader has only a few bytes left. read_to_end grows the
653        // buffer from the reader's actual size hint instead.
654        let n = reader.take(size as u64).read_to_end(self.state.buf_mut())?;
655        if n == 0 {
656            tracing::error!(buf_len = self.state.buf().len(), "fill_buf: EOF");
657            return Err(std::io::ErrorKind::UnexpectedEof.into());
658        }
659
660        tracing::debug!(
661            ?size,
662            ?n,
663            buf_len = self.state.buf().len(),
664            "fill_buf: read bytes"
665        );
666
667        Ok(n)
668    }
669}
670
671impl Buf for MediaParser {
672    fn buffer(&self) -> &[u8] {
673        self.state.buffer()
674    }
675
676    fn clear(&mut self) {
677        self.state.clear();
678    }
679
680    fn set_position(&mut self, pos: usize) {
681        self.state.set_position(pos);
682    }
683
684    fn position(&self) -> usize {
685        self.state.position()
686    }
687}
688
689/// A `MediaParser` can parse media info from a [`MediaSource`].
690///
691/// `MediaParser` manages inner parse buffers that can be shared between
692/// multiple parsing tasks, thus avoiding frequent memory allocations.
693///
694/// Therefore:
695///
696/// - Try to reuse a `MediaParser` instead of creating a new one every time
697///   you need it.
698///
699/// - `MediaSource` should be created directly from `Read`, not from `BufRead`.
700///
701/// ## Example
702///
703/// ```rust
704/// use nom_exif::*;
705/// use chrono::DateTime;
706///
707/// let mut parser = MediaParser::new();
708///
709/// // ------------------- Parse Exif Info
710/// let ms = MediaSource::open("./testdata/exif.heic").unwrap();
711/// assert_eq!(ms.kind(), MediaKind::Image);
712/// let mut iter = parser.parse_exif(ms).unwrap();
713///
714/// let entry = iter.next().unwrap();
715/// assert!(matches!(entry.tag(), nom_exif::TagOrCode::Tag(ExifTag::Make)));
716/// assert_eq!(entry.value().unwrap().as_str().unwrap(), "Apple");
717///
718/// // Convert `ExifIter` into an `Exif`. Clone it before converting, so that
719/// // we can start the iteration from the beginning.
720/// let exif: Exif = iter.clone().into();
721/// assert_eq!(exif.get(ExifTag::Make).unwrap().as_str().unwrap(), "Apple");
722///
723/// // ------------------- Parse Track Info
724/// let ms = MediaSource::open("./testdata/meta.mov").unwrap();
725/// assert_eq!(ms.kind(), MediaKind::Track);
726/// let info = parser.parse_track(ms).unwrap();
727///
728/// assert_eq!(info.get(TrackInfoTag::Make), Some(&"Apple".into()));
729/// assert_eq!(info.get(TrackInfoTag::Model), Some(&"iPhone X".into()));
730/// assert_eq!(info.get(TrackInfoTag::GpsIso6709), Some(&"+27.1281+100.2508+000.000/".into()));
731/// assert_eq!(info.gps_info().unwrap().latitude_ref, LatRef::North);
732/// assert_eq!(
733///     info.gps_info().unwrap().latitude,
734///     LatLng::new(URational::new(27, 1), URational::new(7, 1), URational::new(4116, 100)),
735/// );
736/// ```
737pub struct MediaParser {
738    state: BufferedParserState,
739}
740
741impl Debug for MediaParser {
742    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
743        f.debug_struct("MediaParser")
744            .field("state", &self.state)
745            .finish_non_exhaustive()
746    }
747}
748
749impl Default for MediaParser {
750    fn default() -> Self {
751        Self {
752            state: BufferedParserState::new(),
753        }
754    }
755}
756
757pub(crate) trait ShareBuf {
758    /// Take ownership of the parser's active buffer and return the full
759    /// allocation as `Bytes` plus the parser's `position` at share-time.
760    /// Caller is responsible for slicing: a parse-loop range `r` corresponds
761    /// to absolute range `(r.start + position)..(r.end + position)`.
762    fn share_buf(&mut self) -> (bytes::Bytes, usize);
763}
764
765impl ShareBuf for MediaParser {
766    fn share_buf(&mut self) -> (bytes::Bytes, usize) {
767        self.state.share_buf()
768    }
769}
770
771impl MediaParser {
772    pub fn new() -> Self {
773        Self::default()
774    }
775
776    /// Parse Exif metadata from an image source. Returns `Error::ExifNotFound`
777    /// if the source is a `Track` (use [`Self::parse_track`] instead).
778    ///
779    /// As of v3.3, this method also accepts memory-mode sources built via
780    /// [`MediaSource::from_memory`]. The deprecated [`Self::parse_exif_from_bytes`]
781    /// is now a thin adapter that delegates here.
782    ///
783    /// `MediaParser` reuses its internal parse buffer across calls, so prefer
784    /// reusing a single `MediaParser` over creating a new one per file. Drop
785    /// the returned [`ExifIter`] (or convert it into [`crate::Exif`]) before
786    /// the next `parse_*` call so the buffer can be reclaimed.
787    pub fn parse_exif<R: Read>(&mut self, mut ms: MediaSource<R>) -> crate::Result<ExifIter> {
788        self.reset();
789        let res: crate::Result<ExifIter> = (|| {
790            if let Some(memory) = ms.memory.take() {
791                // Memory-mode: zero-copy share of caller-owned bytes.
792                self.state.set_memory(memory);
793                if !matches!(ms.mime, crate::file::MediaMime::Image(_)) {
794                    return Err(crate::Error::ExifNotFound);
795                }
796                crate::exif::parse_exif_iter(
797                    self,
798                    ms.mime.unwrap_image(),
799                    &mut ms.reader,
800                    ms.skip_by_seek,
801                )
802            } else {
803                // Streaming-mode: existing path verbatim.
804                self.acquire_buf();
805                self.buf_mut().append(&mut ms.buf);
806                // PNG-only EOF tolerance: a tEXt-only PNG can be smaller
807                // than HEADER_PARSE_BUF_SIZE (e.g. 117-byte text-only.png),
808                // so the mime-detection prefill consumes the whole reader
809                // and fill_buf returns UnexpectedEof. The bytes we need
810                // are already in the parse buffer — proceed. Other formats
811                // keep the strict-EOF contract.
812                let is_png = matches!(
813                    ms.mime,
814                    crate::file::MediaMime::Image(
815                        crate::file::MediaMimeImage::Png | crate::file::MediaMimeImage::Webp
816                    )
817                );
818                match self.fill_buf(&mut ms.reader, INIT_BUF_SIZE) {
819                    Ok(_) => {}
820                    Err(e)
821                        if is_png
822                            && !self.buffer().is_empty()
823                            && e.kind() == io::ErrorKind::UnexpectedEof => {}
824                    Err(e) => return Err(e.into()),
825                }
826                if !matches!(ms.mime, crate::file::MediaMime::Image(_)) {
827                    return Err(crate::Error::ExifNotFound);
828                }
829                crate::exif::parse_exif_iter(
830                    self,
831                    ms.mime.unwrap_image(),
832                    &mut ms.reader,
833                    ms.skip_by_seek,
834                )
835            }
836        })();
837        self.reset();
838        res
839    }
840
841    /// Parse track info from a video/audio source.
842    ///
843    /// Parse track info from a video/audio source.
844    ///
845    /// In v3.1, this also accepts JPEG images that carry an embedded
846    /// Pixel/Google Motion Photo trailer. As of v3.3, it also accepts
847    /// memory-mode sources built via [`MediaSource::from_memory`]; the
848    /// deprecated [`Self::parse_track_from_bytes`] is now a thin
849    /// adapter that delegates here.
850    pub fn parse_track<R: Read>(&mut self, mut ms: MediaSource<R>) -> crate::Result<TrackInfo> {
851        self.reset();
852        let res: crate::Result<TrackInfo> = (|| {
853            if let Some(memory) = ms.memory.take() {
854                // Memory mode: zero-copy.
855                self.state.set_memory(memory);
856                let mime_track = match ms.mime {
857                    crate::file::MediaMime::Image(_) => return Err(crate::Error::TrackNotFound),
858                    crate::file::MediaMime::Track(t) => t,
859                };
860                let out = self.load_and_parse(&mut ms.reader, ms.skip_by_seek, |data, _| {
861                    crate::video::parse_track_info(data, mime_track)
862                        .map_err(|e| ParsingErrorState::new(e, None))
863                })?;
864                Ok(out)
865            } else {
866                // Streaming mode: existing path verbatim.
867                self.acquire_buf();
868                self.buf_mut().append(&mut ms.buf);
869                self.fill_buf(&mut ms.reader, INIT_BUF_SIZE)?;
870                match ms.mime {
871                    crate::file::MediaMime::Image(crate::file::MediaMimeImage::Jpeg) => {
872                        self.parse_jpeg_motion_photo(&mut ms.reader)
873                    }
874                    crate::file::MediaMime::Image(_) => Err(crate::Error::TrackNotFound),
875                    crate::file::MediaMime::Track(mime_track) => {
876                        let skip = ms.skip_by_seek;
877                        Ok(self.load_and_parse(ms.reader.by_ref(), skip, |data, _| {
878                            crate::video::parse_track_info(data, mime_track)
879                                .map_err(|e| ParsingErrorState::new(e, None))
880                        })?)
881                    }
882                }
883            }
884        })();
885        self.reset();
886        res
887    }
888
889    /// Read a JPEG to EOF, locate a Pixel-style Motion Photo MP4 trailer,
890    /// and parse it as track metadata. Returns
891    /// [`crate::Error::TrackNotFound`] if no Motion Photo signal is
892    /// present in the JPEG's XMP.
893    fn parse_jpeg_motion_photo<R: Read>(&mut self, reader: &mut R) -> crate::Result<TrackInfo> {
894        // Drain the rest of the JPEG into the parse buffer so we can
895        // address the trailing MP4 by its byte offset from EOF.
896        reader.read_to_end(self.buf_mut())?;
897        let buf = self.buf_mut();
898        let Some(offset) = crate::jpeg::find_motion_photo_offset(buf) else {
899            return Err(crate::Error::TrackNotFound);
900        };
901        let trailer_start = (buf.len() as u64)
902            .checked_sub(offset)
903            .ok_or(crate::Error::TrackNotFound)? as usize;
904        let trailer = &buf[trailer_start..];
905
906        // The trailer can be MP4 / MOV / 3gp depending on the source device;
907        // dispatch by sniffing it as a fresh ISO BMFF input.
908        let trailer_mime =
909            crate::file::MediaMime::try_from(trailer).map_err(|_| crate::Error::TrackNotFound)?;
910        let mime_track = match trailer_mime {
911            crate::file::MediaMime::Track(t) => t,
912            crate::file::MediaMime::Image(_) => return Err(crate::Error::TrackNotFound),
913        };
914        crate::video::parse_track_info(trailer, mime_track).map_err(|e| match e {
915            crate::error::ParsingError::Need(_) | crate::error::ParsingError::ClearAndSkip(_) => {
916                crate::Error::UnexpectedEof {
917                    context: "motion-photo trailer",
918                }
919            }
920            crate::error::ParsingError::Failed { kind, message } => {
921                crate::Error::Malformed { kind, message }
922            }
923        })
924    }
925
926    /// Parse Exif metadata from an in-memory byte payload built via
927    /// the deprecated [`MediaSource::<()>::from_bytes`].
928    ///
929    /// **Deprecated since v3.3.0**: use [`Self::parse_exif`] with
930    /// [`MediaSource::from_memory`] directly.
931    #[deprecated(
932        since = "3.3.0",
933        note = "Use `parse_exif` directly — it now accepts memory-mode \
934                sources built via `MediaSource::from_memory`."
935    )]
936    pub fn parse_exif_from_bytes(&mut self, ms: MediaSource<()>) -> crate::Result<ExifIter> {
937        self.parse_exif(ms.into_empty())
938    }
939
940    /// **Deprecated since v3.3.0**: use [`Self::parse_track`] with
941    /// [`MediaSource::from_memory`] directly.
942    #[deprecated(
943        since = "3.3.0",
944        note = "Use `parse_track` with `MediaSource::from_memory`."
945    )]
946    pub fn parse_track_from_bytes(&mut self, ms: MediaSource<()>) -> crate::Result<TrackInfo> {
947        self.parse_track(ms.into_empty())
948    }
949
950    /// Parse all metadata from an image source: EXIF (if any) and
951    /// format-specific extras (PNG `tEXt` chunks, etc.).
952    ///
953    /// Returns `Err(Error::ExifNotFound)` if neither EXIF nor any
954    /// format-specific metadata is found. Returns
955    /// `Err(Error::TrackNotFound)`-style errors on track inputs (use
956    /// `parse_track` instead).
957    ///
958    /// **Lazy form** — this method returns `ImageMetadata<ExifIter>`.
959    /// Convert to the eager `ImageMetadata<Exif>` via `.into()` if
960    /// desired.
961    pub fn parse_image_metadata<R: Read>(
962        &mut self,
963        mut ms: MediaSource<R>,
964    ) -> crate::Result<crate::image_metadata::ImageMetadata<crate::ExifIter>> {
965        self.reset();
966        let res: crate::Result<crate::image_metadata::ImageMetadata<crate::ExifIter>> = (|| {
967            // Reject track inputs early (parse_track is the right API).
968            let mime_img = match ms.mime {
969                crate::file::MediaMime::Image(img) => img,
970                crate::file::MediaMime::Track(_) => return Err(crate::Error::ExifNotFound),
971            };
972
973            // Memory-mode shortcut + buffer setup mirrors parse_exif.
974            if let Some(memory) = ms.memory.take() {
975                self.state.set_memory(memory);
976            } else {
977                self.acquire_buf();
978                self.buf_mut().append(&mut ms.buf);
979                // PNG-only EOF tolerance: a tEXt-only PNG can be smaller than
980                // HEADER_PARSE_BUF_SIZE (e.g. 117-byte text-only.png) so the
981                // mime-detection prefill consumes the whole reader and
982                // fill_buf returns UnexpectedEof. The bytes we need are
983                // already in the parse buffer — proceed. Other formats keep
984                // the strict-EOF contract.
985                let is_png = matches!(
986                    mime_img,
987                    crate::file::MediaMimeImage::Png | crate::file::MediaMimeImage::Webp
988                );
989                match self.fill_buf(&mut ms.reader, INIT_BUF_SIZE) {
990                    Ok(_) => {}
991                    Err(e)
992                        if is_png
993                            && !self.buffer().is_empty()
994                            && e.kind() == io::ErrorKind::UnexpectedEof => {}
995                    Err(e) => return Err(e.into()),
996                }
997            }
998
999            if mime_img == crate::file::MediaMimeImage::Png {
1000                let (exif, text_chunks) =
1001                    crate::exif::parse_png_full(self, &mut ms.reader, ms.skip_by_seek)?;
1002                let format = if text_chunks.is_empty() {
1003                    None
1004                } else {
1005                    Some(crate::ImageFormatMetadata::Png(crate::PngTextChunks {
1006                        entries: text_chunks,
1007                    }))
1008                };
1009                if exif.is_none() && format.is_none() {
1010                    return Err(crate::Error::ExifNotFound);
1011                }
1012                Ok(crate::ImageMetadata { exif, format })
1013            } else {
1014                // Non-PNG: existing parse_exif_iter path; format always None.
1015                let iter =
1016                    crate::exif::parse_exif_iter(self, mime_img, &mut ms.reader, ms.skip_by_seek)?;
1017                Ok(crate::ImageMetadata {
1018                    exif: Some(iter),
1019                    format: None,
1020                })
1021            }
1022        })(
1023        );
1024        self.reset();
1025        res
1026    }
1027
1028    fn reset(&mut self) {
1029        self.state.reset();
1030    }
1031
1032    fn buf_mut(&mut self) -> &mut Vec<u8> {
1033        self.state.buf_mut()
1034    }
1035
1036    fn acquire_buf(&mut self) {
1037        self.state.acquire_buf();
1038    }
1039}
1040
1041#[cfg(feature = "tokio")]
1042mod tokio_impl {
1043    use super::*;
1044    use crate::error::ParsingErrorState;
1045    use crate::parser_async::{AsyncBufParser, AsyncMediaSource};
1046    use tokio::io::{AsyncRead, AsyncReadExt};
1047
1048    impl AsyncBufParser for MediaParser {
1049        async fn fill_buf<R: AsyncRead + Unpin>(
1050            &mut self,
1051            reader: &mut R,
1052            size: usize,
1053        ) -> std::io::Result<usize> {
1054            if self.state.is_memory_mode() {
1055                // Memory mode owns every byte it will ever have. Surface
1056                // "walked off end of input" the same way the streaming path
1057                // surfaces a 0-byte read.
1058                return Err(std::io::ErrorKind::UnexpectedEof.into());
1059            }
1060            check_fill_size(self.state.buf().len(), size)?;
1061            // Same rationale as the sync version: do not pre-allocate `size` bytes.
1062            let n = reader
1063                .take(size as u64)
1064                .read_to_end(self.state.buf_mut())
1065                .await?;
1066            if n == 0 {
1067                return Err(std::io::ErrorKind::UnexpectedEof.into());
1068            }
1069            Ok(n)
1070        }
1071    }
1072
1073    impl MediaParser {
1074        /// Parse Exif metadata from an async image source. Returns
1075        /// `Error::ExifNotFound` if the source is a `Track`.
1076        ///
1077        /// As of v3.3, also accepts memory-mode sources built via
1078        /// [`AsyncMediaSource::from_memory`]; the memory branch shares
1079        /// caller-owned `Bytes` zero-copy through `state.set_memory`.
1080        pub async fn parse_exif_async<R: AsyncRead + Unpin + Send>(
1081            &mut self,
1082            mut ms: AsyncMediaSource<R>,
1083        ) -> crate::Result<ExifIter> {
1084            self.reset();
1085            let res: crate::Result<ExifIter> = async {
1086                if let Some(memory) = ms.memory.take() {
1087                    // Memory-mode: zero-copy share of caller-owned bytes.
1088                    self.state.set_memory(memory);
1089                    if !matches!(ms.mime, crate::file::MediaMime::Image(_)) {
1090                        return Err(crate::Error::ExifNotFound);
1091                    }
1092                    crate::exif::parse_exif_iter_async(
1093                        self,
1094                        ms.mime.unwrap_image(),
1095                        &mut ms.reader,
1096                        ms.skip_by_seek,
1097                    )
1098                    .await
1099                } else {
1100                    self.acquire_buf();
1101                    self.buf_mut().append(&mut ms.buf);
1102                    // PNG-only EOF tolerance mirrors the sync path: small
1103                    // tEXt-only PNGs (<HEADER_PARSE_BUF_SIZE) are fully
1104                    // consumed during mime detection, so fill_buf returns
1105                    // UnexpectedEof. The bytes are already in the parse
1106                    // buffer; proceed.
1107                    let is_png = matches!(
1108                        ms.mime,
1109                        crate::file::MediaMime::Image(
1110                            crate::file::MediaMimeImage::Png | crate::file::MediaMimeImage::Webp
1111                        )
1112                    );
1113                    match <Self as AsyncBufParser>::fill_buf(self, &mut ms.reader, INIT_BUF_SIZE)
1114                        .await
1115                    {
1116                        Ok(_) => {}
1117                        Err(e)
1118                            if is_png
1119                                && !self.buffer().is_empty()
1120                                && e.kind() == io::ErrorKind::UnexpectedEof => {}
1121                        Err(e) => return Err(e.into()),
1122                    }
1123                    if !matches!(ms.mime, crate::file::MediaMime::Image(_)) {
1124                        return Err(crate::Error::ExifNotFound);
1125                    }
1126                    crate::exif::parse_exif_iter_async(
1127                        self,
1128                        ms.mime.unwrap_image(),
1129                        &mut ms.reader,
1130                        ms.skip_by_seek,
1131                    )
1132                    .await
1133                }
1134            }
1135            .await;
1136            self.reset();
1137            res
1138        }
1139
1140        pub async fn parse_image_metadata_async<R: AsyncRead + Unpin + Send>(
1141            &mut self,
1142            mut ms: AsyncMediaSource<R>,
1143        ) -> crate::Result<crate::image_metadata::ImageMetadata<crate::ExifIter>> {
1144            self.reset();
1145            let res: crate::Result<crate::image_metadata::ImageMetadata<crate::ExifIter>> = async {
1146                let mime_img = match ms.mime {
1147                    crate::file::MediaMime::Image(img) => img,
1148                    crate::file::MediaMime::Track(_) => return Err(crate::Error::ExifNotFound),
1149                };
1150
1151                if let Some(memory) = ms.memory.take() {
1152                    self.state.set_memory(memory);
1153                } else {
1154                    self.acquire_buf();
1155                    self.buf_mut().append(&mut ms.buf);
1156                    // PNG-only EOF tolerance mirrors the sync path: small
1157                    // tEXt-only PNGs (<HEADER_PARSE_BUF_SIZE) are fully
1158                    // consumed during mime detection, so fill_buf returns
1159                    // UnexpectedEof; the bytes we need are already in the
1160                    // parse buffer.
1161                    let is_png = matches!(
1162                        mime_img,
1163                        crate::file::MediaMimeImage::Png | crate::file::MediaMimeImage::Webp
1164                    );
1165                    match <Self as AsyncBufParser>::fill_buf(self, &mut ms.reader, INIT_BUF_SIZE)
1166                        .await
1167                    {
1168                        Ok(_) => {}
1169                        Err(e)
1170                            if is_png
1171                                && !self.buffer().is_empty()
1172                                && e.kind() == io::ErrorKind::UnexpectedEof => {}
1173                        Err(e) => return Err(e.into()),
1174                    }
1175                }
1176
1177                if mime_img == crate::file::MediaMimeImage::Png {
1178                    let (exif, text_chunks) =
1179                        crate::exif::parse_png_full_async(self, &mut ms.reader, ms.skip_by_seek)
1180                            .await?;
1181                    let format = if text_chunks.is_empty() {
1182                        None
1183                    } else {
1184                        Some(crate::ImageFormatMetadata::Png(crate::PngTextChunks {
1185                            entries: text_chunks,
1186                        }))
1187                    };
1188                    if exif.is_none() && format.is_none() {
1189                        return Err(crate::Error::ExifNotFound);
1190                    }
1191                    Ok(crate::image_metadata::ImageMetadata { exif, format })
1192                } else {
1193                    let iter = crate::exif::parse_exif_iter_async(
1194                        self,
1195                        mime_img,
1196                        &mut ms.reader,
1197                        ms.skip_by_seek,
1198                    )
1199                    .await?;
1200                    Ok(crate::image_metadata::ImageMetadata {
1201                        exif: Some(iter),
1202                        format: None,
1203                    })
1204                }
1205            }
1206            .await;
1207            self.reset();
1208            res
1209        }
1210
1211        /// Parse track info from an async video/audio source. Returns
1212        /// `Error::TrackNotFound` if the source is an `Image`.
1213        ///
1214        /// As of v3.3, also accepts memory-mode sources built via
1215        /// [`AsyncMediaSource::from_memory`].
1216        pub async fn parse_track_async<R: AsyncRead + Unpin + Send>(
1217            &mut self,
1218            mut ms: AsyncMediaSource<R>,
1219        ) -> crate::Result<TrackInfo> {
1220            self.reset();
1221            let res: crate::Result<TrackInfo> = async {
1222                let mime_track = match ms.mime {
1223                    crate::file::MediaMime::Image(_) => return Err(crate::Error::TrackNotFound),
1224                    crate::file::MediaMime::Track(t) => t,
1225                };
1226                if let Some(memory) = ms.memory.take() {
1227                    self.state.set_memory(memory);
1228                } else {
1229                    self.acquire_buf();
1230                    self.buf_mut().append(&mut ms.buf);
1231                    <Self as AsyncBufParser>::fill_buf(self, &mut ms.reader, INIT_BUF_SIZE).await?;
1232                }
1233                let skip = ms.skip_by_seek;
1234                let out = <Self as AsyncBufParser>::load_and_parse(
1235                    self,
1236                    &mut ms.reader,
1237                    skip,
1238                    |data, _| {
1239                        crate::video::parse_track_info(data, mime_track)
1240                            .map_err(|e| ParsingErrorState::new(e, None))
1241                    },
1242                )
1243                .await?;
1244                Ok(out)
1245            }
1246            .await;
1247            self.reset();
1248            res
1249        }
1250    }
1251}
1252
1253#[cfg(test)]
1254mod tests {
1255    use std::sync::{LazyLock, Mutex, MutexGuard};
1256
1257    use super::*;
1258    use test_case::case;
1259
1260    enum TrackExif {
1261        Track,
1262        Exif,
1263        NoData,
1264        Invalid,
1265    }
1266    use TrackExif::*;
1267
1268    static PARSER: LazyLock<Mutex<MediaParser>> = LazyLock::new(|| Mutex::new(MediaParser::new()));
1269    fn parser() -> MutexGuard<'static, MediaParser> {
1270        PARSER.lock().unwrap()
1271    }
1272
1273    #[case("3gp_640x360.3gp", Track)]
1274    #[case("broken.jpg", Exif)]
1275    #[case("compatible-brands-fail.heic", Invalid)]
1276    #[case("compatible-brands-fail.mov", Invalid)]
1277    #[case("compatible-brands.heic", NoData)]
1278    #[case("compatible-brands.mov", NoData)]
1279    #[case("embedded-in-heic.mov", Track)]
1280    #[case("exif.heic", Exif)]
1281    #[case("exif.jpg", Exif)]
1282    #[case("exif-no-tz.jpg", Exif)]
1283    #[case("fujifilm_x_t1_01.raf.meta", Exif)]
1284    #[case("meta.mov", Track)]
1285    #[case("meta.mp4", Track)]
1286    #[case("mka.mka", Track)]
1287    #[case("mkv_640x360.mkv", Track)]
1288    #[case("exif-one-entry.heic", Exif)]
1289    #[case("no-exif.jpg", NoData)]
1290    #[case("tif.tif", Exif)]
1291    #[case("ramdisk.img", Invalid)]
1292    #[case("webm_480.webm", Track)]
1293    fn parse_media(path: &str, te: TrackExif) {
1294        let mut parser = parser();
1295        let ms = MediaSource::open(Path::new("testdata").join(path));
1296        match te {
1297            Track => {
1298                let ms = ms.unwrap();
1299                assert_eq!(ms.kind(), MediaKind::Track);
1300                let _: TrackInfo = parser.parse_track(ms).unwrap();
1301            }
1302            Exif => {
1303                let ms = ms.unwrap();
1304                assert_eq!(ms.kind(), MediaKind::Image);
1305                let mut it: ExifIter = parser.parse_exif(ms).unwrap();
1306                let _ = it.parse_gps();
1307
1308                if path.contains("one-entry") {
1309                    assert!(it.next().is_some());
1310                    assert!(it.next().is_none());
1311
1312                    let exif: crate::Exif = it.clone_rewound().into();
1313                    assert!(exif.get(ExifTag::Orientation).is_some());
1314                } else {
1315                    let _: crate::Exif = it.clone_rewound().into();
1316                }
1317            }
1318            NoData => {
1319                let ms = ms.unwrap();
1320                match ms.kind() {
1321                    MediaKind::Image => {
1322                        let res = parser.parse_exif(ms);
1323                        res.unwrap_err();
1324                    }
1325                    MediaKind::Track => {
1326                        let res = parser.parse_track(ms);
1327                        res.unwrap_err();
1328                    }
1329                }
1330            }
1331            Invalid => {
1332                ms.unwrap_err();
1333            }
1334        }
1335    }
1336
1337    use crate::testkit::open_sample;
1338    use crate::{EntryValue, Exif, ExifTag, TrackInfoTag};
1339    use chrono::{DateTime, FixedOffset, NaiveDateTime};
1340    use test_case::test_case;
1341
1342    #[test_case("exif.jpg", ExifTag::DateTimeOriginal, DateTime::parse_from_str("2023-07-09T20:36:33+08:00", "%+").unwrap().into())]
1343    #[test_case("exif.heic", ExifTag::DateTimeOriginal, DateTime::parse_from_str("2022-07-22T21:26:32+08:00", "%+").unwrap().into())]
1344    #[test_case("exif.jpg", ExifTag::DateTimeOriginal, 
1345        (NaiveDateTime::parse_from_str("2023-07-09T20:36:33", "%Y-%m-%dT%H:%M:%S").unwrap(), 
1346            Some(FixedOffset::east_opt(8*3600).unwrap())).into())]
1347    #[test_case("exif-no-tz.jpg", ExifTag::DateTimeOriginal, 
1348        (NaiveDateTime::parse_from_str("2023-07-09T20:36:33", "%Y-%m-%dT%H:%M:%S").unwrap(), None).into())]
1349    fn parse_exif(path: &str, tag: ExifTag, v: EntryValue) {
1350        let mut parser = parser();
1351
1352        let mf = MediaSource::seekable(open_sample(path).unwrap()).unwrap();
1353        assert_eq!(mf.kind(), MediaKind::Image);
1354        let iter: ExifIter = parser.parse_exif(mf).unwrap();
1355        let exif: Exif = iter.into();
1356        assert_eq!(exif.get(tag).unwrap(), &v);
1357
1358        let mf = MediaSource::unseekable(open_sample(path).unwrap()).unwrap();
1359        assert_eq!(mf.kind(), MediaKind::Image);
1360        let iter: ExifIter = parser.parse_exif(mf).unwrap();
1361        let exif: Exif = iter.into();
1362        assert_eq!(exif.get(tag).unwrap(), &v);
1363    }
1364
1365    use crate::video::TrackInfoTag::*;
1366
1367    #[test_case("mkv_640x360.mkv", Width, 640_u32.into())]
1368    #[test_case("mkv_640x360.mkv", Height, 360_u32.into())]
1369    #[test_case("mkv_640x360.mkv", DurationMs, 13346_u64.into())]
1370    #[test_case("mkv_640x360.mkv", CreateDate, DateTime::parse_from_str("2008-08-08T08:08:08Z", "%+").unwrap().into())]
1371    #[test_case("meta.mov", Make, "Apple".into())]
1372    #[test_case("meta.mov", Model, "iPhone X".into())]
1373    #[test_case("meta.mov", GpsIso6709, "+27.1281+100.2508+000.000/".into())]
1374    #[test_case("meta.mov", CreateDate, DateTime::parse_from_str("2019-02-12T15:27:12+08:00", "%+").unwrap().into())]
1375    #[test_case("meta.mp4", Width, 1920_u32.into())]
1376    #[test_case("meta.mp4", Height, 1080_u32.into())]
1377    #[test_case("meta.mp4", DurationMs, 1063_u64.into())]
1378    #[test_case("meta.mp4", GpsIso6709, "+27.2939+112.6932/".into())]
1379    #[test_case("meta.mp4", CreateDate, DateTime::parse_from_str("2024-02-03T07:05:38Z", "%+").unwrap().into())]
1380    #[test_case("udta.auth.mp4", Author, "ReplayKitRecording".into(); "udta author")]
1381    #[test_case("auth.mov", Author, "ReplayKitRecording".into(); "mov author")]
1382    #[test_case("sony-a7-xavc.MP4", Width, 1920_u32.into())]
1383    #[test_case("sony-a7-xavc.MP4", Height, 1080_u32.into())]
1384    #[test_case("sony-a7-xavc.MP4", DurationMs, 1440_u64.into())]
1385    #[test_case("sony-a7-xavc.MP4", CreateDate, DateTime::parse_from_str("2026-04-26T09:25:15+00:00", "%+").unwrap().into())]
1386    fn parse_track_info(path: &str, tag: TrackInfoTag, v: EntryValue) {
1387        let mut parser = parser();
1388
1389        let mf = MediaSource::seekable(open_sample(path).unwrap()).unwrap();
1390        let info: TrackInfo = parser.parse_track(mf).unwrap();
1391        assert_eq!(info.get(tag).unwrap(), &v);
1392
1393        let mf = MediaSource::unseekable(open_sample(path).unwrap()).unwrap();
1394        let info: TrackInfo = parser.parse_track(mf).unwrap();
1395        assert_eq!(info.get(tag).unwrap(), &v);
1396    }
1397
1398    #[test_case("crash_moov-trak")]
1399    #[test_case("crash_skip_large")]
1400    #[test_case("crash_add_large")]
1401    fn parse_track_crash(path: &str) {
1402        let mut parser = parser();
1403
1404        let mf = MediaSource::seekable(open_sample(path).unwrap()).unwrap();
1405        let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
1406
1407        let mf = MediaSource::unseekable(open_sample(path).unwrap()).unwrap();
1408        let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
1409    }
1410
1411    // Regression: a crafted ISOBMFF file declares an extended 64-bit box size
1412    // just under MAX_PARSE_BUF_SIZE (~1 GB). Pre-fix, the unseekable parser called
1413    // reserve_exact() with that size before reading, allocating ~1 GB even when
1414    // the actual stream contained only a few KB. See commit 81f9e8a.
1415    #[test]
1416    fn parse_oom_large_box() {
1417        let mut parser = parser();
1418
1419        let mf = MediaSource::seekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
1420        let _: Result<ExifIter, _> = parser.parse_exif(mf);
1421
1422        let mf = MediaSource::unseekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
1423        let _: Result<ExifIter, _> = parser.parse_exif(mf);
1424
1425        let mf = MediaSource::seekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
1426        let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
1427
1428        let mf = MediaSource::unseekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
1429        let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
1430    }
1431
1432    #[test]
1433    fn media_kind_classifies_image_and_track() {
1434        let img = MediaSource::open("testdata/exif.jpg").unwrap();
1435        assert_eq!(img.kind(), MediaKind::Image);
1436
1437        let trk = MediaSource::open("testdata/meta.mov").unwrap();
1438        assert_eq!(trk.kind(), MediaKind::Track);
1439    }
1440
1441    #[test]
1442    fn media_source_open() {
1443        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1444        assert_eq!(ms.kind(), MediaKind::Image);
1445    }
1446
1447    #[test]
1448    fn parse_exif_returns_exif_iter() {
1449        let mut parser = parser();
1450        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1451        let _: ExifIter = parser.parse_exif(ms).unwrap();
1452    }
1453
1454    #[test]
1455    fn parse_track_returns_track_info() {
1456        let mut parser = parser();
1457        let ms = MediaSource::open("testdata/meta.mov").unwrap();
1458        let _: TrackInfo = parser.parse_track(ms).unwrap();
1459    }
1460
1461    #[test]
1462    fn parse_exif_on_track_returns_exif_not_found_v3() {
1463        let mut parser = parser();
1464        let ms = MediaSource::open("testdata/meta.mov").unwrap();
1465        let res = parser.parse_exif(ms);
1466        assert!(matches!(res, Err(crate::Error::ExifNotFound)));
1467    }
1468
1469    #[test]
1470    fn parse_track_on_image_returns_track_not_found_v3() {
1471        let mut parser = parser();
1472        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1473        let res = parser.parse_track(ms);
1474        assert!(matches!(res, Err(crate::Error::TrackNotFound)));
1475    }
1476
1477    #[cfg(feature = "tokio-fs")]
1478    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1479    async fn media_parser_parse_exif_async() {
1480        use crate::parser_async::AsyncMediaSource;
1481        let mut parser = MediaParser::new();
1482        let ms = AsyncMediaSource::open("testdata/exif.jpg").await.unwrap();
1483        let _: ExifIter = parser.parse_exif_async(ms).await.unwrap();
1484    }
1485
1486    #[cfg(feature = "tokio-fs")]
1487    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1488    async fn media_parser_parse_track_async() {
1489        use crate::parser_async::AsyncMediaSource;
1490        let mut parser = MediaParser::new();
1491        let ms = AsyncMediaSource::open("testdata/meta.mov").await.unwrap();
1492        let _: TrackInfo = parser.parse_track_async(ms).await.unwrap();
1493    }
1494
1495    #[test]
1496    fn parser_recycles_alloc_when_exif_iter_dropped() {
1497        let mut parser = MediaParser::new();
1498
1499        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1500        let iter = parser.parse_exif(ms).unwrap();
1501        let exif: crate::Exif = iter.into();
1502        drop(exif);
1503        let ptr_after_first = parser.state.cached_ptr_for_test();
1504
1505        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1506        let iter = parser.parse_exif(ms).unwrap();
1507        let _exif: crate::Exif = iter.into();
1508        let ptr_after_second = parser.state.cached_ptr_for_test();
1509
1510        assert!(
1511            ptr_after_first.is_some() && ptr_after_first == ptr_after_second,
1512            "expected recycled allocation, got {:?} -> {:?}",
1513            ptr_after_first,
1514            ptr_after_second
1515        );
1516    }
1517
1518    #[test]
1519    fn parser_new_does_no_upfront_allocation() {
1520        let parser = MediaParser::new();
1521        assert!(parser.state.cached_ptr_for_test().is_none());
1522        assert!(parser.state.buf_is_none_for_test());
1523    }
1524
1525    #[test]
1526    fn buffered_state_memory_mode_sets_and_reads() {
1527        let mut s = BufferedParserState::new();
1528        s.set_memory(bytes::Bytes::from_static(b"abcdefgh"));
1529        assert!(s.is_memory_mode());
1530        assert_eq!(s.buffer(), b"abcdefgh");
1531        s.set_position(3);
1532        assert_eq!(s.buffer(), b"defgh");
1533    }
1534
1535    #[test]
1536    fn buffered_state_share_buf_memory_mode_is_zero_copy() {
1537        let original = bytes::Bytes::from_static(b"the parser owns nothing here");
1538        let original_ptr = original.as_ptr();
1539        let mut s = BufferedParserState::new();
1540        s.set_memory(original);
1541        let (shared, position) = s.share_buf();
1542        assert_eq!(position, 0);
1543        assert_eq!(
1544            shared.as_ptr(),
1545            original_ptr,
1546            "memory share must be a Bytes::clone, not a Vec round-trip"
1547        );
1548        // After share_buf, the parser's memory slot is taken — leaving the state
1549        // ready for the next `reset()` cycle.
1550        assert!(!s.is_memory_mode());
1551    }
1552
1553    #[test]
1554    fn buffered_state_reset_clears_memory() {
1555        let mut s = BufferedParserState::new();
1556        s.set_memory(bytes::Bytes::from_static(b"x"));
1557        s.reset();
1558        assert!(!s.is_memory_mode());
1559        assert_eq!(s.position, 0);
1560    }
1561
1562    #[test]
1563    fn buffered_state_acquire_buf_skips_in_memory_mode() {
1564        let mut s = BufferedParserState::new();
1565        s.set_memory(bytes::Bytes::from_static(b"data"));
1566        s.acquire_buf();
1567        // No streaming buf was allocated.
1568        assert!(s.buf.is_none());
1569        // Memory still readable.
1570        assert_eq!(s.buffer(), b"data");
1571    }
1572
1573    #[test]
1574    fn media_source_from_memory_image_jpg() {
1575        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1576        let ms = MediaSource::from_memory(raw).unwrap();
1577        assert_eq!(ms.kind(), MediaKind::Image);
1578        assert!(ms.memory.is_some());
1579    }
1580
1581    #[test]
1582    fn media_source_from_memory_track_mov() {
1583        let raw = std::fs::read("testdata/meta.mov").unwrap();
1584        let ms = MediaSource::from_memory(raw).unwrap();
1585        assert_eq!(ms.kind(), MediaKind::Track);
1586    }
1587
1588    #[test]
1589    fn media_source_from_memory_static_slice() {
1590        let raw: &'static [u8] = include_bytes!("../testdata/exif.jpg");
1591        let ms = MediaSource::from_memory(raw).unwrap();
1592        assert_eq!(ms.kind(), MediaKind::Image);
1593    }
1594
1595    #[test]
1596    fn media_source_from_memory_rejects_too_short() {
1597        let raw = vec![0u8; 4];
1598        let res = MediaSource::from_memory(raw);
1599        assert!(res.is_err());
1600    }
1601
1602    #[test]
1603    fn media_source_from_memory_rejects_unknown_mime() {
1604        let raw = vec![0xAAu8; 256];
1605        let res = MediaSource::from_memory(raw);
1606        assert!(res.is_err());
1607    }
1608
1609    #[test]
1610    fn parse_exif_unified_from_memory_jpg() {
1611        let mut parser = MediaParser::new();
1612        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1613        let ms = MediaSource::from_memory(raw).unwrap();
1614        let iter = parser.parse_exif(ms).unwrap();
1615        let exif: crate::Exif = iter.into();
1616        assert!(exif.get(crate::ExifTag::Make).is_some());
1617    }
1618
1619    #[test]
1620    fn parse_exif_unified_from_memory_heic() {
1621        let mut parser = MediaParser::new();
1622        let raw = std::fs::read("testdata/exif.heic").unwrap();
1623        let ms = MediaSource::from_memory(raw).unwrap();
1624        let iter = parser.parse_exif(ms).unwrap();
1625        let exif: crate::Exif = iter.into();
1626        assert_eq!(
1627            exif.get(crate::ExifTag::Make).and_then(|v| v.as_str()),
1628            Some("Apple")
1629        );
1630    }
1631
1632    #[test]
1633    fn parse_exif_unified_from_memory_zero_copy_preserved() {
1634        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1635        let bytes = bytes::Bytes::from(raw);
1636        let _original_ptr = bytes.as_ptr();
1637
1638        let mut parser = MediaParser::new();
1639        let ms = MediaSource::from_memory(bytes).unwrap();
1640        let iter = parser.parse_exif(ms).unwrap();
1641
1642        // Memory mode must not poison the recycle cache — same invariant
1643        // the old parse_exif_from_bytes route asserts.
1644        assert!(
1645            parser.state.cached_ptr_for_test().is_none(),
1646            "memory mode must not write to the streaming-buf recycle cache"
1647        );
1648        drop(iter);
1649    }
1650
1651    #[test]
1652    fn parse_exif_unified_on_track_returns_exif_not_found() {
1653        let mut parser = MediaParser::new();
1654        let raw = std::fs::read("testdata/meta.mov").unwrap();
1655        let ms = MediaSource::from_memory(raw).unwrap();
1656        let res = parser.parse_exif(ms);
1657        assert!(matches!(res, Err(crate::Error::ExifNotFound)));
1658    }
1659
1660    #[test]
1661    fn parse_exif_unified_on_truncated_returns_io_error() {
1662        let mut raw = std::fs::read("testdata/exif.jpg").unwrap();
1663        raw.truncate(200);
1664        let mut parser = MediaParser::new();
1665        let ms = MediaSource::from_memory(raw).unwrap();
1666        let res = parser.parse_exif(ms);
1667        assert!(
1668            res.is_err(),
1669            "expected error on truncated bytes, got {:?}",
1670            res
1671        );
1672    }
1673
1674    #[test]
1675    fn parse_exif_streaming_tiny_non_png_keeps_strict_eof() {
1676        // The PNG EOF-tolerance branch is scoped to PNG. A tiny non-PNG file
1677        // (here: the 36-byte compatible-brands.heic fixture) consumes its
1678        // entire reader during mime-detection prefill, so the subsequent
1679        // fill_buf hits UnexpectedEof. parse_exif must surface an error
1680        // (any error) — never Ok — for non-PNG inputs.
1681        let mut parser = MediaParser::new();
1682        let ms = MediaSource::open("testdata/compatible-brands.heic").unwrap();
1683        let res = parser.parse_exif(ms);
1684        assert!(res.is_err(), "expected Err for tiny HEIC, got {:?}", res);
1685    }
1686
1687    #[test]
1688    #[allow(deprecated)]
1689    fn media_source_from_bytes_image_jpg() {
1690        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1691        let ms = MediaSource::from_bytes(raw).unwrap();
1692        assert_eq!(ms.kind(), MediaKind::Image);
1693        assert!(ms.memory.is_some());
1694    }
1695
1696    #[test]
1697    #[allow(deprecated)]
1698    fn media_source_from_bytes_track_mov() {
1699        let raw = std::fs::read("testdata/meta.mov").unwrap();
1700        let ms = MediaSource::from_bytes(raw).unwrap();
1701        assert_eq!(ms.kind(), MediaKind::Track);
1702    }
1703
1704    #[test]
1705    #[allow(deprecated)]
1706    fn media_source_from_bytes_static_slice() {
1707        // &'static [u8] should work via Into<Bytes> because the file is read
1708        // into a Vec at compile-time-friendly size; here we use include_bytes.
1709        let raw: &'static [u8] = include_bytes!("../testdata/exif.jpg");
1710        let ms = MediaSource::from_bytes(raw).unwrap();
1711        assert_eq!(ms.kind(), MediaKind::Image);
1712    }
1713
1714    #[test]
1715    #[allow(deprecated)]
1716    fn media_source_from_bytes_rejects_too_short() {
1717        // Below the smallest mime signature length: should fail mime detection.
1718        let raw = vec![0u8; 4];
1719        let res = MediaSource::from_bytes(raw);
1720        assert!(res.is_err(), "expected mime-detection error");
1721    }
1722
1723    #[test]
1724    #[allow(deprecated)]
1725    fn media_source_from_bytes_rejects_unknown_mime() {
1726        // Random bytes long enough to trigger detection but not match any
1727        // signature.
1728        let raw = vec![0xAAu8; 256];
1729        let res = MediaSource::from_bytes(raw);
1730        assert!(
1731            res.is_err(),
1732            "expected mime-detection error for unknown bytes"
1733        );
1734    }
1735
1736    #[test]
1737    fn p4_5_baseline_exif_jpg_full_dump() {
1738        // Lock down the post-refactor invariant: parsing testdata/exif.jpg through
1739        // the public API must yield the same set of (ifd, tag, value) triples
1740        // before and after P4.5. We capture them as a sorted, formatted string so
1741        // the assertion is a single literal comparison.
1742        let mut parser = MediaParser::new();
1743        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1744        let iter: ExifIter = parser.parse_exif(ms).unwrap();
1745
1746        let mut entries: Vec<String> = iter
1747            .map(|e| {
1748                let tag_name = match e.tag() {
1749                    crate::TagOrCode::Tag(t) => format!("{t}"),
1750                    crate::TagOrCode::Unknown(c) => format!("0x{c:04x}"),
1751                };
1752                let value_str = e
1753                    .value()
1754                    .map(|v| format!("{v}"))
1755                    .unwrap_or_else(|| "<err>".into());
1756                format!("{}.{}={:?}", e.ifd(), tag_name, value_str)
1757            })
1758            .collect();
1759        entries.sort();
1760        let snapshot = entries.join("\n");
1761
1762        // Sanity: should produce non-trivial content. Exact content is checked by
1763        // the existing parse_media tests; this one guards against accidental
1764        // re-ordering / dedup changes during the refactor.
1765        assert!(
1766            entries.len() > 5,
1767            "expected >5 entries, got {}",
1768            entries.len()
1769        );
1770        assert!(snapshot.contains("Make"), "expected Make tag in snapshot");
1771    }
1772
1773    #[test]
1774    #[allow(deprecated)]
1775    fn parse_exif_from_bytes_jpg_basic() {
1776        let mut parser = MediaParser::new();
1777        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1778        let ms = MediaSource::from_bytes(raw).unwrap();
1779        let iter = parser.parse_exif_from_bytes(ms).unwrap();
1780        let exif: crate::Exif = iter.into();
1781        assert!(exif.get(crate::ExifTag::Make).is_some());
1782    }
1783
1784    #[test]
1785    #[allow(deprecated)]
1786    fn parse_exif_from_bytes_heic_basic() {
1787        let mut parser = MediaParser::new();
1788        let raw = std::fs::read("testdata/exif.heic").unwrap();
1789        let ms = MediaSource::from_bytes(raw).unwrap();
1790        let iter = parser.parse_exif_from_bytes(ms).unwrap();
1791        let exif: crate::Exif = iter.into();
1792        assert_eq!(
1793            exif.get(crate::ExifTag::Make).and_then(|v| v.as_str()),
1794            Some("Apple")
1795        );
1796    }
1797
1798    #[test]
1799    #[allow(deprecated)]
1800    fn parse_exif_from_bytes_zero_copy_shared_bytes() {
1801        // Build a Bytes whose pointer we can compare. The ExifIter's underlying
1802        // share must point to the same allocation — proving Bytes::clone path.
1803        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1804        let bytes = bytes::Bytes::from(raw);
1805        let original_ptr = bytes.as_ptr();
1806
1807        let mut parser = MediaParser::new();
1808        let ms = MediaSource::from_bytes(bytes).unwrap();
1809        let iter = parser.parse_exif_from_bytes(ms).unwrap();
1810
1811        // The cached pointer in parser state should be None in memory mode
1812        // (memory mode does not write to cache — the user owns the alloc).
1813        assert!(
1814            parser.state.cached_ptr_for_test().is_none(),
1815            "memory mode must not poison the recycle cache"
1816        );
1817
1818        // Drop the iter and confirm parser is clean for the next call.
1819        drop(iter);
1820
1821        // Build again; pointer identity proves we did not duplicate the alloc
1822        // anywhere along the parse path.
1823        let bytes2 = bytes::Bytes::from(std::fs::read("testdata/exif.jpg").unwrap());
1824        let ms2 = MediaSource::from_bytes(bytes2.clone()).unwrap();
1825        let _iter2 = parser.parse_exif_from_bytes(ms2).unwrap();
1826        // (We cannot assert pointer-equality across distinct user Bytes; the
1827        // assertion above on the first parse is the load-bearing one.)
1828        let _ = original_ptr; // explicit: original_ptr is the assertion target.
1829    }
1830
1831    #[test]
1832    #[allow(deprecated)]
1833    fn parse_exif_from_bytes_on_track_returns_exif_not_found() {
1834        let mut parser = MediaParser::new();
1835        let raw = std::fs::read("testdata/meta.mov").unwrap();
1836        let ms = MediaSource::from_bytes(raw).unwrap();
1837        let res = parser.parse_exif_from_bytes(ms);
1838        assert!(matches!(res, Err(crate::Error::ExifNotFound)));
1839    }
1840
1841    #[test]
1842    #[allow(deprecated)]
1843    fn parse_exif_from_bytes_on_truncated_returns_io_error() {
1844        // Truncate exif.jpg to just enough for mime detection but too short
1845        // for the full EXIF block. Memory-mode fill_buf must surface
1846        // UnexpectedEof when the parser walks off the end.
1847        let mut raw = std::fs::read("testdata/exif.jpg").unwrap();
1848        raw.truncate(200);
1849        let mut parser = MediaParser::new();
1850        let ms = MediaSource::from_bytes(raw).unwrap();
1851        let res = parser.parse_exif_from_bytes(ms);
1852        assert!(
1853            res.is_err(),
1854            "expected error on truncated bytes, got {:?}",
1855            res
1856        );
1857    }
1858
1859    #[test]
1860    #[allow(deprecated)]
1861    fn parse_track_from_bytes_mov_basic() {
1862        let mut parser = MediaParser::new();
1863        let raw = std::fs::read("testdata/meta.mov").unwrap();
1864        let ms = MediaSource::from_bytes(raw).unwrap();
1865        let info = parser.parse_track_from_bytes(ms).unwrap();
1866        assert_eq!(info.get(crate::TrackInfoTag::Make), Some(&"Apple".into()));
1867        assert_eq!(
1868            info.get(crate::TrackInfoTag::Model),
1869            Some(&"iPhone X".into())
1870        );
1871    }
1872
1873    #[test]
1874    #[allow(deprecated)]
1875    fn parse_track_from_bytes_mp4_basic() {
1876        let mut parser = MediaParser::new();
1877        let raw = std::fs::read("testdata/meta.mp4").unwrap();
1878        let ms = MediaSource::from_bytes(raw).unwrap();
1879        let info = parser.parse_track_from_bytes(ms).unwrap();
1880        assert!(info.get(crate::TrackInfoTag::CreateDate).is_some());
1881    }
1882
1883    #[test]
1884    #[allow(deprecated)]
1885    fn parse_track_from_bytes_mkv_basic() {
1886        let mut parser = MediaParser::new();
1887        let raw = std::fs::read("testdata/mkv_640x360.mkv").unwrap();
1888        let ms = MediaSource::from_bytes(raw).unwrap();
1889        let info = parser.parse_track_from_bytes(ms).unwrap();
1890        assert_eq!(
1891            info.get(crate::TrackInfoTag::Width),
1892            Some(&(640_u32.into()))
1893        );
1894    }
1895
1896    #[test]
1897    #[allow(deprecated)]
1898    fn parse_track_from_bytes_on_image_returns_track_not_found() {
1899        let mut parser = MediaParser::new();
1900        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1901        let ms = MediaSource::from_bytes(raw).unwrap();
1902        let res = parser.parse_track_from_bytes(ms);
1903        assert!(matches!(res, Err(crate::Error::TrackNotFound)));
1904    }
1905
1906    #[test]
1907    fn parse_track_unified_from_memory_mov() {
1908        let mut parser = MediaParser::new();
1909        let raw = std::fs::read("testdata/meta.mov").unwrap();
1910        let ms = MediaSource::from_memory(raw).unwrap();
1911        let info = parser.parse_track(ms).unwrap();
1912        assert_eq!(info.get(crate::TrackInfoTag::Make), Some(&"Apple".into()));
1913    }
1914
1915    #[test]
1916    fn parse_track_unified_from_memory_mp4() {
1917        let mut parser = MediaParser::new();
1918        let raw = std::fs::read("testdata/meta.mp4").unwrap();
1919        let ms = MediaSource::from_memory(raw).unwrap();
1920        let info = parser.parse_track(ms).unwrap();
1921        assert!(info.get(crate::TrackInfoTag::CreateDate).is_some());
1922    }
1923
1924    #[test]
1925    fn parse_track_unified_on_image_returns_track_not_found() {
1926        let mut parser = MediaParser::new();
1927        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1928        let ms = MediaSource::from_memory(raw).unwrap();
1929        let res = parser.parse_track(ms);
1930        assert!(matches!(res, Err(crate::Error::TrackNotFound)));
1931    }
1932
1933    #[test]
1934    fn parse_image_metadata_jpeg_returns_exif_only() {
1935        let mut parser = MediaParser::new();
1936        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1937        let img = parser.parse_image_metadata(ms).unwrap();
1938        assert!(img.exif.is_some());
1939        assert!(img.format.is_none());
1940    }
1941
1942    #[test]
1943    fn parse_image_metadata_jpeg_from_memory() {
1944        let mut parser = MediaParser::new();
1945        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1946        let ms = MediaSource::from_memory(raw).unwrap();
1947        let img = parser.parse_image_metadata(ms).unwrap();
1948        assert!(img.exif.is_some());
1949        assert!(img.format.is_none());
1950    }
1951
1952    #[test]
1953    fn parse_image_metadata_on_track_returns_exif_not_found() {
1954        let mut parser = MediaParser::new();
1955        let ms = MediaSource::open("testdata/meta.mov").unwrap();
1956        let res = parser.parse_image_metadata(ms);
1957        assert!(matches!(res, Err(crate::Error::ExifNotFound)));
1958    }
1959
1960    /// Regression for issue #55. PNGs whose IDAT body exceeds
1961    /// `INIT_BUF_SIZE` force the chunk walker through `ClearAndSkip`;
1962    /// on retry the parse buffer no longer starts at byte 0 of the
1963    /// file, and the signature recheck must not fire.
1964    #[test]
1965    fn parse_image_metadata_png_large_idat_streaming() {
1966        use std::io::Cursor;
1967        let png = build_png_with_large_idat(INIT_BUF_SIZE + 1024);
1968        let mut parser = MediaParser::new();
1969        let ms = MediaSource::seekable(Cursor::new(png)).unwrap();
1970        assert_eq!(ms.kind(), crate::MediaKind::Image);
1971        let res = parser.parse_image_metadata(ms);
1972        assert!(
1973            matches!(res, Err(crate::Error::ExifNotFound)),
1974            "expected ExifNotFound on PNG with no EXIF / tEXt; got {res:?}"
1975        );
1976    }
1977
1978    /// Same regression via `parse_exif` — exercises `parse_png_exif_iter`,
1979    /// the sibling code path to `parse_png_full`.
1980    #[test]
1981    fn parse_exif_png_large_idat_streaming() {
1982        use std::io::Cursor;
1983        let png = build_png_with_large_idat(INIT_BUF_SIZE + 1024);
1984        let mut parser = MediaParser::new();
1985        let ms = MediaSource::seekable(Cursor::new(png)).unwrap();
1986        let res = parser.parse_exif(ms);
1987        assert!(
1988            matches!(res, Err(crate::Error::ExifNotFound)),
1989            "expected ExifNotFound; got {res:?}"
1990        );
1991    }
1992
1993    /// Same regression via `MediaSource::unseekable` — exercises the
1994    /// read-and-discard skip path in `clear_and_skip` rather than the
1995    /// `seek_relative` shortcut.
1996    #[test]
1997    fn parse_image_metadata_png_large_idat_unseekable() {
1998        let png = build_png_with_large_idat(INIT_BUF_SIZE + 1024);
1999        let mut parser = MediaParser::new();
2000        let ms = MediaSource::unseekable(NoSeek(std::io::Cursor::new(png))).unwrap();
2001        let res = parser.parse_image_metadata(ms);
2002        assert!(
2003            matches!(res, Err(crate::Error::ExifNotFound)),
2004            "expected ExifNotFound; got {res:?}"
2005        );
2006    }
2007
2008    /// Wraps a reader to hide its `Seek` impl, so `MediaSource::unseekable`
2009    /// is forced even when the underlying type happens to implement it.
2010    struct NoSeek<R>(R);
2011    impl<R: io::Read> io::Read for NoSeek<R> {
2012        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2013            self.0.read(buf)
2014        }
2015    }
2016
2017    fn build_png_with_large_idat(idat_body: usize) -> Vec<u8> {
2018        let mut out = Vec::new();
2019        out.extend_from_slice(b"\x89PNG\r\n\x1a\n");
2020        // IHDR (1x1, 8-bit grayscale)
2021        out.extend_from_slice(&13u32.to_be_bytes());
2022        out.extend_from_slice(b"IHDR");
2023        out.extend_from_slice(&[0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0]);
2024        out.extend_from_slice(&[0, 0, 0, 0]); // CRC (chunk walker ignores it)
2025                                              // IDAT
2026        out.extend_from_slice(&(idat_body as u32).to_be_bytes());
2027        out.extend_from_slice(b"IDAT");
2028        out.resize(out.len() + idat_body, 0);
2029        out.extend_from_slice(&[0, 0, 0, 0]); // CRC
2030                                              // IEND
2031        out.extend_from_slice(&0u32.to_be_bytes());
2032        out.extend_from_slice(b"IEND");
2033        out.extend_from_slice(&[0, 0, 0, 0]); // CRC
2034        out
2035    }
2036
2037    #[cfg(feature = "tokio-fs")]
2038    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2039    async fn parse_image_metadata_async_jpeg() {
2040        use crate::parser_async::AsyncMediaSource;
2041        let mut parser = MediaParser::new();
2042        let ms = AsyncMediaSource::open("testdata/exif.jpg").await.unwrap();
2043        let img = parser.parse_image_metadata_async(ms).await.unwrap();
2044        assert!(img.exif.is_some());
2045        assert!(img.format.is_none());
2046    }
2047
2048    #[cfg(feature = "tokio-fs")]
2049    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2050    async fn async_media_source_from_memory_image_jpg() {
2051        use crate::parser_async::AsyncMediaSource;
2052        let raw = std::fs::read("testdata/exif.jpg").unwrap();
2053        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2054        assert_eq!(ms.kind(), MediaKind::Image);
2055        assert!(ms.memory.is_some());
2056    }
2057
2058    #[cfg(feature = "tokio-fs")]
2059    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2060    async fn async_media_source_from_memory_track_mov() {
2061        use crate::parser_async::AsyncMediaSource;
2062        let raw = std::fs::read("testdata/meta.mov").unwrap();
2063        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2064        assert_eq!(ms.kind(), MediaKind::Track);
2065    }
2066
2067    #[cfg(feature = "tokio-fs")]
2068    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2069    async fn async_media_source_from_memory_rejects_unknown_mime() {
2070        use crate::parser_async::AsyncMediaSource;
2071        let raw = vec![0xAAu8; 256];
2072        let res = AsyncMediaSource::from_memory(raw);
2073        assert!(res.is_err());
2074    }
2075
2076    #[cfg(feature = "tokio-fs")]
2077    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2078    async fn parse_exif_async_from_memory_jpg() {
2079        use crate::parser_async::AsyncMediaSource;
2080        let raw = std::fs::read("testdata/exif.jpg").unwrap();
2081        let mut parser = MediaParser::new();
2082        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2083        let iter = parser.parse_exif_async(ms).await.unwrap();
2084        let exif: crate::Exif = iter.into();
2085        assert!(exif.get(crate::ExifTag::Make).is_some());
2086    }
2087
2088    #[cfg(feature = "tokio-fs")]
2089    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2090    async fn parse_exif_async_from_memory_zero_copy_preserved() {
2091        use crate::parser_async::AsyncMediaSource;
2092        let raw = std::fs::read("testdata/exif.jpg").unwrap();
2093        let bytes = bytes::Bytes::from(raw);
2094        let mut parser = MediaParser::new();
2095        let ms = AsyncMediaSource::from_memory(bytes).unwrap();
2096        let iter = parser.parse_exif_async(ms).await.unwrap();
2097        // Memory mode must not poison the recycle cache — same invariant
2098        // as the sync route asserts.
2099        assert!(
2100            parser.state.cached_ptr_for_test().is_none(),
2101            "async memory mode must not write to the streaming-buf recycle cache"
2102        );
2103        drop(iter);
2104    }
2105
2106    #[cfg(feature = "tokio-fs")]
2107    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2108    async fn parse_track_async_from_memory_mov() {
2109        use crate::parser_async::AsyncMediaSource;
2110        let raw = std::fs::read("testdata/meta.mov").unwrap();
2111        let mut parser = MediaParser::new();
2112        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2113        let info = parser.parse_track_async(ms).await.unwrap();
2114        assert_eq!(info.get(crate::TrackInfoTag::Make), Some(&"Apple".into()));
2115    }
2116
2117    #[cfg(feature = "tokio-fs")]
2118    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2119    async fn parse_image_metadata_async_from_memory_png() {
2120        use crate::parser_async::AsyncMediaSource;
2121        let raw = std::fs::read("testdata/exif.png").unwrap();
2122        let mut parser = MediaParser::new();
2123        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2124        let img = parser.parse_image_metadata_async(ms).await.unwrap();
2125        assert!(img.exif.is_some());
2126        assert!(img.format.is_some());
2127    }
2128
2129    /// Async counterpart of `parse_image_metadata_png_large_idat_streaming`
2130    /// (regression for issue #55). `parser_async::clear_and_skip` is a
2131    /// separate implementation; the state-threading through the shared
2132    /// `parse_loop_step` must work identically in the async path.
2133    #[cfg(feature = "tokio-fs")]
2134    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2135    async fn parse_image_metadata_async_png_large_idat_streaming() {
2136        use crate::parser_async::AsyncMediaSource;
2137        let png = build_png_with_large_idat(INIT_BUF_SIZE + 1024);
2138        // tokio's async I/O traits aren't on std::io::Cursor, so route
2139        // through a real file. Pick a unique path to avoid concurrent
2140        // test collisions.
2141        let path =
2142            std::env::temp_dir().join(format!("nom-exif-issue55-{}.png", std::process::id()));
2143        tokio::fs::write(&path, &png).await.unwrap();
2144        let mut parser = MediaParser::new();
2145        let ms = AsyncMediaSource::open(&path).await.unwrap();
2146        let res = parser.parse_image_metadata_async(ms).await;
2147        let _ = tokio::fs::remove_file(&path).await;
2148        assert!(
2149            matches!(res, Err(crate::Error::ExifNotFound)),
2150            "expected ExifNotFound; got {res:?}"
2151        );
2152    }
2153
2154    #[cfg(feature = "tokio")]
2155    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2156    async fn parse_image_metadata_async_from_memory_text_only_png() {
2157        // Memory route: bypasses fill_buf entirely, just verifies the
2158        // memory-mode path returns format-only metadata for a PNG with
2159        // no EXIF.
2160        use crate::parser_async::AsyncMediaSource;
2161        let raw = std::fs::read("testdata/text-only.png").unwrap();
2162        let mut parser = MediaParser::new();
2163        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2164        let img = parser.parse_image_metadata_async(ms).await.unwrap();
2165        assert!(img.exif.is_none());
2166        assert!(img.format.is_some());
2167    }
2168
2169    // Streaming-path coverage for the PNG-scoped EOF tolerance. The
2170    // 117-byte text-only.png is fully consumed during mime detection
2171    // (HEADER_PARSE_BUF_SIZE = 128), so the parse-time fill_buf hits
2172    // UnexpectedEof. The PNG-scoped tolerance must let the bytes already
2173    // in the parse buffer drive the parse to completion. These tests
2174    // would have caught the missed-async-tolerance bug the previous
2175    // memory-mode tests did not.
2176
2177    #[test]
2178    fn parse_exif_streaming_text_only_png_returns_exif_not_found() {
2179        // text-only.png has no EXIF — the contract is ExifNotFound, not
2180        // UnexpectedEof. Pre-EOF-tolerance, this would surface
2181        // UnexpectedEof because mime detection consumed all 117 bytes.
2182        let mut parser = MediaParser::new();
2183        let ms = MediaSource::open("testdata/text-only.png").unwrap();
2184        let res = parser.parse_exif(ms);
2185        assert!(
2186            matches!(res, Err(crate::Error::ExifNotFound)),
2187            "expected ExifNotFound for tEXt-only PNG, got {:?}",
2188            res
2189        );
2190    }
2191
2192    #[test]
2193    fn parse_image_metadata_streaming_text_only_png() {
2194        let mut parser = MediaParser::new();
2195        let ms = MediaSource::open("testdata/text-only.png").unwrap();
2196        let img = parser.parse_image_metadata(ms).unwrap();
2197        assert!(img.exif.is_none());
2198        assert!(img.format.is_some());
2199    }
2200
2201    #[cfg(feature = "tokio")]
2202    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2203    async fn parse_exif_async_streaming_text_only_png_returns_exif_not_found() {
2204        use crate::parser_async::AsyncMediaSource;
2205        let mut parser = MediaParser::new();
2206        let f = tokio::fs::File::open("testdata/text-only.png")
2207            .await
2208            .unwrap();
2209        let ms = AsyncMediaSource::seekable(f).await.unwrap();
2210        let res = parser.parse_exif_async(ms).await;
2211        assert!(
2212            matches!(res, Err(crate::Error::ExifNotFound)),
2213            "expected ExifNotFound for tEXt-only PNG via async streaming, got {:?}",
2214            res
2215        );
2216    }
2217
2218    #[cfg(feature = "tokio")]
2219    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2220    async fn parse_image_metadata_async_streaming_text_only_png() {
2221        use crate::parser_async::AsyncMediaSource;
2222        let mut parser = MediaParser::new();
2223        let f = tokio::fs::File::open("testdata/text-only.png")
2224            .await
2225            .unwrap();
2226        let ms = AsyncMediaSource::seekable(f).await.unwrap();
2227        let img = parser.parse_image_metadata_async(ms).await.unwrap();
2228        assert!(img.exif.is_none());
2229        assert!(img.format.is_some());
2230    }
2231
2232    // --- WebP synthetic-buffer helpers (mirror src/webp.rs test builders) ---
2233    fn webp_minimal_tiff_le() -> Vec<u8> {
2234        // II + 0x002A + IFD0 offset 8 + IFD0 with one tag (Make="A") + next=0.
2235        let mut t = Vec::new();
2236        t.extend_from_slice(b"II");
2237        t.extend_from_slice(&[0x2a, 0x00]);
2238        t.extend_from_slice(&[0x08, 0, 0, 0]); // IFD0 at offset 8
2239        t.extend_from_slice(&[0x01, 0x00]); // 1 entry
2240                                            // Tag 0x010F (Make), type 2 (ASCII), count 2, inline value "A\0".
2241        t.extend_from_slice(&[0x0f, 0x01]); // tag 0x010F
2242        t.extend_from_slice(&[0x02, 0x00]); // type ASCII
2243        t.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // count 2
2244        t.extend_from_slice(b"A\0\0\0"); // value "A\0" padded to 4 bytes
2245        t.extend_from_slice(&[0, 0, 0, 0]); // next IFD = 0
2246        t
2247    }
2248
2249    fn webp_with_exif(tiff: &[u8]) -> Vec<u8> {
2250        let mut exif_chunk = Vec::new();
2251        exif_chunk.extend_from_slice(b"EXIF");
2252        exif_chunk.extend_from_slice(&(tiff.len() as u32).to_le_bytes());
2253        exif_chunk.extend_from_slice(tiff);
2254        if tiff.len() % 2 == 1 {
2255            exif_chunk.push(0);
2256        }
2257        let mut vp8x = Vec::new();
2258        vp8x.extend_from_slice(b"VP8X");
2259        vp8x.extend_from_slice(&10u32.to_le_bytes());
2260        vp8x.extend_from_slice(&[0u8; 10]);
2261
2262        let mut body = Vec::new();
2263        body.extend_from_slice(b"WEBP");
2264        body.extend_from_slice(&vp8x);
2265        body.extend_from_slice(&exif_chunk);
2266
2267        let mut out = Vec::new();
2268        out.extend_from_slice(b"RIFF");
2269        out.extend_from_slice(&(body.len() as u32).to_le_bytes());
2270        out.extend_from_slice(&body);
2271        out
2272    }
2273
2274    #[test]
2275    fn parse_exif_webp_from_memory() {
2276        let tiff = webp_minimal_tiff_le();
2277        let buf = webp_with_exif(&tiff);
2278        let mut parser = MediaParser::new();
2279        let ms = MediaSource::from_memory(buf).unwrap();
2280        let iter = parser.parse_exif(ms).unwrap();
2281        let exif: crate::Exif = iter.into();
2282        assert_eq!(
2283            exif.get(crate::ExifTag::Make).and_then(|v| v.as_str()),
2284            Some("A")
2285        );
2286    }
2287
2288    #[test]
2289    fn parse_exif_webp_no_exif_returns_not_found() {
2290        // RIFF/WEBP with only a VP8X chunk (no EXIF).
2291        let mut vp8x = Vec::new();
2292        vp8x.extend_from_slice(b"VP8X");
2293        vp8x.extend_from_slice(&10u32.to_le_bytes());
2294        vp8x.extend_from_slice(&[0u8; 10]);
2295        let mut body = Vec::new();
2296        body.extend_from_slice(b"WEBP");
2297        body.extend_from_slice(&vp8x);
2298        let mut buf = Vec::new();
2299        buf.extend_from_slice(b"RIFF");
2300        buf.extend_from_slice(&(body.len() as u32).to_le_bytes());
2301        buf.extend_from_slice(&body);
2302
2303        let mut parser = MediaParser::new();
2304        let ms = MediaSource::from_memory(buf).unwrap();
2305        let res = parser.parse_exif(ms);
2306        assert!(matches!(res, Err(crate::Error::ExifNotFound)));
2307    }
2308
2309    #[test]
2310    fn parse_exif_webp_streaming_small_file() {
2311        // Exercises the Png|Webp small-file EOF tolerance on the streaming
2312        // path: a sub-INIT_BUF_SIZE WebP fully consumed during MIME prefill.
2313        use std::io::Cursor;
2314        let tiff = webp_minimal_tiff_le();
2315        let buf = webp_with_exif(&tiff);
2316        let mut parser = MediaParser::new();
2317        let ms = MediaSource::seekable(Cursor::new(buf)).unwrap();
2318        let iter = parser.parse_exif(ms).unwrap();
2319        let exif: crate::Exif = iter.into();
2320        assert_eq!(
2321            exif.get(crate::ExifTag::Make).and_then(|v| v.as_str()),
2322            Some("A")
2323        );
2324    }
2325
2326    #[cfg(feature = "tokio")]
2327    #[tokio::test]
2328    async fn parse_exif_webp_async_from_memory() {
2329        use crate::AsyncMediaSource;
2330        let tiff = webp_minimal_tiff_le();
2331        let buf = webp_with_exif(&tiff);
2332        let mut parser = MediaParser::new();
2333        let ms = AsyncMediaSource::from_memory(buf).unwrap();
2334        let iter = parser.parse_exif_async(ms).await.unwrap();
2335        let exif: crate::Exif = iter.into();
2336        assert_eq!(
2337            exif.get(crate::ExifTag::Make).and_then(|v| v.as_str()),
2338            Some("A")
2339        );
2340    }
2341}