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:
authored and GitHub committed 2026-11-11 14:57:55 -05:00
1 parent 12e24c1029
commit 9adca4a8e5
4 files changed
+307 -31

No files matched your search

+5 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+4
View File
@@ -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