//! Bounds-checked IFD parsing. use crate::{ByteOrder, Entry, FieldType, Ifd, Result, Tiff, TiffError, Value, tags}; use std::collections::{HashMap, HashSet}; /// Limits applied while parsing. The defaults are generous for real files and safe for hostile ones. #[derive(Clone, Debug)] pub struct ParseOptions { /// Maximum number of IFDs visited in total (chain + children). pub max_ifds: usize, /// Maximum nesting depth of child IFDs. pub max_depth: usize, /// Maximum number of entries read from one IFD. pub max_entries: usize, /// Maximum total number of entries read across all IFDs (chain + children). /// /// Hostile files can reuse the same bytes for up to `max_ifds` tables of `max_entries` /// entries each (overlapping IFD tables via SubIFD pointers shifted by one entry), which can /// allocate millions of `Entry`s from a small input (~100 KB → ~400 MB). This hard cap bounds /// that amplification regardless of entry size. 65 536 is generous for real files — a 50 MB /// scanned TIFF with thousands of entries across chain + children stays well under it — yet /// stops a small file from allocating hundreds of megabytes of `Entry`s. pub max_entries_total: usize, /// Maximum total bytes of decoded values; `None` = `8 × input length + 1 MiB`. pub value_budget: Option, /// Follow `SubIFDs` / Exif / GPS / Interop pointers. pub follow_children: bool, /// Accept the TIFF-like magic numbers used by some raw formats (ORF `IIRO`/`IIRS`, RW2 `IIU`). pub accept_raw_magic: bool, } impl Default for ParseOptions { fn default() -> Self { Self { max_ifds: 1024, max_depth: 10, max_entries: 8192, max_entries_total: 65536, value_budget: None, follow_children: true, accept_raw_magic: true, } } } impl ParseOptions { /// The value budget for `data_len` bytes of input: [`Self::value_budget`], else `8 × data_len + 1 MiB`. pub fn budget_for(&self, data_len: usize) -> u64 { self.value_budget.unwrap_or((data_len as u64).saturating_mul(8).saturating_add(1 << 20)) } } /// Where a directory's entries are and how many of them the reader reads, as [`directory_entries`] found them. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct DirectoryEntries { /// Position of the first entry. table: usize, /// Entries read: the declared count, cut to the entries the data holds and to the `max_entries` limit. read: usize, /// The declared entry count. declared: u64, /// Bytes per entry: 12, or 20 in BigTIFF. entry_size: usize, } impl DirectoryEntries { /// Positions of the entries read, in directory order. Checked arithmetic: the iteration ends rather than /// overflow, whatever the layout. pub fn positions(&self) -> impl Iterator + use<> { let (table, size) = (self.table, self.entry_size); (0..self.read).map_while(move |i| i.checked_mul(size)?.checked_add(table)) } /// The number of entries read. pub fn read(&self) -> usize { self.read } /// The entry count the directory declares. pub fn declared(&self) -> u64 { self.declared } } /// The directory at `offset` as the reader walks it: its entry table and how many entries it reads. The same errors /// as [`parse_ifd_at`] for a directory it can't read: past the data, empty, or without room for one entry. pub fn directory_entries(data: &[u8], offset: u64, order: ByteOrder, bigtiff: bool, max_entries: usize) -> Result { let pos = usize::try_from(offset).map_err(|_| TiffError::Truncated(offset))?; let (n, head, entry_size) = if bigtiff { (order.read_u64(data, pos).ok_or(TiffError::Truncated(offset))?, 8usize, 20usize) } else { (order.read_u16(data, pos).ok_or(TiffError::Truncated(offset))? as u64, 2, 12) }; let table = pos + head; let fit = (data.len().saturating_sub(table) / entry_size) as u64; let read = n.min(fit).min(max_entries as u64) as usize; if n == 0 { return Err(TiffError::Invalid(format!("empty IFD at {offset}"))); } if read == 0 { return Err(TiffError::Truncated(offset)); } Ok(DirectoryEntries { table, read, declared: n, entry_size }) } /// A directory entry the reader can read: its tag, type and count, and where its values are. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct EntrySpan { pub tag: u16, pub field_type: FieldType, pub count: u64, /// Position of the values in the data: the entry's own value field when they fit it, else `base` plus the offset /// stored there. pub start: u64, /// `count × size` bytes. pub len: u64, } /// The entry at `pos`, when the reader reads it: a known field type, `count × size` without overflow, and values /// inside `data`. Nothing is decoded; [`parse_ifd_at`] also drops an entry whose values exceed what is left of its /// value budget ([`ParseOptions::budget_for`]), which this leaves to the caller. pub fn entry_span(data: &[u8], pos: usize, order: ByteOrder, bigtiff: bool, base: u64) -> Option { let tag = order.read_u16(data, pos)?; let field_type = FieldType::from_u16(order.read_u16(data, pos.checked_add(2)?)?)?; let (count, field, inline) = if bigtiff { (order.read_u64(data, pos.checked_add(4)?)?, pos.checked_add(12)?, 8u64) } else { (order.read_u32(data, pos.checked_add(4)?)? as u64, pos.checked_add(8)?, 4) }; let len = count.checked_mul(field_type.size() as u64)?; let start = if len <= inline { field as u64 } else { let rel = if bigtiff { order.read_u64(data, field)? } else { order.read_u32(data, field)? as u64 }; base.checked_add(rel)? }; let end = start.checked_add(len)?; (end <= data.len() as u64).then_some(EntrySpan { tag, field_type, count, start, len }) } /// Whether a directory declaring `declared` entries was read whole (`read` entries), so that its next-IFD pointer /// follows them. The counts compare as 64-bit numbers: narrowing the declared count to `usize` instead would, on a /// 32-bit target, let a BigTIFF declaring 4,294,967,297 entries but holding one pass for a whole directory. fn reads_every_entry(declared: u64, read: usize) -> bool { u64::try_from(read).is_ok_and(|read| read == declared) } /// Parse the header: (byte order, bigtiff, first IFD offset). pub(crate) fn header(data: &[u8]) -> Result<(ByteOrder, bool, u64)> { header_with(data, true) } fn header_with(data: &[u8], raw_magic: bool) -> Result<(ByteOrder, bool, u64)> { let order = match data.get(0..2) { Some(b"II") => ByteOrder::Little, Some(b"MM") => ByteOrder::Big, _ => return Err(TiffError::NotTiff), }; let magic = order.read_u16(data, 2).ok_or(TiffError::NotTiff)?; match magic { 42 => Ok((order, false, order.read_u32(data, 4).ok_or(TiffError::Truncated(4))? as u64)), 43 => { let bytesize = order.read_u16(data, 4).ok_or(TiffError::Truncated(4))?; let zero = order.read_u16(data, 6).ok_or(TiffError::Truncated(6))?; if bytesize != 8 || zero != 0 { return Err(TiffError::NotTiff); } Ok((order, true, order.read_u64(data, 8).ok_or(TiffError::Truncated(8))?)) } 0x4f52 | 0x5352 | 0x0055 if raw_magic => Ok((order, false, order.read_u32(data, 4).ok_or(TiffError::Truncated(4))? as u64)), _ => Err(TiffError::NotTiff), } } pub(crate) fn parse(data: &[u8], opts: &ParseOptions) -> Result { parse_with_status(data, opts).map(|(tiff, _)| tiff) } pub(crate) fn parse_with_status(data: &[u8], opts: &ParseOptions) -> Result<(Tiff, bool)> { let (order, big, first) = header_with(data, opts.accept_raw_magic)?; let mut ctx = Ctx::new(data, order, big, opts); let mut ifds = Vec::new(); let mut off = first; // the top-level chain stops at an IFD it already holds (a `next` pointer looping back) let mut chain = HashSet::new(); while off != 0 { if !chain.insert(off) { // a next pointer looping back: the chain as written has no end ctx.complete = false; break; } match ctx.ifd(off, 0, 0) { Ok((ifd, next)) => { ifds.push(ifd); off = next; } Err(e) => { if ifds.is_empty() { return Err(e); } ctx.complete = false; break; } } } if ifds.is_empty() { return Err(TiffError::Invalid("no IFDs".into())); } Ok((Tiff { order, bigtiff: big, ifds }, ctx.complete)) } /// Parse one IFD at absolute `offset` whose value offsets (and child IFD pointers) are relative to /// `base` (absolute position = `base + stored offset`). Returns the IFD and the absolute offset of the /// next IFD (0 when there is none). Used for maker notes, which often use their own offset base. pub fn parse_ifd_at(data: &[u8], offset: u64, order: ByteOrder, base: u64, bigtiff: bool, opts: &ParseOptions) -> Result<(Ifd, u64)> { let mut ctx = Ctx::new(data, order, bigtiff, opts); ctx.ifd(offset, base, 0) } struct Ctx<'a> { data: &'a [u8], order: ByteOrder, big: bool, opts: &'a ParseOptions, /// Offsets of the IFDs being parsed (the current path from the root): a child pointing at one of them is a /// cycle. ancestors: Vec, /// IFDs produced so far, counting every copy of a shared one (bounded by `max_ifds`). visits: usize, /// IFDs already parsed, by (offset, base, depth): a second pointer to one (two `SubIFDs` entries, or an Exif /// pointer shared by two directories) gets a copy instead of being dropped. Keyed by depth too, so a copy has /// exactly the children a fresh parse at that depth would have; a copy is charged the IFDs, entries and value /// bytes of the first parse, so copies stay inside every limit. parsed: HashMap<(u64, u64, usize), Parsed>, budget: u64, entries_total: usize, /// False once an IFD, entry, pointer target or chain link could not be fully inspected. complete: bool, } /// What parsing one IFD and its children used up: a copy of it is charged the same. #[derive(Clone, Copy)] struct Cost { ifds: usize, entries: usize, bytes: u64, } /// An IFD parsed once, kept for later pointers to it. struct Parsed { ifd: Ifd, next: u64, cost: Cost, } impl<'a> Ctx<'a> { fn new(data: &'a [u8], order: ByteOrder, big: bool, opts: &'a ParseOptions) -> Self { let budget = opts.budget_for(data.len()); Self { data, order, big, opts, ancestors: Vec::new(), visits: 0, parsed: HashMap::new(), budget, entries_total: 0, complete: true } } fn read_off(&self, pos: usize) -> Option { if self.big { self.order.read_u64(self.data, pos) } else { self.order.read_u32(self.data, pos).map(|v| v as u64) } } /// Returns (ifd, absolute next offset or 0). fn ifd(&mut self, offset: u64, base: u64, depth: usize) -> Result<(Ifd, u64)> { if depth > self.opts.max_depth { return Err(TiffError::Limit("IFD nesting depth")); } if self.ancestors.contains(&offset) { return Err(TiffError::Invalid(format!("IFD loop at {offset}"))); } let key = (offset, base, depth); if let Some(c) = self.parsed.get(&key) { // a copy costs what the first parse did: its IFDs, entries and value bytes let visits = self.visits.saturating_add(c.cost.ifds); if visits > self.opts.max_ifds { return Err(TiffError::Limit("IFD count")); } let entries = self.entries_total.saturating_add(c.cost.entries); if entries > self.opts.max_entries_total { return Err(TiffError::Limit("entry count")); } if c.cost.bytes > self.budget { return Err(TiffError::Limit("value bytes")); } self.visits = visits; self.entries_total = entries; self.budget -= c.cost.bytes; return Ok((c.ifd.clone(), c.next)); } if self.visits >= self.opts.max_ifds { return Err(TiffError::Limit("IFD count")); } let (visits0, entries0, budget0) = (self.visits, self.entries_total, self.budget); self.visits = self.visits.saturating_add(1); self.ancestors.push(offset); let r = self.parse_dir(offset, base, depth); self.ancestors.pop(); let (ifd, next) = r?; let cost = Cost { ifds: self.visits.saturating_sub(visits0), entries: self.entries_total.saturating_sub(entries0), bytes: budget0.saturating_sub(self.budget), }; self.parsed.insert(key, Parsed { ifd: ifd.clone(), next, cost }); Ok((ifd, next)) } fn parse_dir(&mut self, offset: u64, base: u64, depth: usize) -> Result<(Ifd, u64)> { let dir = directory_entries(self.data, offset, self.order, self.big, self.opts.max_entries)?; if !reads_every_entry(dir.declared, dir.read) { self.complete = false; } // Bound total entries across the whole parse so overlapping IFD tables (SubIFD pointers // shifted by one entry) cannot allocate millions of `Entry`s from a small input. self.entries_total = self.entries_total.saturating_add(dir.read); if self.entries_total > self.opts.max_entries_total { return Err(TiffError::Limit("entry count")); } let mut entries = Vec::with_capacity(dir.read); for p in dir.positions() { if let Some(e) = self.entry(p, base) { entries.push(e); } else { self.complete = false; } } entries.sort_by_key(|e| e.tag); if entries.windows(2).any(|pair| pair[0].tag == pair[1].tag) { self.complete = false; } entries.dedup_by_key(|e| e.tag); // the next-IFD pointer follows the last entry, read only when every declared entry was (checked: a BigTIFF // count can be anything up to u64::MAX) let next = if reads_every_entry(dir.declared, dir.read) { match dir.read.checked_mul(dir.entry_size).and_then(|n| n.checked_add(dir.table)).and_then(|p| self.read_off(p)) { Some(0) => 0, None => { self.complete = false; 0 } Some(v) => match base.checked_add(v) { Some(next) => next, None => { self.complete = false; 0 } }, } } else { self.complete = false; 0 }; let mut ifd = Ifd { offset, entries, ..Default::default() }; if self.opts.follow_children { self.children(&mut ifd, base, depth); } else if [tags::SUB_IFDS, tags::EXIF_IFD, tags::GPS_IFD, tags::INTEROP_IFD].into_iter().any(|tag| ifd.contains(tag)) { self.complete = false; } Ok((ifd, next)) } fn children(&mut self, ifd: &mut Ifd, base: u64, depth: usize) { if let Some(v) = ifd.value(tags::SUB_IFDS) { let offs = v.to_u64_vec(); if offs.len() != v.count() { self.complete = false; } for o in offs { match base.checked_add(o).and_then(|abs| self.ifd(abs, base, depth + 1).ok().map(|(sub, _)| sub)) { Some(sub) => ifd.sub_ifds.push(sub), None => self.complete = false, } } } let exif = self.child(ifd, tags::EXIF_IFD, base, depth); let gps = self.child(ifd, tags::GPS_IFD, base, depth); let interop = self.child(ifd, tags::INTEROP_IFD, base, depth); ifd.exif = exif; ifd.gps = gps; ifd.interop = interop; } /// Follow one singleton child pointer and mark malformed or unreadable targets incomplete. fn child(&mut self, ifd: &Ifd, tag: u16, base: u64, depth: usize) -> Option> { if !ifd.contains(tag) { return None; } let Some(value) = ifd.value(tag) else { self.complete = false; return None; }; if value.count() != 1 { self.complete = false; } let parsed = value .get_u64(0) .and_then(|offset| base.checked_add(offset)) .and_then(|offset| self.ifd(offset, base, depth + 1).ok().map(|(child, _)| Box::new(child))); if parsed.is_none() { self.complete = false; } parsed } fn entry(&mut self, p: usize, base: u64) -> Option { let span = entry_span(self.data, p, self.order, self.big, base)?; if span.len > self.budget { return None; } self.budget -= span.len; let bytes = self.data.get(usize::try_from(span.start).ok()?..usize::try_from(span.start + span.len).ok()?)?; Some(Entry { tag: span.tag, value: decode(self.order, span.field_type, bytes), offset: span.start }) } } /// Decode `bytes` (exactly `count × size` long, or the first values of an entry) as values of type `typ`. pub fn decode(o: ByteOrder, typ: FieldType, b: &[u8]) -> Value { fn arr(c: &[u8]) -> [u8; N] { let mut a = [0u8; N]; a.copy_from_slice(c); a } match typ { FieldType::Byte => Value::Byte(b.to_vec()), FieldType::Undefined => Value::Undefined(b.to_vec()), FieldType::Ascii => { // every NUL-separated string is kept (TIFF 6.0 allows several in one field); only the // terminating NULs go let end = b.iter().rposition(|&c| c != 0).map_or(0, |i| i.saturating_add(1)); Value::Ascii(String::from_utf8_lossy(b.get(..end).unwrap_or_default()).into_owned()) } FieldType::SByte => Value::SByte(b.iter().map(|&x| x as i8).collect()), FieldType::Short => Value::Short(b.as_chunks::<2>().0.iter().map(|c| o.u16(arr(c))).collect()), FieldType::SShort => Value::SShort(b.as_chunks::<2>().0.iter().map(|c| o.u16(arr(c)) as i16).collect()), FieldType::Long => Value::Long(b.as_chunks::<4>().0.iter().map(|c| o.u32(arr(c))).collect()), FieldType::Ifd => Value::Ifd(b.as_chunks::<4>().0.iter().map(|c| o.u32(arr(c))).collect()), FieldType::SLong => Value::SLong(b.as_chunks::<4>().0.iter().map(|c| o.u32(arr(c)) as i32).collect()), FieldType::Float => Value::Float(b.as_chunks::<4>().0.iter().map(|c| f32::from_bits(o.u32(arr(c)))).collect()), FieldType::Rational => Value::Rational(b.as_chunks::<8>().0.iter().map(|c| (o.u32(arr(&c[..4])), o.u32(arr(&c[4..])))).collect()), FieldType::SRational => { Value::SRational(b.as_chunks::<8>().0.iter().map(|c| (o.u32(arr(&c[..4])) as i32, o.u32(arr(&c[4..])) as i32)).collect()) } FieldType::Double => Value::Double(b.as_chunks::<8>().0.iter().map(|c| f64::from_bits(o.u64(arr(c)))).collect()), FieldType::Long8 => Value::Long8(b.as_chunks::<8>().0.iter().map(|c| o.u64(arr(c))).collect()), FieldType::Ifd8 => Value::Ifd8(b.as_chunks::<8>().0.iter().map(|c| o.u64(arr(c))).collect()), FieldType::SLong8 => Value::SLong8(b.as_chunks::<8>().0.iter().map(|c| o.u64(arr(c)) as i64).collect()), } } #[cfg(test)] mod tests { use super::*; use crate::Value; use crate::tags as t; use crate::writer::{IfdBuilder, TiffWriter}; /// A value budget the caller sets bounds what is decoded: an entry claiming more value bytes than /// the budget allows is dropped, entries that fit are read (the sparse header-read path, where the /// buffer holds the file's logical size but only a few kilobytes were ever loaded, sizes its /// budget from what was fetched, not from whatever length the file claims). #[test] fn the_value_budget_drops_over_budget_entries_and_keeps_the_rest() { let mut ifd0 = IfdBuilder::new(); ifd0.set(t::MAKE, Value::Ascii("Keep".into())); ifd0.set(0x9000, Value::Byte(vec![0; 2 << 20])); let file = TiffWriter { order: ByteOrder::Little, bigtiff: false }.write(&[ifd0]).unwrap(); let opts = ParseOptions { value_budget: Some(1 << 20), ..Default::default() }; let tiff = Tiff::parse_with(&file, &opts).unwrap(); assert_eq!(tiff.ifds[0].string(t::MAKE), Some("Keep".to_string()), "a fitting entry is read"); assert!(tiff.ifds[0].get(0x9000).is_none(), "the over-budget entry is dropped"); let (_, complete) = Tiff::parse_with_status(&file, &opts).unwrap(); assert!(!complete, "dropping an entry must be reported as incomplete"); } #[test] fn parse_status_reports_truncated_tables_and_unreadable_children() { let truncated = bigtiff_chain(ByteOrder::Little, 2, 0); let (_, complete) = Tiff::parse_with_status(&truncated, &ParseOptions::default()).unwrap(); assert!(!complete, "a declared entry omitted by a truncated table must be reported"); let mut unreadable_gps = b"II\x2a\0\x08\0\0\0".to_vec(); unreadable_gps.extend_from_slice(&1u16.to_le_bytes()); unreadable_gps.extend_from_slice(&tags::GPS_IFD.to_le_bytes()); unreadable_gps.extend_from_slice(&4u16.to_le_bytes()); unreadable_gps.extend_from_slice(&1u32.to_le_bytes()); unreadable_gps.extend_from_slice(&u32::MAX.to_le_bytes()); unreadable_gps.extend_from_slice(&0u32.to_le_bytes()); let (_, complete) = Tiff::parse_with_status(&unreadable_gps, &ParseOptions::default()).unwrap(); assert!(!complete, "a dangling GPS pointer must be reported"); let valid_ifd = IfdBuilder::new().with(tags::MAKE, Value::Ascii("Camera".into())); let valid = TiffWriter::default().write(&[valid_ifd]).unwrap(); let (_, complete) = Tiff::parse_with_status(&valid, &ParseOptions::default()).unwrap(); assert!(complete, "a fully read TIFF must report complete"); } /// Completeness is false when callers skip child metadata or a singleton pointer is malformed. #[test] fn parse_status_reports_skipped_and_ambiguous_child_pointers() { let child = IfdBuilder::new().with(t::GPS_LATITUDE, Value::Rational(vec![(45, 1), (0, 1), (0, 1)])); let mut root = IfdBuilder::new(); root.set_child(t::GPS_IFD, child); let with_child = TiffWriter::default().write(&[root]).unwrap(); let opts = ParseOptions { follow_children: false, ..Default::default() }; let (_, complete) = Tiff::parse_with_status(&with_child, &opts).unwrap(); assert!(!complete, "an intentionally unvisited GPS IFD is incomplete"); let ambiguous = IfdBuilder::new().with(t::GPS_IFD, Value::Long(vec![8, 8])); let ambiguous = TiffWriter::default().write(&[ambiguous]).unwrap(); let (_, complete) = Tiff::parse_with_status(&ambiguous, &ParseOptions::default()).unwrap(); assert!(!complete, "a pointer field with multiple offsets is ambiguous"); } /// A BigTIFF whose first IFD, at 80, declares `declared` entries but holds one, followed by a next-IFD pointer /// `next`; a valid one-entry IFD sits at 16. fn bigtiff_chain(order: ByteOrder, declared: u64, next: u64) -> Vec { let ifd = |d: &mut Vec, count: u64, tag: u16, next: u64| { order.put_u64(d, count); order.put_u16(d, tag); order.put_u16(d, 3); order.put_u64(d, 1); order.put_u16(d, 8); d.extend([0; 6]); order.put_u64(d, next); }; let mut d = if order == ByteOrder::Little { b"II".to_vec() } else { b"MM".to_vec() }; order.put_u16(&mut d, 43); order.put_u16(&mut d, 8); order.put_u16(&mut d, 0); order.put_u64(&mut d, 80); ifd(&mut d, 1, 0x0101, 0); d.resize(80, 0); ifd(&mut d, declared, 0x0100, next); d } /// A classic little-endian TIFF: IFD0 at 8 with `SubIFDs` = [`a`, `b`] and Exif → `exif`; a one-entry IFD /// (tag 0x0101 = its offset) at 38 and at 56; `IFD0`'s next pointer is `next`. fn shared_children(a: u32, b: u32, exif: u32, next: u32) -> Vec { let o = ByteOrder::Little; let mut d = b"II*\0\x08\0\0\0".to_vec(); o.put_u16(&mut d, 2); // SubIFDs: two LONGs (8 bytes) stored out of line at 74 o.put_u16(&mut d, 330); o.put_u16(&mut d, 4); d.extend(2u32.to_le_bytes()); d.extend(74u32.to_le_bytes()); o.put_u16(&mut d, 34665); o.put_u16(&mut d, 4); d.extend(1u32.to_le_bytes()); d.extend(exif.to_le_bytes()); d.extend(next.to_le_bytes()); for at in [38u32, 56] { d.resize(at as usize, 0); o.put_u16(&mut d, 1); o.put_u16(&mut d, 0x0101); o.put_u16(&mut d, 4); d.extend(1u32.to_le_bytes()); d.extend(at.to_le_bytes()); d.extend(0u32.to_le_bytes()); } d.resize(74, 0); d.extend(a.to_le_bytes()); d.extend(b.to_le_bytes()); d } #[test] fn a_child_ifd_referenced_twice_is_kept_both_times() { let t = Tiff::parse(&shared_children(38, 38, 38, 0)).unwrap(); let ifd = &t.ifds[0]; assert_eq!(ifd.sub_ifds.len(), 2, "the second SubIFDs pointer to the same IFD used to be dropped"); assert!(ifd.sub_ifds.iter().all(|s| s.u64(0x0101) == Some(38))); assert_eq!(ifd.exif.as_ref().and_then(|e| e.u64(0x0101)), Some(38)); // distinct children still parse as before let t = Tiff::parse(&shared_children(38, 56, 56, 0)).unwrap(); let subs: Vec<_> = t.ifds[0].sub_ifds.iter().map(|s| s.u64(0x0101)).collect(); assert_eq!(subs, vec![Some(38), Some(56)]); // a chain IFD that is also a child is parsed in the chain too let t = Tiff::parse(&shared_children(38, 56, 56, 56)).unwrap(); assert_eq!(t.ifds.len(), 2); assert_eq!(t.ifds[1].u64(0x0101), Some(56)); } #[test] fn parse_status_counts_a_shared_child_as_read_and_a_looping_chain_as_incomplete() { let (_, complete) = Tiff::parse_with_status(&shared_children(38, 38, 38, 0), &ParseOptions::default()).unwrap(); assert!(complete, "every pointer to the shared child was followed"); let (t, complete) = Tiff::parse_with_status(&shared_children(38, 56, 56, 8), &ParseOptions::default()).unwrap(); assert_eq!(t.ifds.len(), 1); assert!(!complete, "a next pointer back to the first IFD is a chain without an end"); } #[test] fn a_child_pointing_at_its_parent_is_still_a_loop() { let t = Tiff::parse(&shared_children(8, 38, 8, 8)).unwrap(); assert_eq!(t.ifds.len(), 1); assert_eq!(t.ifds[0].sub_ifds.len(), 1); assert!(t.ifds[0].exif.is_none()); } #[test] fn shared_children_count_against_the_ifd_limit() { let opts = ParseOptions { max_ifds: 2, ..Default::default() }; let t = Tiff::parse_with(&shared_children(38, 38, 38, 0), &opts).unwrap(); assert_eq!(t.ifds[0].sub_ifds.len(), 1); assert!(t.ifds[0].exif.is_none()); } /// Hostile input: many pointers to one shared IFD must not clone past the entry limit (each child has one /// entry, so three copies need three entries plus the root's two). #[test] fn shared_children_count_against_the_entry_limit() { let opts = ParseOptions { max_entries_total: 4, ..Default::default() }; let t = Tiff::parse_with(&shared_children(38, 38, 38, 0), &opts).unwrap(); assert_eq!(t.ifds[0].sub_ifds.len(), 2); assert!(t.ifds[0].exif.is_none(), "the third copy would pass the entry limit"); } #[test] fn ascii_keeps_every_nul_separated_string() { let v = decode(ByteOrder::Little, FieldType::Ascii, b"one\0two\0\0"); assert_eq!(v, Value::Ascii("one\0two".into())); assert_eq!(v.as_str().as_deref(), Some("one")); assert_eq!(v.as_strings(), vec!["one".to_string(), "two".to_string()]); assert_eq!(v.as_bytes(), Some(&b"one"[..]), "byte readers see the first string, as before"); assert_eq!(decode(ByteOrder::Little, FieldType::Ascii, b"\0\0"), Value::Ascii(String::new())); assert!(decode(ByteOrder::Little, FieldType::Ascii, b"\0\0").as_strings().is_empty()); assert_eq!(decode(ByteOrder::Little, FieldType::Ascii, b"abc").as_str().as_deref(), Some("abc")); } #[test] fn only_a_directory_read_whole_has_a_next_pointer() { // the comparison itself, on every target: a declared count past u32::MAX never matches a small read count for (declared, read, whole) in [ (0, 0, true), (1, 1, true), (8192, 8192, true), (8193, 8192, false), (1, 0, false), ((1 << 32) + 1, 1, false), ((2 << 32) + 1, 1, false), ((1 << 63) + 1, 1, false), (u64::MAX, 1, false), ] { assert_eq!(reads_every_entry(declared, read), whole, "{declared} declared, {read} read"); } } #[test] fn a_bigtiff_count_past_u32_max_never_follows_the_next_pointer() { // the whole chain: on 32-bit targets (wasm32) the old cast truncated these counts to 1 and followed the pointer for order in [ByteOrder::Little, ByteOrder::Big] { for declared in [(1u64 << 32) + 1, (2u64 << 32) + 1, (1u64 << 63) + 1] { let d = bigtiff_chain(order, declared, 16); let tiff = Tiff::parse(&d).unwrap(); assert_eq!(tiff.ifds.len(), 1, "{order:?} {declared}"); assert_eq!(tiff.ifds[0].u64(0x0100), Some(8)); assert_eq!(parse_ifd_at(&d, 80, order, 0, true, &ParseOptions::default()).unwrap().1, 0, "{order:?} {declared}"); } // control: the same file declaring its one entry follows the pointer to the IFD at 16 let whole = Tiff::parse(&bigtiff_chain(order, 1, 16)).unwrap(); assert_eq!(whole.ifds.len(), 2, "{order:?}"); assert_eq!(whole.ifds[1].u64(0x0101), Some(8)); } } #[test] fn directory_positions_never_overflow() { // a hand-built layout the reader never produces: the iterator stops instead of overflowing let hostile = DirectoryEntries { table: usize::MAX, read: 2, declared: 2, entry_size: 12 }; assert_eq!(hostile.positions().collect::>(), vec![usize::MAX]); assert_eq!(hostile.positions().nth(1), None); let wide = DirectoryEntries { table: usize::MAX - 30, read: 4, declared: 4, entry_size: 12 }; assert_eq!(wide.positions().collect::>(), vec![usize::MAX - 30, usize::MAX - 18, usize::MAX - 6]); // a directory read from data lists its entries let mut data = Vec::new(); ByteOrder::Little.put_u16(&mut data, 2); data.extend([0; 24]); let dir = directory_entries(&data, 0, ByteOrder::Little, false, 8192).unwrap(); assert_eq!(dir.positions().collect::>(), vec![2, 14]); } #[test] fn a_bigtiff_directory_declaring_u64_max_entries_parses_without_overflow() { // 44 bytes: BigTIFF header (first IFD at 16), an entry count of u64::MAX, one 20-byte entry let mut data = b"II\x2b\x00\x08\x00\x00\x00".to_vec(); data.extend(16u64.to_le_bytes()); data.extend(u64::MAX.to_le_bytes()); data.extend(0x0100u16.to_le_bytes()); data.extend(3u16.to_le_bytes()); data.extend(1u64.to_le_bytes()); data.extend([8, 0, 0, 0, 0, 0, 0, 0]); assert_eq!(data.len(), 44); let tiff = Tiff::parse(&data).unwrap(); assert_eq!(tiff.ifds.len(), 1); assert_eq!(tiff.ifds[0].u64(0x0100), Some(8)); } #[test] fn overlapping_ifd_tables_hit_entry_cap_instead_of_amplifying() { // classic TIFF; IFDs at 8, 20, 32, ... each declaring 8192 entries whose tables overlap // (next table starts one entry after the previous table's start). Without the cap, a single // `Tiff::parse` would run `Vec::with_capacity(8192)` per IFD inside one `Ctx`, amplifying a // small file into millions of `Entry`s; the per-`Ctx` `max_entries_total` cap rejects the // 9th IFD before any allocation. let mut data = vec![0u8; 200_000]; data[0..2].copy_from_slice(b"II"); data[2..4].copy_from_slice(&42u16.to_le_bytes()); data[4..8].copy_from_slice(&8u32.to_le_bytes()); // first IFD at 8 for k in 0..9u64 { let ifd_off = (8 + 12 * k) as usize; data[ifd_off..ifd_off + 2].copy_from_slice(&8192u16.to_le_bytes()); // count let table = ifd_off + 2; let ptr = table + 8192 * 12; // next-IFD pointer lives right after the entries let next = if k + 1 < 9 { (8 + 12 * (k + 1)) as u32 } else { 0 }; data[ptr..ptr + 4].copy_from_slice(&next.to_le_bytes()); } let tiff = Tiff::parse(&data).unwrap(); assert_eq!(tiff.ifds.len(), 8, "8 IFDs parsed, 9th rejected by max_entries_total before allocation"); } #[test] fn a_legitimate_multi_ifd_file_still_parses_under_the_entry_cap() { // classic TIFF: IFD0 @8 (3 entries: value + Exif + GPS child pointers) -> IFD1 @58 chain, // plus Exif child @60 and GPS child @90. Total entries = 6, far under max_entries_total. let mut data = vec![0u8; 128]; data[0..2].copy_from_slice(b"II"); data[2..4].copy_from_slice(&42u16.to_le_bytes()); data[4..8].copy_from_slice(&8u32.to_le_bytes()); // first IFD at 8 // IFD0 @8: count 3, table @10, next-IFD @46 -> IFD1 @58 data[8..10].copy_from_slice(&3u16.to_le_bytes()); // entry0 @10: tag 0x0100, SHORT, count 1, value 8 (inline @18..22) data[10..12].copy_from_slice(&0x0100u16.to_le_bytes()); data[12..14].copy_from_slice(&3u16.to_le_bytes()); data[14..18].copy_from_slice(&1u32.to_le_bytes()); data[18..22].copy_from_slice(&8u32.to_le_bytes()); // entry1 @22: tag 0x8769 ExifIFD, LONG, count 1, value 60 (inline @30..34) data[22..24].copy_from_slice(&0x8769u16.to_le_bytes()); data[24..26].copy_from_slice(&4u16.to_le_bytes()); data[26..30].copy_from_slice(&1u32.to_le_bytes()); data[30..34].copy_from_slice(&60u32.to_le_bytes()); // entry2 @34: tag 0x8825 GPSIFD, LONG, count 1, value 90 (inline @42..46) data[34..36].copy_from_slice(&0x8825u16.to_le_bytes()); data[36..38].copy_from_slice(&4u16.to_le_bytes()); data[38..42].copy_from_slice(&1u32.to_le_bytes()); data[42..46].copy_from_slice(&90u32.to_le_bytes()); data[46..50].copy_from_slice(&58u32.to_le_bytes()); // next IFD // IFD1 @58: count 1, table @60, next 0 data[58..60].copy_from_slice(&1u16.to_le_bytes()); data[60..62].copy_from_slice(&0x0100u16.to_le_bytes()); data[62..64].copy_from_slice(&3u16.to_le_bytes()); data[64..68].copy_from_slice(&1u32.to_le_bytes()); data[68..72].copy_from_slice(&8u32.to_le_bytes()); data[72..76].copy_from_slice(&0u32.to_le_bytes()); // Exif child IFD @60: count 1, entry tag 0x9000 value 1, next 0 data[60..62].copy_from_slice(&1u16.to_le_bytes()); data[62..64].copy_from_slice(&0x9000u16.to_le_bytes()); data[64..66].copy_from_slice(&3u16.to_le_bytes()); data[66..70].copy_from_slice(&1u32.to_le_bytes()); data[70..74].copy_from_slice(&1u32.to_le_bytes()); data[74..78].copy_from_slice(&0u32.to_le_bytes()); // GPS child IFD @90: count 1, entry tag 0 value 1, next 0 data[90..92].copy_from_slice(&1u16.to_le_bytes()); data[92..94].copy_from_slice(&0u16.to_le_bytes()); data[94..96].copy_from_slice(&3u16.to_le_bytes()); data[96..100].copy_from_slice(&1u32.to_le_bytes()); data[100..104].copy_from_slice(&1u32.to_le_bytes()); data[104..108].copy_from_slice(&0u32.to_le_bytes()); let tiff = Tiff::parse(&data).unwrap(); assert_eq!(tiff.ifds.len(), 2, "chain IFDs parse fully"); assert_eq!(tiff.ifds[0].u64(0x0100), Some(8)); let exif = tiff.ifds[0].exif.as_ref().expect("Exif child present"); assert_eq!(exif.u64(0x9000), Some(1)); let gps = tiff.ifds[0].gps.as_ref().expect("GPS child present"); assert_eq!(gps.u64(0x0000), Some(1)); } }