mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
craft-tiff: shared child IFDs, multi-string ASCII, parse_with_status (ports of open LightCraft PRs) (#842)
* 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>
This commit is contained in:
4 files changed
+307
-31
No files matched your search
@@ -13,6 +13,8 @@ on no other Craft crate.
|
||||
- IFD chain + `SubIFDs` (330) + Exif (34665) / GPS (34853) / Interoperability (40965) pointer IFDs, resolved
|
||||
into a tree (`Ifd::sub_ifds`, `exif`, `gps`, `interop`). `Entry::offset` records the absolute position of
|
||||
each value (needed for maker notes and embedded blobs).
|
||||
- `Tiff::parse_with_status` returns the best-effort tree with a completeness flag for callers that must
|
||||
distinguish a clean absence from an IFD, entry, or pointer that could not be inspected.
|
||||
- `parse_ifd_at(data, offset, order, base, bigtiff, opts)` parses an IFD whose offsets are relative to an
|
||||
arbitrary base; `makernote::parse_makernote` knows the header / byte-order / offset-base conventions of
|
||||
Canon, Nikon (types 1 and 3), Sony, Fujifilm, Olympus (old and new), Panasonic and Pentax notes.
|
||||
@@ -36,7 +38,9 @@ overlap at distinct offsets, and an IFD past that bound ends the chain or is ski
|
||||
the first; a next-IFD pointer is followed only when every declared entry was read, with the counts compared as
|
||||
64-bit numbers (a BigTIFF declaring 4,294,967,297 entries but holding one can't pass for a whole directory on a
|
||||
32-bit target); the total bytes of decoded values are capped at
|
||||
`8 × input + 1 MiB` so a file cannot make us allocate far more than its own size.
|
||||
`8 × input + 1 MiB` so a file cannot make us allocate far more than its own size. `parse_with_status` reports
|
||||
truncated tables, skipped values, unresolved pointers and incomplete IFD chains without changing the parser's
|
||||
best-effort tree.
|
||||
|
||||
## Sources
|
||||
|
||||
|
||||
+23
-6
@@ -174,7 +174,8 @@ impl FieldType {
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Value {
|
||||
Byte(Vec<u8>),
|
||||
/// ASCII text (bytes up to the first NUL, decoded lossily as UTF-8).
|
||||
/// ASCII text decoded lossily as UTF-8, without the terminating NULs. A field holding several strings keeps
|
||||
/// them NUL-separated ([`Value::as_strings`]).
|
||||
Ascii(String),
|
||||
Short(Vec<u16>),
|
||||
Long(Vec<u32>),
|
||||
@@ -307,11 +308,11 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
/// Text for ASCII values (trimmed of trailing NULs / spaces); `Byte`/`Undefined` are decoded as UTF-8 (lossy)
|
||||
/// up to the first NUL, which covers XMP packets and UCS-less Exif strings.
|
||||
/// Text for ASCII values (the first string, trimmed of trailing spaces); `Byte`/`Undefined` are decoded as
|
||||
/// UTF-8 (lossy) up to the first NUL, which covers XMP packets and UCS-less Exif strings.
|
||||
pub fn as_str(&self) -> Option<String> {
|
||||
match self {
|
||||
Value::Ascii(s) => Some(s.trim_end_matches([' ', '\0']).to_string()),
|
||||
Value::Ascii(s) => Some(s.split('\0').next().unwrap_or_default().trim_end().to_string()),
|
||||
Value::Byte(b) | Value::Undefined(b) => {
|
||||
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
||||
Some(String::from_utf8_lossy(&b[..end]).trim_end().to_string())
|
||||
@@ -320,11 +321,20 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw bytes for `Byte` / `Undefined` / `SByte` / `Ascii`.
|
||||
/// Every NUL-separated string of an ASCII value, each trimmed of trailing spaces (empty ones dropped); other
|
||||
/// types give what [`Self::as_str`] gives.
|
||||
pub fn as_strings(&self) -> Vec<String> {
|
||||
match self {
|
||||
Value::Ascii(s) => s.split('\0').map(|p| p.trim_end().to_string()).filter(|p| !p.is_empty()).collect(),
|
||||
_ => self.as_str().into_iter().filter(|p| !p.is_empty()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw bytes for `Byte` / `Undefined` / `SByte` / `Ascii` (an ASCII value's first string only).
|
||||
pub fn as_bytes(&self) -> Option<&[u8]> {
|
||||
match self {
|
||||
Value::Byte(b) | Value::Undefined(b) => Some(b),
|
||||
Value::Ascii(s) => Some(s.as_bytes()),
|
||||
Value::Ascii(s) => s.as_bytes().split(|&c| c == 0).next(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -440,6 +450,13 @@ impl Tiff {
|
||||
pub fn parse_with(data: &[u8], opts: &ParseOptions) -> Result<Tiff> {
|
||||
reader::parse(data, opts)
|
||||
}
|
||||
/// Parse a TIFF stream and report whether the complete reachable IFD structure was read.
|
||||
///
|
||||
/// The parsed value retains the reader's best-effort behavior. `complete` is false when an
|
||||
/// IFD, entry value, pointer target, or IFD-chain link could not be fully inspected.
|
||||
pub fn parse_with_status(data: &[u8], opts: &ParseOptions) -> Result<(Tiff, bool)> {
|
||||
reader::parse_with_status(data, opts)
|
||||
}
|
||||
/// Header check only: returns the byte order and BigTIFF flag.
|
||||
pub fn sniff(data: &[u8]) -> Option<(ByteOrder, bool)> {
|
||||
reader::header(data).ok().map(|(o, b, _)| (o, b))
|
||||
|
||||
+275
-24
@@ -1,7 +1,7 @@
|
||||
//! Bounds-checked IFD parsing.
|
||||
|
||||
use crate::{ByteOrder, Entry, FieldType, Ifd, Result, Tiff, TiffError, Value, tags};
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Limits applied while parsing. The defaults are generous for real files and safe for hostile ones.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -173,11 +173,22 @@ fn header_with(data: &[u8], raw_magic: bool) -> Result<(ByteOrder, bool, u64)> {
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -187,6 +198,7 @@ pub(crate) fn parse(data: &[u8], opts: &ParseOptions) -> Result<Tiff> {
|
||||
if ifds.is_empty() {
|
||||
return Err(e);
|
||||
}
|
||||
ctx.complete = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -194,7 +206,7 @@ pub(crate) fn parse(data: &[u8], opts: &ParseOptions) -> Result<Tiff> {
|
||||
if ifds.is_empty() {
|
||||
return Err(TiffError::Invalid("no IFDs".into()));
|
||||
}
|
||||
Ok(Tiff { order, bigtiff: big, ifds })
|
||||
Ok((Tiff { order, bigtiff: big, ifds }, ctx.complete))
|
||||
}
|
||||
|
||||
/// Parse one IFD at absolute `offset` whose value offsets (and child IFD pointers) are relative to
|
||||
@@ -210,15 +222,41 @@ struct Ctx<'a> {
|
||||
order: ByteOrder,
|
||||
big: bool,
|
||||
opts: &'a ParseOptions,
|
||||
visited: HashSet<u64>,
|
||||
/// 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, visited: HashSet::new(), budget, entries_total: 0 }
|
||||
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> {
|
||||
@@ -230,13 +268,51 @@ impl<'a> Ctx<'a> {
|
||||
if depth > self.opts.max_depth {
|
||||
return Err(TiffError::Limit("IFD nesting depth"));
|
||||
}
|
||||
if self.visited.len() >= self.opts.max_ifds {
|
||||
return Err(TiffError::Limit("IFD count"));
|
||||
}
|
||||
if !self.visited.insert(offset) {
|
||||
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);
|
||||
@@ -247,23 +323,41 @@ impl<'a> Ctx<'a> {
|
||||
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) | None => 0,
|
||||
Some(v) => base.checked_add(v).unwrap_or(0),
|
||||
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))
|
||||
}
|
||||
@@ -271,27 +365,46 @@ impl<'a> Ctx<'a> {
|
||||
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 {
|
||||
if let Some(abs) = base.checked_add(o)
|
||||
&& let Ok((sub, _)) = self.ifd(abs, base, depth + 1)
|
||||
{
|
||||
ifd.sub_ifds.push(sub);
|
||||
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 child = |tag: u16, ctx: &mut Self| -> Option<Box<Ifd>> {
|
||||
let o = ifd.value(tag)?.get_u64(0)?;
|
||||
let abs = base.checked_add(o)?;
|
||||
ctx.ifd(abs, base, depth + 1).ok().map(|(i, _)| Box::new(i))
|
||||
};
|
||||
let exif = child(tags::EXIF_IFD, self);
|
||||
let gps = child(tags::GPS_IFD, self);
|
||||
let interop = child(tags::INTEROP_IFD, self);
|
||||
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 {
|
||||
@@ -314,8 +427,10 @@ pub fn decode(o: ByteOrder, typ: FieldType, b: &[u8]) -> Value {
|
||||
FieldType::Byte => Value::Byte(b.to_vec()),
|
||||
FieldType::Undefined => Value::Undefined(b.to_vec()),
|
||||
FieldType::Ascii => {
|
||||
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
||||
Value::Ascii(String::from_utf8_lossy(&b[..end]).into_owned())
|
||||
// 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()),
|
||||
@@ -356,6 +471,47 @@ mod tests {
|
||||
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
|
||||
@@ -381,6 +537,101 @@ mod tests {
|
||||
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
|
||||
|
||||
@@ -44,6 +44,8 @@ pub const CFA_PATTERN_EP: u16 = 33422;
|
||||
pub const COPYRIGHT: u16 = 33432;
|
||||
pub const EXPOSURE_TIME_EP: u16 = 33434;
|
||||
pub const IPTC_NAA: u16 = 33723;
|
||||
/// Photoshop image resources embedded in TIFF tag 34377.
|
||||
pub const PHOTOSHOP_IMAGE_RESOURCES: u16 = 34377;
|
||||
pub const EXIF_IFD: u16 = 34665;
|
||||
pub const ICC_PROFILE: u16 = 34675;
|
||||
pub const GPS_IFD: u16 = 34853;
|
||||
@@ -100,6 +102,8 @@ pub const GPS_ALTITUDE: u16 = 6;
|
||||
pub const GPS_TIME_STAMP: u16 = 7;
|
||||
pub const GPS_IMG_DIRECTION_REF: u16 = 16;
|
||||
pub const GPS_IMG_DIRECTION: u16 = 17;
|
||||
pub const GPS_DEST_LATITUDE: u16 = 20;
|
||||
pub const GPS_DEST_LONGITUDE: u16 = 22;
|
||||
pub const GPS_DATE_STAMP: u16 = 29;
|
||||
|
||||
// --- DNG
|
||||
|
||||
Reference in new issue
Block a user