mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 22:42:46 +00:00
* craft-tiff: keep shared child IFDs and every string of a multi-string ASCII value Two pointers to the same child IFD (two SubIFDs entries, or an Exif IFD shared by two directories) were treated as a loop and the second was dropped. Loops are now detected on the current parent path only; a shared IFD is parsed once and copied for later pointers, and each copy is charged its IFDs, entries and value bytes so max_ifds, max_entries_total and the value budget still hold. ASCII values keep every NUL-separated string (Value::as_strings); as_str and as_bytes return the first. Ported from storytold/lightcraft#3002. * craft-tiff: parse_with_status reports whether the whole IFD structure was read The reader is best effort: truncated tables, over-budget values, dangling pointers and unfollowed children are skipped silently. A caller that must tell a clean absence from something it could not inspect (a privacy scan for GPS data) gets the same tree plus a completeness flag. Also adds the PHOTOSHOP_IMAGE_RESOURCES and GPS destination tag constants. Ported from storytold/lightcraft#3766. On top of the shared-child change, a second pointer to an IFD already read counts as read, and a next pointer that loops back to the chain is incomplete. --------- Co-authored-by: Andrew Frahn <andrew@emmertex.com> Co-authored-by: ronibalthazar <61232335+ronibalthazar@users.noreply.github.com>
788 lines
36 KiB
Rust
788 lines
36 KiB
Rust
//! 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<u64>,
|
||
/// 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<Item = usize> + 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<DirectoryEntries> {
|
||
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<EntrySpan> {
|
||
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<Tiff> {
|
||
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<u64>,
|
||
/// 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<u64> {
|
||
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<Box<Ifd>> {
|
||
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<Entry> {
|
||
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<const N: usize>(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<u8> {
|
||
let ifd = |d: &mut Vec<u8>, 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<u8> {
|
||
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<_>>(), 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<_>>(), 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<_>>(), 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));
|
||
}
|
||
}
|