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 state carried across streaming skips.
507    Png(crate::png::PngState),
508    /// WebP RIFF walker has validated the 12-byte header and skipped some
509    /// chunks. Carries the number of chunk-stream bytes still remaining
510    /// (from the resumed buffer's start) so the walker can stop cleanly at
511    /// the end of the RIFF chunk region — WebP has no terminator chunk.
512    WebpPastHeader(usize),
513}
514
515impl Display for ParsingState {
516    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517        match self {
518            ParsingState::TiffHeader(h) => Display::fmt(&format!("ParsingState: {h:?})"), f),
519            ParsingState::HeifExifSize(n) => Display::fmt(&format!("ParsingState: {n}"), f),
520            ParsingState::Cr3ExifSize(n) => Display::fmt(&format!("ParsingState: {n}"), f),
521            ParsingState::Png(_) => f.write_str("ParsingState: Png"),
522            ParsingState::WebpPastHeader(n) => {
523                Display::fmt(&format!("ParsingState: WebpPastHeader({n})"), f)
524            }
525        }
526    }
527}
528
529// Modern replacement for the `Load` trait in loader.rs. Adds offset-aware
530// parsing and `ParsingState` threading for format-specific state machines.
531pub(crate) trait BufParser: Buf + Debug {
532    fn fill_buf<R: Read>(&mut self, reader: &mut R, size: usize) -> io::Result<usize>;
533
534    fn load_and_parse<R: Read, P, O>(
535        &mut self,
536        reader: &mut R,
537        skip_by_seek: SkipBySeekFn<R>,
538        mut parse: P,
539    ) -> Result<O, ParsedError>
540    where
541        P: FnMut(&[u8], Option<ParsingState>) -> Result<O, ParsingErrorState>,
542    {
543        self.load_and_parse_with_offset(
544            reader,
545            skip_by_seek,
546            |data, _, state| parse(data, state),
547            0,
548        )
549    }
550
551    #[tracing::instrument(skip_all)]
552    fn load_and_parse_with_offset<R: Read, P, O>(
553        &mut self,
554        reader: &mut R,
555        skip_by_seek: SkipBySeekFn<R>,
556        mut parse: P,
557        offset: usize,
558    ) -> Result<O, ParsedError>
559    where
560        P: FnMut(&[u8], usize, Option<ParsingState>) -> Result<O, ParsingErrorState>,
561    {
562        if offset >= self.buffer().len() {
563            self.fill_buf(reader, MIN_GROW_SIZE)?;
564        }
565        let mut parsing_state: Option<ParsingState> = None;
566        loop {
567            match parse_loop_step(self.buffer(), offset, &mut parsing_state, &mut parse) {
568                LoopAction::Done(o) => return Ok(o),
569                LoopAction::NeedFill(needed) => {
570                    let to_read = max(needed, MIN_GROW_SIZE);
571                    let n = self.fill_buf(reader, to_read)?;
572                    if n == 0 {
573                        return Err(ParsedError::NoEnoughBytes);
574                    }
575                }
576                LoopAction::Skip(n) => {
577                    self.clear_and_skip(reader, skip_by_seek, n)?;
578                }
579                LoopAction::Failed { kind, message } => {
580                    return Err(ParsedError::Failed { kind, message })
581                }
582            }
583        }
584    }
585
586    #[tracing::instrument(skip(reader, skip_by_seek))]
587    fn clear_and_skip<R: Read>(
588        &mut self,
589        reader: &mut R,
590        skip_by_seek: SkipBySeekFn<R>,
591        n: usize,
592    ) -> Result<(), ParsedError> {
593        match clear_and_skip_decide(self.buffer().len(), n) {
594            SkipPlan::AdvanceOnly => {
595                self.set_position(self.position() + n);
596                Ok(())
597            }
598            SkipPlan::ClearAndSkip { extra: skip_n } => {
599                self.clear();
600                let done = (skip_by_seek)(
601                    reader,
602                    skip_n.try_into().map_err(|_| ParsedError::Failed {
603                        // No format context available here: the parser
604                        // hit an internal limit honoring a caller's skip.
605                        // Pick a sensible default — see #55 follow-up.
606                        kind: crate::error::MalformedKind::IsoBmffBox,
607                        message: "skip too many bytes".into(),
608                    })?,
609                )?;
610                if !done {
611                    let mut skipped = 0;
612                    while skipped < skip_n {
613                        let mut to_skip = skip_n - skipped;
614                        to_skip = min(to_skip, MAX_PARSE_BUF_SIZE);
615                        let n = self.fill_buf(reader, to_skip)?;
616                        skipped += n;
617                        if skipped <= skip_n {
618                            self.clear();
619                        } else {
620                            let remain = skipped - skip_n;
621                            self.set_position(self.buffer().len() - remain);
622                            break;
623                        }
624                    }
625                }
626
627                if self.buffer().is_empty() {
628                    self.fill_buf(reader, MIN_GROW_SIZE)?;
629                }
630                Ok(())
631            }
632        }
633    }
634}
635
636impl BufParser for MediaParser {
637    #[tracing::instrument(skip(self, reader), fields(buf_len=self.state.buffer().len()))]
638    fn fill_buf<R: Read>(&mut self, reader: &mut R, size: usize) -> io::Result<usize> {
639        if self.state.is_memory_mode() {
640            // Memory mode owns every byte it will ever have. A request for
641            // more is "the parser walked off the end of the input"; surface
642            // it the same way the streaming path surfaces a 0-byte read.
643            return Err(std::io::ErrorKind::UnexpectedEof.into());
644        }
645        check_fill_size(self.state.buf().len(), size)?;
646
647        // Do not pre-allocate `size` bytes: a crafted box header can declare a
648        // huge extended size (up to MAX_PARSE_BUF_SIZE) that far exceeds the actual
649        // stream length. reserve_exact would allocate that memory immediately
650        // even when the reader has only a few bytes left. read_to_end grows the
651        // buffer from the reader's actual size hint instead.
652        let n = reader.take(size as u64).read_to_end(self.state.buf_mut())?;
653        if n == 0 {
654            tracing::error!(buf_len = self.state.buf().len(), "fill_buf: EOF");
655            return Err(std::io::ErrorKind::UnexpectedEof.into());
656        }
657
658        tracing::debug!(
659            ?size,
660            ?n,
661            buf_len = self.state.buf().len(),
662            "fill_buf: read bytes"
663        );
664
665        Ok(n)
666    }
667}
668
669impl Buf for MediaParser {
670    fn buffer(&self) -> &[u8] {
671        self.state.buffer()
672    }
673
674    fn clear(&mut self) {
675        self.state.clear();
676    }
677
678    fn set_position(&mut self, pos: usize) {
679        self.state.set_position(pos);
680    }
681
682    fn position(&self) -> usize {
683        self.state.position()
684    }
685}
686
687/// A `MediaParser` can parse media info from a [`MediaSource`].
688///
689/// `MediaParser` manages inner parse buffers that can be shared between
690/// multiple parsing tasks, thus avoiding frequent memory allocations.
691///
692/// Therefore:
693///
694/// - Try to reuse a `MediaParser` instead of creating a new one every time
695///   you need it.
696///
697/// - `MediaSource` should be created directly from `Read`, not from `BufRead`.
698///
699/// ## Example
700///
701/// ```rust
702/// use nom_exif::*;
703/// use chrono::DateTime;
704///
705/// let mut parser = MediaParser::new();
706///
707/// // ------------------- Parse Exif Info
708/// let ms = MediaSource::open("./testdata/exif.heic").unwrap();
709/// assert_eq!(ms.kind(), MediaKind::Image);
710/// let mut iter = parser.parse_exif(ms).unwrap();
711///
712/// let entry = iter.next().unwrap();
713/// assert!(matches!(entry.tag(), nom_exif::TagOrCode::Tag(ExifTag::Make)));
714/// assert_eq!(entry.value().unwrap().as_str().unwrap(), "Apple");
715///
716/// // Convert `ExifIter` into an `Exif`. Clone it before converting, so that
717/// // we can start the iteration from the beginning.
718/// let exif: Exif = iter.clone().into();
719/// assert_eq!(exif.get(ExifTag::Make).unwrap().as_str().unwrap(), "Apple");
720///
721/// // ------------------- Parse Track Info
722/// let ms = MediaSource::open("./testdata/meta.mov").unwrap();
723/// assert_eq!(ms.kind(), MediaKind::Track);
724/// let info = parser.parse_track(ms).unwrap();
725///
726/// assert_eq!(info.get(TrackInfoTag::Make), Some(&"Apple".into()));
727/// assert_eq!(info.get(TrackInfoTag::Model), Some(&"iPhone X".into()));
728/// assert_eq!(info.get(TrackInfoTag::GpsIso6709), Some(&"+27.1281+100.2508+000.000/".into()));
729/// assert_eq!(info.gps_info().unwrap().latitude_ref, LatRef::North);
730/// assert_eq!(
731///     info.gps_info().unwrap().latitude,
732///     LatLng::new(URational::new(27, 1), URational::new(7, 1), URational::new(4116, 100)),
733/// );
734/// ```
735pub struct MediaParser {
736    state: BufferedParserState,
737}
738
739impl Debug for MediaParser {
740    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
741        f.debug_struct("MediaParser")
742            .field("state", &self.state)
743            .finish_non_exhaustive()
744    }
745}
746
747impl Default for MediaParser {
748    fn default() -> Self {
749        Self {
750            state: BufferedParserState::new(),
751        }
752    }
753}
754
755pub(crate) trait ShareBuf {
756    /// Take ownership of the parser's active buffer and return the full
757    /// allocation as `Bytes` plus the parser's `position` at share-time.
758    /// Caller is responsible for slicing: a parse-loop range `r` corresponds
759    /// to absolute range `(r.start + position)..(r.end + position)`.
760    fn share_buf(&mut self) -> (bytes::Bytes, usize);
761}
762
763impl ShareBuf for MediaParser {
764    fn share_buf(&mut self) -> (bytes::Bytes, usize) {
765        self.state.share_buf()
766    }
767}
768
769impl MediaParser {
770    pub fn new() -> Self {
771        Self::default()
772    }
773
774    /// Parse Exif metadata from an image source. Returns `Error::ExifNotFound`
775    /// if the source is a `Track` (use [`Self::parse_track`] instead).
776    ///
777    /// As of v3.3, this method also accepts memory-mode sources built via
778    /// [`MediaSource::from_memory`]. The deprecated [`Self::parse_exif_from_bytes`]
779    /// is now a thin adapter that delegates here.
780    ///
781    /// `MediaParser` reuses its internal parse buffer across calls, so prefer
782    /// reusing a single `MediaParser` over creating a new one per file. Drop
783    /// the returned [`ExifIter`] (or convert it into [`crate::Exif`]) before
784    /// the next `parse_*` call so the buffer can be reclaimed.
785    pub fn parse_exif<R: Read>(&mut self, mut ms: MediaSource<R>) -> crate::Result<ExifIter> {
786        self.reset();
787        let res: crate::Result<ExifIter> = (|| {
788            if let Some(memory) = ms.memory.take() {
789                // Memory-mode: zero-copy share of caller-owned bytes.
790                self.state.set_memory(memory);
791                if !matches!(ms.mime, crate::file::MediaMime::Image(_)) {
792                    return Err(crate::Error::ExifNotFound);
793                }
794                crate::exif::parse_exif_iter(
795                    self,
796                    ms.mime.unwrap_image(),
797                    &mut ms.reader,
798                    ms.skip_by_seek,
799                )
800            } else {
801                // Streaming-mode: existing path verbatim.
802                self.acquire_buf();
803                self.buf_mut().append(&mut ms.buf);
804                // PNG-only EOF tolerance: a tEXt-only PNG can be smaller
805                // than HEADER_PARSE_BUF_SIZE (e.g. 117-byte text-only.png),
806                // so the mime-detection prefill consumes the whole reader
807                // and fill_buf returns UnexpectedEof. The bytes we need
808                // are already in the parse buffer — proceed. Other formats
809                // keep the strict-EOF contract.
810                let is_png = matches!(
811                    ms.mime,
812                    crate::file::MediaMime::Image(
813                        crate::file::MediaMimeImage::Png | crate::file::MediaMimeImage::Webp
814                    )
815                );
816                match self.fill_buf(&mut ms.reader, INIT_BUF_SIZE) {
817                    Ok(_) => {}
818                    Err(e)
819                        if is_png
820                            && !self.buffer().is_empty()
821                            && e.kind() == io::ErrorKind::UnexpectedEof => {}
822                    Err(e) => return Err(e.into()),
823                }
824                if !matches!(ms.mime, crate::file::MediaMime::Image(_)) {
825                    return Err(crate::Error::ExifNotFound);
826                }
827                crate::exif::parse_exif_iter(
828                    self,
829                    ms.mime.unwrap_image(),
830                    &mut ms.reader,
831                    ms.skip_by_seek,
832                )
833            }
834        })();
835        self.reset();
836        res
837    }
838
839    /// Parse track info from a video/audio source.
840    ///
841    /// Parse track info from a video/audio source.
842    ///
843    /// In v3.1, this also accepts JPEG images that carry an embedded
844    /// Pixel/Google Motion Photo trailer. As of v3.3, it also accepts
845    /// memory-mode sources built via [`MediaSource::from_memory`]; the
846    /// deprecated [`Self::parse_track_from_bytes`] is now a thin
847    /// adapter that delegates here.
848    pub fn parse_track<R: Read>(&mut self, mut ms: MediaSource<R>) -> crate::Result<TrackInfo> {
849        self.reset();
850        let res: crate::Result<TrackInfo> = (|| {
851            if let Some(memory) = ms.memory.take() {
852                // Memory mode: zero-copy.
853                self.state.set_memory(memory);
854                let mime_track = match ms.mime {
855                    crate::file::MediaMime::Image(_) => return Err(crate::Error::TrackNotFound),
856                    crate::file::MediaMime::Track(t) => t,
857                };
858                let out = self.load_and_parse(&mut ms.reader, ms.skip_by_seek, |data, _| {
859                    crate::video::parse_track_info(data, mime_track)
860                        .map_err(|e| ParsingErrorState::new(e, None))
861                })?;
862                Ok(out)
863            } else {
864                // Streaming mode: existing path verbatim.
865                self.acquire_buf();
866                self.buf_mut().append(&mut ms.buf);
867                self.fill_buf(&mut ms.reader, INIT_BUF_SIZE)?;
868                match ms.mime {
869                    crate::file::MediaMime::Image(crate::file::MediaMimeImage::Jpeg) => {
870                        self.parse_jpeg_motion_photo(&mut ms.reader)
871                    }
872                    crate::file::MediaMime::Image(_) => Err(crate::Error::TrackNotFound),
873                    crate::file::MediaMime::Track(mime_track) => {
874                        let skip = ms.skip_by_seek;
875                        Ok(self.load_and_parse(ms.reader.by_ref(), skip, |data, _| {
876                            crate::video::parse_track_info(data, mime_track)
877                                .map_err(|e| ParsingErrorState::new(e, None))
878                        })?)
879                    }
880                }
881            }
882        })();
883        self.reset();
884        res
885    }
886
887    /// Read a JPEG to EOF, locate a Pixel-style Motion Photo MP4 trailer,
888    /// and parse it as track metadata. Returns
889    /// [`crate::Error::TrackNotFound`] if no Motion Photo signal is
890    /// present in the JPEG's XMP.
891    fn parse_jpeg_motion_photo<R: Read>(&mut self, reader: &mut R) -> crate::Result<TrackInfo> {
892        // Drain the rest of the JPEG into the parse buffer so we can
893        // address the trailing MP4 by its byte offset from EOF.
894        reader.read_to_end(self.buf_mut())?;
895        let buf = self.buf_mut();
896        let Some(offset) = crate::jpeg::find_motion_photo_offset(buf) else {
897            return Err(crate::Error::TrackNotFound);
898        };
899        let trailer_start = (buf.len() as u64)
900            .checked_sub(offset)
901            .ok_or(crate::Error::TrackNotFound)? as usize;
902        let trailer = &buf[trailer_start..];
903
904        // The trailer can be MP4 / MOV / 3gp depending on the source device;
905        // dispatch by sniffing it as a fresh ISO BMFF input.
906        let trailer_mime =
907            crate::file::MediaMime::try_from(trailer).map_err(|_| crate::Error::TrackNotFound)?;
908        let mime_track = match trailer_mime {
909            crate::file::MediaMime::Track(t) => t,
910            crate::file::MediaMime::Image(_) => return Err(crate::Error::TrackNotFound),
911        };
912        crate::video::parse_track_info(trailer, mime_track).map_err(|e| match e {
913            crate::error::ParsingError::Need(_) | crate::error::ParsingError::ClearAndSkip(_) => {
914                crate::Error::UnexpectedEof {
915                    context: "motion-photo trailer",
916                }
917            }
918            crate::error::ParsingError::Failed { kind, message } => {
919                crate::Error::Malformed { kind, message }
920            }
921        })
922    }
923
924    /// Parse Exif metadata from an in-memory byte payload built via
925    /// the deprecated [`MediaSource::<()>::from_bytes`].
926    ///
927    /// **Deprecated since v3.3.0**: use [`Self::parse_exif`] with
928    /// [`MediaSource::from_memory`] directly.
929    #[deprecated(
930        since = "3.3.0",
931        note = "Use `parse_exif` directly — it now accepts memory-mode \
932                sources built via `MediaSource::from_memory`."
933    )]
934    pub fn parse_exif_from_bytes(&mut self, ms: MediaSource<()>) -> crate::Result<ExifIter> {
935        self.parse_exif(ms.into_empty())
936    }
937
938    /// **Deprecated since v3.3.0**: use [`Self::parse_track`] with
939    /// [`MediaSource::from_memory`] directly.
940    #[deprecated(
941        since = "3.3.0",
942        note = "Use `parse_track` with `MediaSource::from_memory`."
943    )]
944    pub fn parse_track_from_bytes(&mut self, ms: MediaSource<()>) -> crate::Result<TrackInfo> {
945        self.parse_track(ms.into_empty())
946    }
947
948    /// Parse all metadata from an image source: EXIF (if any) and
949    /// format-specific extras (PNG `tEXt` chunks, etc.).
950    ///
951    /// Returns `Err(Error::ExifNotFound)` if neither EXIF nor any
952    /// format-specific metadata is found. Returns
953    /// `Err(Error::TrackNotFound)`-style errors on track inputs (use
954    /// `parse_track` instead).
955    ///
956    /// **Lazy form** — this method returns `ImageMetadata<ExifIter>`.
957    /// Convert to the eager `ImageMetadata<Exif>` via `.into()` if
958    /// desired.
959    pub fn parse_image_metadata<R: Read>(
960        &mut self,
961        mut ms: MediaSource<R>,
962    ) -> crate::Result<crate::image_metadata::ImageMetadata<crate::ExifIter>> {
963        self.reset();
964        let res: crate::Result<crate::image_metadata::ImageMetadata<crate::ExifIter>> = (|| {
965            // Reject track inputs early (parse_track is the right API).
966            let mime_img = match ms.mime {
967                crate::file::MediaMime::Image(img) => img,
968                crate::file::MediaMime::Track(_) => return Err(crate::Error::ExifNotFound),
969            };
970
971            // Memory-mode shortcut + buffer setup mirrors parse_exif.
972            if let Some(memory) = ms.memory.take() {
973                self.state.set_memory(memory);
974            } else {
975                self.acquire_buf();
976                self.buf_mut().append(&mut ms.buf);
977                // PNG-only EOF tolerance: a tEXt-only PNG can be smaller than
978                // HEADER_PARSE_BUF_SIZE (e.g. 117-byte text-only.png) so the
979                // mime-detection prefill consumes the whole reader and
980                // fill_buf returns UnexpectedEof. The bytes we need are
981                // already in the parse buffer — proceed. Other formats keep
982                // the strict-EOF contract.
983                let is_png = matches!(
984                    mime_img,
985                    crate::file::MediaMimeImage::Png | crate::file::MediaMimeImage::Webp
986                );
987                match self.fill_buf(&mut ms.reader, INIT_BUF_SIZE) {
988                    Ok(_) => {}
989                    Err(e)
990                        if is_png
991                            && !self.buffer().is_empty()
992                            && e.kind() == io::ErrorKind::UnexpectedEof => {}
993                    Err(e) => return Err(e.into()),
994                }
995            }
996
997            if mime_img == crate::file::MediaMimeImage::Png {
998                let (exif, text_chunks) =
999                    crate::exif::parse_png_full(self, &mut ms.reader, ms.skip_by_seek)?;
1000                let format = if text_chunks.is_empty() {
1001                    None
1002                } else {
1003                    Some(crate::ImageFormatMetadata::Png(crate::PngTextChunks {
1004                        entries: text_chunks,
1005                    }))
1006                };
1007                if exif.is_none() && format.is_none() {
1008                    return Err(crate::Error::ExifNotFound);
1009                }
1010                Ok(crate::ImageMetadata { exif, format })
1011            } else {
1012                // Non-PNG: existing parse_exif_iter path; format always None.
1013                let iter =
1014                    crate::exif::parse_exif_iter(self, mime_img, &mut ms.reader, ms.skip_by_seek)?;
1015                Ok(crate::ImageMetadata {
1016                    exif: Some(iter),
1017                    format: None,
1018                })
1019            }
1020        })(
1021        );
1022        self.reset();
1023        res
1024    }
1025
1026    fn reset(&mut self) {
1027        self.state.reset();
1028    }
1029
1030    fn buf_mut(&mut self) -> &mut Vec<u8> {
1031        self.state.buf_mut()
1032    }
1033
1034    fn acquire_buf(&mut self) {
1035        self.state.acquire_buf();
1036    }
1037}
1038
1039#[cfg(feature = "tokio")]
1040mod tokio_impl {
1041    use super::*;
1042    use crate::error::ParsingErrorState;
1043    use crate::parser_async::{AsyncBufParser, AsyncMediaSource};
1044    use tokio::io::{AsyncRead, AsyncReadExt};
1045
1046    impl AsyncBufParser for MediaParser {
1047        async fn fill_buf<R: AsyncRead + Unpin>(
1048            &mut self,
1049            reader: &mut R,
1050            size: usize,
1051        ) -> std::io::Result<usize> {
1052            if self.state.is_memory_mode() {
1053                // Memory mode owns every byte it will ever have. Surface
1054                // "walked off end of input" the same way the streaming path
1055                // surfaces a 0-byte read.
1056                return Err(std::io::ErrorKind::UnexpectedEof.into());
1057            }
1058            check_fill_size(self.state.buf().len(), size)?;
1059            // Same rationale as the sync version: do not pre-allocate `size` bytes.
1060            let n = reader
1061                .take(size as u64)
1062                .read_to_end(self.state.buf_mut())
1063                .await?;
1064            if n == 0 {
1065                return Err(std::io::ErrorKind::UnexpectedEof.into());
1066            }
1067            Ok(n)
1068        }
1069    }
1070
1071    impl MediaParser {
1072        /// Parse Exif metadata from an async image source. Returns
1073        /// `Error::ExifNotFound` if the source is a `Track`.
1074        ///
1075        /// As of v3.3, also accepts memory-mode sources built via
1076        /// [`AsyncMediaSource::from_memory`]; the memory branch shares
1077        /// caller-owned `Bytes` zero-copy through `state.set_memory`.
1078        pub async fn parse_exif_async<R: AsyncRead + Unpin + Send>(
1079            &mut self,
1080            mut ms: AsyncMediaSource<R>,
1081        ) -> crate::Result<ExifIter> {
1082            self.reset();
1083            let res: crate::Result<ExifIter> = async {
1084                if let Some(memory) = ms.memory.take() {
1085                    // Memory-mode: zero-copy share of caller-owned bytes.
1086                    self.state.set_memory(memory);
1087                    if !matches!(ms.mime, crate::file::MediaMime::Image(_)) {
1088                        return Err(crate::Error::ExifNotFound);
1089                    }
1090                    crate::exif::parse_exif_iter_async(
1091                        self,
1092                        ms.mime.unwrap_image(),
1093                        &mut ms.reader,
1094                        ms.skip_by_seek,
1095                    )
1096                    .await
1097                } else {
1098                    self.acquire_buf();
1099                    self.buf_mut().append(&mut ms.buf);
1100                    // PNG-only EOF tolerance mirrors the sync path: small
1101                    // tEXt-only PNGs (<HEADER_PARSE_BUF_SIZE) are fully
1102                    // consumed during mime detection, so fill_buf returns
1103                    // UnexpectedEof. The bytes are already in the parse
1104                    // buffer; proceed.
1105                    let is_png = matches!(
1106                        ms.mime,
1107                        crate::file::MediaMime::Image(
1108                            crate::file::MediaMimeImage::Png | crate::file::MediaMimeImage::Webp
1109                        )
1110                    );
1111                    match <Self as AsyncBufParser>::fill_buf(self, &mut ms.reader, INIT_BUF_SIZE)
1112                        .await
1113                    {
1114                        Ok(_) => {}
1115                        Err(e)
1116                            if is_png
1117                                && !self.buffer().is_empty()
1118                                && e.kind() == io::ErrorKind::UnexpectedEof => {}
1119                        Err(e) => return Err(e.into()),
1120                    }
1121                    if !matches!(ms.mime, crate::file::MediaMime::Image(_)) {
1122                        return Err(crate::Error::ExifNotFound);
1123                    }
1124                    crate::exif::parse_exif_iter_async(
1125                        self,
1126                        ms.mime.unwrap_image(),
1127                        &mut ms.reader,
1128                        ms.skip_by_seek,
1129                    )
1130                    .await
1131                }
1132            }
1133            .await;
1134            self.reset();
1135            res
1136        }
1137
1138        pub async fn parse_image_metadata_async<R: AsyncRead + Unpin + Send>(
1139            &mut self,
1140            mut ms: AsyncMediaSource<R>,
1141        ) -> crate::Result<crate::image_metadata::ImageMetadata<crate::ExifIter>> {
1142            self.reset();
1143            let res: crate::Result<crate::image_metadata::ImageMetadata<crate::ExifIter>> = async {
1144                let mime_img = match ms.mime {
1145                    crate::file::MediaMime::Image(img) => img,
1146                    crate::file::MediaMime::Track(_) => return Err(crate::Error::ExifNotFound),
1147                };
1148
1149                if let Some(memory) = ms.memory.take() {
1150                    self.state.set_memory(memory);
1151                } else {
1152                    self.acquire_buf();
1153                    self.buf_mut().append(&mut ms.buf);
1154                    // PNG-only EOF tolerance mirrors the sync path: small
1155                    // tEXt-only PNGs (<HEADER_PARSE_BUF_SIZE) are fully
1156                    // consumed during mime detection, so fill_buf returns
1157                    // UnexpectedEof; the bytes we need are already in the
1158                    // parse buffer.
1159                    let is_png = matches!(
1160                        mime_img,
1161                        crate::file::MediaMimeImage::Png | crate::file::MediaMimeImage::Webp
1162                    );
1163                    match <Self as AsyncBufParser>::fill_buf(self, &mut ms.reader, INIT_BUF_SIZE)
1164                        .await
1165                    {
1166                        Ok(_) => {}
1167                        Err(e)
1168                            if is_png
1169                                && !self.buffer().is_empty()
1170                                && e.kind() == io::ErrorKind::UnexpectedEof => {}
1171                        Err(e) => return Err(e.into()),
1172                    }
1173                }
1174
1175                if mime_img == crate::file::MediaMimeImage::Png {
1176                    let (exif, text_chunks) =
1177                        crate::exif::parse_png_full_async(self, &mut ms.reader, ms.skip_by_seek)
1178                            .await?;
1179                    let format = if text_chunks.is_empty() {
1180                        None
1181                    } else {
1182                        Some(crate::ImageFormatMetadata::Png(crate::PngTextChunks {
1183                            entries: text_chunks,
1184                        }))
1185                    };
1186                    if exif.is_none() && format.is_none() {
1187                        return Err(crate::Error::ExifNotFound);
1188                    }
1189                    Ok(crate::image_metadata::ImageMetadata { exif, format })
1190                } else {
1191                    let iter = crate::exif::parse_exif_iter_async(
1192                        self,
1193                        mime_img,
1194                        &mut ms.reader,
1195                        ms.skip_by_seek,
1196                    )
1197                    .await?;
1198                    Ok(crate::image_metadata::ImageMetadata {
1199                        exif: Some(iter),
1200                        format: None,
1201                    })
1202                }
1203            }
1204            .await;
1205            self.reset();
1206            res
1207        }
1208
1209        /// Parse track info from an async video/audio source. Returns
1210        /// `Error::TrackNotFound` if the source is an `Image`.
1211        ///
1212        /// As of v3.3, also accepts memory-mode sources built via
1213        /// [`AsyncMediaSource::from_memory`].
1214        pub async fn parse_track_async<R: AsyncRead + Unpin + Send>(
1215            &mut self,
1216            mut ms: AsyncMediaSource<R>,
1217        ) -> crate::Result<TrackInfo> {
1218            self.reset();
1219            let res: crate::Result<TrackInfo> = async {
1220                let mime_track = match ms.mime {
1221                    crate::file::MediaMime::Image(_) => return Err(crate::Error::TrackNotFound),
1222                    crate::file::MediaMime::Track(t) => t,
1223                };
1224                if let Some(memory) = ms.memory.take() {
1225                    self.state.set_memory(memory);
1226                } else {
1227                    self.acquire_buf();
1228                    self.buf_mut().append(&mut ms.buf);
1229                    <Self as AsyncBufParser>::fill_buf(self, &mut ms.reader, INIT_BUF_SIZE).await?;
1230                }
1231                let skip = ms.skip_by_seek;
1232                let out = <Self as AsyncBufParser>::load_and_parse(
1233                    self,
1234                    &mut ms.reader,
1235                    skip,
1236                    |data, _| {
1237                        crate::video::parse_track_info(data, mime_track)
1238                            .map_err(|e| ParsingErrorState::new(e, None))
1239                    },
1240                )
1241                .await?;
1242                Ok(out)
1243            }
1244            .await;
1245            self.reset();
1246            res
1247        }
1248    }
1249}
1250
1251#[cfg(test)]
1252mod tests {
1253    use std::sync::{LazyLock, Mutex, MutexGuard};
1254
1255    use super::*;
1256    use test_case::case;
1257
1258    enum TrackExif {
1259        Track,
1260        Exif,
1261        NoData,
1262        Invalid,
1263    }
1264    use TrackExif::*;
1265
1266    static PARSER: LazyLock<Mutex<MediaParser>> = LazyLock::new(|| Mutex::new(MediaParser::new()));
1267    fn parser() -> MutexGuard<'static, MediaParser> {
1268        PARSER.lock().unwrap()
1269    }
1270
1271    #[case("3gp_640x360.3gp", Track)]
1272    #[case("broken.jpg", Exif)]
1273    #[case("compatible-brands-fail.heic", Invalid)]
1274    #[case("compatible-brands-fail.mov", Invalid)]
1275    #[case("compatible-brands.heic", NoData)]
1276    #[case("compatible-brands.mov", NoData)]
1277    #[case("embedded-in-heic.mov", Track)]
1278    #[case("exif.heic", Exif)]
1279    #[case("exif.jpg", Exif)]
1280    #[case("exif-no-tz.jpg", Exif)]
1281    #[case("fujifilm_x_t1_01.raf.meta", Exif)]
1282    #[case("meta.mov", Track)]
1283    #[case("meta.mp4", Track)]
1284    #[case("mka.mka", Track)]
1285    #[case("mkv_640x360.mkv", Track)]
1286    #[case("exif-one-entry.heic", Exif)]
1287    #[case("no-exif.jpg", NoData)]
1288    #[case("tif.tif", Exif)]
1289    #[case("ramdisk.img", Invalid)]
1290    #[case("webm_480.webm", Track)]
1291    fn parse_media(path: &str, te: TrackExif) {
1292        let mut parser = parser();
1293        let ms = MediaSource::open(Path::new("testdata").join(path));
1294        match te {
1295            Track => {
1296                let ms = ms.unwrap();
1297                assert_eq!(ms.kind(), MediaKind::Track);
1298                let _: TrackInfo = parser.parse_track(ms).unwrap();
1299            }
1300            Exif => {
1301                let ms = ms.unwrap();
1302                assert_eq!(ms.kind(), MediaKind::Image);
1303                let mut it: ExifIter = parser.parse_exif(ms).unwrap();
1304                let _ = it.parse_gps();
1305
1306                if path.contains("one-entry") {
1307                    assert!(it.next().is_some());
1308                    assert!(it.next().is_none());
1309
1310                    let exif: crate::Exif = it.clone_rewound().into();
1311                    assert!(exif.get(ExifTag::Orientation).is_some());
1312                } else {
1313                    let _: crate::Exif = it.clone_rewound().into();
1314                }
1315            }
1316            NoData => {
1317                let ms = ms.unwrap();
1318                match ms.kind() {
1319                    MediaKind::Image => {
1320                        let res = parser.parse_exif(ms);
1321                        res.unwrap_err();
1322                    }
1323                    MediaKind::Track => {
1324                        let res = parser.parse_track(ms);
1325                        res.unwrap_err();
1326                    }
1327                }
1328            }
1329            Invalid => {
1330                ms.unwrap_err();
1331            }
1332        }
1333    }
1334
1335    use crate::testkit::open_sample;
1336    use crate::{EntryValue, Exif, ExifTag, TrackInfoTag};
1337    use chrono::{DateTime, FixedOffset, NaiveDateTime};
1338    use test_case::test_case;
1339
1340    #[test_case("exif.jpg", ExifTag::DateTimeOriginal, DateTime::parse_from_str("2023-07-09T20:36:33+08:00", "%+").unwrap().into())]
1341    #[test_case("exif.heic", ExifTag::DateTimeOriginal, DateTime::parse_from_str("2022-07-22T21:26:32+08:00", "%+").unwrap().into())]
1342    #[test_case("exif.jpg", ExifTag::DateTimeOriginal, 
1343        (NaiveDateTime::parse_from_str("2023-07-09T20:36:33", "%Y-%m-%dT%H:%M:%S").unwrap(), 
1344            Some(FixedOffset::east_opt(8*3600).unwrap())).into())]
1345    #[test_case("exif-no-tz.jpg", ExifTag::DateTimeOriginal, 
1346        (NaiveDateTime::parse_from_str("2023-07-09T20:36:33", "%Y-%m-%dT%H:%M:%S").unwrap(), None).into())]
1347    fn parse_exif(path: &str, tag: ExifTag, v: EntryValue) {
1348        let mut parser = parser();
1349
1350        let mf = MediaSource::seekable(open_sample(path).unwrap()).unwrap();
1351        assert_eq!(mf.kind(), MediaKind::Image);
1352        let iter: ExifIter = parser.parse_exif(mf).unwrap();
1353        let exif: Exif = iter.into();
1354        assert_eq!(exif.get(tag).unwrap(), &v);
1355
1356        let mf = MediaSource::unseekable(open_sample(path).unwrap()).unwrap();
1357        assert_eq!(mf.kind(), MediaKind::Image);
1358        let iter: ExifIter = parser.parse_exif(mf).unwrap();
1359        let exif: Exif = iter.into();
1360        assert_eq!(exif.get(tag).unwrap(), &v);
1361    }
1362
1363    use crate::video::TrackInfoTag::*;
1364
1365    #[test_case("mkv_640x360.mkv", Width, 640_u32.into())]
1366    #[test_case("mkv_640x360.mkv", Height, 360_u32.into())]
1367    #[test_case("mkv_640x360.mkv", DurationMs, 13346_u64.into())]
1368    #[test_case("mkv_640x360.mkv", CreateDate, DateTime::parse_from_str("2008-08-08T08:08:08Z", "%+").unwrap().into())]
1369    #[test_case("meta.mov", Make, "Apple".into())]
1370    #[test_case("meta.mov", Model, "iPhone X".into())]
1371    #[test_case("meta.mov", GpsIso6709, "+27.1281+100.2508+000.000/".into())]
1372    #[test_case("meta.mov", CreateDate, DateTime::parse_from_str("2019-02-12T15:27:12+08:00", "%+").unwrap().into())]
1373    #[test_case("meta.mp4", Width, 1920_u32.into())]
1374    #[test_case("meta.mp4", Height, 1080_u32.into())]
1375    #[test_case("meta.mp4", DurationMs, 1063_u64.into())]
1376    #[test_case("meta.mp4", GpsIso6709, "+27.2939+112.6932/".into())]
1377    #[test_case("meta.mp4", CreateDate, DateTime::parse_from_str("2024-02-03T07:05:38Z", "%+").unwrap().into())]
1378    #[test_case("udta.auth.mp4", Author, "ReplayKitRecording".into(); "udta author")]
1379    #[test_case("auth.mov", Author, "ReplayKitRecording".into(); "mov author")]
1380    #[test_case("sony-a7-xavc.MP4", Width, 1920_u32.into())]
1381    #[test_case("sony-a7-xavc.MP4", Height, 1080_u32.into())]
1382    #[test_case("sony-a7-xavc.MP4", DurationMs, 1440_u64.into())]
1383    #[test_case("sony-a7-xavc.MP4", CreateDate, DateTime::parse_from_str("2026-04-26T09:25:15+00:00", "%+").unwrap().into())]
1384    fn parse_track_info(path: &str, tag: TrackInfoTag, v: EntryValue) {
1385        let mut parser = parser();
1386
1387        let mf = MediaSource::seekable(open_sample(path).unwrap()).unwrap();
1388        let info: TrackInfo = parser.parse_track(mf).unwrap();
1389        assert_eq!(info.get(tag).unwrap(), &v);
1390
1391        let mf = MediaSource::unseekable(open_sample(path).unwrap()).unwrap();
1392        let info: TrackInfo = parser.parse_track(mf).unwrap();
1393        assert_eq!(info.get(tag).unwrap(), &v);
1394    }
1395
1396    #[test_case("crash_moov-trak")]
1397    #[test_case("crash_skip_large")]
1398    #[test_case("crash_add_large")]
1399    fn parse_track_crash(path: &str) {
1400        let mut parser = parser();
1401
1402        let mf = MediaSource::seekable(open_sample(path).unwrap()).unwrap();
1403        let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
1404
1405        let mf = MediaSource::unseekable(open_sample(path).unwrap()).unwrap();
1406        let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
1407    }
1408
1409    // Regression: a crafted ISOBMFF file declares an extended 64-bit box size
1410    // just under MAX_PARSE_BUF_SIZE (~1 GB). Pre-fix, the unseekable parser called
1411    // reserve_exact() with that size before reading, allocating ~1 GB even when
1412    // the actual stream contained only a few KB. See commit 81f9e8a.
1413    #[test]
1414    fn parse_oom_large_box() {
1415        let mut parser = parser();
1416
1417        let mf = MediaSource::seekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
1418        let _: Result<ExifIter, _> = parser.parse_exif(mf);
1419
1420        let mf = MediaSource::unseekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
1421        let _: Result<ExifIter, _> = parser.parse_exif(mf);
1422
1423        let mf = MediaSource::seekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
1424        let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
1425
1426        let mf = MediaSource::unseekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
1427        let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
1428    }
1429
1430    #[test]
1431    fn media_kind_classifies_image_and_track() {
1432        let img = MediaSource::open("testdata/exif.jpg").unwrap();
1433        assert_eq!(img.kind(), MediaKind::Image);
1434
1435        let trk = MediaSource::open("testdata/meta.mov").unwrap();
1436        assert_eq!(trk.kind(), MediaKind::Track);
1437    }
1438
1439    #[test]
1440    fn media_source_open() {
1441        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1442        assert_eq!(ms.kind(), MediaKind::Image);
1443    }
1444
1445    #[test]
1446    fn parse_exif_returns_exif_iter() {
1447        let mut parser = parser();
1448        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1449        let _: ExifIter = parser.parse_exif(ms).unwrap();
1450    }
1451
1452    #[test]
1453    fn parse_track_returns_track_info() {
1454        let mut parser = parser();
1455        let ms = MediaSource::open("testdata/meta.mov").unwrap();
1456        let _: TrackInfo = parser.parse_track(ms).unwrap();
1457    }
1458
1459    #[test]
1460    fn parse_exif_on_track_returns_exif_not_found_v3() {
1461        let mut parser = parser();
1462        let ms = MediaSource::open("testdata/meta.mov").unwrap();
1463        let res = parser.parse_exif(ms);
1464        assert!(matches!(res, Err(crate::Error::ExifNotFound)));
1465    }
1466
1467    #[test]
1468    fn parse_track_on_image_returns_track_not_found_v3() {
1469        let mut parser = parser();
1470        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1471        let res = parser.parse_track(ms);
1472        assert!(matches!(res, Err(crate::Error::TrackNotFound)));
1473    }
1474
1475    #[cfg(feature = "tokio-fs")]
1476    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1477    async fn media_parser_parse_exif_async() {
1478        use crate::parser_async::AsyncMediaSource;
1479        let mut parser = MediaParser::new();
1480        let ms = AsyncMediaSource::open("testdata/exif.jpg").await.unwrap();
1481        let _: ExifIter = parser.parse_exif_async(ms).await.unwrap();
1482    }
1483
1484    #[cfg(feature = "tokio-fs")]
1485    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1486    async fn media_parser_parse_track_async() {
1487        use crate::parser_async::AsyncMediaSource;
1488        let mut parser = MediaParser::new();
1489        let ms = AsyncMediaSource::open("testdata/meta.mov").await.unwrap();
1490        let _: TrackInfo = parser.parse_track_async(ms).await.unwrap();
1491    }
1492
1493    #[test]
1494    fn parser_recycles_alloc_when_exif_iter_dropped() {
1495        let mut parser = MediaParser::new();
1496
1497        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1498        let iter = parser.parse_exif(ms).unwrap();
1499        let exif: crate::Exif = iter.into();
1500        drop(exif);
1501        let ptr_after_first = parser.state.cached_ptr_for_test();
1502
1503        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1504        let iter = parser.parse_exif(ms).unwrap();
1505        let _exif: crate::Exif = iter.into();
1506        let ptr_after_second = parser.state.cached_ptr_for_test();
1507
1508        assert!(
1509            ptr_after_first.is_some() && ptr_after_first == ptr_after_second,
1510            "expected recycled allocation, got {:?} -> {:?}",
1511            ptr_after_first,
1512            ptr_after_second
1513        );
1514    }
1515
1516    #[test]
1517    fn parser_new_does_no_upfront_allocation() {
1518        let parser = MediaParser::new();
1519        assert!(parser.state.cached_ptr_for_test().is_none());
1520        assert!(parser.state.buf_is_none_for_test());
1521    }
1522
1523    #[test]
1524    fn buffered_state_memory_mode_sets_and_reads() {
1525        let mut s = BufferedParserState::new();
1526        s.set_memory(bytes::Bytes::from_static(b"abcdefgh"));
1527        assert!(s.is_memory_mode());
1528        assert_eq!(s.buffer(), b"abcdefgh");
1529        s.set_position(3);
1530        assert_eq!(s.buffer(), b"defgh");
1531    }
1532
1533    #[test]
1534    fn buffered_state_share_buf_memory_mode_is_zero_copy() {
1535        let original = bytes::Bytes::from_static(b"the parser owns nothing here");
1536        let original_ptr = original.as_ptr();
1537        let mut s = BufferedParserState::new();
1538        s.set_memory(original);
1539        let (shared, position) = s.share_buf();
1540        assert_eq!(position, 0);
1541        assert_eq!(
1542            shared.as_ptr(),
1543            original_ptr,
1544            "memory share must be a Bytes::clone, not a Vec round-trip"
1545        );
1546        // After share_buf, the parser's memory slot is taken — leaving the state
1547        // ready for the next `reset()` cycle.
1548        assert!(!s.is_memory_mode());
1549    }
1550
1551    #[test]
1552    fn buffered_state_reset_clears_memory() {
1553        let mut s = BufferedParserState::new();
1554        s.set_memory(bytes::Bytes::from_static(b"x"));
1555        s.reset();
1556        assert!(!s.is_memory_mode());
1557        assert_eq!(s.position, 0);
1558    }
1559
1560    #[test]
1561    fn buffered_state_acquire_buf_skips_in_memory_mode() {
1562        let mut s = BufferedParserState::new();
1563        s.set_memory(bytes::Bytes::from_static(b"data"));
1564        s.acquire_buf();
1565        // No streaming buf was allocated.
1566        assert!(s.buf.is_none());
1567        // Memory still readable.
1568        assert_eq!(s.buffer(), b"data");
1569    }
1570
1571    #[test]
1572    fn media_source_from_memory_image_jpg() {
1573        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1574        let ms = MediaSource::from_memory(raw).unwrap();
1575        assert_eq!(ms.kind(), MediaKind::Image);
1576        assert!(ms.memory.is_some());
1577    }
1578
1579    #[test]
1580    fn media_source_from_memory_track_mov() {
1581        let raw = std::fs::read("testdata/meta.mov").unwrap();
1582        let ms = MediaSource::from_memory(raw).unwrap();
1583        assert_eq!(ms.kind(), MediaKind::Track);
1584    }
1585
1586    #[test]
1587    fn media_source_from_memory_static_slice() {
1588        let raw: &'static [u8] = include_bytes!("../testdata/exif.jpg");
1589        let ms = MediaSource::from_memory(raw).unwrap();
1590        assert_eq!(ms.kind(), MediaKind::Image);
1591    }
1592
1593    #[test]
1594    fn media_source_from_memory_rejects_too_short() {
1595        let raw = vec![0u8; 4];
1596        let res = MediaSource::from_memory(raw);
1597        assert!(res.is_err());
1598    }
1599
1600    #[test]
1601    fn media_source_from_memory_rejects_unknown_mime() {
1602        let raw = vec![0xAAu8; 256];
1603        let res = MediaSource::from_memory(raw);
1604        assert!(res.is_err());
1605    }
1606
1607    #[test]
1608    fn parse_exif_unified_from_memory_jpg() {
1609        let mut parser = MediaParser::new();
1610        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1611        let ms = MediaSource::from_memory(raw).unwrap();
1612        let iter = parser.parse_exif(ms).unwrap();
1613        let exif: crate::Exif = iter.into();
1614        assert!(exif.get(crate::ExifTag::Make).is_some());
1615    }
1616
1617    #[test]
1618    fn parse_exif_unified_from_memory_heic() {
1619        let mut parser = MediaParser::new();
1620        let raw = std::fs::read("testdata/exif.heic").unwrap();
1621        let ms = MediaSource::from_memory(raw).unwrap();
1622        let iter = parser.parse_exif(ms).unwrap();
1623        let exif: crate::Exif = iter.into();
1624        assert_eq!(
1625            exif.get(crate::ExifTag::Make).and_then(|v| v.as_str()),
1626            Some("Apple")
1627        );
1628    }
1629
1630    #[test]
1631    fn parse_exif_unified_from_memory_zero_copy_preserved() {
1632        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1633        let bytes = bytes::Bytes::from(raw);
1634        let _original_ptr = bytes.as_ptr();
1635
1636        let mut parser = MediaParser::new();
1637        let ms = MediaSource::from_memory(bytes).unwrap();
1638        let iter = parser.parse_exif(ms).unwrap();
1639
1640        // Memory mode must not poison the recycle cache — same invariant
1641        // the old parse_exif_from_bytes route asserts.
1642        assert!(
1643            parser.state.cached_ptr_for_test().is_none(),
1644            "memory mode must not write to the streaming-buf recycle cache"
1645        );
1646        drop(iter);
1647    }
1648
1649    #[test]
1650    fn parse_exif_unified_on_track_returns_exif_not_found() {
1651        let mut parser = MediaParser::new();
1652        let raw = std::fs::read("testdata/meta.mov").unwrap();
1653        let ms = MediaSource::from_memory(raw).unwrap();
1654        let res = parser.parse_exif(ms);
1655        assert!(matches!(res, Err(crate::Error::ExifNotFound)));
1656    }
1657
1658    #[test]
1659    fn parse_exif_unified_on_truncated_returns_io_error() {
1660        let mut raw = std::fs::read("testdata/exif.jpg").unwrap();
1661        raw.truncate(200);
1662        let mut parser = MediaParser::new();
1663        let ms = MediaSource::from_memory(raw).unwrap();
1664        let res = parser.parse_exif(ms);
1665        assert!(
1666            res.is_err(),
1667            "expected error on truncated bytes, got {:?}",
1668            res
1669        );
1670    }
1671
1672    #[test]
1673    fn parse_exif_streaming_tiny_non_png_keeps_strict_eof() {
1674        // The PNG EOF-tolerance branch is scoped to PNG. A tiny non-PNG file
1675        // (here: the 36-byte compatible-brands.heic fixture) consumes its
1676        // entire reader during mime-detection prefill, so the subsequent
1677        // fill_buf hits UnexpectedEof. parse_exif must surface an error
1678        // (any error) — never Ok — for non-PNG inputs.
1679        let mut parser = MediaParser::new();
1680        let ms = MediaSource::open("testdata/compatible-brands.heic").unwrap();
1681        let res = parser.parse_exif(ms);
1682        assert!(res.is_err(), "expected Err for tiny HEIC, got {:?}", res);
1683    }
1684
1685    #[test]
1686    #[allow(deprecated)]
1687    fn media_source_from_bytes_image_jpg() {
1688        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1689        let ms = MediaSource::from_bytes(raw).unwrap();
1690        assert_eq!(ms.kind(), MediaKind::Image);
1691        assert!(ms.memory.is_some());
1692    }
1693
1694    #[test]
1695    #[allow(deprecated)]
1696    fn media_source_from_bytes_track_mov() {
1697        let raw = std::fs::read("testdata/meta.mov").unwrap();
1698        let ms = MediaSource::from_bytes(raw).unwrap();
1699        assert_eq!(ms.kind(), MediaKind::Track);
1700    }
1701
1702    #[test]
1703    #[allow(deprecated)]
1704    fn media_source_from_bytes_static_slice() {
1705        // &'static [u8] should work via Into<Bytes> because the file is read
1706        // into a Vec at compile-time-friendly size; here we use include_bytes.
1707        let raw: &'static [u8] = include_bytes!("../testdata/exif.jpg");
1708        let ms = MediaSource::from_bytes(raw).unwrap();
1709        assert_eq!(ms.kind(), MediaKind::Image);
1710    }
1711
1712    #[test]
1713    #[allow(deprecated)]
1714    fn media_source_from_bytes_rejects_too_short() {
1715        // Below the smallest mime signature length: should fail mime detection.
1716        let raw = vec![0u8; 4];
1717        let res = MediaSource::from_bytes(raw);
1718        assert!(res.is_err(), "expected mime-detection error");
1719    }
1720
1721    #[test]
1722    #[allow(deprecated)]
1723    fn media_source_from_bytes_rejects_unknown_mime() {
1724        // Random bytes long enough to trigger detection but not match any
1725        // signature.
1726        let raw = vec![0xAAu8; 256];
1727        let res = MediaSource::from_bytes(raw);
1728        assert!(
1729            res.is_err(),
1730            "expected mime-detection error for unknown bytes"
1731        );
1732    }
1733
1734    #[test]
1735    fn p4_5_baseline_exif_jpg_full_dump() {
1736        // Lock down the post-refactor invariant: parsing testdata/exif.jpg through
1737        // the public API must yield the same set of (ifd, tag, value) triples
1738        // before and after P4.5. We capture them as a sorted, formatted string so
1739        // the assertion is a single literal comparison.
1740        let mut parser = MediaParser::new();
1741        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1742        let iter: ExifIter = parser.parse_exif(ms).unwrap();
1743
1744        let mut entries: Vec<String> = iter
1745            .map(|e| {
1746                let tag_name = match e.tag() {
1747                    crate::TagOrCode::Tag(t) => format!("{t}"),
1748                    crate::TagOrCode::Unknown(c) => format!("0x{c:04x}"),
1749                };
1750                let value_str = e
1751                    .value()
1752                    .map(|v| format!("{v}"))
1753                    .unwrap_or_else(|| "<err>".into());
1754                format!("{}.{}={:?}", e.ifd(), tag_name, value_str)
1755            })
1756            .collect();
1757        entries.sort();
1758        let snapshot = entries.join("\n");
1759
1760        // Sanity: should produce non-trivial content. Exact content is checked by
1761        // the existing parse_media tests; this one guards against accidental
1762        // re-ordering / dedup changes during the refactor.
1763        assert!(
1764            entries.len() > 5,
1765            "expected >5 entries, got {}",
1766            entries.len()
1767        );
1768        assert!(snapshot.contains("Make"), "expected Make tag in snapshot");
1769    }
1770
1771    #[test]
1772    #[allow(deprecated)]
1773    fn parse_exif_from_bytes_jpg_basic() {
1774        let mut parser = MediaParser::new();
1775        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1776        let ms = MediaSource::from_bytes(raw).unwrap();
1777        let iter = parser.parse_exif_from_bytes(ms).unwrap();
1778        let exif: crate::Exif = iter.into();
1779        assert!(exif.get(crate::ExifTag::Make).is_some());
1780    }
1781
1782    #[test]
1783    #[allow(deprecated)]
1784    fn parse_exif_from_bytes_heic_basic() {
1785        let mut parser = MediaParser::new();
1786        let raw = std::fs::read("testdata/exif.heic").unwrap();
1787        let ms = MediaSource::from_bytes(raw).unwrap();
1788        let iter = parser.parse_exif_from_bytes(ms).unwrap();
1789        let exif: crate::Exif = iter.into();
1790        assert_eq!(
1791            exif.get(crate::ExifTag::Make).and_then(|v| v.as_str()),
1792            Some("Apple")
1793        );
1794    }
1795
1796    #[test]
1797    #[allow(deprecated)]
1798    fn parse_exif_from_bytes_zero_copy_shared_bytes() {
1799        // Build a Bytes whose pointer we can compare. The ExifIter's underlying
1800        // share must point to the same allocation — proving Bytes::clone path.
1801        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1802        let bytes = bytes::Bytes::from(raw);
1803        let original_ptr = bytes.as_ptr();
1804
1805        let mut parser = MediaParser::new();
1806        let ms = MediaSource::from_bytes(bytes).unwrap();
1807        let iter = parser.parse_exif_from_bytes(ms).unwrap();
1808
1809        // The cached pointer in parser state should be None in memory mode
1810        // (memory mode does not write to cache — the user owns the alloc).
1811        assert!(
1812            parser.state.cached_ptr_for_test().is_none(),
1813            "memory mode must not poison the recycle cache"
1814        );
1815
1816        // Drop the iter and confirm parser is clean for the next call.
1817        drop(iter);
1818
1819        // Build again; pointer identity proves we did not duplicate the alloc
1820        // anywhere along the parse path.
1821        let bytes2 = bytes::Bytes::from(std::fs::read("testdata/exif.jpg").unwrap());
1822        let ms2 = MediaSource::from_bytes(bytes2.clone()).unwrap();
1823        let _iter2 = parser.parse_exif_from_bytes(ms2).unwrap();
1824        // (We cannot assert pointer-equality across distinct user Bytes; the
1825        // assertion above on the first parse is the load-bearing one.)
1826        let _ = original_ptr; // explicit: original_ptr is the assertion target.
1827    }
1828
1829    #[test]
1830    #[allow(deprecated)]
1831    fn parse_exif_from_bytes_on_track_returns_exif_not_found() {
1832        let mut parser = MediaParser::new();
1833        let raw = std::fs::read("testdata/meta.mov").unwrap();
1834        let ms = MediaSource::from_bytes(raw).unwrap();
1835        let res = parser.parse_exif_from_bytes(ms);
1836        assert!(matches!(res, Err(crate::Error::ExifNotFound)));
1837    }
1838
1839    #[test]
1840    #[allow(deprecated)]
1841    fn parse_exif_from_bytes_on_truncated_returns_io_error() {
1842        // Truncate exif.jpg to just enough for mime detection but too short
1843        // for the full EXIF block. Memory-mode fill_buf must surface
1844        // UnexpectedEof when the parser walks off the end.
1845        let mut raw = std::fs::read("testdata/exif.jpg").unwrap();
1846        raw.truncate(200);
1847        let mut parser = MediaParser::new();
1848        let ms = MediaSource::from_bytes(raw).unwrap();
1849        let res = parser.parse_exif_from_bytes(ms);
1850        assert!(
1851            res.is_err(),
1852            "expected error on truncated bytes, got {:?}",
1853            res
1854        );
1855    }
1856
1857    #[test]
1858    #[allow(deprecated)]
1859    fn parse_track_from_bytes_mov_basic() {
1860        let mut parser = MediaParser::new();
1861        let raw = std::fs::read("testdata/meta.mov").unwrap();
1862        let ms = MediaSource::from_bytes(raw).unwrap();
1863        let info = parser.parse_track_from_bytes(ms).unwrap();
1864        assert_eq!(info.get(crate::TrackInfoTag::Make), Some(&"Apple".into()));
1865        assert_eq!(
1866            info.get(crate::TrackInfoTag::Model),
1867            Some(&"iPhone X".into())
1868        );
1869    }
1870
1871    #[test]
1872    #[allow(deprecated)]
1873    fn parse_track_from_bytes_mp4_basic() {
1874        let mut parser = MediaParser::new();
1875        let raw = std::fs::read("testdata/meta.mp4").unwrap();
1876        let ms = MediaSource::from_bytes(raw).unwrap();
1877        let info = parser.parse_track_from_bytes(ms).unwrap();
1878        assert!(info.get(crate::TrackInfoTag::CreateDate).is_some());
1879    }
1880
1881    #[test]
1882    #[allow(deprecated)]
1883    fn parse_track_from_bytes_mkv_basic() {
1884        let mut parser = MediaParser::new();
1885        let raw = std::fs::read("testdata/mkv_640x360.mkv").unwrap();
1886        let ms = MediaSource::from_bytes(raw).unwrap();
1887        let info = parser.parse_track_from_bytes(ms).unwrap();
1888        assert_eq!(
1889            info.get(crate::TrackInfoTag::Width),
1890            Some(&(640_u32.into()))
1891        );
1892    }
1893
1894    #[test]
1895    #[allow(deprecated)]
1896    fn parse_track_from_bytes_on_image_returns_track_not_found() {
1897        let mut parser = MediaParser::new();
1898        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1899        let ms = MediaSource::from_bytes(raw).unwrap();
1900        let res = parser.parse_track_from_bytes(ms);
1901        assert!(matches!(res, Err(crate::Error::TrackNotFound)));
1902    }
1903
1904    #[test]
1905    fn parse_track_unified_from_memory_mov() {
1906        let mut parser = MediaParser::new();
1907        let raw = std::fs::read("testdata/meta.mov").unwrap();
1908        let ms = MediaSource::from_memory(raw).unwrap();
1909        let info = parser.parse_track(ms).unwrap();
1910        assert_eq!(info.get(crate::TrackInfoTag::Make), Some(&"Apple".into()));
1911    }
1912
1913    #[test]
1914    fn parse_track_unified_from_memory_mp4() {
1915        let mut parser = MediaParser::new();
1916        let raw = std::fs::read("testdata/meta.mp4").unwrap();
1917        let ms = MediaSource::from_memory(raw).unwrap();
1918        let info = parser.parse_track(ms).unwrap();
1919        assert!(info.get(crate::TrackInfoTag::CreateDate).is_some());
1920    }
1921
1922    #[test]
1923    fn parse_track_unified_on_image_returns_track_not_found() {
1924        let mut parser = MediaParser::new();
1925        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1926        let ms = MediaSource::from_memory(raw).unwrap();
1927        let res = parser.parse_track(ms);
1928        assert!(matches!(res, Err(crate::Error::TrackNotFound)));
1929    }
1930
1931    #[test]
1932    fn parse_image_metadata_jpeg_returns_exif_only() {
1933        let mut parser = MediaParser::new();
1934        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1935        let img = parser.parse_image_metadata(ms).unwrap();
1936        assert!(img.exif.is_some());
1937        assert!(img.format.is_none());
1938    }
1939
1940    #[test]
1941    fn parse_image_metadata_jpeg_from_memory() {
1942        let mut parser = MediaParser::new();
1943        let raw = std::fs::read("testdata/exif.jpg").unwrap();
1944        let ms = MediaSource::from_memory(raw).unwrap();
1945        let img = parser.parse_image_metadata(ms).unwrap();
1946        assert!(img.exif.is_some());
1947        assert!(img.format.is_none());
1948    }
1949
1950    #[test]
1951    fn parse_image_metadata_on_track_returns_exif_not_found() {
1952        let mut parser = MediaParser::new();
1953        let ms = MediaSource::open("testdata/meta.mov").unwrap();
1954        let res = parser.parse_image_metadata(ms);
1955        assert!(matches!(res, Err(crate::Error::ExifNotFound)));
1956    }
1957
1958    /// Regression for issue #55. PNGs whose IDAT body exceeds
1959    /// `INIT_BUF_SIZE` force the chunk walker through `ClearAndSkip`;
1960    /// on retry the parse buffer no longer starts at byte 0 of the
1961    /// file, and the signature recheck must not fire.
1962    #[test]
1963    fn parse_image_metadata_png_large_idat_streaming() {
1964        use std::io::Cursor;
1965        let png = build_png_with_large_idat(INIT_BUF_SIZE + 1024, false);
1966        let mut parser = MediaParser::new();
1967        let ms = MediaSource::seekable(Cursor::new(png)).unwrap();
1968        assert_eq!(ms.kind(), crate::MediaKind::Image);
1969        let res = parser.parse_image_metadata(ms);
1970        assert!(
1971            matches!(res, Err(crate::Error::ExifNotFound)),
1972            "expected ExifNotFound on PNG with no EXIF / tEXt; got {res:?}"
1973        );
1974    }
1975
1976    /// Same regression via `parse_exif` — exercises `parse_png_exif_iter`,
1977    /// the sibling code path to `parse_png_full`.
1978    #[test]
1979    fn parse_exif_png_large_idat_streaming() {
1980        use std::io::Cursor;
1981        let png = build_png_with_large_idat(INIT_BUF_SIZE + 1024, false);
1982        let mut parser = MediaParser::new();
1983        let ms = MediaSource::seekable(Cursor::new(png)).unwrap();
1984        let res = parser.parse_exif(ms);
1985        assert!(
1986            matches!(res, Err(crate::Error::ExifNotFound)),
1987            "expected ExifNotFound; got {res:?}"
1988        );
1989    }
1990
1991    /// Same regression via `MediaSource::unseekable` — exercises the
1992    /// read-and-discard skip path in `clear_and_skip` rather than the
1993    /// `seek_relative` shortcut.
1994    #[test]
1995    fn parse_image_metadata_png_large_idat_unseekable() {
1996        let png = build_png_with_large_idat(INIT_BUF_SIZE + 1024, false);
1997        let mut parser = MediaParser::new();
1998        let ms = MediaSource::unseekable(NoSeek(std::io::Cursor::new(png))).unwrap();
1999        let res = parser.parse_image_metadata(ms);
2000        assert!(
2001            matches!(res, Err(crate::Error::ExifNotFound)),
2002            "expected ExifNotFound; got {res:?}"
2003        );
2004    }
2005
2006    /// Wraps a reader to hide its `Seek` impl, so `MediaSource::unseekable`
2007    /// is forced even when the underlying type happens to implement it.
2008    struct NoSeek<R>(R);
2009    impl<R: io::Read> io::Read for NoSeek<R> {
2010        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2011            self.0.read(buf)
2012        }
2013    }
2014
2015    /// Metadata found before a skipped IDAT must survive the parse retry.
2016    #[test]
2017    fn parse_image_metadata_png_exif_before_large_idat() {
2018        use std::io::Cursor;
2019        let png = build_png_with_large_idat(INIT_BUF_SIZE + 1024, true);
2020        let mut parser = MediaParser::new();
2021        let ms = MediaSource::seekable(Cursor::new(png)).unwrap();
2022        let image = parser.parse_image_metadata(ms).unwrap();
2023        assert!(image.exif.is_some());
2024        let Some(crate::ImageFormatMetadata::Png(text)) = image.format else {
2025            panic!("missing PNG text metadata");
2026        };
2027        assert_eq!(text.get("Title"), Some("before IDAT"));
2028    }
2029
2030    fn build_png_with_large_idat(idat_body: usize, with_metadata: bool) -> Vec<u8> {
2031        let mut out = Vec::new();
2032        out.extend_from_slice(b"\x89PNG\r\n\x1a\n");
2033        // IHDR (1x1, 8-bit grayscale)
2034        out.extend_from_slice(&13u32.to_be_bytes());
2035        out.extend_from_slice(b"IHDR");
2036        out.extend_from_slice(&[0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0]);
2037        out.extend_from_slice(&[0, 0, 0, 0]); // CRC (chunk walker ignores it)
2038        if with_metadata {
2039            let text = b"Title\0before IDAT";
2040            out.extend_from_slice(&(text.len() as u32).to_be_bytes());
2041            out.extend_from_slice(b"tEXt");
2042            out.extend_from_slice(text);
2043            out.extend_from_slice(&[0, 0, 0, 0]);
2044            let tiff = b"II\x2a\0\x08\0\0\0\0\0\0\0\0\0";
2045            out.extend_from_slice(&(tiff.len() as u32).to_be_bytes());
2046            out.extend_from_slice(b"eXIf");
2047            out.extend_from_slice(tiff);
2048            out.extend_from_slice(&[0, 0, 0, 0]);
2049        }
2050        // IDAT
2051        out.extend_from_slice(&(idat_body as u32).to_be_bytes());
2052        out.extend_from_slice(b"IDAT");
2053        out.resize(out.len() + idat_body, 0);
2054        out.extend_from_slice(&[0, 0, 0, 0]); // CRC
2055                                              // IEND
2056        out.extend_from_slice(&0u32.to_be_bytes());
2057        out.extend_from_slice(b"IEND");
2058        out.extend_from_slice(&[0, 0, 0, 0]); // CRC
2059        out
2060    }
2061
2062    #[cfg(feature = "tokio-fs")]
2063    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2064    async fn parse_image_metadata_async_jpeg() {
2065        use crate::parser_async::AsyncMediaSource;
2066        let mut parser = MediaParser::new();
2067        let ms = AsyncMediaSource::open("testdata/exif.jpg").await.unwrap();
2068        let img = parser.parse_image_metadata_async(ms).await.unwrap();
2069        assert!(img.exif.is_some());
2070        assert!(img.format.is_none());
2071    }
2072
2073    #[cfg(feature = "tokio-fs")]
2074    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2075    async fn async_media_source_from_memory_image_jpg() {
2076        use crate::parser_async::AsyncMediaSource;
2077        let raw = std::fs::read("testdata/exif.jpg").unwrap();
2078        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2079        assert_eq!(ms.kind(), MediaKind::Image);
2080        assert!(ms.memory.is_some());
2081    }
2082
2083    #[cfg(feature = "tokio-fs")]
2084    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2085    async fn async_media_source_from_memory_track_mov() {
2086        use crate::parser_async::AsyncMediaSource;
2087        let raw = std::fs::read("testdata/meta.mov").unwrap();
2088        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2089        assert_eq!(ms.kind(), MediaKind::Track);
2090    }
2091
2092    #[cfg(feature = "tokio-fs")]
2093    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2094    async fn async_media_source_from_memory_rejects_unknown_mime() {
2095        use crate::parser_async::AsyncMediaSource;
2096        let raw = vec![0xAAu8; 256];
2097        let res = AsyncMediaSource::from_memory(raw);
2098        assert!(res.is_err());
2099    }
2100
2101    #[cfg(feature = "tokio-fs")]
2102    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2103    async fn parse_exif_async_from_memory_jpg() {
2104        use crate::parser_async::AsyncMediaSource;
2105        let raw = std::fs::read("testdata/exif.jpg").unwrap();
2106        let mut parser = MediaParser::new();
2107        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2108        let iter = parser.parse_exif_async(ms).await.unwrap();
2109        let exif: crate::Exif = iter.into();
2110        assert!(exif.get(crate::ExifTag::Make).is_some());
2111    }
2112
2113    #[cfg(feature = "tokio-fs")]
2114    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2115    async fn parse_exif_async_from_memory_zero_copy_preserved() {
2116        use crate::parser_async::AsyncMediaSource;
2117        let raw = std::fs::read("testdata/exif.jpg").unwrap();
2118        let bytes = bytes::Bytes::from(raw);
2119        let mut parser = MediaParser::new();
2120        let ms = AsyncMediaSource::from_memory(bytes).unwrap();
2121        let iter = parser.parse_exif_async(ms).await.unwrap();
2122        // Memory mode must not poison the recycle cache — same invariant
2123        // as the sync route asserts.
2124        assert!(
2125            parser.state.cached_ptr_for_test().is_none(),
2126            "async memory mode must not write to the streaming-buf recycle cache"
2127        );
2128        drop(iter);
2129    }
2130
2131    #[cfg(feature = "tokio-fs")]
2132    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2133    async fn parse_track_async_from_memory_mov() {
2134        use crate::parser_async::AsyncMediaSource;
2135        let raw = std::fs::read("testdata/meta.mov").unwrap();
2136        let mut parser = MediaParser::new();
2137        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2138        let info = parser.parse_track_async(ms).await.unwrap();
2139        assert_eq!(info.get(crate::TrackInfoTag::Make), Some(&"Apple".into()));
2140    }
2141
2142    #[cfg(feature = "tokio-fs")]
2143    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2144    async fn parse_image_metadata_async_from_memory_png() {
2145        use crate::parser_async::AsyncMediaSource;
2146        let raw = std::fs::read("testdata/exif.png").unwrap();
2147        let mut parser = MediaParser::new();
2148        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2149        let img = parser.parse_image_metadata_async(ms).await.unwrap();
2150        assert!(img.exif.is_some());
2151        assert!(img.format.is_some());
2152    }
2153
2154    /// Async counterpart of `parse_image_metadata_png_large_idat_streaming`
2155    /// (regression for issue #55). `parser_async::clear_and_skip` is a
2156    /// separate implementation; the state-threading through the shared
2157    /// `parse_loop_step` must work identically in the async path.
2158    #[cfg(feature = "tokio-fs")]
2159    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2160    async fn parse_image_metadata_async_png_large_idat_streaming() {
2161        use crate::parser_async::AsyncMediaSource;
2162        let png = build_png_with_large_idat(INIT_BUF_SIZE + 1024, false);
2163        // tokio's async I/O traits aren't on std::io::Cursor, so route
2164        // through a real file. Pick a unique path to avoid concurrent
2165        // test collisions.
2166        let path =
2167            std::env::temp_dir().join(format!("nom-exif-issue55-{}.png", std::process::id()));
2168        tokio::fs::write(&path, &png).await.unwrap();
2169        let mut parser = MediaParser::new();
2170        let ms = AsyncMediaSource::open(&path).await.unwrap();
2171        let res = parser.parse_image_metadata_async(ms).await;
2172        let _ = tokio::fs::remove_file(&path).await;
2173        assert!(
2174            matches!(res, Err(crate::Error::ExifNotFound)),
2175            "expected ExifNotFound; got {res:?}"
2176        );
2177    }
2178
2179    #[cfg(feature = "tokio")]
2180    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2181    async fn parse_image_metadata_async_from_memory_text_only_png() {
2182        // Memory route: bypasses fill_buf entirely, just verifies the
2183        // memory-mode path returns format-only metadata for a PNG with
2184        // no EXIF.
2185        use crate::parser_async::AsyncMediaSource;
2186        let raw = std::fs::read("testdata/text-only.png").unwrap();
2187        let mut parser = MediaParser::new();
2188        let ms = AsyncMediaSource::from_memory(raw).unwrap();
2189        let img = parser.parse_image_metadata_async(ms).await.unwrap();
2190        assert!(img.exif.is_none());
2191        assert!(img.format.is_some());
2192    }
2193
2194    // Streaming-path coverage for the PNG-scoped EOF tolerance. The
2195    // 117-byte text-only.png is fully consumed during mime detection
2196    // (HEADER_PARSE_BUF_SIZE = 128), so the parse-time fill_buf hits
2197    // UnexpectedEof. The PNG-scoped tolerance must let the bytes already
2198    // in the parse buffer drive the parse to completion. These tests
2199    // would have caught the missed-async-tolerance bug the previous
2200    // memory-mode tests did not.
2201
2202    #[test]
2203    fn parse_exif_streaming_text_only_png_returns_exif_not_found() {
2204        // text-only.png has no EXIF — the contract is ExifNotFound, not
2205        // UnexpectedEof. Pre-EOF-tolerance, this would surface
2206        // UnexpectedEof because mime detection consumed all 117 bytes.
2207        let mut parser = MediaParser::new();
2208        let ms = MediaSource::open("testdata/text-only.png").unwrap();
2209        let res = parser.parse_exif(ms);
2210        assert!(
2211            matches!(res, Err(crate::Error::ExifNotFound)),
2212            "expected ExifNotFound for tEXt-only PNG, got {:?}",
2213            res
2214        );
2215    }
2216
2217    #[test]
2218    fn parse_image_metadata_streaming_text_only_png() {
2219        let mut parser = MediaParser::new();
2220        let ms = MediaSource::open("testdata/text-only.png").unwrap();
2221        let img = parser.parse_image_metadata(ms).unwrap();
2222        assert!(img.exif.is_none());
2223        assert!(img.format.is_some());
2224    }
2225
2226    #[cfg(feature = "tokio")]
2227    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2228    async fn parse_exif_async_streaming_text_only_png_returns_exif_not_found() {
2229        use crate::parser_async::AsyncMediaSource;
2230        let mut parser = MediaParser::new();
2231        let f = tokio::fs::File::open("testdata/text-only.png")
2232            .await
2233            .unwrap();
2234        let ms = AsyncMediaSource::seekable(f).await.unwrap();
2235        let res = parser.parse_exif_async(ms).await;
2236        assert!(
2237            matches!(res, Err(crate::Error::ExifNotFound)),
2238            "expected ExifNotFound for tEXt-only PNG via async streaming, got {:?}",
2239            res
2240        );
2241    }
2242
2243    #[cfg(feature = "tokio")]
2244    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2245    async fn parse_image_metadata_async_streaming_text_only_png() {
2246        use crate::parser_async::AsyncMediaSource;
2247        let mut parser = MediaParser::new();
2248        let f = tokio::fs::File::open("testdata/text-only.png")
2249            .await
2250            .unwrap();
2251        let ms = AsyncMediaSource::seekable(f).await.unwrap();
2252        let img = parser.parse_image_metadata_async(ms).await.unwrap();
2253        assert!(img.exif.is_none());
2254        assert!(img.format.is_some());
2255    }
2256
2257    // --- WebP synthetic-buffer helpers (mirror src/webp.rs test builders) ---
2258    fn webp_minimal_tiff_le() -> Vec<u8> {
2259        // II + 0x002A + IFD0 offset 8 + IFD0 with one tag (Make="A") + next=0.
2260        let mut t = Vec::new();
2261        t.extend_from_slice(b"II");
2262        t.extend_from_slice(&[0x2a, 0x00]);
2263        t.extend_from_slice(&[0x08, 0, 0, 0]); // IFD0 at offset 8
2264        t.extend_from_slice(&[0x01, 0x00]); // 1 entry
2265                                            // Tag 0x010F (Make), type 2 (ASCII), count 2, inline value "A\0".
2266        t.extend_from_slice(&[0x0f, 0x01]); // tag 0x010F
2267        t.extend_from_slice(&[0x02, 0x00]); // type ASCII
2268        t.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // count 2
2269        t.extend_from_slice(b"A\0\0\0"); // value "A\0" padded to 4 bytes
2270        t.extend_from_slice(&[0, 0, 0, 0]); // next IFD = 0
2271        t
2272    }
2273
2274    fn webp_with_exif(tiff: &[u8]) -> Vec<u8> {
2275        let mut exif_chunk = Vec::new();
2276        exif_chunk.extend_from_slice(b"EXIF");
2277        exif_chunk.extend_from_slice(&(tiff.len() as u32).to_le_bytes());
2278        exif_chunk.extend_from_slice(tiff);
2279        if tiff.len() % 2 == 1 {
2280            exif_chunk.push(0);
2281        }
2282        let mut vp8x = Vec::new();
2283        vp8x.extend_from_slice(b"VP8X");
2284        vp8x.extend_from_slice(&10u32.to_le_bytes());
2285        vp8x.extend_from_slice(&[0u8; 10]);
2286
2287        let mut body = Vec::new();
2288        body.extend_from_slice(b"WEBP");
2289        body.extend_from_slice(&vp8x);
2290        body.extend_from_slice(&exif_chunk);
2291
2292        let mut out = Vec::new();
2293        out.extend_from_slice(b"RIFF");
2294        out.extend_from_slice(&(body.len() as u32).to_le_bytes());
2295        out.extend_from_slice(&body);
2296        out
2297    }
2298
2299    #[test]
2300    fn parse_exif_webp_from_memory() {
2301        let tiff = webp_minimal_tiff_le();
2302        let buf = webp_with_exif(&tiff);
2303        let mut parser = MediaParser::new();
2304        let ms = MediaSource::from_memory(buf).unwrap();
2305        let iter = parser.parse_exif(ms).unwrap();
2306        let exif: crate::Exif = iter.into();
2307        assert_eq!(
2308            exif.get(crate::ExifTag::Make).and_then(|v| v.as_str()),
2309            Some("A")
2310        );
2311    }
2312
2313    #[test]
2314    fn parse_exif_webp_no_exif_returns_not_found() {
2315        // RIFF/WEBP with only a VP8X chunk (no EXIF).
2316        let mut vp8x = Vec::new();
2317        vp8x.extend_from_slice(b"VP8X");
2318        vp8x.extend_from_slice(&10u32.to_le_bytes());
2319        vp8x.extend_from_slice(&[0u8; 10]);
2320        let mut body = Vec::new();
2321        body.extend_from_slice(b"WEBP");
2322        body.extend_from_slice(&vp8x);
2323        let mut buf = Vec::new();
2324        buf.extend_from_slice(b"RIFF");
2325        buf.extend_from_slice(&(body.len() as u32).to_le_bytes());
2326        buf.extend_from_slice(&body);
2327
2328        let mut parser = MediaParser::new();
2329        let ms = MediaSource::from_memory(buf).unwrap();
2330        let res = parser.parse_exif(ms);
2331        assert!(matches!(res, Err(crate::Error::ExifNotFound)));
2332    }
2333
2334    #[test]
2335    fn parse_exif_webp_streaming_small_file() {
2336        // Exercises the Png|Webp small-file EOF tolerance on the streaming
2337        // path: a sub-INIT_BUF_SIZE WebP fully consumed during MIME prefill.
2338        use std::io::Cursor;
2339        let tiff = webp_minimal_tiff_le();
2340        let buf = webp_with_exif(&tiff);
2341        let mut parser = MediaParser::new();
2342        let ms = MediaSource::seekable(Cursor::new(buf)).unwrap();
2343        let iter = parser.parse_exif(ms).unwrap();
2344        let exif: crate::Exif = iter.into();
2345        assert_eq!(
2346            exif.get(crate::ExifTag::Make).and_then(|v| v.as_str()),
2347            Some("A")
2348        );
2349    }
2350
2351    #[cfg(feature = "tokio")]
2352    #[tokio::test]
2353    async fn parse_exif_webp_async_from_memory() {
2354        use crate::AsyncMediaSource;
2355        let tiff = webp_minimal_tiff_le();
2356        let buf = webp_with_exif(&tiff);
2357        let mut parser = MediaParser::new();
2358        let ms = AsyncMediaSource::from_memory(buf).unwrap();
2359        let iter = parser.parse_exif_async(ms).await.unwrap();
2360        let exif: crate::Exif = iter.into();
2361        assert_eq!(
2362            exif.get(crate::ExifTag::Make).and_then(|v| v.as_str()),
2363            Some("A")
2364        );
2365    }
2366}