use crate::c2pa_formats::{AssetFormat, DataHashExclusion, FormatError};
use std::collections::BTreeMap;
const FMT: AssetFormat = AssetFormat::Tiff;
const TAG_C2PA: u16 = 0xCD41;
const TYPE_UNDEFINED: u16 = 7;
const TYPE_BYTE: u16 = 1;
const MAX_IFD_CHAIN: usize = 1024;
const MAX_IFD_TABLE_BYTES: u64 = 64 * 1024 * 1024;
fn invalid(detail: &'static str) -> FormatError {
FormatError::InvalidStructure {
format: FMT,
detail,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Variant {
Classic,
Big,
}
#[derive(Clone, Copy, Debug)]
struct Tiff {
little: bool,
variant: Variant,
first_ifd: u64,
}
impl Tiff {
fn header_len(self) -> u64 {
match self.variant {
Variant::Classic => 8,
Variant::Big => 16,
}
}
fn count_len(self) -> usize {
match self.variant {
Variant::Classic => 2,
Variant::Big => 8,
}
}
fn entry_len(self) -> usize {
match self.variant {
Variant::Classic => 12,
Variant::Big => 20,
}
}
fn offset_len(self) -> usize {
match self.variant {
Variant::Classic => 4,
Variant::Big => 8,
}
}
fn u16_at(self, bytes: &[u8], at: usize) -> Option<u16> {
let window: [u8; 2] = bytes.get(at..)?.get(..2)?.try_into().ok()?;
Some(if self.little {
u16::from_le_bytes(window)
} else {
u16::from_be_bytes(window)
})
}
fn offset_at(self, bytes: &[u8], at: usize) -> Option<u64> {
let window = bytes.get(at..)?.get(..self.offset_len())?;
Some(match self.variant {
Variant::Classic => {
let narrow: [u8; 4] = window.try_into().ok()?;
u64::from(if self.little {
u32::from_le_bytes(narrow)
} else {
u32::from_be_bytes(narrow)
})
}
Variant::Big => {
let wide: [u8; 8] = window.try_into().ok()?;
if self.little {
u64::from_le_bytes(wide)
} else {
u64::from_be_bytes(wide)
}
}
})
}
fn count_at(self, bytes: &[u8], at: usize) -> Option<u64> {
match self.variant {
Variant::Classic => self.u16_at(bytes, at).map(u64::from),
Variant::Big => self.offset_at(bytes, at),
}
}
}
fn read_header(data: &[u8]) -> Result<Tiff, FormatError> {
let prefix = data.get(..8).ok_or(FormatError::Truncated(FMT))?;
let little = match &prefix[0..2] {
b"II" => true,
b"MM" => false,
_ => return Err(invalid("bad byte-order mark")),
};
let classic = Tiff {
little,
variant: Variant::Classic,
first_ifd: 0,
};
match classic
.u16_at(prefix, 2)
.ok_or(FormatError::Truncated(FMT))?
{
42 => Ok(Tiff {
first_ifd: classic
.offset_at(prefix, 4)
.ok_or(FormatError::Truncated(FMT))?,
..classic
}),
43 => {
let header = data.get(..16).ok_or(FormatError::Truncated(FMT))?;
if classic.u16_at(header, 4) != Some(8) {
return Err(invalid("unsupported BigTIFF offset width"));
}
if classic.u16_at(header, 6) != Some(0) {
return Err(invalid("BigTIFF reserved header field is not zero"));
}
let big = Tiff {
variant: Variant::Big,
..classic
};
Ok(Tiff {
first_ifd: big
.offset_at(header, 8)
.ok_or(FormatError::Truncated(FMT))?,
..big
})
}
_ => Err(invalid("bad TIFF magic")),
}
}
struct Ifd<'a> {
count: usize,
entries_start: u64,
entries: &'a [u8],
next: u64,
}
impl Ifd<'_> {
fn entry(&self, t: Tiff, index: usize) -> &[u8] {
&self.entries[index * t.entry_len()..(index + 1) * t.entry_len()]
}
fn tag(&self, t: Tiff, index: usize) -> u16 {
t.u16_at(self.entry(t, index), 0)
.expect("walk sized the entry table")
}
}
fn walk<'a>(data: &'a [u8], t: Tiff, strict: bool) -> Result<Vec<Ifd<'a>>, FormatError> {
let mut chain: Vec<Ifd<'a>> = Vec::new();
let mut state = Walk {
spans: BTreeMap::new(),
budget: MAX_IFD_TABLE_BYTES,
};
let mut offset = t.first_ifd;
while offset != 0 {
match read_ifd(data, t, offset, &mut state) {
Ok(ifd) => {
offset = ifd.next;
chain.push(ifd);
}
Err(error) if strict => return Err(error),
Err(_) => break,
}
}
Ok(chain)
}
fn read_ifd<'a>(
data: &'a [u8],
t: Tiff,
offset: u64,
state: &mut Walk,
) -> Result<Ifd<'a>, FormatError> {
if offset < t.header_len() {
return Err(invalid("TIFF IFD offset points into the header"));
}
if state.spans.contains_key(&offset) {
return Err(invalid("cyclic TIFF IFD chain"));
}
if state.spans.len() >= MAX_IFD_CHAIN {
return Err(invalid("TIFF IFD chain exceeds the supported length"));
}
let count_start = usize::try_from(offset).map_err(|_| FormatError::Truncated(FMT))?;
let count_field = data
.get(count_start..)
.and_then(|tail| tail.get(..t.count_len()))
.ok_or(FormatError::Truncated(FMT))?;
let declared = t
.count_at(count_field, 0)
.ok_or(FormatError::Truncated(FMT))?;
let entries_start = offset
.checked_add(t.count_len() as u64)
.ok_or(FormatError::Truncated(FMT))?;
let table_bytes = declared
.checked_mul(t.entry_len() as u64)
.and_then(|bytes| bytes.checked_add(t.offset_len() as u64))
.ok_or(FormatError::Truncated(FMT))?;
let Some(budget) = state.budget.checked_sub(table_bytes) else {
return Err(invalid(
"TIFF IFD chain exceeds the supported directory size",
));
};
state.budget = budget;
let end = entries_start
.checked_add(table_bytes)
.ok_or(FormatError::Truncated(FMT))?;
let count = usize::try_from(declared).map_err(|_| FormatError::Truncated(FMT))?;
let entry_bytes = count * t.entry_len();
let table_len = entry_bytes + t.offset_len();
if state.overlaps(offset, end) {
return Err(invalid("overlapping TIFF IFD tables"));
}
let table_start = usize::try_from(entries_start).map_err(|_| FormatError::Truncated(FMT))?;
let table = data
.get(table_start..)
.and_then(|tail| tail.get(..table_len))
.ok_or(FormatError::Truncated(FMT))?;
let next = t
.offset_at(table, entry_bytes)
.ok_or(FormatError::Truncated(FMT))?;
state.spans.insert(offset, end);
Ok(Ifd {
count,
entries_start,
entries: &table[..entry_bytes],
next,
})
}
struct Walk {
spans: BTreeMap<u64, u64>,
budget: u64,
}
impl Walk {
fn overlaps(&self, start: u64, end: u64) -> bool {
self.spans
.range(..start)
.next_back()
.is_some_and(|(_, prior_end)| *prior_end > start)
|| self
.spans
.range(start..)
.next()
.is_some_and(|(next_start, _)| *next_start < end)
}
}
fn value_span(data: &[u8], t: Tiff, ifd: &Ifd<'_>, index: usize) -> Option<(usize, usize)> {
let entry = ifd.entry(t, index);
let length = t.offset_at(entry, 4)?;
let start = if length <= t.offset_len() as u64 {
ifd.entries_start
.checked_add((index as u64).checked_mul(t.entry_len() as u64)?)?
.checked_add(4 + t.offset_len() as u64)?
} else {
t.offset_at(entry, 4 + t.offset_len())?
};
if start.checked_add(length)? > data.len() as u64 {
return None;
}
Some((usize::try_from(start).ok()?, usize::try_from(length).ok()?))
}
fn manifest_span(data: &[u8]) -> Result<Option<(usize, usize)>, FormatError> {
let t = read_header(data)?;
for ifd in walk(data, t, true)? {
for index in 0..ifd.count {
if ifd.tag(t, index) != TAG_C2PA {
continue;
}
if t.u16_at(ifd.entry(t, index), 2) != Some(TYPE_UNDEFINED) {
return Err(invalid(
"C2PA TIFF entry has a tag type other than UNDEFINED",
));
}
return value_span(data, t, &ifd, index)
.map(Some)
.ok_or(FormatError::Truncated(FMT));
}
}
Ok(None)
}
pub(crate) fn extract(data: &[u8]) -> Result<Option<Vec<u8>>, FormatError> {
Ok(manifest_span(data)?.map(|(start, length)| data[start..start + length].to_vec()))
}
pub(crate) fn xmp_packet(data: &[u8]) -> Option<&[u8]> {
let t = read_header(data).ok()?;
for ifd in walk(data, t, false).ok()? {
for index in 0..ifd.count {
if ifd.tag(t, index) != crate::c2pa_formats::xmp::TIFF_XMP_TAG {
continue;
}
if !matches!(
t.u16_at(ifd.entry(t, index), 2),
Some(TYPE_BYTE | TYPE_UNDEFINED)
) {
return None;
}
let (start, length) = value_span(data, t, &ifd, index)?;
return data.get(start..start + length);
}
}
None
}
pub(crate) fn placement_error(data: &[u8]) -> Result<Option<&'static str>, FormatError> {
let t = read_header(data)?;
let chain = walk(data, t, true)?;
let mut carriers: Vec<usize> = Vec::new();
for (position, ifd) in chain.iter().enumerate() {
for index in 0..ifd.count {
if ifd.tag(t, index) == TAG_C2PA {
carriers.push(position);
}
}
}
if carriers.len() > 1 {
return Ok(Some(
"TIFF main-IFD chain contains more than one C2PA entry",
));
}
let Some(&carrier) = carriers.first() else {
return Ok(None);
};
if carrier + 1 != chain.len() {
return Ok(Some("TIFF C2PA entry is not in the last main IFD"));
}
if chain.len() > 1 && chain[carrier].count != 1 {
return Ok(Some(
"TIFF C2PA entry is not the only entry in its main IFD",
));
}
Ok(None)
}
pub(crate) fn exclusions(data: &[u8]) -> Result<Vec<DataHashExclusion>, FormatError> {
Ok(
manifest_span(data)?.map_or_else(Vec::new, |(start, length)| {
vec![DataHashExclusion { start, length }]
}),
)
}
#[cfg(test)]
pub(crate) fn embed(asset: &[u8], manifest_store: &[u8]) -> Result<Vec<u8>, FormatError> {
let t = read_header(asset)?;
if t.variant != Variant::Classic {
return Err(FormatError::UnsupportedVariant {
format: FMT,
detail: "BigTIFF is read-only in this build",
});
}
let chain = walk(asset, t, true)?;
let mut entries: Vec<[u8; 12]> = Vec::new();
let mut next_ifd: u32 = 0;
if let Some(first) = chain.first() {
for index in 0..first.count {
let entry = first.entry(t, index);
if t.u16_at(entry, 0) == Some(TAG_C2PA) {
continue;
}
let mut row = [0u8; 12];
row.copy_from_slice(entry);
entries.push(row);
}
next_ifd = u32::try_from(first.next).expect("classic next-IFD pointer is 32 bits");
}
let mut out = asset.to_vec();
if !out.len().is_multiple_of(2) {
out.push(0);
}
let manifest_off = out.len();
out.extend_from_slice(manifest_store);
if !out.len().is_multiple_of(2) {
out.push(0);
}
let new_ifd_off = out.len();
let mut rec = Vec::with_capacity(12);
write_u16(&mut rec, t.little, TAG_C2PA);
write_u16(&mut rec, t.little, TYPE_UNDEFINED);
write_u32(&mut rec, t.little, manifest_store.len() as u32);
if manifest_store.len() <= 4 {
let mut val = [0u8; 4];
val[..manifest_store.len()].copy_from_slice(manifest_store);
rec.extend_from_slice(&val);
} else {
write_u32(&mut rec, t.little, manifest_off as u32);
}
let mut c2pa = [0u8; 12];
c2pa.copy_from_slice(&rec);
entries.push(c2pa);
entries.sort_by_key(|row| t.u16_at(row, 0).expect("entry row is 12 bytes"));
write_u16(&mut out, t.little, entries.len() as u16);
for row in &entries {
out.extend_from_slice(row);
}
write_u32(&mut out, t.little, next_ifd);
let pointer = new_ifd_off as u32;
out[4..8].copy_from_slice(&if t.little {
pointer.to_le_bytes()
} else {
pointer.to_be_bytes()
});
Ok(out)
}
#[cfg(test)]
fn write_u16(out: &mut Vec<u8>, little: bool, x: u16) {
out.extend_from_slice(&if little {
x.to_le_bytes()
} else {
x.to_be_bytes()
});
}
#[cfg(test)]
fn write_u32(out: &mut Vec<u8>, little: bool, x: u32) {
out.extend_from_slice(&if little {
x.to_le_bytes()
} else {
x.to_be_bytes()
});
}
#[cfg(test)]
mod tests {
use super::*;
fn tiff_with_manifest(manifest: &[u8]) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(b"II");
v.extend_from_slice(&42u16.to_le_bytes());
v.extend_from_slice(&8u32.to_le_bytes()); v.extend_from_slice(&1u16.to_le_bytes());
let manifest_off = 8 + 2 + 12 + 4; v.extend_from_slice(&TAG_C2PA.to_le_bytes());
v.extend_from_slice(&7u16.to_le_bytes());
v.extend_from_slice(&(manifest.len() as u32).to_le_bytes());
v.extend_from_slice(&(manifest_off as u32).to_le_bytes());
v.extend_from_slice(&0u32.to_le_bytes()); v.extend_from_slice(manifest);
v
}
fn bare_tiff() -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(b"II");
v.extend_from_slice(&42u16.to_le_bytes());
v.extend_from_slice(&8u32.to_le_bytes());
v.extend_from_slice(&0u16.to_le_bytes()); v.extend_from_slice(&0u32.to_le_bytes()); v
}
fn big_entry(little: bool, tag: u16, tag_type: u16, count: u64, value: [u8; 8]) -> Vec<u8> {
let mut row = Vec::with_capacity(20);
write_u16(&mut row, little, tag);
write_u16(&mut row, little, tag_type);
row.extend_from_slice(&if little {
count.to_le_bytes()
} else {
count.to_be_bytes()
});
row.extend_from_slice(&value);
row
}
fn big_offset(little: bool, value: u64) -> [u8; 8] {
if little {
value.to_le_bytes()
} else {
value.to_be_bytes()
}
}
fn big_ifd(little: bool, rows: &[Vec<u8>], next: u64) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&big_offset(little, rows.len() as u64));
for row in rows {
out.extend_from_slice(row);
}
out.extend_from_slice(&big_offset(little, next));
out
}
fn big_header(little: bool, first_ifd: u64) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(if little { b"II" } else { b"MM" });
write_u16(&mut out, little, 43);
write_u16(&mut out, little, 8); write_u16(&mut out, little, 0); out.extend_from_slice(&big_offset(little, first_ifd));
out
}
fn bigtiff_single_page(little: bool, manifest: &[u8]) -> (Vec<u8>, usize, usize) {
let description = b"hello-bigtiff-description";
let ifd_len = 8 + 2 * 20 + 8;
let description_off = 16 + ifd_len;
let manifest_off = description_off + description.len();
let rows = vec![
big_entry(
little,
0x010E, 2, description.len() as u64,
big_offset(little, description_off as u64),
),
big_entry(
little,
TAG_C2PA,
TYPE_UNDEFINED,
manifest.len() as u64,
big_offset(little, manifest_off as u64),
),
];
let mut asset = big_header(little, 16);
asset.extend_from_slice(&big_ifd(little, &rows, 0));
assert_eq!(asset.len(), description_off);
asset.extend_from_slice(description);
asset.extend_from_slice(manifest);
(asset, manifest_off, manifest.len())
}
fn bigtiff_multi_page(little: bool, manifest: &[u8]) -> (Vec<u8>, usize, usize) {
let page_len = 8 + 20 + 8; let first_page = 16u64;
let second_page = first_page + page_len as u64;
let manifest_off = second_page as usize + page_len;
let carrier_off = manifest_off + manifest.len();
let page_row = |little: bool| {
big_entry(
little,
0x0100, 3, 1,
big_offset(little, 1),
)
};
let mut asset = big_header(little, first_page);
asset.extend_from_slice(&big_ifd(little, &[page_row(little)], second_page));
asset.extend_from_slice(&big_ifd(little, &[page_row(little)], carrier_off as u64));
assert_eq!(asset.len(), manifest_off);
asset.extend_from_slice(manifest);
let carrier_row = big_entry(
little,
TAG_C2PA,
TYPE_UNDEFINED,
manifest.len() as u64,
big_offset(little, manifest_off as u64),
);
asset.extend_from_slice(&big_ifd(little, &[carrier_row], 0));
(asset, manifest_off, manifest.len())
}
#[test]
fn extract_tagged_manifest() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let asset = tiff_with_manifest(&store);
assert_eq!(extract(&asset).unwrap().as_deref(), Some(store.as_slice()));
assert_eq!(placement_error(&asset).unwrap(), None);
}
#[test]
fn bare_asset_has_no_manifest() {
assert_eq!(extract(&bare_tiff()).unwrap(), None);
}
fn tiff_with_desc(le: bool) -> (Vec<u8>, Vec<u8>) {
let desc = b"hello-tiff-description".to_vec();
let w16 = |v: &mut Vec<u8>, x: u16| {
if le {
v.extend_from_slice(&x.to_le_bytes());
} else {
v.extend_from_slice(&x.to_be_bytes());
}
};
let w32 = |v: &mut Vec<u8>, x: u32| {
if le {
v.extend_from_slice(&x.to_le_bytes());
} else {
v.extend_from_slice(&x.to_be_bytes());
}
};
let mut v = Vec::new();
v.extend_from_slice(if le { b"II" } else { b"MM" });
w16(&mut v, 42);
w32(&mut v, 8); let desc_off = 8 + 2 + 12 + 4; w16(&mut v, 1); w16(&mut v, 0x010E); w16(&mut v, 2); w32(&mut v, desc.len() as u32);
w32(&mut v, desc_off as u32);
w32(&mut v, 0); v.extend_from_slice(&desc);
(v, desc)
}
#[test]
fn embed_round_trips_le() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let (asset, _) = tiff_with_desc(true);
let out = embed(&asset, &store).unwrap();
assert_eq!(extract(&out).unwrap().as_deref(), Some(store.as_slice()));
}
#[test]
fn embed_round_trips_be() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let (asset, _) = tiff_with_desc(false);
let out = embed(&asset, &store).unwrap();
assert_eq!(extract(&out).unwrap().as_deref(), Some(store.as_slice()));
}
#[test]
fn embed_into_bare_round_trips() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let out = embed(&bare_tiff(), &store).unwrap();
assert_eq!(extract(&out).unwrap().as_deref(), Some(store.as_slice()));
}
#[test]
fn embed_preserves_existing_entries() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let (asset, desc) = tiff_with_desc(true);
let out = embed(&asset, &store).unwrap();
let t = read_header(&out).unwrap();
let chain = walk(&out, t, true).unwrap();
let ifd = chain.first().expect("one IFD");
let mut found = None;
for index in 0..ifd.count {
if ifd.tag(t, index) == 0x010E {
let (start, length) = value_span(&out, t, ifd, index).expect("description span");
found = Some(out[start..start + length].to_vec());
}
}
assert_eq!(found.as_deref(), Some(desc.as_slice()));
}
#[test]
fn re_embed_replaces_manifest() {
let (asset, _) = tiff_with_desc(true);
let first = embed(&asset, b"FIRSTMANIFEST-store").unwrap();
let second = embed(&first, b"SECONDMANIFEST-store").unwrap();
assert_eq!(
extract(&second).unwrap().as_deref(),
Some(&b"SECONDMANIFEST-store"[..])
);
let t = read_header(&second).unwrap();
let chain = walk(&second, t, true).unwrap();
let ifd = chain.first().expect("one IFD");
let c2pa = (0..ifd.count)
.filter(|&index| ifd.tag(t, index) == TAG_C2PA)
.count();
assert_eq!(c2pa, 1);
}
#[test]
fn strict_placement_requires_manifest_in_last_main_ifd() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let mut asset = tiff_with_manifest(&store);
let second_ifd = asset.len() as u32;
asset[22..26].copy_from_slice(&second_ifd.to_le_bytes());
asset.extend_from_slice(&0u16.to_le_bytes());
asset.extend_from_slice(&0u32.to_le_bytes());
assert!(placement_error(&asset).unwrap().is_some());
}
#[test]
fn rejects_non_tiff() {
assert!(extract(b"\x00\x01\x02\x03\x04\x05\x06\x07").is_err());
}
#[test]
fn reads_a_single_page_bigtiff_in_either_byte_order() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
for little in [true, false] {
let (asset, start, length) = bigtiff_single_page(little, &store);
assert_eq!(
extract(&asset).unwrap().as_deref(),
Some(store.as_slice()),
"little={little}"
);
assert_eq!(placement_error(&asset).unwrap(), None, "little={little}");
assert_eq!(
exclusions(&asset).unwrap(),
vec![DataHashExclusion { start, length }],
"little={little}"
);
}
}
#[test]
fn reads_a_multi_page_bigtiff_in_either_byte_order() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
for little in [true, false] {
let (asset, start, length) = bigtiff_multi_page(little, &store);
assert_eq!(
extract(&asset).unwrap().as_deref(),
Some(store.as_slice()),
"little={little}"
);
assert_eq!(placement_error(&asset).unwrap(), None, "little={little}");
assert_eq!(
exclusions(&asset).unwrap(),
vec![DataHashExclusion { start, length }],
"little={little}"
);
}
}
#[test]
fn reads_an_inline_bigtiff_value() {
let manifest = b"12345678";
let little = true;
let mut value = [0u8; 8];
value.copy_from_slice(manifest);
let row = big_entry(
little,
TAG_C2PA,
TYPE_UNDEFINED,
manifest.len() as u64,
value,
);
let mut asset = big_header(little, 16);
asset.extend_from_slice(&big_ifd(little, &[row], 0));
assert_eq!(extract(&asset).unwrap().as_deref(), Some(&manifest[..]));
assert_eq!(
exclusions(&asset).unwrap(),
vec![DataHashExclusion {
start: 24 + 12,
length: 8
}]
);
}
#[test]
fn reads_a_bigtiff_xmp_packet_only_when_it_is_byte_typed() {
let packet = b"<x:xmpmeta xmlns:x='adobe:ns:meta/'></x:xmpmeta>";
let little = true;
let build = |tag_type: u16| {
let row = big_entry(
little,
crate::c2pa_formats::xmp::TIFF_XMP_TAG,
tag_type,
packet.len() as u64,
big_offset(little, (16 + 8 + 20 + 8) as u64),
);
let mut asset = big_header(little, 16);
asset.extend_from_slice(&big_ifd(little, &[row], 0));
asset.extend_from_slice(packet);
asset
};
assert_eq!(xmp_packet(&build(TYPE_BYTE)).unwrap(), &packet[..]);
assert_eq!(xmp_packet(&build(TYPE_UNDEFINED)).unwrap(), &packet[..]);
assert_eq!(xmp_packet(&build(3)), None);
}
#[test]
fn refuses_a_bigtiff_c2pa_entry_with_the_wrong_tag_type() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let (mut asset, _, _) = bigtiff_single_page(true, &store);
let type_field = 16 + 8 + 20 + 2;
asset[type_field..type_field + 2].copy_from_slice(&1u16.to_le_bytes()); assert!(extract(&asset).is_err());
assert!(exclusions(&asset).is_err());
}
#[test]
fn refuses_a_bigtiff_carrier_outside_the_last_ifd() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let little = true;
let carrier_row = big_entry(
little,
TAG_C2PA,
TYPE_UNDEFINED,
store.len() as u64,
big_offset(little, (16 + 2 * (8 + 20 + 8)) as u64),
);
let page_row = big_entry(little, 0x0100, 3, 1, big_offset(little, 1));
let second_page = 16 + (8 + 20 + 8) as u64;
let mut asset = big_header(little, 16);
asset.extend_from_slice(&big_ifd(little, &[carrier_row], second_page));
asset.extend_from_slice(&big_ifd(little, &[page_row], 0));
asset.extend_from_slice(&store);
assert_eq!(
placement_error(&asset).unwrap(),
Some("TIFF C2PA entry is not in the last main IFD")
);
}
#[test]
fn refuses_a_shared_last_ifd_in_a_multi_page_bigtiff() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let little = true;
let page_len = 8 + 20 + 8;
let last_len = 8 + 2 * 20 + 8;
let manifest_off = 16 + page_len + last_len;
let page_row = big_entry(little, 0x0100, 3, 1, big_offset(little, 1));
let last_rows = vec![
big_entry(little, 0x0100, 3, 1, big_offset(little, 1)),
big_entry(
little,
TAG_C2PA,
TYPE_UNDEFINED,
store.len() as u64,
big_offset(little, manifest_off as u64),
),
];
let mut asset = big_header(little, 16);
asset.extend_from_slice(&big_ifd(little, &[page_row], (16 + page_len) as u64));
asset.extend_from_slice(&big_ifd(little, &last_rows, 0));
asset.extend_from_slice(&store);
assert_eq!(
placement_error(&asset).unwrap(),
Some("TIFF C2PA entry is not the only entry in its main IFD")
);
}
#[test]
fn refuses_a_bigtiff_header_this_reader_cannot_address() {
let little = true;
let mut wrong_width = big_header(little, 16);
wrong_width[4..6].copy_from_slice(&4u16.to_le_bytes());
assert_eq!(
read_header(&wrong_width).unwrap_err(),
invalid("unsupported BigTIFF offset width")
);
let mut reserved = big_header(little, 16);
reserved[6..8].copy_from_slice(&1u16.to_le_bytes());
assert_eq!(
read_header(&reserved).unwrap_err(),
invalid("BigTIFF reserved header field is not zero")
);
let truncated = &big_header(little, 16)[..12];
assert_eq!(
read_header(truncated).unwrap_err(),
FormatError::Truncated(FMT)
);
}
#[test]
fn refuses_a_bigtiff_ifd_inside_the_header() {
let mut asset = big_header(true, 8);
asset.extend_from_slice(&big_ifd(true, &[], 0));
assert_eq!(
extract(&asset).unwrap_err(),
invalid("TIFF IFD offset points into the header")
);
}
#[test]
fn refuses_a_cyclic_bigtiff_chain() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let little = true;
let page_row = big_entry(little, 0x0100, 3, 1, big_offset(little, 1));
let second = 16 + (8 + 20 + 8) as u64;
let mut asset = big_header(little, 16);
asset.extend_from_slice(&big_ifd(little, &[page_row.clone()], second));
asset.extend_from_slice(&big_ifd(little, &[page_row], 16));
asset.extend_from_slice(&store);
assert_eq!(
extract(&asset).unwrap_err(),
invalid("cyclic TIFF IFD chain")
);
}
#[test]
fn refuses_overlapping_bigtiff_ifds() {
let little = true;
let page_row = big_entry(little, 0x0100, 3, 1, big_offset(little, 1));
let mut asset = big_header(little, 16);
asset.extend_from_slice(&big_ifd(little, &[page_row], 44));
assert_eq!(asset.len(), 52);
assert_eq!(
extract(&asset).unwrap_err(),
invalid("overlapping TIFF IFD tables")
);
}
#[test]
fn refuses_an_oversized_bigtiff_entry_count() {
let little = true;
let mut asset = big_header(little, 16);
asset.extend_from_slice(&big_offset(little, u64::MAX));
asset.extend_from_slice(&[0u8; 64]);
assert!(extract(&asset).is_err());
let mut budget = big_header(little, 16);
budget.extend_from_slice(&big_offset(little, 1 << 40));
budget.extend_from_slice(&[0u8; 64]);
assert_eq!(
extract(&budget).unwrap_err(),
invalid("TIFF IFD chain exceeds the supported directory size")
);
}
#[test]
fn refuses_a_truncated_bigtiff_ifd_and_value() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let (asset, _, _) = bigtiff_single_page(true, &store);
let short_table = &asset[..30];
assert_eq!(
extract(short_table).unwrap_err(),
FormatError::Truncated(FMT)
);
let short_value = &asset[..asset.len() - 1];
assert_eq!(
extract(short_value).unwrap_err(),
FormatError::Truncated(FMT)
);
}
#[test]
fn refuses_to_write_a_bigtiff() {
let store = crate::c2pa_formats::tests::dummy_manifest_store();
let (asset, _, _) = bigtiff_single_page(true, &store);
assert_eq!(
embed(&asset, &store).unwrap_err(),
FormatError::UnsupportedVariant {
format: FMT,
detail: "BigTIFF is read-only in this build",
}
);
}
}