Skip to main content

zenmeta/ifd/
walk.rs

1//! Crate-agnostic TIFF structure walk for structural inventories.
2//!
3//! Shared by `zentiff` and `zenraw`; each supplies its decoder rules.
4//! The walk parses a TIFF header, every IFD reachable from the IFD
5//! chain and from pointer entries, out-of-line values and the extents
6//! (strips, tiles, free space, JPEG streams) the directories reference. A
7//! crate supplies [`Rules`] saying what its decode path does with each piece;
8//! the walk never decodes pixels.
9//!
10//! TIFF parts can interleave in any order, so a TIFF's parts are siblings in
11//! file order: header, IFDs (each with one child `Field` per entry),
12//! out-of-line values (`Field`, same tag as their entry) and extents.
13//! Where parts overlap, each byte takes the strongest fate among the parts
14//! covering it ([`Placed`]); the others are split around it, with a note in
15//! both details. Nothing overlapping is ever emitted.
16
17use alloc::borrow::Cow;
18use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
19use alloc::format;
20use alloc::string::String;
21use alloc::vec::Vec;
22use core::cell::Cell;
23use core::ops::Range;
24
25use zencodec::inventory::{
26    Disposition, Inventory, InventoryError, Part, PartId, PartKind, PartTag,
27};
28
29use super::tags::tag_name;
30
31// ── Limits ─────────────────────────────────────────────────────────────
32
33/// IFDs walked per TIFF. Real files carry a handful; multi-page scans a few
34/// thousand.
35const MAX_IFDS: usize = 4096;
36/// Detail for bytes a walk could not account for because a budget or cap
37/// stopped it.
38pub const BUDGET_GAP: &str = "the walk stopped at a budget; the decoder may read these bytes";
39/// Notes appended to one part's detail before the rest are only counted.
40const MAX_NOTES: usize = 4;
41/// Pieces one overlapping part may be split into.
42const MAX_PIECES: usize = 64;
43/// Kinds of path recorded for one directory.
44const MAX_ROLES: usize = 8;
45
46// ── Tags the walk itself interprets ────────────────────────────────────
47
48pub const STRIP_OFFSETS: u16 = 273;
49pub const STRIP_BYTE_COUNTS: u16 = 279;
50pub const FREE_OFFSETS: u16 = 288;
51pub const FREE_BYTE_COUNTS: u16 = 289;
52pub const TILE_OFFSETS: u16 = 324;
53pub const TILE_BYTE_COUNTS: u16 = 325;
54pub const SUB_IFDS: u16 = 330;
55pub const JPEG_IF_OFFSET: u16 = 513;
56pub const JPEG_IF_LENGTH: u16 = 514;
57pub const JPEG_Q_TABLES: u16 = 519;
58pub const JPEG_DC_TABLES: u16 = 520;
59pub const JPEG_AC_TABLES: u16 = 521;
60pub const EXIF_IFD: u16 = 34665;
61pub const GPS_IFD: u16 = 34853;
62pub const INTEROP_IFD: u16 = 40965;
63
64// ── Byte access ────────────────────────────────────────────────────────
65
66pub fn get(d: &[u8], at: u64, n: u64) -> Option<&[u8]> {
67    let s = usize::try_from(at).ok()?;
68    let e = s.checked_add(usize::try_from(n).ok()?)?;
69    d.get(s..e)
70}
71
72/// Byte order and offset width.
73#[derive(Clone, Copy, Debug)]
74pub struct Layout {
75    pub le: bool,
76    pub big: bool,
77}
78
79impl Layout {
80    pub fn u16(self, d: &[u8], at: u64) -> Option<u16> {
81        let b: [u8; 2] = get(d, at, 2)?.try_into().ok()?;
82        Some(if self.le {
83            u16::from_le_bytes(b)
84        } else {
85            u16::from_be_bytes(b)
86        })
87    }
88
89    pub fn u32(self, d: &[u8], at: u64) -> Option<u32> {
90        let b: [u8; 4] = get(d, at, 4)?.try_into().ok()?;
91        Some(if self.le {
92            u32::from_le_bytes(b)
93        } else {
94            u32::from_be_bytes(b)
95        })
96    }
97
98    pub fn u64(self, d: &[u8], at: u64) -> Option<u64> {
99        let b: [u8; 8] = get(d, at, 8)?.try_into().ok()?;
100        Some(if self.le {
101            u64::from_le_bytes(b)
102        } else {
103            u64::from_be_bytes(b)
104        })
105    }
106
107    /// An offset field: 4 bytes, 8 in BigTIFF.
108    pub fn offset(self, d: &[u8], at: u64) -> Option<u64> {
109        if self.big {
110            self.u64(d, at)
111        } else {
112            self.u32(d, at).map(u64::from)
113        }
114    }
115
116    pub fn header_len(self) -> u64 {
117        if self.big { 16 } else { 8 }
118    }
119
120    pub fn count_len(self) -> u64 {
121        if self.big { 8 } else { 2 }
122    }
123
124    pub fn entry_len(self) -> u64 {
125        if self.big { 20 } else { 12 }
126    }
127
128    /// Bytes a value may occupy inside its entry.
129    pub fn inline_cap(self) -> u64 {
130        if self.big { 8 } else { 4 }
131    }
132}
133
134/// Size of one value of a TIFF field type, `None` for a type TIFF does not
135/// define.
136pub fn type_size(typ: u16) -> Option<u64> {
137    Some(match typ {
138        1 | 2 | 6 | 7 => 1,
139        3 | 8 => 2,
140        4 | 9 | 11 | 13 => 4,
141        5 | 10 | 12 | 16 | 17 | 18 => 8,
142        _ => return None,
143    })
144}
145
146pub fn type_name(typ: u16) -> Cow<'static, str> {
147    Cow::Borrowed(match typ {
148        1 => "BYTE",
149        2 => "ASCII",
150        3 => "SHORT",
151        4 => "LONG",
152        5 => "RATIONAL",
153        6 => "SBYTE",
154        7 => "UNDEFINED",
155        8 => "SSHORT",
156        9 => "SLONG",
157        10 => "SRATIONAL",
158        11 => "FLOAT",
159        12 => "DOUBLE",
160        13 => "IFD",
161        16 => "LONG8",
162        17 => "SLONG8",
163        18 => "IFD8",
164        other => return Cow::Owned(format!("type {other}")),
165    })
166}
167
168// ── Parsed structure ───────────────────────────────────────────────────
169
170/// Which directory an IFD is, by how the walk reached it.
171#[derive(Clone, Copy, Debug, PartialEq, Eq)]
172pub enum Kind {
173    /// The n-th IFD of the main chain (page n).
174    Page(u32),
175    /// Reached through a SubIFDs-style pointer, or chained after one.
176    Sub,
177    /// The EXIF IFD (34665), or chained after it.
178    Exif,
179    /// The GPS IFD (34853), or chained after it.
180    Gps,
181    /// The Interoperability IFD (40965), or chained after it.
182    Interop,
183    /// A maker-note IFD inside a MakerNote (37500) value, walked by
184    /// [`walk_ifd`] with the vendor's offset base.
185    #[allow(dead_code)] // not every crate walks maker notes
186    MakerNote,
187}
188
189/// One IFD entry.
190#[derive(Clone, Debug)]
191pub struct Entry {
192    /// Absolute offset of the entry.
193    pub at: u64,
194    pub tag: u16,
195    pub typ: u16,
196    pub count: u64,
197    /// Absolute offset of the 4-byte (BigTIFF: 8-byte) value-or-offset field.
198    pub field: u64,
199    /// Remarks the walk attached to this entry.
200    pub notes: Vec<String>,
201}
202
203/// Where an entry's value lives.
204pub enum Loc {
205    /// The field type is not one TIFF defines.
206    UnknownType,
207    /// `count * size` overflows, or the offset does.
208    Overflow,
209    /// No value bytes.
210    Empty,
211    /// Inside the entry.
212    Inline,
213    /// Out of line: absolute offset, byte count.
214    At(u64, u64),
215}
216
217/// One directory.
218#[derive(Debug)]
219pub struct Ifd {
220    pub at: u64,
221    /// End of the bytes it covers, clipped to the TIFF's limit.
222    pub end: u64,
223    pub kind: Kind,
224    pub name: String,
225    /// Whether the decode path reads this directory by any path.
226    pub followed: bool,
227    pub entries: Vec<Entry>,
228    /// Raw next-IFD pointer (relative to the TIFF's base).
229    pub next: u64,
230    /// Why the directory could not be read whole.
231    pub problem: Option<String>,
232    /// Remarks about its next pointer.
233    pub notes: Vec<String>,
234    /// The IFD and entry whose pointer led here (`None` for the IFD chain).
235    pub parent: Option<(usize, usize)>,
236    /// Every kind of path that leads here, the first one first, each with
237    /// whether the decode path follows it (a SubIFD that is also the EXIF
238    /// IFD, an EXIF IFD that is also page 1).
239    pub roles: Vec<(Kind, bool)>,
240    /// Index of the last entry with each tag (decoders keeping a map keep
241    /// the last of duplicate tags).
242    pub last: BTreeMap<u16, usize>,
243    /// Index of the first entry with a field type TIFF does not define.
244    pub first_unknown: Option<usize>,
245    /// Pointer hops from the first directory of the walk (chained IFDs keep
246    /// their predecessor's depth), as decoders that cap recursion count.
247    pub depth: u8,
248}
249
250impl Ifd {
251    /// Whether the IFD was reached as `kind` by any path.
252    #[allow(dead_code)] // not every crate's rules need it
253    pub fn is(&self, kind: Kind) -> bool {
254        self.roles.iter().any(|&(k, _)| k == kind)
255    }
256
257    /// Whether the decode path follows a path that reaches it as `kind`.
258    pub fn followed_as(&self, kind: Kind) -> bool {
259        self.roles.iter().any(|&(k, f)| k == kind && f)
260    }
261
262    /// Whether some path reaches it as a page of the main chain.
263    pub fn page(&self) -> Option<u32> {
264        self.roles.iter().find_map(|&(k, _)| match k {
265            Kind::Page(n) => Some(n),
266            _ => None,
267        })
268    }
269
270    /// Whether some path reaches it as an image directory (a page or a
271    /// SubIFD).
272    pub fn holds_image(&self) -> bool {
273        self.roles
274            .iter()
275            .any(|&(k, _)| matches!(k, Kind::Page(_) | Kind::Sub))
276    }
277}
278
279/// Push `text` onto `notes`, keeping at most `MAX_NOTES` and one marker.
280pub fn add_note(notes: &mut Vec<String>, text: String) {
281    if notes.len() < MAX_NOTES {
282        notes.push(text);
283    } else if notes.len() == MAX_NOTES {
284        notes.push("further notes not listed".into());
285    }
286}
287
288/// One parsed TIFF.
289pub struct Walk<'a> {
290    /// The input up to the TIFF's limit; positions are absolute.
291    pub d: &'a [u8],
292    /// Offsets stored in the TIFF are relative to this.
293    pub base: u64,
294    pub lay: Layout,
295    /// The TIFF version number from the header (42, 43, or a raw variant).
296    pub magic: u16,
297    pub header: Range<u64>,
298    /// Whether the header parsed completely.
299    pub header_ok: bool,
300    /// Bytes after the header that belong to the file format (CR2), with
301    /// their disposition and detail.
302    pub header_ext: Option<(Range<u64>, Disposition, String)>,
303    /// Why the header or IFD0 could not be read.
304    pub fatal: Option<String>,
305    pub ifds: Vec<Ifd>,
306    /// Entries this walk may still parse. Disjoint entries cannot exceed
307    /// `limit / 12`, so only overlapping or repeated IFDs (crafted files)
308    /// run it out.
309    pub entries_left: u64,
310    /// IFD tables parsed so far: start → end. A table overlapping one of
311    /// these is not walked.
312    tables: BTreeMap<u64, u64>,
313    /// Index into `ifds` by absolute offset.
314    by_at: BTreeMap<u64, usize>,
315    /// Whether [`Rules::cancelled`] stopped the walk.
316    pub cancelled: bool,
317    /// Whether a budget or cap stopped part of the walk: its gaps may hold
318    /// bytes the decoder reads.
319    pub budget_hit: bool,
320    /// Directory pointers read from pointer entries ([`Rules::pointer_offsets`]),
321    /// the walk's work on pointer arrays; the crates' tests bound it.
322    #[allow(dead_code)] // read only by the crates' tests
323    pub pointer_reads: u64,
324    /// Bytes count-only chunk readers may still scan. Overlapping chunks
325    /// would otherwise rescan the same bytes once per chunk.
326    scan_left: Cell<u64>,
327}
328
329/// The entry budget for a walk over `limit` bytes.
330fn entry_budget(limit: u64) -> u64 {
331    limit / 12 + 4096
332}
333
334/// The count-only scan budget for a walk over `limit` bytes.
335fn scan_budget(limit: u64) -> Cell<u64> {
336    Cell::new(limit.saturating_mul(2).saturating_add(1 << 20))
337}
338
339impl Walk<'_> {
340    pub fn limit(&self) -> u64 {
341        self.d.len() as u64
342    }
343
344    /// Take `n` bytes from the count-only scan budget; false once it is
345    /// spent.
346    pub fn take_scan(&self, n: u64) -> bool {
347        let left = self.scan_left.get();
348        if n > left {
349            return false;
350        }
351        self.scan_left.set(left - n);
352        true
353    }
354
355    /// The start of an already-parsed IFD table that a table at `at` would
356    /// overlap.
357    fn table_overlap(&self, at: u64) -> Option<u64> {
358        use core::ops::Bound::Excluded;
359        if let Some((&s, &e)) = self.tables.range(..=at).next_back()
360            && e > at
361        {
362            return Some(s);
363        }
364        let count = if self.lay.big {
365            self.lay.u64(self.d, at)
366        } else {
367            self.lay.u16(self.d, at).map(u64::from)
368        }
369        .unwrap_or(0);
370        let end = count
371            .saturating_mul(self.lay.entry_len())
372            .saturating_add(at)
373            .saturating_add(self.lay.count_len() + self.lay.inline_cap())
374            .min(self.limit());
375        // A table at or past the limit is never parsed (and `at + 1` could
376        // wrap): nothing to overlap.
377        if end <= at {
378            return None;
379        }
380        self.tables
381            .range((Excluded(at), Excluded(end)))
382            .next()
383            .map(|(&s, _)| s)
384    }
385
386    /// The absolute position of a TIFF-relative offset.
387    pub fn abs(&self, off: u64) -> Option<u64> {
388        self.base.checked_add(off)
389    }
390
391    pub fn loc(&self, e: &Entry) -> Loc {
392        let Some(size) = type_size(e.typ) else {
393            return Loc::UnknownType;
394        };
395        let Some(total) = size.checked_mul(e.count) else {
396            return Loc::Overflow;
397        };
398        if total == 0 {
399            Loc::Empty
400        } else if total <= self.lay.inline_cap() {
401            Loc::Inline
402        } else {
403            match self.lay.offset(self.d, e.field).and_then(|o| self.abs(o)) {
404                Some(off) => Loc::At(off, total),
405                None => Loc::Overflow,
406            }
407        }
408    }
409
410    /// The value's bytes, when they lie inside the TIFF.
411    pub fn bytes(&self, e: &Entry) -> Option<&[u8]> {
412        match self.loc(e) {
413            Loc::Inline => {
414                let total = type_size(e.typ)?.checked_mul(e.count)?;
415                get(self.d, e.field, total)
416            }
417            Loc::At(off, total) => get(self.d, off, total),
418            Loc::Empty => Some(&[]),
419            _ => None,
420        }
421    }
422
423    /// Unsigned integer values (BYTE, UNDEFINED, SHORT, LONG, IFD, LONG8,
424    /// IFD8), at most `max` of them; other types yield nothing.
425    pub fn uints(&self, e: &Entry, max: usize) -> Vec<u64> {
426        let mut out = Vec::new();
427        let size = match e.typ {
428            1 | 7 => 1,
429            3 => 2,
430            4 | 13 => 4,
431            16 | 18 => 8,
432            _ => return out,
433        };
434        let Some(bytes) = self.bytes(e) else {
435            return out;
436        };
437        for chunk in bytes.chunks_exact(size).take(max) {
438            let v = match size {
439                1 => u64::from(chunk[0]),
440                2 => u64::from(self.lay.u16(chunk, 0).unwrap_or(0)),
441                4 => u64::from(self.lay.u32(chunk, 0).unwrap_or(0)),
442                _ => self.lay.u64(chunk, 0).unwrap_or(0),
443            };
444            out.push(v);
445        }
446        out
447    }
448
449    /// The single unsigned value of a one-element integer entry.
450    #[allow(dead_code)] // not every crate's rules need it
451    pub fn single_uint(&self, e: &Entry) -> Option<u64> {
452        if e.count != 1 {
453            return None;
454        }
455        self.uints(e, 1).first().copied()
456    }
457
458    /// The last entry with `tag` (decoders keeping a map keep the last of
459    /// duplicate tags).
460    pub fn find<'i>(&self, ifd: &'i Ifd, tag: u16) -> Option<&'i Entry> {
461        ifd.last.get(&tag).and_then(|&i| ifd.entries.get(i))
462    }
463}
464
465/// Parse the directory at absolute offset `at`. `None` when `at` is outside
466/// the TIFF.
467fn parse_ifd(
468    d: &[u8],
469    lay: Layout,
470    at: u64,
471    kind: Kind,
472    name: String,
473    followed: bool,
474    budget: &mut u64,
475) -> Option<Ifd> {
476    let len = d.len() as u64;
477    if at >= len {
478        return None;
479    }
480    let mut ifd = Ifd {
481        at,
482        end: len,
483        kind,
484        name,
485        followed,
486        entries: Vec::new(),
487        next: 0,
488        problem: None,
489        notes: Vec::new(),
490        parent: None,
491        roles: alloc::vec![(kind, followed)],
492        last: BTreeMap::new(),
493        first_unknown: None,
494        depth: 0,
495    };
496    let count = if lay.big {
497        lay.u64(d, at)
498    } else {
499        lay.u16(d, at).map(u64::from)
500    };
501    let Some(count) = count else {
502        ifd.problem = Some("entry count runs past the end of the data".into());
503        return Some(ifd);
504    };
505    let declared_end = count
506        .checked_mul(lay.entry_len())
507        .and_then(|n| n.checked_add(at))
508        .and_then(|n| n.checked_add(lay.count_len()))
509        .and_then(|n| n.checked_add(lay.inline_cap()));
510    let mut e_at = at + lay.count_len();
511    let mut i = 0u64;
512    while i < count {
513        if *budget == 0 {
514            ifd.problem = Some(
515                "not read further: the file's entry budget is spent (overlapping or repeated IFDs)"
516                    .into(),
517            );
518            break;
519        }
520        let Some(e_end) = e_at.checked_add(lay.entry_len()) else {
521            break;
522        };
523        if e_end > len {
524            break;
525        }
526        let (Some(tag), Some(typ)) = (lay.u16(d, e_at), lay.u16(d, e_at + 2)) else {
527            break;
528        };
529        let (count_v, field) = if lay.big {
530            (lay.u64(d, e_at + 4), e_at + 12)
531        } else {
532            (lay.u32(d, e_at + 4).map(u64::from), e_at + 8)
533        };
534        let Some(count_v) = count_v else { break };
535        ifd.entries.push(Entry {
536            at: e_at,
537            tag,
538            typ,
539            count: count_v,
540            field,
541            notes: Vec::new(),
542        });
543        *budget -= 1;
544        e_at = e_end;
545        i += 1;
546    }
547    match declared_end {
548        Some(end) if end <= len => {
549            ifd.end = end;
550            ifd.next = lay.offset(d, end - lay.inline_cap()).unwrap_or(0);
551        }
552        _ if ifd.problem.is_none() => {
553            ifd.problem = Some(format!(
554                "declares {count} entries but runs past the end of the data"
555            ));
556        }
557        _ => {}
558    }
559    for (i, e) in ifd.entries.iter().enumerate() {
560        ifd.last.insert(e.tag, i);
561        if ifd.first_unknown.is_none() && type_size(e.typ).is_none() {
562            ifd.first_unknown = Some(i);
563        }
564    }
565    Some(ifd)
566}
567
568// ── What a decode path does ────────────────────────────────────────────
569
570/// A disposition with an optional remark for the detail.
571pub struct Fate {
572    pub d: Disposition,
573    pub note: Option<Cow<'static, str>>,
574}
575
576impl Fate {
577    pub fn new(d: Disposition) -> Self {
578        Self { d, note: None }
579    }
580
581    pub fn because(d: Disposition, note: impl Into<Cow<'static, str>>) -> Self {
582        Self {
583            d,
584            note: Some(note.into()),
585        }
586    }
587}
588
589/// How strongly a disposition says the decode path uses bytes, for picking
590/// one fate among the paths that reach a directory.
591fn rank(d: Disposition) -> u8 {
592    match d {
593        Disposition::Metadata(_) => 6,
594        Disposition::Structure | Disposition::ImageData => 5,
595        Disposition::Malformed => 4,
596        Disposition::Dropped => 3,
597        Disposition::Padding => 2,
598        _ => 1,
599    }
600}
601
602/// The fate of a piece of a directory reached by several paths: the
603/// strongest of `fate(kind, followed)` over its roles, the first role on a
604/// tie. A decoder that reads the directory by any path reads the piece.
605pub fn union_fate(ifd: &Ifd, fate: impl Fn(Kind, bool) -> Fate) -> Fate {
606    let mut best: Option<Fate> = None;
607    for &(kind, followed) in &ifd.roles {
608        let f = fate(kind, followed);
609        if best.as_ref().is_none_or(|b| rank(f.d) > rank(b.d)) {
610            best = Some(f);
611        }
612    }
613    best.unwrap_or_else(|| Fate::new(Disposition::Unknown))
614}
615
616/// A short name for a kind of directory.
617pub fn kind_name(kind: Kind) -> String {
618    match kind {
619        Kind::Page(n) => format!("page {n}"),
620        Kind::Sub => "SubIFD".into(),
621        Kind::Exif => "EXIF IFD".into(),
622        Kind::Gps => "GPS IFD".into(),
623        Kind::Interop => "Interop IFD".into(),
624        Kind::MakerNote => "maker-note IFD".into(),
625    }
626}
627
628/// How long the extents of an [`ExtentRule`] are.
629#[allow(dead_code)] // not every crate's rules use every variant
630pub enum Count {
631    /// Byte counts in this tag, one per offset.
632    Tag(u16),
633    /// One extent from the first offset to the end of the TIFF.
634    ToEnd,
635    /// One extent of this many bytes from the first offset.
636    Bytes(u64),
637    /// A 64-byte old-style JPEG quantization table at each nonzero offset.
638    /// Zero means no separate table; repeated offsets may share a table.
639    JpegQuantization,
640    /// An old-style JPEG Huffman table at each nonzero offset: 16 code-count
641    /// bytes followed by their sum in symbol bytes. Reading the counts uses
642    /// the walk's scan budget. Zero means no separate table.
643    JpegHuffman,
644}
645
646/// An offset tag with the lengths of the extents it locates.
647pub struct ExtentRule {
648    pub offsets: u16,
649    pub counts: Count,
650    pub what: &'static str,
651    pub kind: PartKind,
652    pub fate: Fate,
653}
654
655/// What one decode path does with a TIFF's directories, entries and extents.
656pub trait Rules {
657    /// Accept a TIFF version number other than 42 and 43 (ORF, RW2).
658    fn magic(&self, _magic: u16) -> bool {
659        false
660    }
661
662    /// Bytes after the classic 8-byte header that the file format defines
663    /// (CR2), and what the decode path does with them.
664    fn header_extension(&self, _w: &Walk<'_>) -> Option<(u64, Fate)> {
665        None
666    }
667
668    /// For a value the decode path consumes only in part: how many leading
669    /// bytes it uses, and the fate of the rest (an ASCII value's bytes after
670    /// its NUL, for example).
671    fn value_tail(&self, _w: &Walk<'_>, _ifd: &Ifd, _index: usize) -> Option<(u64, Fate)> {
672        None
673    }
674
675    /// How much of each strip or tile, located at `offs` with declared
676    /// lengths `cnts`, the decoder reads. The default follows image-tiff:
677    /// [`default_chunk_use`].
678    fn chunk_use(
679        &self,
680        w: &Walk<'_>,
681        ifd: &Ifd,
682        tiles: bool,
683        offs: &[u64],
684        cnts: &[u64],
685    ) -> Vec<ChunkUse> {
686        default_chunk_use(w, ifd, tiles, offs, cnts)
687    }
688
689    /// The directory pointer entry `e` in a `kind` IFD leads to.
690    fn pointer(&self, kind: Kind, e: &Entry) -> Option<(Kind, &'static str)> {
691        match (e.tag, kind) {
692            (SUB_IFDS, Kind::Page(_) | Kind::Sub) => Some((Kind::Sub, "SubIFD")),
693            (EXIF_IFD, Kind::Page(_) | Kind::Sub) => Some((Kind::Exif, "EXIF IFD")),
694            (GPS_IFD, Kind::Page(_) | Kind::Sub) => Some((Kind::Gps, "GPS IFD")),
695            (INTEROP_IFD, Kind::Exif) => Some((Kind::Interop, "Interop IFD")),
696            _ => None,
697        }
698    }
699
700    /// The TIFF-relative offsets a pointer entry holds, at most `max` of
701    /// them (the walk reads only as many as it can still queue).
702    fn pointer_offsets(&self, w: &Walk<'_>, e: &Entry, max: usize) -> Vec<u64> {
703        w.uints(e, max)
704    }
705
706    /// Whether the decode path reads the directory the pointer entry `e` of
707    /// `parent`, reached as a `kind` directory, leads to.
708    fn follows(&self, w: &Walk<'_>, parent: &Ifd, kind: Kind, e: &Entry) -> bool;
709
710    /// Whether the decode path reads the directory the next pointer of
711    /// `ifd`, reached as a `kind` directory, leads to.
712    fn follows_next(&self, w: &Walk<'_>, ifd: &Ifd, kind: Kind) -> bool;
713
714    fn ifd(&self, w: &Walk<'_>, ifd: &Ifd) -> Fate;
715
716    /// The entry (and, unless [`value`](Rules::value) says otherwise, its
717    /// out-of-line value).
718    fn entry(&self, w: &Walk<'_>, ifd: &Ifd, index: usize) -> Fate;
719
720    /// The entry's out-of-line value.
721    fn value(&self, w: &Walk<'_>, ifd: &Ifd, index: usize) -> Fate {
722        self.entry(w, ifd, index)
723    }
724
725    /// Strips and tiles.
726    fn image_data(&self, w: &Walk<'_>, ifd: &Ifd) -> Fate;
727
728    /// A JPEG stream located by JPEGInterchangeFormat (513/514).
729    fn jpeg_stream(&self, w: &Walk<'_>, ifd: &Ifd) -> Fate;
730
731    /// External old-style JPEG tables (519, 520 or 521). Locating a table
732    /// does not establish that the decoder uses it: an embedded JPEG header
733    /// can supply tables instead. The default deliberately leaves consumption
734    /// unknown, independently of strips and JPEGInterchangeFormat streams.
735    ///
736    /// Override only when the codec's rules establish the same disposition
737    /// for every table reached by this tag. This is not privacy evidence.
738    fn jpeg_table(&self, _w: &Walk<'_>, _ifd: &Ifd, _tag: u16) -> Fate {
739        Fate::because(
740            Disposition::Unknown,
741            "referenced JPEG table; decoder consumption is not established",
742        )
743    }
744
745    /// Whether the caller's stop token asks the walk to end early.
746    fn cancelled(&self) -> bool {
747        false
748    }
749
750    /// Why the decoder rejects the directory at absolute offset `at`,
751    /// reached as `kind`, before reading its entries (an entry-count cap),
752    /// if it does. Such a directory is walked as not followed.
753    fn rejects(&self, _w: &Walk<'_>, _at: u64, _kind: Kind) -> Option<&'static str> {
754        None
755    }
756
757    /// Extents `ifd` locates. The default covers strips, tiles, free space
758    /// and JPEGInterchangeFormat streams and external JPEG tables of image IFDs.
759    fn extents(&self, w: &Walk<'_>, ifd: &Ifd) -> Vec<ExtentRule> {
760        if !ifd.holds_image() {
761            return Vec::new();
762        }
763        alloc::vec![
764            ExtentRule {
765                offsets: STRIP_OFFSETS,
766                counts: Count::Tag(STRIP_BYTE_COUNTS),
767                what: "strip",
768                kind: PartKind::Extent,
769                fate: self.image_data(w, ifd),
770            },
771            ExtentRule {
772                offsets: TILE_OFFSETS,
773                counts: Count::Tag(TILE_BYTE_COUNTS),
774                what: "tile",
775                kind: PartKind::Extent,
776                fate: self.image_data(w, ifd),
777            },
778            ExtentRule {
779                offsets: FREE_OFFSETS,
780                counts: Count::Tag(FREE_BYTE_COUNTS),
781                what: "free space",
782                kind: PartKind::Extent,
783                fate: Fate::because(Disposition::Padding, "declared free space"),
784            },
785            ExtentRule {
786                offsets: JPEG_IF_OFFSET,
787                counts: Count::Tag(JPEG_IF_LENGTH),
788                what: "JPEG stream",
789                kind: PartKind::EmbeddedImage,
790                fate: self.jpeg_stream(w, ifd),
791            },
792            ExtentRule {
793                offsets: JPEG_Q_TABLES,
794                counts: Count::JpegQuantization,
795                what: "JPEG quantization table",
796                kind: PartKind::Extent,
797                fate: self.jpeg_table(w, ifd, JPEG_Q_TABLES),
798            },
799            ExtentRule {
800                offsets: JPEG_DC_TABLES,
801                counts: Count::JpegHuffman,
802                what: "JPEG DC Huffman table",
803                kind: PartKind::Extent,
804                fate: self.jpeg_table(w, ifd, JPEG_DC_TABLES),
805            },
806            ExtentRule {
807                offsets: JPEG_AC_TABLES,
808                counts: Count::JpegHuffman,
809                what: "JPEG AC Huffman table",
810                kind: PartKind::Extent,
811                fate: self.jpeg_table(w, ifd, JPEG_AC_TABLES),
812            },
813        ]
814    }
815}
816
817/// Table lengths describe structural references, not JPEG validity or use.
818/// No table payload is allocated or copied. A truncated count prefix still
819/// claims its 16 bytes so placement marks the available part malformed.
820fn jpeg_table_counts(
821    w: &Walk<'_>,
822    offsets: &[u64],
823    huffman: bool,
824    rules: &dyn Rules,
825) -> (Vec<u64>, bool, bool) {
826    let mut lengths = Vec::new();
827    for &offset in offsets {
828        if rules.cancelled() {
829            return (lengths, false, true);
830        }
831        if offset == 0 {
832            lengths.push(0);
833        } else if !huffman {
834            lengths.push(64);
835        } else {
836            if !w.take_scan(16) {
837                return (lengths, true, false);
838            }
839            let counts = w.abs(offset).and_then(|at| get(w.d, at, 16));
840            lengths.push(16 + counts.map_or(0, |v| v.iter().map(|&n| u64::from(n)).sum()));
841        }
842    }
843    (lengths, false, false)
844}
845
846/// How much of one strip or tile the decoder reads.
847#[derive(Clone, Debug, Default)]
848pub struct ChunkUse {
849    /// Leading bytes the decoder reads; the rest of the declared length is
850    /// a `Dropped` tail. `None` when the end is not found (compressed data
851    /// without a count-only reader).
852    pub used: Option<u64>,
853    /// What the tail is, for its detail.
854    pub tail: &'static str,
855    /// A remark for the chunk's detail.
856    pub note: Option<String>,
857    /// The decoder reads `used` bytes whatever the declared length, so a
858    /// shorter declared length is read past (image-tiff reads uncompressed
859    /// data by row size).
860    pub past_count: bool,
861    /// Another chunk's extent already covers this one (a decoder reading
862    /// every row from the first strip): not listed on its own.
863    pub covered: bool,
864    /// The decoder never reads this chunk: listed alone as `Skipped`, why.
865    pub unread: Option<&'static str>,
866    /// What the decoder reads of this chunk was not worked out (a budget ran
867    /// out): listed alone as `Unknown`, why.
868    pub unknown: Option<&'static str>,
869}
870
871/// The decoded layout of one strip or tile.
872#[derive(Clone, Copy, Debug)]
873pub struct ChunkGeom {
874    /// Decoded bytes of one chunk row (a whole tile row, padding included).
875    pub row: u64,
876    /// Chunk rows the decoder reads: those inside the image.
877    pub rows_in: u64,
878    /// Chunk rows declared (`TileLength` or `RowsPerStrip`).
879    pub rows: u64,
880    /// Padding bytes per row past the image's right edge.
881    pub pad_per_row: u64,
882}
883
884impl ChunkGeom {
885    /// Decoded bytes the decoder reads from this chunk.
886    pub fn read(&self) -> Option<u64> {
887        self.row.checked_mul(self.rows_in)
888    }
889}
890
891/// The decoded layout of `chunks` strips or tiles of `ifd` from its tags, as
892/// image-tiff computes it (`readout_for_size`, `chunk_data_dimensions`);
893/// `None` for subsampled YCbCr or missing dimensions.
894pub fn chunk_geometry(
895    w: &Walk<'_>,
896    ifd: &Ifd,
897    tiles: bool,
898    chunks: usize,
899) -> Option<Vec<ChunkGeom>> {
900    let one = |tag: u16| {
901        w.find(ifd, tag)
902            .and_then(|e| w.uints(e, 1).first().copied())
903    };
904    if one(262) == Some(6) {
905        return None;
906    }
907    let width = one(256)?;
908    let height = one(257)?;
909    let spp = one(277).unwrap_or(1).max(1);
910    let bps = w
911        .find(ifd, 258)
912        .map(|e| w.uints(e, 64))
913        .unwrap_or_else(|| alloc::vec![1]);
914    let planar = one(284) == Some(2);
915    let bits = if planar {
916        *bps.first()?
917    } else if bps.len() as u64 == spp {
918        bps.iter().try_fold(0u64, |a, &b| a.checked_add(b))?
919    } else {
920        bps.first()?.checked_mul(spp)?
921    };
922    let row_bytes = |px: u64| px.checked_mul(bits).map(|b| b.div_ceil(8));
923    let mut out = Vec::with_capacity(chunks.min(1 << 20));
924    if tiles {
925        let (tw, tl) = (one(322)?.max(1), one(323)?.max(1));
926        let across = width.div_ceil(tw).max(1);
927        let per_plane = across.checked_mul(height.div_ceil(tl).max(1))?;
928        let row = row_bytes(tw)?;
929        for k in 0..chunks as u64 {
930            let i = k % per_plane;
931            let (col, down) = (i % across, i / across);
932            let cols_in = tw.min(width.saturating_sub(col.saturating_mul(tw)));
933            out.push(ChunkGeom {
934                row,
935                rows_in: tl.min(height.saturating_sub(down.saturating_mul(tl))),
936                rows: tl,
937                pad_per_row: row.saturating_sub(row_bytes(cols_in)?),
938            });
939        }
940        return Some(out);
941    }
942    let rps = one(278).unwrap_or(height).clamp(1, height.max(1));
943    let per_plane = height.div_ceil(rps).max(1);
944    let row = row_bytes(width)?;
945    for k in 0..chunks as u64 {
946        let rows_in = rps.min(height.saturating_sub((k % per_plane).saturating_mul(rps)));
947        out.push(ChunkGeom {
948            row,
949            rows_in,
950            rows: rows_in,
951            pad_per_row: 0,
952        });
953    }
954    Some(out)
955}
956
957/// Where a PackBits stream at `data` has produced `need` decoded bytes: the
958/// compressed bytes image-tiff's `PackBitsReader` reads by then. `None` when
959/// the stream ends first.
960fn packbits_end(data: &[u8], need: u64) -> Option<u64> {
961    let (mut pos, mut out) = (0usize, 0u64);
962    while out < need {
963        let h = *data.get(pos)? as i8;
964        pos += 1;
965        match h {
966            -128 => {}
967            -127..=-1 => {
968                data.get(pos)?;
969                pos += 1;
970                out += (1 - i64::from(h)) as u64;
971            }
972            _ => {
973                let take = (h as u64 + 1).min(need - out);
974                pos = pos.checked_add(usize::try_from(take).ok()?)?;
975                if pos > data.len() {
976                    return None;
977                }
978                out += take;
979            }
980        }
981    }
982    Some(pos as u64)
983}
984
985/// [`Rules::chunk_use`]'s default, after image-tiff's `expand_chunk`: it
986/// reads whole chunk rows (right-edge padding included, then discarded) for
987/// the rows inside the image only. Uncompressed and PackBits chunks are split
988/// at that point; for other compression the end of the coded data is not
989/// found.
990pub fn default_chunk_use(
991    w: &Walk<'_>,
992    ifd: &Ifd,
993    tiles: bool,
994    offs: &[u64],
995    cnts: &[u64],
996) -> Vec<ChunkUse> {
997    let compression = w
998        .find(ifd, 259)
999        .and_then(|e| w.uints(e, 1).first().copied())
1000        .unwrap_or(1);
1001    let geom = chunk_geometry(w, ifd, tiles, cnts.len());
1002    let mut out = Vec::with_capacity(cnts.len().min(1 << 20));
1003    for (k, &n) in cnts.iter().enumerate() {
1004        let g = geom.as_ref().and_then(|g| g.get(k).copied());
1005        let mut u = ChunkUse::default();
1006        let Some(g) = g else {
1007            out.push(u);
1008            continue;
1009        };
1010        let below = g.rows_in < g.rows;
1011        u.tail = if below {
1012            "rows below the image (edge-tile padding) and any bytes after them; never read"
1013        } else {
1014            "after the rows the decoder reads; never read"
1015        };
1016        if g.pad_per_row > 0 {
1017            u.note = Some(format!(
1018                "{} padding bytes per decoded row past the image's right edge are read and discarded",
1019                g.pad_per_row
1020            ));
1021        }
1022        match compression {
1023            // `create_reader` reads uncompressed data straight from the
1024            // file: by row size, not by the declared count.
1025            1 => {
1026                u.used = g.read();
1027                u.past_count = true;
1028            }
1029            32773 => {
1030                // Count-only PackBits over the bytes present, within the walk's
1031                // scan budget (overlapping chunks would rescan the same bytes).
1032                let start = offs.get(k).and_then(|&o| w.abs(o));
1033                let avail = start.map_or(0, |s| n.min(w.limit().saturating_sub(s)));
1034                let budget = start.is_some() && g.read().is_some() && w.take_scan(avail);
1035                if !budget {
1036                    u.unknown = Some(
1037                        "the count-only scan budget is spent; the decoder may read some or all of these bytes",
1038                    );
1039                }
1040                if let (true, Some(s), Some(need)) = (budget, start, g.read())
1041                    && let Some(d) = get(w.d, s, avail)
1042                {
1043                    match packbits_end(d, need) {
1044                        Some(end) => u.used = Some(end),
1045                        None if avail == n => {
1046                            u.used = Some(n);
1047                            u.note = Some(
1048                                "the PackBits data ends before the chunk's rows are complete"
1049                                    .into(),
1050                            );
1051                        }
1052                        None => {}
1053                    }
1054                }
1055            }
1056            // Deflate and ZSTD read the file to the end of the stream, which
1057            // may lie past the declared count.
1058            8 | 32946 | 50000 => {
1059                u.note = Some(
1060                    "the stream is read to its end, which may lie past the declared count".into(),
1061                );
1062            }
1063            _ => {}
1064        }
1065        if u.used.is_none() && (below || g.pad_per_row > 0) {
1066            let edge = "edge tile: its padding pixels are decoded and discarded";
1067            u.note = Some(match u.note.take() {
1068                Some(n) => format!("{n}; {edge}"),
1069                None => edge.into(),
1070            });
1071        }
1072        out.push(u);
1073    }
1074    out
1075}
1076
1077/// Children for the parts of an entry's inline value field nothing reads:
1078/// an inline value's tail ([`Rules::value_tail`]) and, when not zero, the
1079/// field bytes past the value.
1080fn inline_children(w: &Walk<'_>, rules: &dyn Rules, ifd: &Ifd, ei: usize, entry: &mut Cand) {
1081    let Some(e) = ifd.entries.get(ei) else { return };
1082    let size = match w.loc(e) {
1083        Loc::Inline => type_size(e.typ)
1084            .and_then(|s| s.checked_mul(e.count))
1085            .unwrap_or(0),
1086        Loc::Empty => 0,
1087        _ => return,
1088    };
1089    let cap = w.lay.inline_cap();
1090    let field = e.field..e.field + cap;
1091    if size > 0
1092        && let Some((keep, tail)) = rules.value_tail(w, ifd, ei)
1093        && keep < size
1094    {
1095        let mut detail = format!("{} value bytes {keep}..{size}", tag_name(ifd.kind, e.tag));
1096        if let Some(n) = &tail.note {
1097            push_note(&mut detail, n);
1098        }
1099        entry.children.push(Cand::new(
1100            field.start + keep..field.start + size,
1101            PartKind::Field,
1102            PartTag::Code(u32::from(e.tag)),
1103            tail.d,
1104            detail,
1105        ));
1106    }
1107    let unused = field.start + size..field.end;
1108    if !unused.is_empty()
1109        && get(w.d, unused.start, unused.end - unused.start)
1110            .is_some_and(|b| b.iter().any(|&x| x != 0))
1111    {
1112        entry.children.push(Cand::new(
1113            unused,
1114            PartKind::Gap,
1115            PartTag::None,
1116            Disposition::Padding,
1117            "unused bytes of the inline value field, not zero; nothing reads them".into(),
1118        ));
1119    }
1120}
1121
1122/// Parse the TIFF whose header is at `base`, with every position below
1123/// `limit`, following pointers the way `rules` describes.
1124pub fn walk<'a>(data: &'a [u8], base: u64, limit: u64, rules: &dyn Rules) -> Walk<'a> {
1125    let limit = limit.min(data.len() as u64);
1126    let d = &data[..usize::try_from(limit).unwrap_or(data.len())];
1127    let mut w = Walk {
1128        d,
1129        base,
1130        lay: Layout {
1131            le: true,
1132            big: false,
1133        },
1134        magic: 0,
1135        header: base..base,
1136        header_ok: false,
1137        header_ext: None,
1138        fatal: None,
1139        ifds: Vec::new(),
1140        entries_left: entry_budget(limit),
1141        scan_left: scan_budget(limit),
1142        tables: BTreeMap::new(),
1143        by_at: BTreeMap::new(),
1144        cancelled: false,
1145        budget_hit: false,
1146        pointer_reads: 0,
1147    };
1148    let rest = limit.saturating_sub(base);
1149    let lay = match get(d, base, 2) {
1150        Some(b"II") => Layout {
1151            le: true,
1152            big: false,
1153        },
1154        Some(b"MM") => Layout {
1155            le: false,
1156            big: false,
1157        },
1158        _ => {
1159            w.header = base..base + rest.min(2);
1160            w.fatal = Some("not a TIFF byte-order mark".into());
1161            return w;
1162        }
1163    };
1164    let (lay, ifd0) = match lay.u16(d, base + 2) {
1165        Some(43) => {
1166            let big = Layout { big: true, ..lay };
1167            // Bytesize of offsets (8) and a reserved zero.
1168            if big.u16(d, base + 4).is_some_and(|v| v != 8)
1169                || big.u16(d, base + 6).is_some_and(|v| v != 0)
1170            {
1171                w.header = base..base + rest.min(8);
1172                w.fatal = Some("BigTIFF header with an offset size other than 8".into());
1173                return w;
1174            }
1175            w.magic = 43;
1176            (big, big.u64(d, base + 8))
1177        }
1178        Some(m) if m == 42 || rules.magic(m) => {
1179            w.magic = m;
1180            (lay, lay.u32(d, base + 4).map(u64::from))
1181        }
1182        Some(m) => {
1183            w.header = base..base + rest.min(4);
1184            w.fatal = Some(format!("not a TIFF version number ({m})"));
1185            return w;
1186        }
1187        None => {
1188            w.header = base..limit;
1189            w.fatal = Some("truncated TIFF header".into());
1190            return w;
1191        }
1192    };
1193    w.lay = lay;
1194    let Some(ifd0) = ifd0 else {
1195        w.header = base..limit;
1196        w.fatal = Some("truncated TIFF header".into());
1197        return w;
1198    };
1199    w.header = base..base + lay.header_len();
1200    w.header_ok = true;
1201    if let Some((extra, fate)) = rules.header_extension(&w)
1202        && w.header.end + extra <= limit
1203    {
1204        let r = w.header.end..w.header.end + extra;
1205        let detail = fate.note.map(|n| n.into_owned()).unwrap_or_default();
1206        w.header_ext = Some((r, fate.d, detail));
1207    }
1208
1209    let first = w.abs(ifd0);
1210    run_queue(&mut w, first, Kind::Page(0), "IFD0".into(), 0, rules);
1211    w
1212}
1213
1214/// Walk the directories starting at the absolute offset `at`, which has no
1215/// TIFF header of its own (a maker-note IFD): offsets inside are relative to
1216/// `base`, and `lay` gives the byte order. Every position stays below
1217/// `limit`; the first directory is at pointer depth `depth`.
1218#[allow(clippy::too_many_arguments, dead_code)] // not every crate walks maker notes
1219pub fn walk_ifd<'a>(
1220    data: &'a [u8],
1221    base: u64,
1222    limit: u64,
1223    lay: Layout,
1224    at: u64,
1225    kind: Kind,
1226    name: String,
1227    depth: u8,
1228    rules: &dyn Rules,
1229) -> Walk<'a> {
1230    let limit = limit.min(data.len() as u64);
1231    let d = &data[..usize::try_from(limit).unwrap_or(data.len())];
1232    let mut w = Walk {
1233        d,
1234        base,
1235        lay,
1236        magic: 0,
1237        header: at..at,
1238        header_ok: true,
1239        header_ext: None,
1240        fatal: None,
1241        ifds: Vec::new(),
1242        entries_left: entry_budget(limit),
1243        scan_left: scan_budget(limit),
1244        tables: BTreeMap::new(),
1245        by_at: BTreeMap::new(),
1246        cancelled: false,
1247        budget_hit: false,
1248        pointer_reads: 0,
1249    };
1250    run_queue(&mut w, Some(at), kind, name, depth, rules);
1251    w
1252}
1253
1254/// Where a pending directory's pointer came from.
1255#[derive(Clone, Copy)]
1256enum From {
1257    Header,
1258    Entry(usize, usize),
1259    Next(usize),
1260}
1261
1262struct Pending {
1263    at: Option<u64>,
1264    kind: Kind,
1265    name: String,
1266    followed: bool,
1267    from: From,
1268    depth: u8,
1269}
1270
1271/// Walk every directory reachable from the first one, breadth first. A
1272/// directory reached again by another kind of path records that kind, and
1273/// the pointers that kind follows are queued too.
1274fn run_queue(
1275    w: &mut Walk<'_>,
1276    first: Option<u64>,
1277    kind: Kind,
1278    name: String,
1279    depth: u8,
1280    rules: &dyn Rules,
1281) {
1282    let d = w.d;
1283    let lay = w.lay;
1284    let mut queue = VecDeque::new();
1285    queue.push_back(Pending {
1286        at: first,
1287        kind,
1288        name,
1289        followed: true,
1290        from: From::Header,
1291        depth,
1292    });
1293    while let Some(p) = queue.pop_front() {
1294        let note = |w: &mut Walk<'_>, text: String| match p.from {
1295            From::Header => w.fatal = Some(text),
1296            From::Entry(i, e) => add_note(&mut w.ifds[i].entries[e].notes, text),
1297            From::Next(i) => add_note(&mut w.ifds[i].notes, text),
1298        };
1299        let Some(at) = p.at else {
1300            note(w, format!("{} offset overflows", p.name));
1301            continue;
1302        };
1303        if rules.cancelled() {
1304            w.cancelled = true;
1305            break;
1306        }
1307        if let Some(&idx) = w.by_at.get(&at) {
1308            // Reached again: record the new kind of path and queue what it
1309            // follows from here.
1310            let ifd = &mut w.ifds[idx];
1311            // A page chain that comes back to a page is a loop (image-tiff's
1312            // cycle check ends the chain there), not another kind of path.
1313            let looped = matches!(p.kind, Kind::Page(_)) && ifd.page().is_some();
1314            let role = ifd.roles.iter().position(|&(k, _)| k == p.kind);
1315            let new_kind = !looped && role.is_none() && ifd.roles.len() < MAX_ROLES;
1316            let upgraded = !looped && p.followed && role.is_some_and(|r| !ifd.roles[r].1);
1317            match role {
1318                Some(r) if !looped => ifd.roles[r].1 |= p.followed,
1319                None if new_kind => ifd.roles.push((p.kind, p.followed)),
1320                _ => {}
1321            }
1322            if new_kind || upgraded {
1323                ifd.followed |= p.followed;
1324            }
1325            let text = if new_kind {
1326                format!(
1327                    "{} at {at} is also reached as {}",
1328                    p.name,
1329                    kind_name(p.kind)
1330                )
1331            } else {
1332                format!(
1333                    "{} at {at} was already walked (cycle or shared IFD)",
1334                    p.name
1335                )
1336            };
1337            note(w, text);
1338            if (new_kind || upgraded) && w.ifds[idx].problem.is_none() {
1339                enqueue(w, &mut queue, idx, p.kind, rules);
1340            }
1341            continue;
1342        }
1343        if w.ifds.len() >= MAX_IFDS {
1344            w.budget_hit = true;
1345            note(w, format!("not walked: more than {MAX_IFDS} IFDs"));
1346            continue;
1347        }
1348        // A table overlapping one already parsed is refused only when the
1349        // decode path does not read it: a directory the decoder follows (the
1350        // page chain, the EXIF target) is walked anyway, its parts split
1351        // around the other table, and the entry budget still bounds the
1352        // total.
1353        if let Some(other) = w.table_overlap(at) {
1354            if !p.followed {
1355                note(
1356                    w,
1357                    format!(
1358                        "{} at {at} overlaps the IFD table at {other}; not walked",
1359                        p.name
1360                    ),
1361                );
1362                continue;
1363            }
1364            note(
1365                w,
1366                format!(
1367                    "{} at {at} overlaps the IFD table at {other}; walked, because the decoder reads it",
1368                    p.name
1369                ),
1370            );
1371        }
1372        // A directory the decoder rejects outright (too many entries) is
1373        // walked as not followed.
1374        let mut followed = p.followed;
1375        if followed && let Some(why) = rules.rejects(w, at, p.kind) {
1376            followed = false;
1377            note(w, format!("{} at {at}: {why}", p.name));
1378        }
1379        let mut budget = w.entries_left;
1380        let parsed = parse_ifd(d, lay, at, p.kind, p.name.clone(), followed, &mut budget);
1381        w.entries_left = budget;
1382        if budget == 0 {
1383            w.budget_hit = true;
1384        }
1385        let Some(mut ifd) = parsed else {
1386            note(
1387                w,
1388                format!("{} offset {at} is past the end of the data", p.name),
1389            );
1390            continue;
1391        };
1392        if let From::Entry(i, e) = p.from {
1393            ifd.parent = Some((i, e));
1394        }
1395        ifd.depth = p.depth;
1396        if p.kind == Kind::Page(0) && ifd.problem.is_some() {
1397            w.fatal = Some(format!(
1398                "IFD0 {}",
1399                ifd.problem.as_deref().unwrap_or_default()
1400            ));
1401        }
1402        w.tables.insert(ifd.at, ifd.end);
1403        let idx = w.ifds.len();
1404        w.by_at.insert(at, idx);
1405        let readable = ifd.problem.is_none();
1406        w.ifds.push(ifd);
1407        if readable {
1408            enqueue(w, &mut queue, idx, p.kind, rules);
1409        }
1410    }
1411}
1412
1413/// Queue the directories IFD `idx`'s pointer entries and next pointer lead
1414/// to, as a `kind` directory sees them. Stops queueing at `MAX_IFDS`.
1415fn enqueue(
1416    w: &mut Walk<'_>,
1417    queue: &mut VecDeque<Pending>,
1418    idx: usize,
1419    kind: Kind,
1420    rules: &dyn Rules,
1421) {
1422    let base_name = w.ifds[idx].name.clone();
1423    let followed_here = w.ifds[idx].followed_as(kind);
1424    for ei in 0..w.ifds[idx].entries.len() {
1425        let e = &w.ifds[idx].entries[ei];
1426        let Some((child_kind, label)) = rules.pointer(kind, e) else {
1427            continue;
1428        };
1429        let followed = followed_here && rules.follows(w, &w.ifds[idx], kind, e);
1430        // Read only the pointers that can still be queued, plus one to tell
1431        // whether more were left: reading every array in full is quadratic.
1432        let room = MAX_IFDS.saturating_sub(queue.len() + w.ifds.len());
1433        if room == 0 {
1434            w.budget_hit = true;
1435            add_note(
1436                &mut w.ifds[idx].entries[ei].notes,
1437                format!("{label} pointers not walked: more than {MAX_IFDS} IFDs"),
1438            );
1439            continue;
1440        }
1441        let ptrs = rules.pointer_offsets(w, e, room + 1);
1442        w.pointer_reads += ptrs.len() as u64;
1443        if ptrs.is_empty() {
1444            let text = format!("{label} pointer of type {} not read", type_name(e.typ));
1445            add_note(&mut w.ifds[idx].entries[ei].notes, text);
1446        }
1447        let numbered = ptrs.len() > 1 || child_kind == Kind::Sub;
1448        for (k, off) in ptrs.into_iter().enumerate() {
1449            if queue.len() + w.ifds.len() >= MAX_IFDS {
1450                w.budget_hit = true;
1451                add_note(
1452                    &mut w.ifds[idx].entries[ei].notes,
1453                    format!("further {label} pointers not walked: more than {MAX_IFDS} IFDs"),
1454                );
1455                break;
1456            }
1457            let name = match (numbered, base_name.len() > 48) {
1458                (true, false) => format!("{label} {k} of {base_name}"),
1459                (true, true) => format!("{label} {k} (nested)"),
1460                (false, false) => format!("{label} of {base_name}"),
1461                (false, true) => format!("{label} (nested)"),
1462            };
1463            queue.push_back(Pending {
1464                at: w.abs(off),
1465                kind: child_kind,
1466                name,
1467                followed,
1468                from: From::Entry(idx, ei),
1469                depth: w.ifds[idx].depth.saturating_add(1),
1470            });
1471        }
1472    }
1473    let next = w.ifds[idx].next;
1474    if next != 0 && queue.len() + w.ifds.len() >= MAX_IFDS {
1475        w.budget_hit = true;
1476        add_note(
1477            &mut w.ifds[idx].notes,
1478            format!("not walked: more than {MAX_IFDS} IFDs"),
1479        );
1480    } else if next != 0 {
1481        let followed_next = followed_here && rules.follows_next(w, &w.ifds[idx], kind);
1482        let (kind, name) = match kind {
1483            Kind::Page(n) => (
1484                Kind::Page(n.saturating_add(1)),
1485                format!("IFD{}", n.saturating_add(1)),
1486            ),
1487            other if base_name.len() > 48 => (other, "IFD chained after a sub-IFD".into()),
1488            other => (other, format!("IFD chained after {base_name}")),
1489        };
1490        queue.push_back(Pending {
1491            at: w.abs(next),
1492            kind,
1493            name,
1494            followed: followed_next,
1495            from: From::Next(idx),
1496            depth: w.ifds[idx].depth,
1497        });
1498    }
1499}
1500
1501// ── Parts before overlap resolution ────────────────────────────────────
1502
1503/// A part before overlap resolution, with its own children.
1504pub struct Cand {
1505    pub range: Range<u64>,
1506    pub kind: PartKind,
1507    pub tag: PartTag,
1508    pub label: Option<String>,
1509    pub disp: Disposition,
1510    pub detail: String,
1511    pub children: Vec<Cand>,
1512    /// For a container whose children account for its contents: the range
1513    /// they tile; uncovered bytes become `Unreferenced` gaps on emission.
1514    pub body: Option<Range<u64>>,
1515    /// The fate and detail of the body's gaps instead of `Unreferenced`
1516    /// (a walk inside stopped at a budget).
1517    pub gap: Option<(Disposition, String)>,
1518    notes: usize,
1519}
1520
1521impl Cand {
1522    pub fn new(
1523        range: Range<u64>,
1524        kind: PartKind,
1525        tag: PartTag,
1526        disp: Disposition,
1527        detail: String,
1528    ) -> Self {
1529        Self {
1530            range,
1531            kind,
1532            tag,
1533            label: None,
1534            disp,
1535            detail,
1536            children: Vec::new(),
1537            body: None,
1538            gap: None,
1539            notes: 0,
1540        }
1541    }
1542
1543    pub fn note(&mut self, text: &str) {
1544        self.notes += 1;
1545        if self.notes <= MAX_NOTES {
1546            push_note(&mut self.detail, text);
1547        } else if self.notes == MAX_NOTES + 1 {
1548            push_note(&mut self.detail, "further overlaps not listed");
1549        }
1550    }
1551}
1552
1553pub fn push_note(detail: &mut String, text: &str) {
1554    if !detail.is_empty() {
1555        detail.push_str("; ");
1556    }
1557    detail.push_str(text);
1558}
1559
1560fn describe(c: &Cand) -> String {
1561    let head = c.detail.split(';').next().unwrap_or("");
1562    format!(
1563        "{} {}..{} ({head})",
1564        c.kind.name(),
1565        c.range.start,
1566        c.range.end
1567    )
1568}
1569
1570/// `c` cut to `r`, with its body and children cut the same way; `None`
1571/// when they do not meet. A cut child says so when `mark`.
1572fn clip(c: &Cand, r: &Range<u64>, mark: bool) -> Option<Cand> {
1573    let range = c.range.start.max(r.start)..c.range.end.min(r.end);
1574    if range.start >= range.end {
1575        return None;
1576    }
1577    let mut out = Cand::new(
1578        range.clone(),
1579        c.kind,
1580        c.tag.clone(),
1581        c.disp,
1582        c.detail.clone(),
1583    );
1584    out.label = c.label.clone();
1585    out.notes = c.notes;
1586    out.gap = c.gap.clone();
1587    out.body = c
1588        .body
1589        .as_ref()
1590        .map(|b| b.start.max(range.start)..b.end.min(range.end))
1591        .filter(|b| b.start < b.end);
1592    out.children = c
1593        .children
1594        .iter()
1595        .filter_map(|ch| clip(ch, &range, true))
1596        .collect();
1597    if mark && range != c.range {
1598        out.note("split by an overlapping part");
1599    }
1600    Some(out)
1601}
1602
1603/// Parts that may overlap, resolved into non-overlapping siblings. Every
1604/// byte goes to the candidate whose fate at that byte (the deepest part of
1605/// its subtree there, or its body's gap fate) ranks strongest; on a tie the
1606/// candidate inserted first keeps it. Each candidate is then cut to the
1607/// stretches it won.
1608#[derive(Default)]
1609pub struct Placed {
1610    cands: Vec<Cand>,
1611}
1612
1613/// One stretch of a candidate with a single fate.
1614struct Seg {
1615    range: Range<u64>,
1616    rank: u8,
1617    cand: usize,
1618}
1619
1620/// The fate rank of a container's body bytes no child covers.
1621fn gap_rank(c: &Cand) -> u8 {
1622    rank(c.gap.as_ref().map_or(Disposition::Unreferenced, |g| g.0))
1623}
1624
1625/// Split `c` (clipped to `within`) into stretches of one fate each.
1626fn segments(c: &Cand, within: &Range<u64>, cand: usize, out: &mut Vec<Seg>) {
1627    let range = c.range.start.max(within.start)..c.range.end.min(within.end);
1628    if range.start >= range.end {
1629        return;
1630    }
1631    let body = c
1632        .body
1633        .as_ref()
1634        .filter(|b| b.start >= c.range.start && b.end <= c.range.end);
1635    let own = |r: Range<u64>, out: &mut Vec<Seg>| {
1636        if r.start >= r.end {
1637            return;
1638        }
1639        let Some(b) = body else {
1640            out.push(Seg {
1641                range: r,
1642                rank: rank(c.disp),
1643                cand,
1644            });
1645            return;
1646        };
1647        let cuts = [
1648            (r.start..r.end.min(b.start), rank(c.disp)),
1649            (r.start.max(b.start)..r.end.min(b.end), gap_rank(c)),
1650            (r.start.max(b.end)..r.end, rank(c.disp)),
1651        ];
1652        for (r, rank) in cuts {
1653            if r.start < r.end {
1654                out.push(Seg {
1655                    range: r,
1656                    rank,
1657                    cand,
1658                });
1659            }
1660        }
1661    };
1662    let mut kids: Vec<&Cand> = c.children.iter().collect();
1663    kids.sort_by_key(|ch| ch.range.start);
1664    let mut cursor = range.start;
1665    for ch in kids {
1666        let start = ch.range.start.clamp(range.start, range.end);
1667        if start > cursor {
1668            own(cursor..start, out);
1669        }
1670        segments(ch, &range, cand, out);
1671        cursor = cursor.max(ch.range.end.min(range.end));
1672    }
1673    own(cursor..range.end, out);
1674}
1675
1676/// Record that `winner` took bytes `loser` covers.
1677fn beaten(by: &mut [Vec<usize>], loser: usize, winner: usize) {
1678    let list = &mut by[loser];
1679    if list.len() <= MAX_NOTES && !list.contains(&winner) {
1680        list.push(winner);
1681    }
1682}
1683
1684impl Placed {
1685    /// Add a candidate. Insertion order breaks ties between equal fates.
1686    pub fn insert(&mut self, c: Cand) {
1687        if c.range.start < c.range.end {
1688            self.cands.push(c);
1689        }
1690    }
1691
1692    /// The parts in file order: each candidate cut to the bytes it won (at
1693    /// most `MAX_PIECES` pieces), plus an `Unknown` part for each stretch it
1694    /// won past that cap.
1695    pub fn into_sorted(self) -> Vec<Cand> {
1696        use core::cmp::Reverse;
1697        let mut cands = self.cands;
1698        let n = cands.len();
1699        let mut segs = Vec::new();
1700        for (i, c) in cands.iter().enumerate() {
1701            segments(c, &c.range, i, &mut segs);
1702        }
1703        // Sweep the segment boundaries; the strongest active segment (first
1704        // candidate on a tie) wins each stretch between two boundaries.
1705        let mut events: Vec<(u64, bool, usize)> = Vec::with_capacity(segs.len() * 2);
1706        for (k, sg) in segs.iter().enumerate() {
1707            events.push((sg.range.start, true, k));
1708            events.push((sg.range.end, false, k));
1709        }
1710        events.sort_unstable_by_key(|&(x, start, k)| (x, start, k));
1711        let mut active: BTreeSet<(Reverse<u8>, usize, usize)> = BTreeSet::new();
1712        let mut live = alloc::vec![0u32; n];
1713        let mut won: Vec<Vec<Range<u64>>> = (0..n).map(|_| Vec::new()).collect();
1714        let mut by: Vec<Vec<usize>> = (0..n).map(|_| Vec::new()).collect();
1715        let mut top: Option<usize> = None;
1716        let mut added = Vec::new();
1717        let mut i = 0;
1718        while i < events.len() {
1719            let x = events[i].0;
1720            added.clear();
1721            while i < events.len() && events[i].0 == x {
1722                let (_, start, k) = events[i];
1723                let sg = &segs[k];
1724                let key = (Reverse(sg.rank), sg.cand, k);
1725                if start {
1726                    active.insert(key);
1727                    live[sg.cand] += 1;
1728                    added.push(sg.cand);
1729                } else {
1730                    active.remove(&key);
1731                    live[sg.cand] -= 1;
1732                }
1733                i += 1;
1734            }
1735            let now = active.first().map(|&(_, c, _)| c);
1736            if let Some(wc) = now {
1737                for &c in &added {
1738                    if c != wc {
1739                        beaten(&mut by, c, wc);
1740                    }
1741                }
1742                if let Some(p) = top
1743                    && p != wc
1744                    && live[p] > 0
1745                {
1746                    beaten(&mut by, p, wc);
1747                }
1748                if let Some(&(next, _, _)) = events.get(i) {
1749                    let r = &mut won[wc];
1750                    match r.last_mut() {
1751                        Some(last) if last.end == x => last.end = next,
1752                        _ => r.push(x..next),
1753                    }
1754                }
1755            }
1756            top = now;
1757        }
1758        // Notes: each winner names what it overlaps, and the children of a
1759        // candidate left in bytes others won.
1760        let descs: Vec<Option<String>> = (0..n)
1761            .map(|k| (!by[k].is_empty()).then(|| describe(&cands[k])))
1762            .collect();
1763        for loser in 0..n {
1764            let Some(desc) = &descs[loser] else { continue };
1765            for &wc in &by[loser] {
1766                cands[wc].note(&format!("overlaps {desc}"));
1767            }
1768            let mine = &won[loser];
1769            let lost = cands[loser]
1770                .children
1771                .iter()
1772                .filter(|ch| {
1773                    let k = mine.partition_point(|r| r.end <= ch.range.start);
1774                    mine.get(k).is_none_or(|r| r.start >= ch.range.end)
1775                })
1776                .count();
1777            if lost > 0 {
1778                let wc = by[loser][0];
1779                cands[wc].note(&format!(
1780                    "{lost} part(s) of {desc} lie in bytes other parts take and are not listed"
1781                ));
1782            }
1783        }
1784        let mut parts = Vec::new();
1785        for (k, c) in cands.into_iter().enumerate() {
1786            let pieces = core::mem::take(&mut won[k]);
1787            if pieces.len() == 1 && pieces[0] == c.range {
1788                parts.push(c);
1789                continue;
1790            }
1791            let others = by[k].len();
1792            let more = if others > MAX_NOTES { "+" } else { "" };
1793            let total = pieces.len();
1794            let listed = total.min(MAX_PIECES);
1795            let desc = descs[k].clone().unwrap_or_else(|| describe(&c));
1796            for (j, r) in pieces.into_iter().enumerate() {
1797                if j >= MAX_PIECES {
1798                    // Past the cap a piece is not listed, but the decoder may
1799                    // still read its bytes.
1800                    parts.push(Cand::new(
1801                        r,
1802                        PartKind::Gap,
1803                        PartTag::None,
1804                        Disposition::Unknown,
1805                        format!(
1806                            "part of {desc}; not listed past {MAX_PIECES} overlaps; the decoder may read it"
1807                        ),
1808                    ));
1809                    continue;
1810                }
1811                let Some(mut piece) = clip(&c, &r, false) else {
1812                    continue;
1813                };
1814                piece.note(&format!(
1815                    "overlaps {}{more} other part(s); piece {} of {listed}{}",
1816                    others.max(1),
1817                    j + 1,
1818                    if total > listed {
1819                        "; the rest is not listed"
1820                    } else {
1821                        ""
1822                    }
1823                ));
1824                parts.push(piece);
1825            }
1826        }
1827        parts.sort_by_key(|c| c.range.start);
1828        parts
1829    }
1830}
1831
1832/// A placed TIFF.
1833pub struct Placement {
1834    /// Parts in file order.
1835    pub parts: Vec<Cand>,
1836    /// Furthest byte any part or claim reaches, clipped to the TIFF.
1837    pub logical_end: u64,
1838    /// Whether IFD0 was read whole.
1839    pub ifd0_ok: bool,
1840    /// Whether a budget or cap stopped part of the walk (gaps are then
1841    /// `Unknown`, [`BUDGET_GAP`]).
1842    pub budget_hit: bool,
1843}
1844
1845/// Turn a walk into non-overlapping parts. `first` are placed right after
1846/// the header, ahead of the TIFF's own parts.
1847pub fn place(w: &mut Walk<'_>, rules: &dyn Rules, first: Vec<Cand>) -> Placement {
1848    let limit = w.limit();
1849    let mut placed = Placed::default();
1850    let mut logical_end = w.header.end;
1851    let ifd0_ok = w
1852        .ifds
1853        .first()
1854        .is_some_and(|i| i.kind == Kind::Page(0) && i.problem.is_none());
1855    if !ifd0_ok {
1856        logical_end = limit;
1857    }
1858    if w.header.end > w.header.start {
1859        let mut c = if w.header_ok {
1860            let what = if w.lay.big {
1861                "BigTIFF header"
1862            } else {
1863                "TIFF header"
1864            };
1865            Cand::new(
1866                w.header.clone(),
1867                PartKind::Header,
1868                PartTag::None,
1869                Disposition::Structure,
1870                what.into(),
1871            )
1872        } else {
1873            let why = w.fatal.clone().unwrap_or_default();
1874            Cand::new(
1875                w.header.clone(),
1876                PartKind::Header,
1877                PartTag::None,
1878                Disposition::Malformed,
1879                why,
1880            )
1881        };
1882        if w.header_ok
1883            && let Some(why) = &w.fatal
1884        {
1885            c.note(why);
1886        }
1887        placed.insert(c);
1888    }
1889    if let Some((r, d, detail)) = w.header_ext.clone() {
1890        placed.insert(Cand::new(r, PartKind::Header, PartTag::None, d, detail));
1891    }
1892    for c in first {
1893        logical_end = logical_end.max(c.range.end);
1894        placed.insert(c);
1895    }
1896
1897    // Values and extents first, so pointer problems land on entries before
1898    // the entries become parts.
1899    let mut values: Vec<Cand> = Vec::new();
1900    // Extent candidates for the whole TIFF: every listed part covers a byte,
1901    // so more than the input's length (plus slack) can never be listed.
1902    let mut extents_left = limit.saturating_add(4096);
1903    for i in 0..w.ifds.len() {
1904        if rules.cancelled() {
1905            w.cancelled = true;
1906            break;
1907        }
1908        let ifd = &w.ifds[i];
1909        if ifd.problem.is_some() {
1910            continue;
1911        }
1912        let mut notes: Vec<(usize, String)> = Vec::new();
1913        for (ei, e) in ifd.entries.iter().enumerate() {
1914            let Loc::At(off, size) = w.loc(e) else {
1915                continue;
1916            };
1917            let fate = rules.value(w, ifd, ei);
1918            let mut detail = format!("{} value, {}", tag_name(ifd.kind, e.tag), ifd.name);
1919            if let Some(n) = &fate.note {
1920                push_note(&mut detail, n);
1921            }
1922            let end = off.saturating_add(size);
1923            if off >= limit {
1924                notes.push((ei, format!("value at {off} is past the end of the data")));
1925                logical_end = limit;
1926                continue;
1927            }
1928            let mut disp = fate.d;
1929            if end > limit {
1930                push_note(&mut detail, "truncated by the end of the data");
1931                disp = Disposition::Malformed;
1932                logical_end = limit;
1933            }
1934            logical_end = logical_end.max(end.min(limit));
1935            let mut c = Cand::new(
1936                off..end.min(limit),
1937                PartKind::Field,
1938                PartTag::Code(u32::from(e.tag)),
1939                disp,
1940                detail,
1941            );
1942            if end <= limit
1943                && let Some((keep, tail)) = rules.value_tail(w, ifd, ei)
1944                && keep < size
1945            {
1946                let mut t = String::new();
1947                if let Some(n) = &tail.note {
1948                    push_note(&mut t, n);
1949                }
1950                c.children.push(Cand::new(
1951                    off + keep..end,
1952                    PartKind::Field,
1953                    PartTag::Code(u32::from(e.tag)),
1954                    tail.d,
1955                    t,
1956                ));
1957            }
1958            values.push(c);
1959        }
1960        for rule in rules.extents(w, ifd) {
1961            let Some(oe) = w.find(ifd, rule.offsets) else {
1962                continue;
1963            };
1964            if extents_left == 0 {
1965                w.budget_hit = true;
1966                notes.push((
1967                    ifd.entries
1968                        .iter()
1969                        .rposition(|e| e.tag == rule.offsets)
1970                        .unwrap_or(0),
1971                    "further extents not listed: the file's extent budget is spent".into(),
1972                ));
1973                continue;
1974            }
1975            let max = usize::try_from((limit / 4 + 1).min(extents_left)).unwrap_or(usize::MAX);
1976            let offs = w.uints(oe, max);
1977            let cnts = match rule.counts {
1978                Count::Tag(ct) => match w.find(ifd, ct) {
1979                    Some(ce) => w.uints(ce, max),
1980                    None => continue,
1981                },
1982                Count::ToEnd => offs
1983                    .first()
1984                    .and_then(|&o| w.abs(o))
1985                    .map(|a| alloc::vec![limit.saturating_sub(a)])
1986                    .unwrap_or_default(),
1987                Count::Bytes(n) => alloc::vec![n],
1988                Count::JpegQuantization | Count::JpegHuffman => {
1989                    let huffman = matches!(rule.counts, Count::JpegHuffman);
1990                    let (lengths, exhausted, cancelled) =
1991                        jpeg_table_counts(w, &offs, huffman, rules);
1992                    w.budget_hit |= exhausted || oe.count > max as u64;
1993                    w.cancelled |= cancelled;
1994                    lengths
1995                }
1996            };
1997            let oi = ifd
1998                .entries
1999                .iter()
2000                .rposition(|e| e.tag == rule.offsets)
2001                .unwrap_or(0);
2002            let pixels = rule.fate.d == Disposition::ImageData
2003                && matches!(rule.offsets, STRIP_OFFSETS | TILE_OFFSETS);
2004            let uses = if pixels {
2005                let n = offs.len().min(cnts.len());
2006                rules.chunk_use(w, ifd, rule.offsets == TILE_OFFSETS, &offs[..n], &cnts[..n])
2007            } else {
2008                Vec::new()
2009            };
2010            let emit = |r: Range<u64>,
2011                        first: usize,
2012                        last: usize,
2013                        values: &mut Vec<Cand>,
2014                        notes: &mut Vec<(usize, String)>| {
2015                let which = if first == last {
2016                    format!("{} {first}", rule.what)
2017                } else {
2018                    format!("{}s {first}..={last}", rule.what)
2019                };
2020                if r.start >= limit {
2021                    notes.push((
2022                        oi,
2023                        format!("{which} at {} is past the end of the data", r.start),
2024                    ));
2025                    return;
2026                }
2027                let mut detail = format!("{}, {which}", ifd.name);
2028                if let Some(n) = &rule.fate.note {
2029                    push_note(&mut detail, n);
2030                }
2031                if pixels
2032                    && (first..=last).any(|k| {
2033                        uses.get(k).is_none_or(|u| {
2034                            u.used.is_none() && u.unread.is_none() && u.unknown.is_none()
2035                        })
2036                    })
2037                {
2038                    push_note(
2039                        &mut detail,
2040                        "bytes after the end of the coded data are not split out",
2041                    );
2042                }
2043                if first == last
2044                    && let Some(n) = uses.get(first).and_then(|u| u.note.as_deref())
2045                {
2046                    push_note(&mut detail, n);
2047                }
2048                let mut disp = rule.fate.d;
2049                if r.end > limit {
2050                    push_note(&mut detail, "truncated by the end of the data");
2051                    disp = Disposition::Malformed;
2052                }
2053                values.push(Cand::new(
2054                    r.start..r.end.min(limit),
2055                    rule.kind,
2056                    PartTag::Code(u32::from(rule.offsets)),
2057                    disp,
2058                    detail,
2059                ));
2060            };
2061            // Merge index-consecutive extents that are also byte-contiguous.
2062            let mut run: Option<(Range<u64>, usize, usize)> = None;
2063            for (k, (&o, &n)) in offs.iter().zip(cnts.iter()).enumerate() {
2064                let Some(start) = w.abs(o) else {
2065                    notes.push((
2066                        oi,
2067                        format!("{} {k}: offset overflows the TIFF base", rule.what),
2068                    ));
2069                    continue;
2070                };
2071                if n == 0 || uses.get(k).is_some_and(|u| u.covered) {
2072                    continue;
2073                }
2074                if extents_left == 0 {
2075                    w.budget_hit = true;
2076                    break;
2077                }
2078                extents_left -= 1;
2079                // A chunk the decoder never reads is listed alone.
2080                let alone = uses.get(k).and_then(|u| {
2081                    u.unread
2082                        .map(|why| (Disposition::Skipped, why))
2083                        .or(u.unknown.map(|why| (Disposition::Unknown, why)))
2084                });
2085                if let Some((fate, why)) = alone {
2086                    if let Some((r, first, last)) = run.take() {
2087                        emit(r, first, last, &mut values, &mut notes);
2088                    }
2089                    let before = values.len();
2090                    let end = start.saturating_add(n);
2091                    logical_end = logical_end.max(end.min(limit));
2092                    emit(start..end, k, k, &mut values, &mut notes);
2093                    if values.len() > before
2094                        && let Some(c) = values.last_mut()
2095                    {
2096                        if c.disp != Disposition::Malformed {
2097                            c.disp = fate;
2098                        }
2099                        push_note(&mut c.detail, why);
2100                    }
2101                    continue;
2102                }
2103                // A chunk the decoder reads by size runs to what it reads.
2104                let read = uses
2105                    .get(k)
2106                    .filter(|u| u.past_count)
2107                    .and_then(|u| u.used)
2108                    .filter(|&used| used > n);
2109                let end = start.saturating_add(read.unwrap_or(n));
2110                logical_end = logical_end.max(end.min(limit));
2111                if end > limit {
2112                    logical_end = limit;
2113                }
2114                // A chunk declared longer than the bytes the decoder reads, or
2115                // with a remark of its own, is listed alone; the unread tail
2116                // becomes a child.
2117                let u = uses.get(k);
2118                let tail = u
2119                    .and_then(|u| u.used)
2120                    .filter(|&used| used < n && end <= limit);
2121                if tail.is_some() || read.is_some() || u.is_some_and(|u| u.note.is_some()) {
2122                    if let Some((r, first, last)) = run.take() {
2123                        emit(r, first, last, &mut values, &mut notes);
2124                    }
2125                    let before = values.len();
2126                    emit(start..end, k, k, &mut values, &mut notes);
2127                    if let Some(used) = read
2128                        && values.len() > before
2129                        && let Some(c) = values.last_mut()
2130                    {
2131                        push_note(
2132                            &mut c.detail,
2133                            &format!("declared {n} bytes; the decoder reads {used}, by row size"),
2134                        );
2135                    }
2136                    if let Some(used) = tail
2137                        && values.len() > before
2138                        && let Some(c) = values.last_mut()
2139                    {
2140                        let what = u.map_or("", |u| u.tail);
2141                        c.children.push(Cand::new(
2142                            start + used..end,
2143                            rule.kind,
2144                            PartTag::Code(u32::from(rule.offsets)),
2145                            Disposition::Dropped,
2146                            format!("{what} (the decoder reads the first {used} bytes)"),
2147                        ));
2148                    }
2149                    continue;
2150                }
2151                run = match run {
2152                    Some((r, first, last)) if r.end == start && last + 1 == k => {
2153                        Some((r.start..end, first, k))
2154                    }
2155                    other => {
2156                        if let Some((r, first, last)) = other {
2157                            emit(r, first, last, &mut values, &mut notes);
2158                        }
2159                        Some((start..end, k, k))
2160                    }
2161                };
2162            }
2163            if let Some((r, first, last)) = run {
2164                emit(r, first, last, &mut values, &mut notes);
2165            }
2166        }
2167        for (ei, text) in notes {
2168            if let Some(e) = w.ifds[i].entries.get_mut(ei) {
2169                add_note(&mut e.notes, text);
2170            }
2171        }
2172    }
2173
2174    // Directories with their entries.
2175    for ifd in &w.ifds {
2176        let fate = match &ifd.problem {
2177            Some(p) => Fate::because(Disposition::Malformed, p.clone()),
2178            None => rules.ifd(w, ifd),
2179        };
2180        let mut detail = format!("{}, {} entries", ifd.name, ifd.entries.len());
2181        if let Some(n) = &fate.note {
2182            push_note(&mut detail, n);
2183        }
2184        if ifd.roles.len() > 1 {
2185            let others: Vec<String> = ifd.roles[1..].iter().map(|&(k, _)| kind_name(k)).collect();
2186            push_note(
2187                &mut detail,
2188                &format!("also reached as {}", others.join(", ")),
2189            );
2190        }
2191        for n in &ifd.notes {
2192            push_note(&mut detail, &format!("next IFD: {n}"));
2193        }
2194        let mut c = Cand::new(
2195            ifd.at..ifd.end,
2196            PartKind::Ifd,
2197            PartTag::None,
2198            fate.d,
2199            detail,
2200        );
2201        logical_end = logical_end.max(ifd.end);
2202        if ifd.problem.is_some() {
2203            logical_end = limit;
2204        }
2205        for (ei, e) in ifd.entries.iter().enumerate() {
2206            let fate = if ifd.problem.is_some() {
2207                Fate::because(Disposition::Malformed, "in an unreadable IFD")
2208            } else {
2209                rules.entry(w, ifd, ei)
2210            };
2211            let mut detail = format!(
2212                "{} {}[{}]",
2213                tag_name(ifd.kind, e.tag),
2214                type_name(e.typ),
2215                e.count
2216            );
2217            if let Some(n) = &fate.note {
2218                push_note(&mut detail, n);
2219            }
2220            for n in &e.notes {
2221                push_note(&mut detail, n);
2222            }
2223            let mut entry = Cand::new(
2224                e.at..e.at + w.lay.entry_len(),
2225                PartKind::Field,
2226                PartTag::Code(u32::from(e.tag)),
2227                fate.d,
2228                detail,
2229            );
2230            if ifd.problem.is_none() {
2231                inline_children(w, rules, ifd, ei, &mut entry);
2232            }
2233            c.children.push(entry);
2234        }
2235        placed.insert(c);
2236    }
2237    // Overlapping bytes take the strongest fate (`Placed`); on a tie the
2238    // header, directories and then values in file order keep them.
2239    for v in values {
2240        placed.insert(v);
2241    }
2242    Placement {
2243        parts: placed.into_sorted(),
2244        logical_end,
2245        ifd0_ok,
2246        budget_hit: w.budget_hit,
2247    }
2248}
2249
2250// ── Emission ───────────────────────────────────────────────────────────
2251
2252/// Push `c` (and its children) under `parent`.
2253pub fn emit(inv: &mut Inventory, parent: Option<PartId>, c: Cand) -> Result<(), InventoryError> {
2254    let body = c
2255        .body
2256        .clone()
2257        .filter(|b| b.start >= c.range.start && b.end <= c.range.end);
2258    let gap = c.gap.clone();
2259    let mut p = Part::new(c.kind, c.tag, c.range, c.disp);
2260    if let Some(l) = c.label {
2261        p = p.with_label(l);
2262    }
2263    if !c.detail.is_empty() {
2264        p = p.with_detail(c.detail);
2265    }
2266    if let Some(b) = body.clone() {
2267        p = p.with_body(b);
2268    }
2269    let id = inv.push(parent, p)?;
2270    let mut children = c.children;
2271    children.sort_by_key(|ch| ch.range.start);
2272    for ch in children {
2273        emit(inv, Some(id), ch)?;
2274    }
2275    if body.is_some() {
2276        match gap {
2277            Some((d, detail)) => {
2278                let before = inv.parts().len();
2279                inv.fill_gaps(Some(id), d)?;
2280                for gap in inv.children(Some(id)) {
2281                    if gap.index() >= before {
2282                        inv.set_detail(gap, detail.clone());
2283                    }
2284                }
2285            }
2286            None => inv.fill_gaps(Some(id), Disposition::Unreferenced)?,
2287        }
2288    }
2289    Ok(())
2290}
2291
2292/// Push top-level `parts` (non-overlapping, in file order) with gaps between
2293/// them: before `logical_end` a gap is `gap` (a lone odd-offset byte between
2294/// parts is a word-alignment pad); the caller's trailing fill covers the
2295/// rest.
2296pub fn emit_top(
2297    inv: &mut Inventory,
2298    data: &[u8],
2299    parts: Vec<Cand>,
2300    logical_end: u64,
2301    gap: (Disposition, Option<String>),
2302) -> Result<(), InventoryError> {
2303    let len = data.len() as u64;
2304    let mut cursor = 0u64;
2305    let push_gap = |inv: &mut Inventory, r: Range<u64>| -> Result<(), InventoryError> {
2306        let end = r.end.min(logical_end);
2307        if r.start >= end {
2308            return Ok(());
2309        }
2310        let mut part = Part::new(PartKind::Gap, PartTag::None, r.start..end, gap.0);
2311        if let Some(d) = &gap.1 {
2312            part = part.with_detail(d.clone());
2313        } else if gap.0 == Disposition::Unreferenced
2314            && end - r.start == 1
2315            && r.start % 2 == 1
2316            && end < logical_end
2317        {
2318            let byte = get(data, r.start, 1).map_or(0, |b| b[0]);
2319            part = Part::new(
2320                PartKind::Gap,
2321                PartTag::None,
2322                r.start..end,
2323                Disposition::Padding,
2324            )
2325            .with_detail(if byte == 0 {
2326                "word-alignment pad byte"
2327            } else {
2328                "word-alignment pad byte (non-zero)"
2329            });
2330        }
2331        inv.push(None, part)?;
2332        Ok(())
2333    };
2334    for c in parts {
2335        if c.range.start > cursor {
2336            push_gap(inv, cursor..c.range.start)?;
2337        }
2338        cursor = cursor.max(c.range.end);
2339        emit(inv, None, c)?;
2340    }
2341    if cursor < len {
2342        push_gap(inv, cursor..len)?;
2343    }
2344    Ok(())
2345}
2346
2347#[cfg(test)]
2348mod tests {
2349    use super::*;
2350    use zencodec::inventory::MetadataKind;
2351
2352    /// xorshift64*: deterministic, no dependency.
2353    struct Rng(u64);
2354
2355    impl Rng {
2356        fn next(&mut self) -> u64 {
2357            self.0 ^= self.0 >> 12;
2358            self.0 ^= self.0 << 25;
2359            self.0 ^= self.0 >> 27;
2360            self.0.wrapping_mul(0x2545_f491_4f6c_dd1d)
2361        }
2362
2363        fn below(&mut self, n: u64) -> u64 {
2364            self.next() % n
2365        }
2366    }
2367
2368    const FATES: [Disposition; 10] = [
2369        Disposition::Metadata(MetadataKind::Xmp),
2370        Disposition::Metadata(MetadataKind::Exif),
2371        Disposition::Structure,
2372        Disposition::ImageData,
2373        Disposition::Malformed,
2374        Disposition::Dropped,
2375        Disposition::Padding,
2376        Disposition::Skipped,
2377        Disposition::Unknown,
2378        Disposition::Unreferenced,
2379    ];
2380
2381    /// A random candidate inside `r`: a table with entry children, a value
2382    /// with a tail child, or a container with a body, children and a gap
2383    /// fate, nested up to `depth` levels.
2384    fn random_cand(rng: &mut Rng, r: Range<u64>, depth: u32) -> Cand {
2385        let disp = FATES[rng.below(FATES.len() as u64) as usize];
2386        let mut c = Cand::new(
2387            r.clone(),
2388            PartKind::Field,
2389            PartTag::None,
2390            disp,
2391            String::new(),
2392        );
2393        let len = r.end - r.start;
2394        if depth == 0 || len < 2 {
2395            return c;
2396        }
2397        let inner = match rng.below(4) {
2398            // A value with a tail it does not consume.
2399            0 => {
2400                let keep = rng.below(len);
2401                c.children
2402                    .push(random_cand(rng, r.start + keep..r.end, depth - 1));
2403                return c;
2404            }
2405            // A container: children tile part of its body.
2406            1 => {
2407                let a = r.start + rng.below(len);
2408                let b = a + 1 + rng.below(r.end - a);
2409                c.body = Some(a..b);
2410                if rng.below(2) == 0 {
2411                    c.gap = Some((Disposition::Unknown, "budget".into()));
2412                }
2413                a..b
2414            }
2415            // A table: entries anywhere in it.
2416            2 => r.clone(),
2417            _ => return c,
2418        };
2419        let mut at = inner.start;
2420        while at < inner.end && c.children.len() < 4 {
2421            let start = at + rng.below(3);
2422            if start >= inner.end {
2423                break;
2424            }
2425            let end = start + 1 + rng.below((inner.end - start).min(12));
2426            c.children.push(random_cand(rng, start..end, depth - 1));
2427            at = end;
2428        }
2429        c
2430    }
2431
2432    /// The fate rank the inventory gives byte `b` of `c`: its deepest part
2433    /// there, or its body's gap fate.
2434    fn at(c: &Cand, b: u64) -> Option<u8> {
2435        if !c.range.contains(&b) {
2436            return None;
2437        }
2438        for ch in &c.children {
2439            if let Some(r) = at(ch, b) {
2440                return Some(r);
2441            }
2442        }
2443        match &c.body {
2444            Some(body) if body.contains(&b) => Some(gap_rank(c)),
2445            _ => Some(rank(c.disp)),
2446        }
2447    }
2448
2449    /// Children lie inside their parent (and its body) and do not overlap.
2450    fn well_formed(c: &Cand) {
2451        let mut kids: Vec<&Cand> = c.children.iter().collect();
2452        kids.sort_by_key(|ch| ch.range.start);
2453        let mut end = c.range.start;
2454        for ch in kids {
2455            assert!(ch.range.start >= end && ch.range.end <= c.range.end);
2456            assert!(ch.range.start < ch.range.end);
2457            end = ch.range.end;
2458            well_formed(ch);
2459        }
2460        if let Some(b) = &c.body {
2461            assert!(b.start >= c.range.start && b.end <= c.range.end);
2462        }
2463    }
2464
2465    #[test]
2466    fn every_byte_takes_the_strongest_fate() {
2467        const SPACE: u64 = 48;
2468        let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
2469        for round in 0..4000 {
2470            let n = 1 + rng.below(8) as usize;
2471            let cands: Vec<Cand> = (0..n)
2472                .map(|_| {
2473                    let a = rng.below(SPACE);
2474                    let b = a + 1 + rng.below(SPACE - a);
2475                    random_cand(&mut rng, a..b, 3)
2476                })
2477                .collect();
2478            let expect: Vec<Option<u8>> = (0..SPACE)
2479                .map(|b| cands.iter().filter_map(|c| at(c, b)).max())
2480                .collect();
2481            let mut placed = Placed::default();
2482            for c in cands {
2483                placed.insert(c);
2484            }
2485            let parts = placed.into_sorted();
2486            let mut end = 0;
2487            for p in &parts {
2488                assert!(p.range.start >= end, "round {round}: siblings overlap");
2489                assert!(!p.detail.contains("not listed past"), "round {round}");
2490                end = p.range.end;
2491                well_formed(p);
2492            }
2493            for b in 0..SPACE {
2494                let got = parts.iter().find_map(|p| at(p, b));
2495                assert_eq!(got, expect[b as usize], "round {round}, byte {b}");
2496            }
2497        }
2498    }
2499}