Skip to main content

blad_container/
ifd.rs

1//! Public, lossless access to a TIFF file's directory structure.
2//!
3//! The archival path reads only the handful of tags needed to find pixels. This module
4//! is the other half: every entry in every directory, with its type, count, and the
5//! offset its bytes live at, and no interpretation whatsoever.
6//!
7//! The split matters. [`crate::tiff::analyze`] must never be influenced by metadata it
8//! does not understand, and a metadata reader must never drop something merely because
9//! archival had no use for it. Keeping them separate means neither constrains the other;
10//! naming and units are somebody else's problem (see `blad-meta`).
11
12use crate::tiff::{
13    malformed, read_ifd, type_size, Reader, MAX_ENTRIES_PER_IFD, MAX_IFDS, MAX_VALUES,
14};
15use crate::{Error, Result};
16use std::fs::File;
17use std::io::{Read, Seek};
18use std::path::Path;
19
20/// Tags whose value is a pointer to another directory.
21const TAG_SUB_IFDS: u16 = 330;
22const TAG_EXIF_IFD: u16 = 34665;
23const TAG_GPS_IFD: u16 = 34853;
24const TAG_INTEROP_IFD: u16 = 40965;
25
26/// Which directory an entry was found in.
27///
28/// Worth distinguishing because the same tag number means different things in different
29/// directories — tag 1 is `InteroperabilityIndex` under Interop and `GPSLatitudeRef`
30/// under GPS. A reader that forgets where an entry came from cannot name it correctly.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub enum IfdKind {
33    /// Main image directory, and any further directories chained after it.
34    Main(u16),
35    /// A directory referenced by tag 330. On raw files this is where the sensor lives.
36    Sub(u16),
37    Exif,
38    Gps,
39    Interop,
40}
41
42impl IfdKind {
43    pub fn label(&self) -> String {
44        match self {
45            IfdKind::Main(0) => "IFD0".into(),
46            IfdKind::Main(n) => format!("IFD{n}"),
47            IfdKind::Sub(n) => format!("SubIFD{n}"),
48            IfdKind::Exif => "Exif".into(),
49            IfdKind::Gps => "GPS".into(),
50            IfdKind::Interop => "Interop".into(),
51        }
52    }
53}
54
55/// One directory entry, with its bytes but without any interpretation of them.
56#[derive(Debug, Clone)]
57pub struct RawEntry {
58    pub tag: u16,
59    /// TIFF type code. Kept as a number because an unrecognised type must still be
60    /// reportable — refusing to describe an entry we cannot decode would hide it.
61    pub dtype: u16,
62    pub count: u32,
63    /// Where the value bytes live. For values of four bytes or fewer this points inside
64    /// the 12-byte entry itself.
65    pub value_offset: u64,
66    /// True when the value was small enough to be stored in the entry.
67    pub inline: bool,
68    /// The value bytes, in file order. Empty if the entry could not be read — which is
69    /// reported rather than treated as fatal, because one bad entry should not cost you
70    /// the other two hundred.
71    pub bytes: Vec<u8>,
72    /// Why `bytes` is empty, when it is.
73    pub unreadable: Option<String>,
74}
75
76impl RawEntry {
77    /// Byte length the type and count imply, whether or not it was readable.
78    pub fn declared_len(&self) -> u64 {
79        type_size(self.dtype).unwrap_or(0) * u64::from(self.count)
80    }
81}
82
83#[derive(Debug, Clone)]
84pub struct RawIfd {
85    pub kind: IfdKind,
86    pub offset: u64,
87    pub entries: Vec<RawEntry>,
88}
89
90#[derive(Debug, Clone)]
91pub struct Directories {
92    pub little_endian: bool,
93    pub file_len: u64,
94    /// Byte offset of the TIFF header within the file. Zero for a plain TIFF; for a JPEG
95    /// it is where the APP1 Exif block's TIFF header begins.
96    ///
97    /// TIFF offsets are relative to that header, so every offset reported here has the
98    /// base already added — an offset you can seek to directly, which is the only kind
99    /// worth printing.
100    pub tiff_base: u64,
101    pub ifds: Vec<RawIfd>,
102}
103
104impl Directories {
105    pub fn entry(&self, kind: IfdKind, tag: u16) -> Option<&RawEntry> {
106        self.ifds
107            .iter()
108            .find(|i| i.kind == kind)?
109            .entries
110            .iter()
111            .find(|e| e.tag == tag)
112    }
113}
114
115/// A `Read + Seek` view of a byte range, presenting it as if it started at zero.
116///
117/// TIFF offsets are relative to the TIFF header. Inside a JPEG that header sits some way
118/// into the file, so the parser is given a window rather than being taught about a base
119/// offset it would have to add in a dozen places and could forget in one.
120pub struct Window<R> {
121    inner: R,
122    base: u64,
123    len: u64,
124    pos: u64,
125}
126
127impl<R: Read + Seek> Window<R> {
128    pub fn new(inner: R, base: u64, len: u64) -> Self {
129        Self {
130            inner,
131            base,
132            len,
133            pos: 0,
134        }
135    }
136}
137
138impl<R: Read + Seek> Read for Window<R> {
139    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
140        let left = self.len.saturating_sub(self.pos);
141        if left == 0 {
142            return Ok(0);
143        }
144        let want = buf.len().min(left as usize);
145        self.inner
146            .seek(std::io::SeekFrom::Start(self.base + self.pos))?;
147        let n = self.inner.read(&mut buf[..want])?;
148        self.pos += n as u64;
149        Ok(n)
150    }
151}
152
153impl<R: Read + Seek> Seek for Window<R> {
154    fn seek(&mut self, from: std::io::SeekFrom) -> std::io::Result<u64> {
155        use std::io::SeekFrom::*;
156        let p = match from {
157            Start(n) => n as i64,
158            End(n) => self.len as i64 + n,
159            Current(n) => self.pos as i64 + n,
160        };
161        if p < 0 {
162            return Err(std::io::Error::new(
163                std::io::ErrorKind::InvalidInput,
164                "seek before start of window",
165            ));
166        }
167        self.pos = p as u64;
168        Ok(self.pos)
169    }
170}
171
172/// Locate the TIFF header inside a JPEG's APP1 Exif segment.
173///
174/// Returns the offset of the TIFF header and the bytes available after it.
175fn jpeg_exif_window<R: Read + Seek>(src: &mut R, file_len: u64) -> Result<(u64, u64)> {
176    let mut pos = 2u64; // past SOI
177    loop {
178        if pos + 4 > file_len {
179            return Err(malformed("no APP1 Exif segment".into()));
180        }
181        src.seek(std::io::SeekFrom::Start(pos))?;
182        let mut hdr = [0u8; 4];
183        src.read_exact(&mut hdr)
184            .map_err(|_| malformed("truncated JPEG segment header".into()))?;
185        if hdr[0] != 0xFF {
186            return Err(malformed(format!("bad JPEG marker at {pos}")));
187        }
188        let marker = hdr[1];
189        // Start of scan or end of image: entropy-coded data follows, no more metadata.
190        if marker == 0xDA || marker == 0xD9 {
191            return Err(malformed("no APP1 Exif segment before image data".into()));
192        }
193        // Standalone markers carry no length field.
194        if marker == 0x01 || (0xD0..=0xD8).contains(&marker) {
195            pos += 2;
196            continue;
197        }
198        let seg_len = u64::from(u16::from_be_bytes([hdr[2], hdr[3]]));
199        if seg_len < 2 {
200            return Err(malformed(format!(
201                "JPEG segment at {pos} declares {seg_len}"
202            )));
203        }
204        let data = pos + 4;
205        let data_len = seg_len - 2;
206
207        if marker == 0xE1 && data_len > 6 {
208            let mut tag = [0u8; 6];
209            src.seek(std::io::SeekFrom::Start(data))?;
210            if src.read_exact(&mut tag).is_ok() && &tag == b"Exif\0\0" {
211                return Ok((data + 6, data_len - 6));
212            }
213        }
214        pos = data + data_len;
215    }
216}
217
218/// Read every directory in a TIFF-based file.
219pub fn read(path: &Path) -> Result<Directories> {
220    let mut f = File::open(path)?;
221    let file_len = f.metadata()?.len();
222    read_from(&mut f, file_len)
223}
224
225pub fn read_from<R: Read + Seek>(src: &mut R, file_len: u64) -> Result<Directories> {
226    let mut magic = [0u8; 4];
227    src.seek(std::io::SeekFrom::Start(0))?;
228    src.read_exact(&mut magic)
229        .map_err(|_| Error::UnknownFormat)?;
230
231    // A JPEG keeps its Exif in an APP1 segment holding a complete TIFF structure. Parse
232    // it through a window so the TIFF code needs no knowledge of JPEG at all.
233    if magic[0] == 0xFF && magic[1] == 0xD8 {
234        let (base, len) = jpeg_exif_window(src, file_len)?;
235        let mut w = Window::new(src, base, len);
236        let mut d = read_inner(&mut w, len)?;
237        d.tiff_base = base;
238        d.file_len = file_len;
239        for i in &mut d.ifds {
240            i.offset += base;
241            for e in &mut i.entries {
242                e.value_offset += base;
243            }
244        }
245        return Ok(d);
246    }
247
248    read_inner(src, file_len)
249}
250
251fn read_inner<R: Read + Seek>(src: &mut R, file_len: u64) -> Result<Directories> {
252    let mut magic = [0u8; 4];
253    src.seek(std::io::SeekFrom::Start(0))?;
254    src.read_exact(&mut magic)
255        .map_err(|_| Error::UnknownFormat)?;
256    let little_endian = match &magic {
257        [b'I', b'I', 42, 0] => true,
258        [b'M', b'M', 0, 42] => false,
259        _ => return Err(Error::UnknownFormat),
260    };
261
262    let mut r = Reader {
263        src,
264        little_endian,
265        file_len,
266    };
267    let first = u64::from(r.u32_at(4)?);
268
269    // Breadth-first, so IFD0 is reported before the directories it points at — which is
270    // both the order a reader expects and the order the file is usually laid out in.
271    let mut ifds: Vec<RawIfd> = Vec::new();
272    let mut seen: Vec<u64> = Vec::new();
273    let mut queue: std::collections::VecDeque<(u64, IfdKind)> = std::collections::VecDeque::new();
274    queue.push_back((first, IfdKind::Main(0)));
275
276    let mut sub_n = 0u16;
277    let mut main_n = 0u16;
278
279    while let Some((off, kind)) = queue.pop_front() {
280        if off == 0 || off >= file_len || seen.contains(&off) || ifds.len() >= MAX_IFDS {
281            continue;
282        }
283        seen.push(off);
284
285        let ifd = match read_ifd(&mut r, off) {
286            Ok(i) => i,
287            // A directory that will not parse should not cost us the ones that will.
288            Err(_) => continue,
289        };
290        if ifd.entries.len() > usize::from(MAX_ENTRIES_PER_IFD) {
291            continue;
292        }
293
294        let mut entries = Vec::with_capacity(ifd.entries.len());
295        for (i, e) in ifd.entries.iter().enumerate() {
296            let entry_off = off + 2 + (i as u64) * 12;
297            let esize = type_size(e.dtype).unwrap_or(0);
298            let total = esize.saturating_mul(u64::from(e.count));
299            let inline = total <= 4;
300            let value_offset = if inline {
301                entry_off + 8
302            } else {
303                u64::from(if little_endian {
304                    u32::from_le_bytes(e.value_field)
305                } else {
306                    u32::from_be_bytes(e.value_field)
307                })
308            };
309
310            let (bytes, unreadable) = if esize == 0 {
311                (Vec::new(), Some(format!("unknown TIFF type {}", e.dtype)))
312            } else if u64::from(e.count) > MAX_VALUES {
313                (
314                    Vec::new(),
315                    Some(format!("{} values exceeds the {MAX_VALUES} cap", e.count)),
316                )
317            } else if inline {
318                (e.value_field[..total as usize].to_vec(), None)
319            } else {
320                match r.bytes_at(value_offset, total as usize) {
321                    Ok(b) => (b, None),
322                    Err(err) => (Vec::new(), Some(err.to_string())),
323                }
324            };
325
326            // Follow pointers to nested directories.
327            if unreadable.is_none() {
328                let ptrs: Vec<u64> = match e.tag {
329                    TAG_SUB_IFDS => decode_offsets(&bytes, esize, little_endian),
330                    TAG_EXIF_IFD | TAG_GPS_IFD | TAG_INTEROP_IFD => {
331                        decode_offsets(&bytes, esize, little_endian)
332                    }
333                    _ => Vec::new(),
334                };
335                for p in ptrs {
336                    let k = match e.tag {
337                        TAG_SUB_IFDS => {
338                            sub_n += 1;
339                            IfdKind::Sub(sub_n - 1)
340                        }
341                        TAG_EXIF_IFD => IfdKind::Exif,
342                        TAG_GPS_IFD => IfdKind::Gps,
343                        _ => IfdKind::Interop,
344                    };
345                    queue.push_back((p, k));
346                }
347            }
348
349            entries.push(RawEntry {
350                tag: e.tag,
351                dtype: e.dtype,
352                count: e.count,
353                value_offset,
354                inline,
355                bytes,
356                unreadable,
357            });
358        }
359
360        // The next-IFD pointer follows the entry array. Only main directories chain;
361        // Exif and GPS sub-directories terminate.
362        if matches!(kind, IfdKind::Main(_)) {
363            let next_off = off + 2 + (ifd.entries.len() as u64) * 12;
364            if next_off + 4 <= file_len {
365                if let Ok(next) = r.u32_at(next_off) {
366                    main_n += 1;
367                    queue.push_back((u64::from(next), IfdKind::Main(main_n)));
368                }
369            }
370        }
371
372        ifds.push(RawIfd {
373            kind,
374            offset: off,
375            entries,
376        });
377    }
378
379    if ifds.is_empty() {
380        return Err(malformed("no readable directories".into()));
381    }
382
383    Ok(Directories {
384        little_endian,
385        file_len,
386        tiff_base: 0,
387        ifds,
388    })
389}
390
391fn decode_offsets(bytes: &[u8], esize: u64, le: bool) -> Vec<u64> {
392    if esize != 4 {
393        return Vec::new();
394    }
395    bytes
396        .chunks_exact(4)
397        .map(|c| {
398            let b = [c[0], c[1], c[2], c[3]];
399            u64::from(if le {
400                u32::from_le_bytes(b)
401            } else {
402                u32::from_be_bytes(b)
403            })
404        })
405        .collect()
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411    use std::io::Cursor;
412
413    /// Minimal little-endian TIFF: header, one IFD with `entries`, then a next-IFD of 0.
414    fn tiff(entries: &[(u16, u16, u32, [u8; 4])]) -> Vec<u8> {
415        let mut b = vec![b'I', b'I', 42, 0, 8, 0, 0, 0];
416        b.extend((entries.len() as u16).to_le_bytes());
417        for (tag, dtype, count, val) in entries {
418            b.extend(tag.to_le_bytes());
419            b.extend(dtype.to_le_bytes());
420            b.extend(count.to_le_bytes());
421            b.extend(val);
422        }
423        b.extend(0u32.to_le_bytes());
424        b
425    }
426
427    #[test]
428    fn reads_entries_with_offsets_and_inline_flag() {
429        let buf = tiff(&[(271, 2, 3, *b"HB\0\0")]);
430        let len = buf.len() as u64;
431        let d = read_from(&mut Cursor::new(&buf), len).unwrap();
432        assert_eq!(d.ifds.len(), 1);
433        let e = &d.ifds[0].entries[0];
434        assert_eq!(e.tag, 271);
435        assert!(e.inline);
436        assert_eq!(e.bytes, b"HB\0");
437        assert_eq!(e.declared_len(), 3);
438    }
439
440    /// An entry we cannot decode must still be reported. Dropping it would make the
441    /// output silently incomplete, which is worse than saying "this one is broken".
442    #[test]
443    fn unknown_type_is_reported_not_dropped() {
444        let buf = tiff(&[(999, 77, 1, [0; 4])]);
445        let len = buf.len() as u64;
446        let d = read_from(&mut Cursor::new(&buf), len).unwrap();
447        let e = &d.ifds[0].entries[0];
448        assert_eq!(e.tag, 999);
449        assert!(e.unreadable.is_some());
450        assert!(e.bytes.is_empty());
451    }
452
453    /// An out-of-line value pointing past EOF must not be fatal.
454    #[test]
455    fn value_pointer_past_eof_is_survivable() {
456        let buf = tiff(&[(700, 1, 64, 0xFFFF_0000u32.to_le_bytes())]);
457        let len = buf.len() as u64;
458        let d = read_from(&mut Cursor::new(&buf), len).unwrap();
459        assert!(d.ifds[0].entries[0].unreadable.is_some());
460    }
461
462    /// A JPEG keeps Exif in APP1. Offsets must come back file-absolute, not relative to
463    /// the embedded TIFF header, or they cannot be seeked to.
464    #[test]
465    fn jpeg_app1_exif_is_found_and_offsets_are_absolute() {
466        let inner = tiff(&[(271, 2, 3, *b"HB\0\0")]);
467        let mut j = vec![0xFF, 0xD8]; // SOI
468        j.extend([0xFF, 0xE0, 0x00, 0x04, 0x00, 0x00]); // APP0, skipped
469        let payload_len = (inner.len() + 6 + 2) as u16;
470        j.extend([0xFF, 0xE1]);
471        j.extend(payload_len.to_be_bytes());
472        let tiff_base = j.len() as u64 + 6;
473        j.extend(b"Exif\0\0");
474        j.extend(&inner);
475        j.extend([0xFF, 0xD9]);
476
477        let len = j.len() as u64;
478        let d = read_from(&mut Cursor::new(&j), len).unwrap();
479        assert_eq!(d.tiff_base, tiff_base);
480        assert_eq!(d.file_len, len);
481        assert_eq!(d.ifds[0].entries[0].tag, 271);
482        // The IFD sits 8 bytes into the embedded TIFF, reported absolutely.
483        assert_eq!(d.ifds[0].offset, tiff_base + 8);
484    }
485
486    /// A JPEG with no Exif must fail cleanly rather than scanning into entropy data.
487    #[test]
488    fn jpeg_without_exif_is_rejected_at_the_scan_marker() {
489        let j = vec![0xFF, 0xD8, 0xFF, 0xDA, 0x00, 0x02, 0xFF, 0xD9];
490        assert!(read_from(&mut Cursor::new(&j), j.len() as u64).is_err());
491    }
492
493    #[test]
494    fn non_tiff_is_rejected() {
495        let buf = vec![0u8; 32];
496        assert!(read_from(&mut Cursor::new(&buf), 32).is_err());
497    }
498
499    /// A directory that points at itself must terminate.
500    #[test]
501    fn self_referential_subifd_terminates() {
502        let buf = tiff(&[(330, 4, 1, 8u32.to_le_bytes())]);
503        let len = buf.len() as u64;
504        let d = read_from(&mut Cursor::new(&buf), len).unwrap();
505        assert_eq!(d.ifds.len(), 1);
506    }
507}