Skip to main content

nom_exif/exif/
exif_iter.rs

1use std::{collections::HashSet, fmt::Debug};
2
3use bytes::Bytes;
4use nom::{number::complete, Parser};
5
6use crate::{
7    error::EntryError,
8    slice::SliceChecked,
9    values::{DataFormat, EntryData, IRational, URational},
10    EntryValue, ExifTag,
11};
12
13use super::{exif_exif::IFD_ENTRY_SIZE, GPSInfo, LatLng, TiffHeader};
14use crate::TagOrCode;
15
16/// Index of an IFD (Image File Directory) within an EXIF blob.
17///
18/// `0` = main image (`IfdIndex::MAIN`), `1` = thumbnail (`IfdIndex::THUMBNAIL`),
19/// `>=2` = sub-IFDs in the order encountered. Use the constants for the common
20/// cases and [`IfdIndex::new`] for raw indexing.
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
23pub struct IfdIndex(usize);
24
25impl IfdIndex {
26    /// Index of the main image IFD (always `0`).
27    pub const MAIN: Self = IfdIndex(0);
28
29    /// Index of the thumbnail IFD (`1` when present).
30    pub const THUMBNAIL: Self = IfdIndex(1);
31
32    /// Construct from a raw index. `0`/`1` correspond to [`Self::MAIN`] /
33    /// [`Self::THUMBNAIL`]; values `>= 2` are sub-IFDs.
34    pub const fn new(index: usize) -> Self {
35        IfdIndex(index)
36    }
37
38    /// Underlying raw index as a `usize`.
39    pub const fn as_usize(self) -> usize {
40        self.0
41    }
42}
43
44impl std::fmt::Display for IfdIndex {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        write!(f, "ifd{}", self.0)
47    }
48}
49
50/// Which IFD *namespace* an entry belongs to.
51///
52/// Orthogonal to [`IfdIndex`]: the index identifies a position in the IFD
53/// chain (IFD0, IFD1, …), while the kind identifies the tag namespace. A GPS
54/// sub-IFD hanging off IFD0 is `IfdIndex::MAIN` + `IfdKind::Gps`.
55///
56/// This distinction matters because each namespace assigns its own meaning to
57/// the same 16-bit code — `0x000b` is `ProcessingSoftware` in [`Self::Tiff`]
58/// but `GPSDOP` in [`Self::Gps`]. Use [`crate::TagOrCode::from_code_in`] to
59/// resolve a code within a known namespace.
60///
61/// Marked `#[non_exhaustive]`: more namespaces (MakerNote, SubIFD, …) may be
62/// added, so `match` must carry a `_ =>` arm.
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
65#[non_exhaustive]
66pub enum IfdKind {
67    /// The TIFF/Exif image directories themselves (IFD0, IFD1, …).
68    Tiff,
69
70    /// The Exif private sub-IFD, reached via `ExifOffset` (`0x8769`).
71    Exif,
72
73    /// The GPS sub-IFD, reached via `GPSInfo` (`0x8825`).
74    Gps,
75
76    /// The Interoperability sub-IFD, reached via `InteropOffset` (`0xa005`).
77    Interop,
78}
79
80impl std::fmt::Display for IfdKind {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        let s = match self {
83            IfdKind::Tiff => "tiff",
84            IfdKind::Exif => "exif",
85            IfdKind::Gps => "gps",
86            IfdKind::Interop => "interop",
87        };
88        f.write_str(s)
89    }
90}
91
92/// Eager view into a single Exif entry. Yielded by [`crate::Exif::iter`] and
93/// designed to be cheap to copy: the `value` is a borrow into the parent
94/// [`crate::Exif`].
95///
96/// # Why pub fields instead of getters?
97///
98/// `ifd`, `tag`, and `value` are independent — there is no cross-field
99/// invariant to enforce. The Rust idiom for plain data carriers (cf.
100/// [`std::ops::Range`]) is `pub` fields. The lazy yield type
101/// [`crate::ExifIterEntry`] uses *private* fields because it carries a
102/// `value xor error` invariant.
103#[derive(Clone, Copy, Debug)]
104pub struct ExifEntry<'a> {
105    pub ifd: IfdIndex,
106    pub tag: TagOrCode,
107    pub value: &'a crate::EntryValue,
108}
109
110/// Eager view into a single Exif entry, carrying the IFD namespace it came
111/// from. Yielded by [`crate::Exif::entries`].
112///
113/// Supersedes [`ExifEntry`], which exposes `pub` fields and therefore cannot
114/// grow the `ifd_kind` accessor without a breaking change. Fields here are
115/// private specifically so that future namespace/context information can be
116/// added without another type.
117#[derive(Clone, Copy, Debug)]
118pub struct ExifEntryRef<'a> {
119    ifd: IfdIndex,
120    kind: IfdKind,
121    tag: TagOrCode,
122    value: &'a crate::EntryValue,
123}
124
125impl<'a> ExifEntryRef<'a> {
126    pub(crate) fn new(
127        ifd: IfdIndex,
128        kind: IfdKind,
129        tag: TagOrCode,
130        value: &'a crate::EntryValue,
131    ) -> Self {
132        Self {
133            ifd,
134            kind,
135            tag,
136            value,
137        }
138    }
139
140    /// Position in the IFD chain (`IfdIndex::MAIN` for the primary image).
141    pub fn ifd(&self) -> IfdIndex {
142        self.ifd
143    }
144
145    /// IFD *namespace* this entry was found in. Together with [`Self::ifd`]
146    /// and [`Self::tag`] this identifies the entry unambiguously.
147    pub fn ifd_kind(&self) -> IfdKind {
148        self.kind
149    }
150
151    /// Recognized tag, or raw `u16` code if not in [`ExifTag`].
152    pub fn tag(&self) -> TagOrCode {
153        self.tag
154    }
155
156    /// The parsed value, borrowed from the parent [`crate::Exif`].
157    pub fn value(&self) -> &'a crate::EntryValue {
158        self.value
159    }
160}
161
162/// Represents an additional TIFF data block to be processed after the primary block.
163/// Used for CR3 files with multiple CMT boxes (CMT1, CMT2, CMT3).
164#[derive(Clone)]
165pub(crate) struct TiffDataBlock {
166    /// Block identifier (e.g., "CMT1", "CMT2", "CMT3")
167    #[allow(dead_code)]
168    pub block_id: String,
169    /// Pre-sliced bytes view for this block's data
170    pub data: Bytes,
171    /// TIFF header information (optional, if known)
172    pub header: Option<TiffHeader>,
173}
174
175/// Parses header from input data, and returns an [`ExifIter`].
176///
177/// All entries are lazy-parsed. That is, only when you iterate over
178/// [`ExifIter`] will the IFD entries be parsed one by one.
179///
180/// The one exception is the time zone entries. The method will try to find
181/// and parse the time zone data first, so we can correctly parse all time
182/// information in subsequent iterates.
183#[tracing::instrument]
184pub(crate) fn input_into_iter(
185    input: impl Into<bytes::Bytes> + Debug,
186    state: Option<TiffHeader>,
187) -> crate::Result<ExifIter> {
188    let input: bytes::Bytes = input.into();
189    let header = match state {
190        // header has been parsed, and header has been skipped, input data
191        // is the IFD data
192        Some(header) => header,
193        _ => {
194            // header has not been parsed, input data includes IFD header
195            let (_, header) = TiffHeader::parse(&input[..]).map_err(|e| {
196                crate::error::nom_err_to_malformed(e, crate::error::MalformedKind::TiffHeader)
197            })?;
198
199            tracing::debug!(
200                ?header,
201                data_len = format!("{:#x}", input.len()),
202                "TIFF header parsed"
203            );
204            header
205        }
206    };
207
208    let start = header.ifd0_offset as usize;
209    if start > input.len() {
210        return Err(crate::Error::UnexpectedEof {
211            context: "exif iter init",
212        });
213    }
214    tracing::debug!(?header, offset = start);
215
216    let mut ifd0 = IfdIter::try_new(0, input.clone(), header.to_owned(), start, None)?;
217
218    let tz = ifd0.find_tz_offset();
219    ifd0.tz = tz.clone();
220    let iter: ExifIter = ExifIter::new(input, header, tz, ifd0);
221
222    tracing::debug!(?iter, "got IFD0");
223
224    Ok(iter)
225}
226
227/// An iterator version of [`Exif`](crate::Exif). Use [`ExifIterEntry`] as
228/// iterator items.
229///
230/// Clone an `ExifIter` is very cheap; the underlying data is shared
231/// via `bytes::Bytes` reference counting.
232///
233/// The new cloned `ExifIter`'s iteration index will be reset to the first one.
234///
235/// If you want to convert an `ExifIter` `into` an [`Exif`](crate::Exif), you probably want
236/// to clone the `ExifIter` and use the new cloned one to do the converting.
237/// Since the original's iteration index may have been modified by
238/// `Iterator::next()` calls.
239pub struct ExifIter {
240    input: Bytes,
241    tiff_header: TiffHeader,
242    tz: Option<String>,
243    ifd0: IfdIter,
244
245    // Iterating status
246    ifds: Vec<IfdIter>,
247    visited_offsets: HashSet<usize>,
248
249    // Multi-block support for CR3 files with multiple CMT boxes
250    /// Additional TIFF data blocks to process after the primary block
251    additional_blocks: Vec<TiffDataBlock>,
252    /// Current block index: 0 = primary block, 1+ = additional blocks
253    current_block_index: usize,
254    /// Tags encountered so far for duplicate filtering
255    /// (ifd_index, ifd_kind, tag_code). The namespace is part of the key
256    /// because sub-IFDs share their parent's index, so keying on the index
257    /// alone made a GPS tag look like a duplicate of an IFD0 tag.
258    encountered_tags: HashSet<(usize, IfdKind, u16)>,
259    has_embedded_track: bool,
260}
261
262impl Debug for ExifIter {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        f.debug_struct("ExifIter")
265            .field("data len", &self.input.len())
266            .field("tiff_header", &self.tiff_header)
267            .field("ifd0", &self.ifd0)
268            .field("state", &self.ifds.first().map(|x| (x.index, x.pos)))
269            .field("ifds num", &self.ifds.len())
270            .field("additional_blocks", &self.additional_blocks.len())
271            .field("current_block_index", &self.current_block_index)
272            .finish_non_exhaustive()
273    }
274}
275
276impl Clone for ExifIter {
277    fn clone(&self) -> Self {
278        self.clone_rewound()
279    }
280}
281
282impl ExifIter {
283    pub(crate) fn new(
284        input: bytes::Bytes,
285        tiff_header: TiffHeader,
286        tz: Option<String>,
287        ifd0: IfdIter,
288    ) -> ExifIter {
289        let ifds = vec![ifd0.clone()];
290        ExifIter {
291            input,
292            tiff_header,
293            tz,
294            ifd0,
295            ifds,
296            visited_offsets: HashSet::new(),
297            additional_blocks: Vec::new(),
298            current_block_index: 0,
299            encountered_tags: HashSet::new(),
300            has_embedded_track: false,
301        }
302    }
303
304    /// Clone with iteration state reset to entry 0.
305    ///
306    /// Cheap: `ExifIter` shares its underlying `bytes::Bytes` via refcount.
307    pub fn clone_rewound(&self) -> Self {
308        let ifd0 = self.ifd0.clone_and_rewind();
309        let ifds = vec![ifd0.clone()];
310        Self {
311            input: self.input.clone(),
312            tiff_header: self.tiff_header.clone(),
313            tz: self.tz.clone(),
314            ifd0,
315            ifds,
316            visited_offsets: HashSet::new(),
317            additional_blocks: self.additional_blocks.clone(),
318            current_block_index: 0,
319            encountered_tags: HashSet::new(),
320            has_embedded_track: self.has_embedded_track,
321        }
322    }
323
324    /// Reset iteration to the first entry (in-place). After this call,
325    /// `next()` yields entries starting from IFD0 entry 0 again.
326    pub fn rewind(&mut self) {
327        let ifd0 = self.ifd0.clone_and_rewind();
328        self.ifds = vec![ifd0.clone()];
329        self.ifd0 = ifd0;
330        self.visited_offsets.clear();
331        self.current_block_index = 0;
332        self.encountered_tags.clear();
333    }
334
335    /// Try to find and parse GPS information.
336    ///
337    /// Calling this method won't affect the iterator's state.
338    ///
339    /// Returns:
340    ///
341    /// - An `Ok<Some<GPSInfo>>` if gps info is found and parsed successfully.
342    /// - An `Ok<None>` if gps info is not found.
343    /// - An `Err` if gps info is found but parsing failed.
344    #[tracing::instrument(skip_all)]
345    pub fn parse_gps(&self) -> crate::Result<Option<GPSInfo>> {
346        let mut iter = self.clone_rewound();
347        let Some(gps) = iter.find(|x| {
348            tracing::info!(?x, "find");
349            x.tag().tag().is_some_and(|t| t == ExifTag::GPSInfo)
350        }) else {
351            tracing::warn!(ifd0 = ?iter.ifds.first(), "GPSInfo not found");
352            return Ok(None);
353        };
354
355        let offset = match gps.result() {
356            Ok(v) => {
357                if let Some(offset) = v.as_u32() {
358                    offset
359                } else {
360                    return Err(EntryError::InvalidValue("invalid gps offset").into());
361                }
362            }
363            Err(e) => return Err(e.clone().into()),
364        };
365        if offset as usize >= iter.input.len() {
366            return Err(crate::Error::Malformed {
367                kind: crate::error::MalformedKind::IfdEntry,
368                message: "GPSInfo offset out of range".into(),
369            });
370        }
371
372        let mut gps_subifd = IfdIter::try_new(
373            gps.ifd().as_usize(),
374            iter.input.clone(),
375            iter.tiff_header,
376            offset as usize,
377            iter.tz.clone(),
378        )?
379        .tag_code(ExifTag::GPSInfo.code());
380        Ok(gps_subifd.parse_gps_info())
381    }
382
383    /// Add an additional TIFF data block to be iterated after the current block.
384    /// Used internally for CR3 files with multiple CMT boxes.
385    ///
386    /// # Arguments
387    /// * `block_id` - Identifier for this TIFF block (e.g., "CMT2", "CMT3")
388    /// * `data` - Pre-sliced `Bytes` view containing this block's TIFF data
389    /// * `header` - Optional TIFF header if already parsed
390    pub(crate) fn add_tiff_block(
391        &mut self,
392        block_id: String,
393        data: bytes::Bytes,
394        header: Option<TiffHeader>,
395    ) {
396        self.additional_blocks.push(TiffDataBlock {
397            block_id,
398            data,
399            header,
400        });
401    }
402
403    /// Internal-only setter used by [`crate::MediaParser::parse_exif`] to
404    /// stamp the iterator with content-detected embedded-track information.
405    pub(crate) fn set_has_embedded_track(&mut self, v: bool) {
406        self.has_embedded_track = v;
407    }
408
409    /// Whether the source file is known to embed a paired media track that
410    /// `parse_exif` did *not* surface — a Pixel/Google or Samsung Galaxy
411    /// Motion Photo (JPEG with `GCamera:MotionPhoto` XMP and an MP4
412    /// trailer). Use [`crate::MediaParser::parse_track`] on the same
413    /// source to extract the embedded track.
414    ///
415    /// **Content-detected, not MIME-guessed**: returns `true` only when
416    /// the parser observes concrete signals during `parse_exif`
417    /// (`GCamera:MotionPhoto="1"` plus a `Container:Directory` /
418    /// `MotionPhotoOffset` / `MicroVideoOffset`). A plain JPEG or HEIC
419    /// without such signals returns `false`.
420    ///
421    /// **Coverage**: Pixel/Google Motion Photos and Samsung Galaxy
422    /// Motion Photos that use the Adobe XMP Container directory format
423    /// (JPEG variants).
424    pub fn has_embedded_track(&self) -> bool {
425        self.has_embedded_track
426    }
427
428    /// Deprecated alias for [`Self::has_embedded_track`].
429    #[deprecated(
430        since = "3.1.0",
431        note = "renamed to `has_embedded_track`; the original `has_embedded_media` was too vague and lumped in still-image previews"
432    )]
433    pub fn has_embedded_media(&self) -> bool {
434        self.has_embedded_track()
435    }
436}
437
438/// Lazy yield from [`ExifIter`]. Carries a *value xor error* invariant —
439/// every entry holds exactly one of [`Self::value`] or [`Self::error`].
440///
441/// # Why private fields?
442///
443/// Public fields would let callers construct nonsense like `value=Some,
444/// error=Some`. Private fields + getters preserve the invariant while
445/// exposing the natural API: [`Self::result`] for borrowed access,
446/// [`Self::into_result`] for ownership transfer (consumes `self`, no panic
447/// path).
448#[derive(Clone)]
449pub struct ExifIterEntry {
450    ifd: IfdIndex,
451    kind: IfdKind,
452    tag: TagOrCode,
453    res: Result<EntryValue, crate::error::EntryError>,
454}
455
456impl ExifIterEntry {
457    /// IFD this entry was found in (`IfdIndex::MAIN` for the primary image).
458    pub fn ifd(&self) -> IfdIndex {
459        self.ifd
460    }
461
462    /// IFD *namespace* this entry was found in. Pair with [`Self::ifd`] to
463    /// identify an entry unambiguously: the same code means different things
464    /// in different namespaces.
465    pub fn ifd_kind(&self) -> IfdKind {
466        self.kind
467    }
468
469    /// Recognized tag, or raw `u16` code if not in [`ExifTag`].
470    pub fn tag(&self) -> TagOrCode {
471        self.tag
472    }
473
474    /// Borrow the value. `None` iff this entry hit a parse error.
475    pub fn value(&self) -> Option<&EntryValue> {
476        self.res.as_ref().ok()
477    }
478
479    /// Borrow the error. `None` iff this entry parsed successfully.
480    pub fn error(&self) -> Option<&crate::error::EntryError> {
481        self.res.as_ref().err()
482    }
483
484    /// Borrow either value or error, mirroring the underlying invariant.
485    pub fn result(&self) -> Result<&EntryValue, &crate::error::EntryError> {
486        self.res.as_ref()
487    }
488
489    /// Consume self and return the value or error. No second-call panic
490    /// path (the entry is moved out).
491    pub fn into_result(self) -> Result<EntryValue, crate::error::EntryError> {
492        self.res
493    }
494
495    pub(crate) fn make_ok(ifd: usize, kind: IfdKind, tag: TagOrCode, v: EntryValue) -> Self {
496        Self {
497            ifd: IfdIndex::new(ifd),
498            kind,
499            tag,
500            res: Ok(v),
501        }
502    }
503}
504
505impl std::fmt::Debug for ExifIterEntry {
506    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507        let value = match &self.res {
508            Ok(v) => format!("{v}"),
509            Err(e) => format!("{e:?}"),
510        };
511        f.debug_struct("ExifIterEntry")
512            .field("ifd", &self.ifd)
513            .field("kind", &self.kind)
514            .field("tag", &self.tag)
515            .field("value", &value)
516            .finish()
517    }
518}
519
520const MAX_IFD_DEPTH: usize = 8;
521
522impl ExifIter {
523    /// Attempt to load and start iterating the next additional TIFF block.
524    /// Returns true if a new block was successfully loaded, false if no more blocks.
525    fn load_next_block(&mut self) -> bool {
526        // Move to the next additional block
527        let block_index = self.current_block_index;
528        if block_index >= self.additional_blocks.len() {
529            return false;
530        }
531
532        let block = &self.additional_blocks[block_index];
533        tracing::debug!(
534            block_id = block.block_id,
535            block_index,
536            "Loading additional TIFF block"
537        );
538
539        // Get the data for this block from the shared input
540        let block_data = block.data.clone();
541        let header = block.header.clone();
542
543        // Try to create an ExifIter for this block
544        match input_into_iter(block_data, header) {
545            Ok(iter) => {
546                // Update our state with the new block's data
547                self.ifd0 = iter.ifd0;
548                self.ifds = vec![self.ifd0.clone()];
549                self.visited_offsets.clear();
550                self.current_block_index += 1;
551
552                tracing::debug!(block_index, "Successfully loaded additional TIFF block");
553                true
554            }
555            Err(e) => {
556                tracing::warn!(
557                    block_index,
558                    error = %e,
559                    "Failed to load additional TIFF block, skipping"
560                );
561                // Move to next block and try again
562                self.current_block_index += 1;
563                self.load_next_block()
564            }
565        }
566    }
567
568    /// Check if a tag should be included based on duplicate filtering.
569    /// Returns true if the tag should be included, false if it's a duplicate.
570    fn should_include_tag(&mut self, ifd_index: usize, kind: IfdKind, tag_code: u16) -> bool {
571        let tag_key = (ifd_index, kind, tag_code);
572        if self.encountered_tags.contains(&tag_key) {
573            tracing::debug!(ifd_index, %kind, tag_code, "Skipping duplicate tag");
574            false
575        } else {
576            self.encountered_tags.insert(tag_key);
577            true
578        }
579    }
580}
581
582impl Iterator for ExifIter {
583    type Item = ExifIterEntry;
584
585    #[tracing::instrument(skip_all)]
586    fn next(&mut self) -> Option<Self::Item> {
587        loop {
588            if self.ifds.is_empty() {
589                // Current block exhausted, try to load next additional block
590                if !self.load_next_block() {
591                    tracing::debug!(?self, "all IFDs and blocks have been parsed");
592                    return None;
593                }
594                // Continue with the newly loaded block
595                continue;
596            }
597
598            if self.ifds.len() > MAX_IFD_DEPTH {
599                let depth = self.ifds.len();
600                self.ifds.clear();
601                tracing::error!(
602                    ifds_depth = depth,
603                    "ifd depth is too deep, just go back to ifd0"
604                );
605                self.ifds.push(self.ifd0.clone_with_state());
606            }
607
608            let mut ifd = self.ifds.pop()?;
609            let cur_ifd_idx = ifd.ifd_idx;
610            // A pointer entry belongs to the directory that *holds* it, not to
611            // the one it points at -- capture before `ifd` is moved below.
612            let parent_kind = ifd.kind;
613            match ifd.next() {
614                Some((tag_code, entry)) => {
615                    tracing::debug!(ifd = ifd.ifd_idx, ?tag_code, "next tag entry");
616
617                    match entry {
618                        IfdEntry::IfdNew(new_ifd) => {
619                            if new_ifd.offset > 0 {
620                                if self.visited_offsets.contains(&new_ifd.offset) {
621                                    // Ignore repeated ifd parsing to avoid dead looping
622                                    continue;
623                                }
624                                self.visited_offsets.insert(new_ifd.offset);
625                            }
626
627                            let is_subifd = if new_ifd.ifd_idx == ifd.ifd_idx {
628                                // Push the current ifd before enter sub-ifd.
629                                self.ifds.push(ifd);
630                                tracing::debug!(?tag_code, ?new_ifd, "got new SUB-IFD");
631                                true
632                            } else {
633                                // Otherwise this is a next ifd. It means that the
634                                // current ifd has been parsed, so we don't need to
635                                // push it.
636                                tracing::debug!("IFD{} parsing completed", cur_ifd_idx);
637                                tracing::debug!(?new_ifd, "got new IFD");
638                                false
639                            };
640
641                            let (ifd_idx, offset) = (new_ifd.ifd_idx, new_ifd.offset);
642                            self.ifds.push(new_ifd);
643
644                            if is_subifd {
645                                // Check for duplicates before returning sub-ifd entry
646                                let tc = tag_code.unwrap();
647                                if !self.should_include_tag(ifd_idx, parent_kind, tc.code()) {
648                                    continue;
649                                }
650                                // Return sub-ifd as an entry
651                                return Some(ExifIterEntry::make_ok(
652                                    ifd_idx,
653                                    parent_kind,
654                                    tc,
655                                    EntryValue::U32(offset as u32),
656                                ));
657                            }
658                        }
659                        IfdEntry::Entry(v) => {
660                            let tc = tag_code.unwrap();
661                            // Check for duplicates before returning entry
662                            if !self.should_include_tag(ifd.ifd_idx, ifd.kind, tc.code()) {
663                                self.ifds.push(ifd);
664                                continue;
665                            }
666                            let res = Some(ExifIterEntry::make_ok(ifd.ifd_idx, ifd.kind, tc, v));
667                            self.ifds.push(ifd);
668                            return res;
669                        }
670                        IfdEntry::Err(e) => {
671                            tracing::warn!(?tag_code, ?e, "parse ifd entry error");
672                            self.ifds.push(ifd);
673                            continue;
674                        }
675                    }
676                }
677                None => continue,
678            }
679        }
680    }
681}
682
683#[derive(Clone)]
684pub(crate) struct IfdIter {
685    ifd_idx: usize,
686    tag_code: Option<TagOrCode>,
687    /// Namespace of the entries this IFD yields, derived from `tag_code`
688    /// (the tag that pointed here). A directory reached by following the
689    /// IFD chain rather than a pointer is `Tiff`.
690    kind: IfdKind,
691
692    // starts from TIFF header
693    input: Bytes,
694
695    // ifd data offset
696    offset: usize,
697
698    header: TiffHeader,
699    entry_num: u16,
700
701    pub tz: Option<String>,
702
703    // Iterating status
704    index: u16,
705    pos: usize,
706}
707
708impl Debug for IfdIter {
709    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
710        f.debug_struct("IfdIter")
711            .field("ifd_idx", &self.ifd_idx)
712            .field("kind", &self.kind)
713            .field("tag", &self.tag_code)
714            .field("data len", &self.input.len())
715            .field("tz", &self.tz)
716            .field("header", &self.header)
717            .field("entry_num", &self.entry_num)
718            .field("index", &self.index)
719            .field("pos", &self.pos)
720            .finish()
721    }
722}
723
724impl IfdIter {
725    pub fn rewind(&mut self) {
726        self.index = 0;
727        // Skip the first two bytes, which is the entry num
728        self.pos = self.offset + 2;
729    }
730
731    pub fn clone_and_rewind(&self) -> Self {
732        let mut it = self.clone();
733        it.rewind();
734        it
735    }
736
737    pub fn tag_code_maybe(mut self, code: Option<u16>) -> Self {
738        self.tag_code = code.map(|x| x.into());
739        self.kind = Self::kind_for(code);
740        self
741    }
742
743    pub fn tag_code(self, code: u16) -> Self {
744        self.tag_code_maybe(Some(code))
745    }
746
747    /// Which namespace a sub-IFD pointer leads into. `None` (an IFD reached
748    /// through the chain, not a pointer) stays in the TIFF namespace.
749    fn kind_for(code: Option<u16>) -> IfdKind {
750        match code {
751            Some(c) if c == ExifTag::ExifOffset.code() => IfdKind::Exif,
752            Some(c) if c == ExifTag::GPSInfo.code() => IfdKind::Gps,
753            Some(c) if c == ExifTag::InteropOffset.code() => IfdKind::Interop,
754            _ => IfdKind::Tiff,
755        }
756    }
757
758    fn is_gps_subifd(&self) -> bool {
759        matches!(
760            self.tag_code.as_ref().and_then(|t| t.tag()),
761            Some(ExifTag::GPSInfo)
762        )
763    }
764
765    #[allow(unused)]
766    pub fn tag(mut self, tag: TagOrCode) -> Self {
767        self.tag_code = Some(tag);
768        self
769    }
770
771    #[tracing::instrument(skip(input))]
772    pub fn try_new(
773        ifd_idx: usize,
774        input: Bytes,
775        header: TiffHeader,
776        offset: usize,
777        tz: Option<String>,
778    ) -> crate::Result<Self> {
779        if input.len() < 2 {
780            return Err(crate::Error::Malformed {
781                kind: crate::error::MalformedKind::TiffHeader,
782                message: "ifd data too small to decode entry num".into(),
783            });
784        }
785        // should use the complete header data to parse ifd entry num
786        assert!(offset <= input.len());
787        let ifd_data = input.slice(offset..);
788        let (_, entry_num) =
789            TiffHeader::parse_ifd_entry_num(&ifd_data, header.endian).map_err(|e| {
790                crate::error::nom_err_to_malformed(e, crate::error::MalformedKind::TiffHeader)
791            })?;
792
793        Ok(Self {
794            ifd_idx,
795            tag_code: None,
796            kind: IfdKind::Tiff,
797            input,
798            offset,
799            header,
800            entry_num,
801            tz,
802            // Skip the first two bytes, which is the entry num
803            pos: offset + 2,
804            index: 0,
805        })
806    }
807
808    fn parse_tag_entry(&self, entry_data: &[u8]) -> Option<(u16, IfdEntry)> {
809        let endian = self.header.endian;
810        let (_, (tag, data_format, components_num, value_or_offset)) = (
811            complete::u16::<_, nom::error::Error<_>>(endian),
812            complete::u16(endian),
813            complete::u32(endian),
814            complete::u32(endian),
815        )
816            .parse(entry_data)
817            .ok()?;
818
819        // Tag 0 outside the GPS sub-IFD is treated as a sentinel for
820        // zero-padded malformed IFDs (overstated `entry_num`) and aborts
821        // iteration. Inside the GPS sub-IFD it is the legitimate
822        // GPSVersionID — let it parse normally.
823        if tag == 0 && !self.is_gps_subifd() {
824            return None;
825        }
826
827        let df: DataFormat = match DataFormat::try_from(data_format) {
828            Ok(df) => df,
829            Err(bad) => {
830                let t: TagOrCode = tag.into();
831                tracing::warn!(tag = ?t, format = bad, "invalid entry data format");
832                return Some((
833                    tag,
834                    IfdEntry::Err(EntryError::InvalidShape {
835                        format: bad,
836                        count: components_num,
837                    }),
838                ));
839            }
840        };
841        let (tag, res) = self.parse_entry(tag, df, components_num, entry_data, value_or_offset);
842        Some((tag, res))
843    }
844
845    fn get_data_pos(&self, value_or_offset: u32) -> usize {
846        // value_or_offset.saturating_sub(self.offset)
847        value_or_offset as usize
848    }
849
850    fn parse_entry(
851        &self,
852        tag: u16,
853        data_format: DataFormat,
854        components_num: u32,
855        entry_data: &[u8],
856        value_or_offset: u32,
857    ) -> (u16, IfdEntry) {
858        // get component_size according to data format
859        let component_size = data_format.component_size();
860
861        // get entry data
862        let size = components_num as usize * component_size;
863        let data = if size <= 4 {
864            &entry_data[8..8 + size] // Safe-slice
865        } else {
866            let start = self.get_data_pos(value_or_offset);
867            let end = start + size;
868            let Some(data) = self.input.slice_checked(start..end) else {
869                tracing::warn!(
870                    "entry data overflow, tag: {:04x} start: {:08x} end: {:08x} ifd data len {:08x}",
871                    tag,
872                    start,
873                    end,
874                    self.input.len(),
875                );
876                return (
877                    tag,
878                    IfdEntry::Err(EntryError::Truncated {
879                        needed: size,
880                        available: self.input.len().saturating_sub(start),
881                    }),
882                );
883            };
884
885            data
886        };
887
888        if SUBIFD_TAGS.contains(&tag) {
889            if let Some(value) = self.new_ifd_iter(self.ifd_idx, value_or_offset, Some(tag)) {
890                return (tag, value);
891            }
892        }
893
894        let entry = EntryData {
895            endian: self.header.endian,
896            tag,
897            data,
898            data_format,
899            components_num,
900        };
901        match EntryValue::parse(&entry, &self.tz) {
902            Ok(v) => (tag, IfdEntry::Entry(v)),
903            Err(e) => (tag, IfdEntry::Err(e)),
904        }
905    }
906
907    fn new_ifd_iter(
908        &self,
909        ifd_idx: usize,
910        value_or_offset: u32,
911        tag: Option<u16>,
912    ) -> Option<IfdEntry> {
913        let offset = self.get_data_pos(value_or_offset);
914        if offset < self.input.len() {
915            match IfdIter::try_new(
916                ifd_idx,
917                self.input.clone(),
918                self.header.to_owned(),
919                offset,
920                self.tz.clone(),
921            ) {
922                Ok(iter) => return Some(IfdEntry::IfdNew(iter.tag_code_maybe(tag))),
923                Err(e) => {
924                    tracing::warn!(?tag, ?e, "Create next/sub IFD failed");
925                }
926            }
927            // return (
928            //     tag,
929            //     // IfdEntry::Ifd {
930            //     //     idx: self.ifd_idx,
931            //     //     offset: value_or_offset,
932            //     // },
933            //     IfdEntry::IfdNew(),
934            // );
935        }
936        None
937    }
938
939    pub fn find_exif_iter(&self) -> Option<IfdIter> {
940        let endian = self.header.endian;
941        // find ExifOffset
942        for i in 0..self.entry_num {
943            let pos = self.pos + i as usize * IFD_ENTRY_SIZE;
944            let (_, tag) =
945                complete::u16::<_, nom::error::Error<_>>(endian)(&self.input[pos..]).ok()?;
946            if tag == ExifTag::ExifOffset.code() {
947                let entry_data = self.input.slice_checked(pos..pos + IFD_ENTRY_SIZE)?;
948                let (_, entry) = self.parse_tag_entry(entry_data)?;
949                match entry {
950                    IfdEntry::IfdNew(iter) => return Some(iter),
951                    IfdEntry::Entry(_) | IfdEntry::Err(_) => return None,
952                }
953            }
954        }
955        None
956    }
957
958    pub fn find_tz_offset(&self) -> Option<String> {
959        let iter = self.find_exif_iter()?;
960        let mut offset = None;
961        for entry in iter {
962            let Some(tag) = entry.0 else {
963                continue;
964            };
965            if tag.code() == ExifTag::OffsetTimeOriginal.code()
966                || tag.code() == ExifTag::OffsetTimeDigitized.code()
967            {
968                return entry.1.as_str().map(|x| x.to_owned());
969            } else if tag.code() == ExifTag::OffsetTime.code() {
970                offset = entry.1.as_str().map(|x| x.to_owned());
971            }
972        }
973
974        offset
975    }
976
977    // Assume the current ifd is GPSInfo subifd.
978    pub fn parse_gps_info(&mut self) -> Option<GPSInfo> {
979        use crate::exif::gps::{Altitude, LatRef, LonRef, Speed, SpeedUnit};
980
981        let mut latitude_ref = None;
982        let mut latitude = None;
983        let mut longitude_ref = None;
984        let mut longitude = None;
985        let mut altitude_ref = None;
986        let mut altitude_value = None;
987        let mut speed_unit = None;
988        let mut speed_value = None;
989        let mut has_data = false;
990
991        for (tag, entry) in self {
992            let Some(tag) = tag.and_then(|x| x.tag()) else {
993                continue;
994            };
995            has_data = true;
996            match tag {
997                ExifTag::GPSLatitudeRef => {
998                    latitude_ref = entry.as_char().and_then(LatRef::from_char);
999                }
1000                ExifTag::GPSLongitudeRef => {
1001                    longitude_ref = entry.as_char().and_then(LonRef::from_char);
1002                }
1003                ExifTag::GPSAltitudeRef => {
1004                    altitude_ref = entry.as_u8();
1005                }
1006                ExifTag::GPSLatitude => {
1007                    if let Some(v) = entry.as_urational_slice() {
1008                        latitude = LatLng::try_from(v).ok();
1009                    } else if let Some(v) = entry.as_irational_slice() {
1010                        latitude = LatLng::try_from(v).ok();
1011                    }
1012                }
1013                ExifTag::GPSLongitude => {
1014                    if let Some(v) = entry.as_urational_slice() {
1015                        longitude = LatLng::try_from(v).ok();
1016                    } else if let Some(v) = entry.as_irational_slice() {
1017                        longitude = LatLng::try_from(v).ok();
1018                    }
1019                }
1020                ExifTag::GPSAltitude => {
1021                    if let Some(v) = entry.as_urational() {
1022                        altitude_value = Some(*v);
1023                    } else if let Some(v) = entry.as_irational() {
1024                        if let Ok(u) = URational::try_from(*v) {
1025                            altitude_value = Some(u);
1026                        }
1027                    }
1028                }
1029                ExifTag::GPSSpeedRef => {
1030                    speed_unit = entry.as_char().and_then(SpeedUnit::from_char);
1031                }
1032                ExifTag::GPSSpeed => {
1033                    if let Some(v) = entry.as_urational() {
1034                        speed_value = Some(*v);
1035                    } else if let Some(v) = entry.as_irational() {
1036                        if let Ok(u) = URational::try_from(*v) {
1037                            speed_value = Some(u);
1038                        }
1039                    }
1040                }
1041                _ => (),
1042            }
1043        }
1044
1045        if !has_data {
1046            tracing::warn!("GPSInfo data not found");
1047            return None;
1048        }
1049
1050        let altitude = match (altitude_ref, altitude_value) {
1051            (Some(0), Some(v)) => Altitude::AboveSeaLevel(v),
1052            (Some(1), Some(v)) => Altitude::BelowSeaLevel(v),
1053            _ => Altitude::Unknown,
1054        };
1055
1056        let speed = match (speed_unit, speed_value) {
1057            (Some(unit), Some(value)) => Some(Speed { unit, value }),
1058            _ => None,
1059        };
1060
1061        Some(GPSInfo {
1062            latitude_ref: latitude_ref.unwrap_or(LatRef::North),
1063            latitude: latitude.unwrap_or_default(),
1064            longitude_ref: longitude_ref.unwrap_or(LonRef::East),
1065            longitude: longitude.unwrap_or_default(),
1066            altitude,
1067            speed,
1068        })
1069    }
1070
1071    fn clone_with_state(&self) -> IfdIter {
1072        let mut it = self.clone();
1073        it.index = self.index;
1074        it.pos = self.pos;
1075        it
1076    }
1077}
1078
1079#[derive(Debug)]
1080pub(crate) enum IfdEntry {
1081    IfdNew(IfdIter), // ifd index
1082    Entry(EntryValue),
1083    Err(EntryError),
1084}
1085
1086impl IfdEntry {
1087    pub fn as_u8(&self) -> Option<u8> {
1088        if let IfdEntry::Entry(EntryValue::U8(v)) = self {
1089            Some(*v)
1090        } else {
1091            None
1092        }
1093    }
1094
1095    pub fn as_char(&self) -> Option<char> {
1096        if let IfdEntry::Entry(EntryValue::Text(s)) = self {
1097            s.chars().next()
1098        } else {
1099            None
1100        }
1101    }
1102
1103    fn as_irational(&self) -> Option<&IRational> {
1104        if let IfdEntry::Entry(EntryValue::IRational(v)) = self {
1105            Some(v)
1106        } else {
1107            None
1108        }
1109    }
1110
1111    fn as_irational_slice(&self) -> Option<&Vec<IRational>> {
1112        if let IfdEntry::Entry(EntryValue::IRationalArray(v)) = self {
1113            Some(v)
1114        } else {
1115            None
1116        }
1117    }
1118
1119    fn as_urational(&self) -> Option<&URational> {
1120        if let IfdEntry::Entry(EntryValue::URational(v)) = self {
1121            Some(v)
1122        } else {
1123            None
1124        }
1125    }
1126
1127    fn as_urational_slice(&self) -> Option<&Vec<URational>> {
1128        if let IfdEntry::Entry(EntryValue::URationalArray(v)) = self {
1129            Some(v)
1130        } else {
1131            None
1132        }
1133    }
1134
1135    fn as_str(&self) -> Option<&str> {
1136        if let IfdEntry::Entry(e) = self {
1137            e.as_str()
1138        } else {
1139            None
1140        }
1141    }
1142}
1143
1144/// Tags whose value is an offset to a nested IFD rather than data. Each one
1145/// opens a distinct tag namespace — see [`IfdKind`].
1146pub(crate) const SUBIFD_TAGS: &[u16] = &[
1147    ExifTag::ExifOffset.code(),
1148    ExifTag::GPSInfo.code(),
1149    ExifTag::InteropOffset.code(),
1150];
1151
1152impl Iterator for IfdIter {
1153    type Item = (Option<TagOrCode>, IfdEntry);
1154
1155    #[tracing::instrument(skip(self))]
1156    fn next(&mut self) -> Option<Self::Item> {
1157        tracing::debug!(
1158            ifd = self.ifd_idx,
1159            index = self.index,
1160            entry_num = self.entry_num,
1161            offset = format!("{:08x}", self.offset),
1162            pos = format!("{:08x}", self.pos),
1163            "next IFD entry"
1164        );
1165        if self.input.len() < self.pos + IFD_ENTRY_SIZE {
1166            return None;
1167        }
1168
1169        let endian = self.header.endian;
1170        if self.index > self.entry_num {
1171            return None;
1172        }
1173        if self.index == self.entry_num {
1174            tracing::debug!(
1175                self.ifd_idx,
1176                self.index,
1177                pos = self.pos,
1178                "try to get next ifd"
1179            );
1180            self.index += 1;
1181
1182            // next IFD offset
1183            let (_, offset) =
1184                complete::u32::<_, nom::error::Error<_>>(endian)(&self.input[self.pos..]).ok()?;
1185
1186            if offset == 0 {
1187                // IFD parsing completed
1188                tracing::debug!(?self, "IFD parsing completed");
1189                return None;
1190            }
1191
1192            return self
1193                .new_ifd_iter(self.ifd_idx + 1, offset, None)
1194                .map(|x| (None, x));
1195        }
1196
1197        let entry_data = self
1198            .input
1199            .slice_checked(self.pos..self.pos + IFD_ENTRY_SIZE)?;
1200        self.index += 1;
1201        self.pos += IFD_ENTRY_SIZE;
1202
1203        let (tag, res) = self.parse_tag_entry(entry_data)?;
1204
1205        Some((Some(TagOrCode::from_code_in(self.kind, tag)), res)) // Safe-slice
1206    }
1207}
1208
1209#[cfg(test)]
1210mod tests {
1211
1212    use crate::exif::extract_exif_with_mime;
1213    use crate::exif::input_into_iter;
1214    use crate::file::MediaMimeImage;
1215    use crate::slice::SubsliceRange;
1216    use crate::testkit::read_sample;
1217    use crate::Exif;
1218    use test_case::test_case;
1219
1220    #[test_case(
1221        "exif.jpg",
1222        "+08:00",
1223        "2023-07-09T20:36:33+08:00",
1224        MediaMimeImage::Jpeg
1225    )]
1226    #[test_case("exif-no-tz.jpg", "", "2023-07-09 20:36:33", MediaMimeImage::Jpeg)]
1227    #[test_case("broken.jpg", "-", "2014-09-21 15:51:22", MediaMimeImage::Jpeg)]
1228    #[test_case(
1229        "exif.heic",
1230        "+08:00",
1231        "2022-07-22T21:26:32+08:00",
1232        MediaMimeImage::Heic
1233    )]
1234    #[test_case(
1235        "exif.avif",
1236        "+08:00",
1237        "2022-07-22T21:26:32+08:00",
1238        MediaMimeImage::Avif
1239    )]
1240    #[test_case("tif.tif", "-", "-", MediaMimeImage::Tiff)]
1241    #[test_case(
1242        "fujifilm_x_t1_01.raf.meta",
1243        "-",
1244        "2014-01-30 12:49:13",
1245        MediaMimeImage::Raf
1246    )]
1247    fn exif_iter_tz(path: &str, tz: &str, time: &str, img_type: MediaMimeImage) {
1248        let buf = read_sample(path).unwrap();
1249        let (data, _) = extract_exif_with_mime(img_type, &buf, None).unwrap();
1250        let range = data.and_then(|x| buf.subslice_in_range(x)).unwrap();
1251        let iter = input_into_iter(bytes::Bytes::from(buf).slice(range), None).unwrap();
1252        let expect = if tz == "-" {
1253            None
1254        } else {
1255            Some(tz.to_string())
1256        };
1257        assert_eq!(iter.tz, expect);
1258        let exif: Exif = iter.into();
1259        let value = exif.get(crate::ExifTag::DateTimeOriginal);
1260        if time == "-" {
1261            assert!(value.is_none());
1262        } else {
1263            let value = value.unwrap();
1264            assert_eq!(value.to_string(), time);
1265        }
1266    }
1267
1268    #[test]
1269    fn ifd_index_constants() {
1270        use crate::IfdIndex;
1271        assert_eq!(IfdIndex::MAIN.as_usize(), 0);
1272        assert_eq!(IfdIndex::THUMBNAIL.as_usize(), 1);
1273    }
1274
1275    #[test]
1276    fn ifd_index_roundtrip_via_new_and_as_usize() {
1277        use crate::IfdIndex;
1278        for raw in [0, 1, 2, 3, 7, 99] {
1279            assert_eq!(IfdIndex::new(raw).as_usize(), raw);
1280        }
1281    }
1282
1283    #[test]
1284    fn ifd_index_equality_and_hash() {
1285        use crate::IfdIndex;
1286        use std::collections::HashSet;
1287        let mut set: HashSet<IfdIndex> = HashSet::new();
1288        set.insert(IfdIndex::MAIN);
1289        set.insert(IfdIndex::new(0)); // duplicate
1290        set.insert(IfdIndex::THUMBNAIL);
1291        assert_eq!(set.len(), 2);
1292    }
1293
1294    #[test]
1295    fn ifd_index_display_format() {
1296        use crate::IfdIndex;
1297        assert_eq!(format!("{}", IfdIndex::MAIN), "ifd0");
1298        assert_eq!(format!("{}", IfdIndex::new(7)), "ifd7");
1299    }
1300
1301    #[test]
1302    fn tag_or_code_for_known_tag_resolves_to_tag_variant() {
1303        use crate::{ExifTag, TagOrCode};
1304        let t: TagOrCode = ExifTag::Make.code().into();
1305        assert_eq!(t, TagOrCode::Tag(ExifTag::Make));
1306        assert_eq!(t.code(), ExifTag::Make.code());
1307    }
1308
1309    #[test]
1310    fn tag_or_code_for_unknown_tag_resolves_to_unknown_variant() {
1311        use crate::TagOrCode;
1312        let t: TagOrCode = 0xffff_u16.into();
1313        assert_eq!(t, TagOrCode::Unknown(0xffff));
1314        assert_eq!(t.code(), 0xffff);
1315    }
1316
1317    #[test]
1318    fn exif_entry_pub_fields_construct_and_destructure() {
1319        use crate::{EntryValue, ExifEntry, ExifTag, IfdIndex, TagOrCode};
1320        let val = EntryValue::Text("vivo X90 Pro+".into());
1321        let e = ExifEntry {
1322            ifd: IfdIndex::MAIN,
1323            tag: TagOrCode::Tag(ExifTag::Model),
1324            value: &val,
1325        };
1326        // Pub fields: just match.
1327        let ExifEntry { ifd, tag, value } = e;
1328        assert_eq!(ifd, IfdIndex::MAIN);
1329        assert_eq!(tag.code(), ExifTag::Model.code());
1330        assert!(matches!(value, EntryValue::Text(_)));
1331        // Copy works because EntryValue is borrowed.
1332        let _e2 = e;
1333        let _e3 = e;
1334    }
1335
1336    #[test]
1337    fn exif_iter_entry_value_xor_error_invariant() {
1338        use crate::{MediaParser, MediaSource};
1339        let mut parser = MediaParser::new();
1340        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1341        for entry in parser.parse_exif(ms).unwrap() {
1342            // Exactly one of value / error is Some.
1343            let has_v = entry.value().is_some();
1344            let has_e = entry.error().is_some();
1345            assert!(has_v ^ has_e, "entry must be value xor error");
1346            // result() agrees with value()/error().
1347            match entry.result() {
1348                Ok(v) => assert_eq!(Some(v), entry.value()),
1349                Err(e) => assert_eq!(Some(e), entry.error()),
1350            }
1351        }
1352    }
1353
1354    #[test]
1355    fn exif_iter_entry_into_result_consumes_self() {
1356        use crate::{MediaParser, MediaSource};
1357        let mut parser = MediaParser::new();
1358        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1359        let mut count_ok = 0usize;
1360        for entry in parser.parse_exif(ms).unwrap() {
1361            // into_result consumes; once consumed, we can't call any other
1362            // method (the entry is gone). This is the spec's panic-free
1363            // replacement for v2's take_result.
1364            if entry.into_result().is_ok() {
1365                count_ok += 1;
1366            }
1367        }
1368        assert!(count_ok > 0);
1369    }
1370
1371    #[test]
1372    fn exif_iter_entry_tag_returns_tag_or_code() {
1373        use crate::{ExifTag, MediaParser, MediaSource, TagOrCode};
1374        let mut parser = MediaParser::new();
1375        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1376        let make_present = parser
1377            .parse_exif(ms)
1378            .unwrap()
1379            .any(|e| matches!(e.tag(), TagOrCode::Tag(ExifTag::Make)));
1380        assert!(make_present);
1381    }
1382
1383    #[test]
1384    fn exif_iter_rewind_resets_iteration_state() {
1385        use crate::{MediaParser, MediaSource};
1386        let mut parser = MediaParser::new();
1387        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1388        let mut iter = parser.parse_exif(ms).unwrap();
1389        let first_count = iter.by_ref().count();
1390        assert!(first_count > 0);
1391        // Already exhausted.
1392        assert_eq!(iter.by_ref().count(), 0);
1393        iter.rewind();
1394        let after_rewind = iter.count();
1395        assert_eq!(first_count, after_rewind);
1396    }
1397
1398    #[test]
1399    fn exif_iter_clone_rewound_yields_independent_full_iter() {
1400        use crate::{MediaParser, MediaSource};
1401        let mut parser = MediaParser::new();
1402        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1403        let mut iter = parser.parse_exif(ms).unwrap();
1404        let _consumed = iter.by_ref().take(2).count();
1405        let cloned = iter.clone_rewound();
1406        // cloned starts from entry 0 even though `iter` consumed 2 entries.
1407        let cloned_total = cloned.count();
1408        let remaining = iter.count();
1409        assert!(cloned_total > remaining);
1410    }
1411
1412    #[test]
1413    fn exif_iter_parse_gps_returns_option_no_iteration_advance() {
1414        use crate::{MediaParser, MediaSource};
1415        let mut parser = MediaParser::new();
1416        let ms = MediaSource::open("testdata/exif.jpg").unwrap();
1417        let iter = parser.parse_exif(ms).unwrap();
1418        let gps = iter.parse_gps().unwrap();
1419        assert!(gps.is_some());
1420        // parse_gps doesn't drive the outer iterator.
1421        let count = iter.count();
1422        assert!(count > 0);
1423    }
1424
1425    // Regression test for https://github.com/mindeng/nom-exif/issues/50:
1426    // GPS sub-IFDs whose first entry is GPSVersionID (tag 0x0000), as emitted
1427    // by Sony A7C2 HIF files. A previous defensive `tag == 0` short-circuit
1428    // in `parse_tag_entry` aborted iteration on that entry and discarded the
1429    // whole sub-IFD. This builds the minimal little-endian TIFF that triggers
1430    // it: IFD0 → GPSInfo → GPS sub-IFD with GPSVersionID up front.
1431    #[test]
1432    fn gps_subifd_first_entry_is_gpsversion_id_issue_50() {
1433        use crate::exif::exif_iter::input_into_iter;
1434        #[rustfmt::skip]
1435        let tiff: &[u8] = &[
1436            // TIFF header: little-endian, IFD0 at 0x08
1437            b'I', b'I', 0x2a, 0x00,
1438            0x08, 0x00, 0x00, 0x00,
1439
1440            // IFD0 @ 0x08: 1 entry → GPSInfo pointer to GPS sub-IFD @ 0x1a
1441            0x01, 0x00,
1442            0x25, 0x88, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00,
1443            0x1a, 0x00, 0x00, 0x00,
1444            0x00, 0x00, 0x00, 0x00,                         // no IFD1
1445
1446            // GPS sub-IFD @ 0x1a: 5 entries
1447            0x05, 0x00,
1448            // [0] GPSVersionID tag=0, BYTE×4, inline [2,3,0,0]
1449            0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00,
1450            0x02, 0x03, 0x00, 0x00,
1451            // [1] GPSLatitudeRef tag=1, ASCII×2 "N\0"
1452            0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00,
1453            b'N', 0x00, 0x00, 0x00,
1454            // [2] GPSLatitude tag=2, RATIONAL×3 @ 0x5c
1455            0x02, 0x00, 0x05, 0x00, 0x03, 0x00, 0x00, 0x00,
1456            0x5c, 0x00, 0x00, 0x00,
1457            // [3] GPSLongitudeRef tag=3, ASCII×2 "E\0"
1458            0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00,
1459            b'E', 0x00, 0x00, 0x00,
1460            // [4] GPSLongitude tag=4, RATIONAL×3 @ 0x74
1461            0x04, 0x00, 0x05, 0x00, 0x03, 0x00, 0x00, 0x00,
1462            0x74, 0x00, 0x00, 0x00,
1463            0x00, 0x00, 0x00, 0x00,                         // no next sub-IFD
1464
1465            // GPSLatitude rational data @ 0x5c: 36/1, 0/1, 0/1
1466            0x24, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
1467            0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
1468            0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
1469
1470            // GPSLongitude rational data @ 0x74: 120/1, 0/1, 0/1
1471            0x78, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
1472            0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
1473            0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
1474        ];
1475
1476        let iter = input_into_iter(tiff.to_vec(), None).unwrap();
1477
1478        // parse_gps recovers the full sub-IFD despite GPSVersionID being first.
1479        let gps = iter
1480            .parse_gps()
1481            .expect("parse_gps must succeed")
1482            .expect("GPS sub-IFD with GPSVersionID first must yield GPSInfo");
1483        assert_eq!(gps.latitude_decimal(), Some(36.0));
1484        assert_eq!(gps.longitude_decimal(), Some(120.0));
1485
1486        // GPSVersionID itself is also surfaced through normal iteration —
1487        // tag 0 is no longer dropped inside the GPS sub-IFD.
1488        let tags: Vec<u16> = iter.map(|e| e.tag().code()).collect();
1489        assert!(
1490            tags.contains(&crate::ExifTag::GPSVersionID.code()),
1491            "GPSVersionID (tag 0) should be visible to iterators; got {tags:?}"
1492        );
1493    }
1494
1495    /// Map tag code -> IfdKind for every entry a file yields.
1496    fn kinds_by_code(path: &str) -> std::collections::HashMap<u16, crate::IfdKind> {
1497        use crate::{ExifIter, MediaParser, MediaSource};
1498        let mut parser = MediaParser::new();
1499        let ms = MediaSource::open(path).unwrap();
1500        let iter: ExifIter = parser.parse_exif(ms).unwrap();
1501        iter.map(|e| (e.tag().code(), e.ifd_kind())).collect()
1502    }
1503
1504    #[test]
1505    fn entries_report_the_ifd_namespace_they_came_from() {
1506        use crate::{ExifTag, IfdKind};
1507        let kinds = kinds_by_code("./testdata/exif.jpg");
1508
1509        assert_eq!(kinds[&ExifTag::Make.code()], IfdKind::Tiff);
1510        assert_eq!(kinds[&ExifTag::DateTimeOriginal.code()], IfdKind::Exif);
1511        assert_eq!(kinds[&ExifTag::GPSLatitude.code()], IfdKind::Gps);
1512    }
1513
1514    #[test]
1515    fn subifd_pointer_entries_belong_to_the_parent_namespace() {
1516        use crate::{ExifTag, IfdKind};
1517        let kinds = kinds_by_code("./testdata/exif.jpg");
1518
1519        // The pointer lives in IFD0, only what it points *at* is Exif/GPS.
1520        assert_eq!(kinds[&ExifTag::ExifOffset.code()], IfdKind::Tiff);
1521        assert_eq!(kinds[&ExifTag::GPSInfo.code()], IfdKind::Tiff);
1522    }
1523
1524    #[test]
1525    fn thumbnail_ifd_entries_are_tiff_namespace() {
1526        use crate::{ExifIter, IfdIndex, IfdKind, MediaParser, MediaSource};
1527        let mut parser = MediaParser::new();
1528        let ms = MediaSource::open("./testdata/exif.jpg").unwrap();
1529        let iter: ExifIter = parser.parse_exif(ms).unwrap();
1530        let thumb: Vec<_> = iter.filter(|e| e.ifd() == IfdIndex::THUMBNAIL).collect();
1531
1532        assert!(!thumb.is_empty(), "exif.jpg should have a thumbnail IFD");
1533        for e in thumb {
1534            assert_eq!(e.ifd_kind(), IfdKind::Tiff, "entry {:?}", e.tag());
1535        }
1536    }
1537}