use std::{
collections::{BTreeMap, HashMap},
fs::OpenOptions,
io::{Cursor, Read, Seek, SeekFrom, Write},
path::Path,
vec,
};
use atree::{Arena, Token};
use byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
use byteordered::{with_order, ByteOrdered, Endianness};
use crate::{
asset_io::{
rename_or_move, AssetIO, AssetPatch, CAIRead, CAIReadWrite, CAIReader, CAIWriter,
ComposedManifestRef, HashBlockObjectType, HashObjectPositions, RemoteRefEmbed,
RemoteRefEmbedType,
},
error::{Error, Result},
utils::{
io_utils::{safe_vec, stream_len, tempfile_builder, ReaderUtils},
xmp_inmemory_utils::{add_provenance, MIN_XMP},
},
};
const II: [u8; 2] = *b"II";
const MM: [u8; 2] = *b"MM";
const C2PA_TAG: u16 = 0xcd41;
const XMP_TAG: u16 = 0x02bc;
const SUBFILE_TAG: u16 = 0x014a;
const EXIFIFD_TAG: u16 = 0x8769;
const GPSIFD_TAG: u16 = 0x8825;
const INTEROPERABILITY_TAG: u16 = 40965;
const C2PA_FIELD_TYPE: u16 = 7;
const STRIPBYTECOUNTS: u16 = 279;
const STRIPOFFSETS: u16 = 273;
const TILEBYTECOUNTS: u16 = 325;
const TILEOFFSETS: u16 = 324;
const SUBFILES: [u16; 4] = [SUBFILE_TAG, EXIFIFD_TAG, GPSIFD_TAG, INTEROPERABILITY_TAG];
static SUPPORTED_TYPES: [&str; 10] = [
"tif",
"tiff",
"image/tiff",
"dng",
"image/dng",
"image/x-adobe-dng",
"arw",
"image/x-sony-arw",
"nef",
"image/x-nikon-nef",
];
static SUPPORTED_WRITER_TYPES: [&str; 6] = [
"tif",
"tiff",
"image/tiff",
"dng",
"image/dng",
"image/x-adobe-dng",
];
#[derive(Debug, PartialEq)]
enum IFDEntryType {
Byte = 1, Ascii = 2, Short = 3, Long = 4, Rational = 5, Sbyte = 6, Undefined = 7, Sshort = 8, Slong = 9, Srational = 10, Float = 11, Double = 12, Ifd = 13, Long8 = 16, Slong8 = 17, Ifd8 = 18, }
impl IFDEntryType {
pub fn from_u16(val: u16) -> Option<IFDEntryType> {
match val {
1 => Some(IFDEntryType::Byte),
2 => Some(IFDEntryType::Ascii),
3 => Some(IFDEntryType::Short),
4 => Some(IFDEntryType::Long),
5 => Some(IFDEntryType::Rational),
6 => Some(IFDEntryType::Sbyte),
7 => Some(IFDEntryType::Undefined),
8 => Some(IFDEntryType::Sshort),
9 => Some(IFDEntryType::Slong),
10 => Some(IFDEntryType::Srational),
11 => Some(IFDEntryType::Float),
12 => Some(IFDEntryType::Double),
13 => Some(IFDEntryType::Ifd),
16 => Some(IFDEntryType::Long8),
17 => Some(IFDEntryType::Slong8),
18 => Some(IFDEntryType::Ifd8),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IfdEntry {
entry_tag: u16,
entry_type: u16,
value_count: u64,
value_offset: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IfdType {
Page,
Subfile,
Exif,
Gps,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImageFileDirectory {
offset: u64,
entry_cnt: u64,
ifd_type: IfdType,
entries: HashMap<u16, IfdEntry>,
next_ifd_offset: Option<u64>,
next_idf_offset_location: u64,
}
impl ImageFileDirectory {
pub fn get_tag(&self, tag_id: u16) -> Option<&IfdEntry> {
self.entries.get(&tag_id)
}
#[allow(dead_code)]
pub fn get_tag_mut(&mut self, tag_id: u16) -> Option<&mut IfdEntry> {
self.entries.get_mut(&tag_id)
}
}
pub(crate) struct TiffStructure {
byte_order: Endianness,
big_tiff: bool,
#[allow(dead_code)]
first_ifd_offset: u64,
first_ifd: Option<ImageFileDirectory>,
}
impl TiffStructure {
pub fn load<R>(reader: &mut R) -> Result<Self>
where
R: Read + Seek + ?Sized,
{
let mut endianness = [0u8, 2];
reader.read_exact(&mut endianness)?;
let byte_order = match endianness {
II => Endianness::Little,
MM => Endianness::Big,
endianness => {
return Err(TiffError::InvalidFileSignature {
reason: format!(
"invalid header signature: expected endianness \"II\" or \"MM\", found \"{}\"",
String::from_utf8_lossy(&endianness)
),
}
.into())
}
};
let mut byte_reader = ByteOrdered::runtime(reader, byte_order);
let big_tiff = match byte_reader.read_u16()? {
42 => false,
43 => {
let first_ifd_offset = byte_reader.read_u16()?;
if first_ifd_offset != 8 {
return Err(TiffError::InvalidFileSignature {
reason: format!(
"invalid header signature: expected first IFD offset for BigTiff to be \"8\", found \"{first_ifd_offset}\""
),
}.into());
}
let reserved = byte_reader.read_u16()?;
if reserved != 0 {
return Err(TiffError::InvalidFileSignature {
reason: format!(
"invalid header signature: expected bytes after first IFD offset for BigTiff to be \"0\", found \"{reserved}\""),
}.into());
}
true
}
magic => {
return Err(TiffError::InvalidFileSignature {
reason: format!(
"invalid header signature: expected magic \"2A\" (TIFF) \"2B\" (BigTIFF), found \"{magic:02X}\""),
}.into());
}
};
let first_ifd_offset = if big_tiff {
byte_reader.read_u64()?
} else {
byte_reader.read_u32()?.into()
};
byte_reader.seek(SeekFrom::Start(first_ifd_offset))?;
let first_ifd = TiffStructure::read_ifd(
byte_reader.into_inner(),
byte_order,
big_tiff,
IfdType::Page,
)?;
let ts = TiffStructure {
byte_order,
big_tiff,
first_ifd_offset,
first_ifd: Some(first_ifd),
};
Ok(ts)
}
pub fn read_ifd_entries<R>(
byte_reader: &mut ByteOrdered<&mut R, Endianness>,
big_tiff: bool,
entry_cnt: u64,
entries: &mut HashMap<u16, IfdEntry>,
) -> Result<()>
where
R: Read + Seek + ?Sized,
{
for _ in 0..entry_cnt {
let tag = byte_reader.read_u16()?;
let tag_type = byte_reader.read_u16()?;
let (count, data_offset) = if big_tiff {
let count = byte_reader.read_u64()?;
let mut buf = [0; 8];
byte_reader.read_exact(&mut buf)?;
let data_offset = buf
.as_slice()
.read_u64::<NativeEndian>()
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
(count, data_offset)
} else {
let count = byte_reader.read_u32()?;
let mut buf = [0; 4];
byte_reader.read_exact(&mut buf)?;
let data_offset = buf
.as_slice()
.read_u32::<NativeEndian>()
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
(count.into(), data_offset.into())
};
let ifd_entry = IfdEntry {
entry_tag: tag,
entry_type: tag_type,
value_count: count,
value_offset: data_offset,
};
entries.insert(tag, ifd_entry);
}
Ok(())
}
pub fn read_ifd<R>(
reader: &mut R,
byte_order: Endianness,
big_tiff: bool,
ifd_type: IfdType,
) -> Result<ImageFileDirectory>
where
R: Read + Seek + ReadBytesExt + ?Sized,
{
let mut byte_reader = ByteOrdered::runtime(reader, byte_order);
let ifd_offset = byte_reader.stream_position()?;
let entry_cnt = if big_tiff {
byte_reader.read_u64()?
} else {
byte_reader.read_u16()?.into()
};
let mut ifd = ImageFileDirectory {
offset: ifd_offset,
entry_cnt,
ifd_type,
entries: HashMap::new(),
next_ifd_offset: None,
next_idf_offset_location: 0,
};
TiffStructure::read_ifd_entries(&mut byte_reader, big_tiff, entry_cnt, &mut ifd.entries)?;
ifd.next_idf_offset_location = byte_reader.stream_position()?;
let next_ifd = if big_tiff {
byte_reader.read_u64()?
} else {
byte_reader.read_u32()?.into()
};
match next_ifd {
0 => (),
_ => ifd.next_ifd_offset = Some(next_ifd),
};
Ok(ifd)
}
}
fn decode_offset(offset_file_native: u64, endianness: Endianness, big_tiff: bool) -> Result<u64> {
let offset: u64;
let offset_bytes = offset_file_native.to_ne_bytes();
let offset_reader = Cursor::new(offset_bytes);
with_order!(offset_reader, endianness, |src| {
if big_tiff {
let o = src.read_u64()?;
offset = o;
} else {
let o = src.read_u32()?;
offset = o.into();
}
});
Ok(offset)
}
fn map_tiff<R>(
mut input: &mut R,
) -> Result<(Arena<ImageFileDirectory>, Vec<Token>, Endianness, bool)>
where
R: Read + Seek + ?Sized,
{
let _size = stream_len(input)?;
input.rewind()?;
let mut tokens = Vec::new();
let ts = TiffStructure::load(input)?;
let tiff_tree: Arena<ImageFileDirectory> = if let Some(ifd) = ts.first_ifd.clone() {
let (mut tiff_tree, page_0) = Arena::with_data(ifd);
let mut current_token = page_0;
loop {
tokens.push(current_token);
let page_subifd = tiff_tree[current_token].data.get_tag(SUBFILE_TAG).copied();
if let Some(subifd) = page_subifd {
let decoded_offset =
decode_offset(subifd.value_offset, ts.byte_order, ts.big_tiff)?;
input.seek(SeekFrom::Start(decoded_offset))?;
let num_longs_x4 = usize::try_from(
subifd
.value_count
.checked_mul(4)
.ok_or_else(|| Error::InvalidAsset("value out of range".to_string()))?,
)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
let mut subfile_offsets = safe_vec(subifd.value_count, Some(0u32))?;
if num_longs_x4 <= 4 || ts.big_tiff && num_longs_x4 <= 8 {
let offset_bytes = subifd.value_offset.to_ne_bytes();
let offset_reader = Cursor::new(offset_bytes);
with_order!(offset_reader, ts.byte_order, |src| {
for item in subfile_offsets.iter_mut().take(num_longs_x4 / 4) {
let s = src.read_u32()?; *item = s; }
});
} else {
let buf = input.read_to_vec(num_longs_x4 as u64)?;
let offsets_buf = Cursor::new(buf);
with_order!(offsets_buf, ts.byte_order, |src| {
for item in subfile_offsets.iter_mut().take(num_longs_x4 / 4) {
let s = src.read_u32()?; *item = s; }
});
}
for subfile_offset in subfile_offsets {
let u64_offset = subfile_offset as u64;
input.seek(SeekFrom::Start(u64_offset))?;
let subfile_ifd = TiffStructure::read_ifd(
input,
ts.byte_order,
ts.big_tiff,
IfdType::Subfile,
)?;
let subfile_token = tiff_tree.new_node(subfile_ifd);
current_token
.append_node(&mut tiff_tree, subfile_token)
.map_err(|_err| Error::InvalidAsset("Bad TIFF Structure".to_string()))?;
}
}
if let Some(exififd) = tiff_tree[current_token].data.get_tag(EXIFIFD_TAG) {
let decoded_offset =
decode_offset(exififd.value_offset, ts.byte_order, ts.big_tiff)?;
input.seek(SeekFrom::Start(decoded_offset))?;
let exif_ifd =
TiffStructure::read_ifd(input, ts.byte_order, ts.big_tiff, IfdType::Exif)?;
let exif_token = tiff_tree.new_node(exif_ifd);
current_token
.append_node(&mut tiff_tree, exif_token)
.map_err(|_err| Error::InvalidAsset("Bad TIFF Structure".to_string()))?;
}
if let Some(gpsifd) = tiff_tree[current_token].data.get_tag(GPSIFD_TAG) {
let decoded_offset =
decode_offset(gpsifd.value_offset, ts.byte_order, ts.big_tiff)?;
input.seek(SeekFrom::Start(decoded_offset))?;
let gps_ifd =
TiffStructure::read_ifd(input, ts.byte_order, ts.big_tiff, IfdType::Gps)?;
let gps_token = tiff_tree.new_node(gps_ifd);
current_token
.append_node(&mut tiff_tree, gps_token)
.map_err(|_err| Error::InvalidAsset("Bad TIFF Structure".to_string()))?;
}
if let Some(next_ifd_offset) = tiff_tree[current_token].data.next_ifd_offset {
input.seek(SeekFrom::Start(next_ifd_offset))?;
let next_ifd =
TiffStructure::read_ifd(input, ts.byte_order, ts.big_tiff, IfdType::Page)?;
current_token = current_token.insert_after(&mut tiff_tree, next_ifd);
} else {
break;
}
}
tiff_tree
} else {
return Err(Error::InvalidAsset("TIFF structure invalid".to_string()));
};
Ok((tiff_tree, tokens, ts.byte_order, ts.big_tiff))
}
#[derive(Eq, PartialEq, Clone)]
pub(crate) struct IfdClonedEntry {
pub entry_tag: u16,
pub entry_type: u16,
pub value_count: u64,
pub value_bytes: Vec<u8>,
}
pub(crate) struct TiffCloner<T>
where
T: Read + Write + Seek,
{
endianness: Endianness,
big_tiff: bool,
first_idf_offset: u64,
writer: ByteOrdered<T, Endianness>,
additional_ifds: BTreeMap<u16, IfdClonedEntry>,
}
impl<T: Read + Write + Seek> TiffCloner<T> {
pub fn new(endianness: Endianness, big_tiff: bool, writer: T) -> Result<TiffCloner<T>> {
let bo = ByteOrdered::runtime(writer, endianness);
let mut tc = TiffCloner {
endianness,
big_tiff,
first_idf_offset: 0,
writer: bo,
additional_ifds: BTreeMap::new(),
};
tc.write_header()?;
Ok(tc)
}
fn offset(&mut self) -> Result<u64> {
Ok(self.writer.stream_position()?)
}
fn pad_word_boundary(&mut self) -> Result<()> {
let curr_offset = self.offset()?;
if curr_offset % 4 != 0 {
let padding = [0, 0, 0];
let pad_len = 4 - (curr_offset % 4);
self.writer.write_all(&padding[..pad_len as usize])?;
}
Ok(())
}
fn write_header(&mut self) -> Result<u64> {
let boi = match self.endianness {
Endianness::Big => 0x4d,
Endianness::Little => 0x49,
};
let offset;
if self.big_tiff {
self.writer.write_all(&[boi, boi])?;
self.writer.write_u16(43u16)?;
self.writer.write_u16(8u16)?;
self.writer.write_u16(0u16)?;
offset = self.writer.stream_position()?;
self.writer.write_u64(0)?;
} else {
self.writer.write_all(&[boi, boi])?;
self.writer.write_u16(42u16)?;
offset = self.writer.stream_position()?;
self.writer.write_u32(0)?;
}
self.first_idf_offset = offset;
Ok(offset)
}
fn write_entry_count(&mut self, count: usize) -> Result<()> {
if self.big_tiff {
let cnt = u64::try_from(count)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
self.writer.write_u64(cnt)?;
} else {
let cnt = u16::try_from(count)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
self.writer.write_u16(cnt)?;
}
Ok(())
}
fn write_ifd(&mut self, target_ifd: &mut BTreeMap<u16, IfdClonedEntry>) -> Result<u64> {
let is_c2pa_idf = target_ifd.len() == 1 && target_ifd.contains_key(&C2PA_TAG);
if !is_c2pa_idf {
for &mut IfdClonedEntry {
value_bytes: ref mut value_bytes_ref,
..
} in target_ifd.values_mut()
{
let data_bytes = if self.big_tiff { 8 } else { 4 };
if value_bytes_ref.len() > data_bytes {
let offset = self.writer.stream_position()?;
self.writer.write_all(value_bytes_ref)?;
let mut offset_vec = vec![0; data_bytes];
with_order!(offset_vec.as_mut_slice(), self.endianness, |ew| {
if self.big_tiff {
ew.write_u64(offset)?;
} else {
let offset_u32 = u32::try_from(offset).map_err(|_err| {
Error::InvalidAsset("value out of range".to_string())
})?;
ew.write_u32(offset_u32)?;
}
});
*value_bytes_ref = offset_vec;
} else {
while value_bytes_ref.len() < data_bytes {
value_bytes_ref.push(0);
}
}
}
}
self.pad_word_boundary()?;
let ifd_offset = self.writer.stream_position()?;
self.write_entry_count(target_ifd.len())?;
let mut c2pa_bytes = None;
for (tag, entry) in target_ifd.iter() {
self.writer.write_u16(*tag)?;
self.writer.write_u16(entry.entry_type)?;
if self.big_tiff {
self.writer.write_u64(entry.value_count)?;
} else {
let cnt = u32::try_from(entry.value_count)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
self.writer.write_u32(cnt)?;
}
if *tag == C2PA_TAG && is_c2pa_idf {
let data_bytes = if self.big_tiff { 8u64 } else { 4u64 };
if entry.value_bytes.len() > data_bytes as usize {
c2pa_bytes = Some(entry.value_bytes.to_owned());
let c2pa_bytes_pos = self.writer.stream_position()?
+ data_bytes + data_bytes;
if self.big_tiff {
self.writer.write_u64(c2pa_bytes_pos)?;
} else {
let offset32 = u32::try_from(c2pa_bytes_pos)?;
self.writer.write_u32(offset32)?;
}
} else {
self.writer.write_all(&entry.value_bytes)?;
}
} else {
self.writer.write_all(&entry.value_bytes)?;
}
}
if self.big_tiff {
self.writer.write_u64(0)?;
} else {
self.writer.write_u32(0)?;
}
if let Some(c2pa_bytes) = c2pa_bytes {
self.writer.write_all(&c2pa_bytes)?;
}
Ok(ifd_offset)
}
pub fn add_target_tag(&mut self, entry: IfdClonedEntry) {
self.additional_ifds.insert(entry.entry_tag, entry);
}
pub fn set_next_ifd_offset(&mut self, entry: &ImageFileDirectory, offset: u64) -> Result<()> {
self.writer
.seek(SeekFrom::Start(entry.next_idf_offset_location))?;
if self.big_tiff {
self.writer.write_u64(offset)?;
} else {
let offset32 = u32::try_from(offset)?;
self.writer.write_u32(offset32)?;
}
Ok(())
}
fn clone_image_data<R: Read + Seek + ?Sized>(
&mut self,
target_ifd: &mut BTreeMap<u16, IfdClonedEntry>,
mut asset_reader: &mut R,
) -> Result<()> {
match (
target_ifd.contains_key(&STRIPBYTECOUNTS),
target_ifd.contains_key(&STRIPOFFSETS),
target_ifd.contains_key(&TILEBYTECOUNTS),
target_ifd.contains_key(&TILEOFFSETS),
) {
(true, true, false, false) => {
let sbc_entry = target_ifd[&STRIPBYTECOUNTS].clone();
let so_entry = target_ifd.get_mut(&STRIPOFFSETS).ok_or(Error::NotFound)?;
if so_entry.value_count != sbc_entry.value_count {
return Err(Error::InvalidAsset(
"TIFF strip count does not match strip offset count".to_string(),
));
}
let mut sbcs: Vec<u64> = safe_vec(sbc_entry.value_count, Some(0))?;
let mut dest_offsets: Vec<u64> = Vec::new();
with_order!(sbc_entry.value_bytes.as_slice(), self.endianness, |src| {
for c in &mut sbcs {
match sbc_entry.entry_type {
4u16 => {
let s = src.read_u32()?;
*c = s.into();
}
3u16 => {
let s = src.read_u16()?;
*c = s.into();
}
16u16 => {
let s = src.read_u64()?;
*c = s;
}
_ => return Err(Error::InvalidAsset("invalid TIFF strip".to_string())),
}
}
});
let current_offset = self.offset()?;
self.writer.seek(SeekFrom::Start(current_offset))?;
with_order!(so_entry.value_bytes.as_slice(), self.endianness, |src| {
for c in sbcs.iter() {
let cnt = usize::try_from(*c).map_err(|_err| {
Error::InvalidAsset("value out of range".to_string())
})?;
let so: u64 = match so_entry.entry_type {
4u16 => {
let s = src.read_u32()?;
s.into()
}
3u16 => {
let s = src.read_u16()?;
s.into()
}
16u16 => src.read_u64()?,
_ => return Err(Error::InvalidAsset("invalid TIFF strip".to_string())),
};
let dest_offset = self.writer.stream_position()?;
dest_offsets.push(dest_offset);
asset_reader.seek(SeekFrom::Start(so))?;
let data = asset_reader.read_to_vec(cnt as u64)?;
self.writer.write_all(data.as_slice())?;
}
});
with_order!(
so_entry.value_bytes.as_mut_slice(),
self.endianness,
|dest| {
for o in dest_offsets.iter() {
match so_entry.entry_type {
4u16 => {
let offset = u32::try_from(*o).map_err(|_err| {
Error::InvalidAsset("value out of range".to_string())
})?;
dest.write_u32(offset)?;
}
3u16 => {
let offset = u16::try_from(*o).map_err(|_err| {
Error::InvalidAsset("value out of range".to_string())
})?;
dest.write_u16(offset)?;
}
16u16 => {
let offset = *o;
dest.write_u64(offset)?;
}
_ => {
return Err(Error::InvalidAsset(
"invalid TIFF strip".to_string(),
))
}
}
}
}
);
}
(false, false, true, true) => {
let tbc_entry = target_ifd[&TILEBYTECOUNTS].clone();
let to_entry = target_ifd.get_mut(&TILEOFFSETS).ok_or(Error::NotFound)?;
if to_entry.value_count != tbc_entry.value_count {
return Err(Error::InvalidAsset(
"TIFF tile count does not match tile offset count".to_string(),
));
}
let mut tbcs: Vec<u64> = safe_vec(tbc_entry.value_count, Some(0u64))?;
let mut dest_offsets: Vec<u64> = Vec::new();
with_order!(tbc_entry.value_bytes.as_slice(), self.endianness, |src| {
for val in &mut tbcs {
match tbc_entry.entry_type {
4u16 => {
let s = src.read_u32()?;
*val = s.into();
}
3u16 => {
let s = src.read_u16()?;
*val = s.into();
}
16u16 => {
let s = src.read_u64()?;
*val = s;
}
_ => return Err(Error::InvalidAsset("invalid TIFF tile".to_string())),
}
}
});
let current_offset = self.offset()?;
self.writer.seek(SeekFrom::Start(current_offset))?;
with_order!(to_entry.value_bytes.as_slice(), self.endianness, |src| {
for c in tbcs.iter() {
let cnt = usize::try_from(*c).map_err(|_err| {
Error::InvalidAsset("value out of range".to_string())
})?;
let to: u64 = match to_entry.entry_type {
4u16 => {
let s = src.read_u32()?;
s.into()
}
16u16 => src.read_u64()?,
_ => return Err(Error::InvalidAsset("invalid TIFF tile".to_string())),
};
let dest_offset = self.writer.stream_position()?;
dest_offsets.push(dest_offset);
asset_reader.seek(SeekFrom::Start(to))?;
let data = asset_reader.read_to_vec(cnt as u64)?;
self.writer.write_all(data.as_slice())?;
}
});
with_order!(
to_entry.value_bytes.as_mut_slice(),
self.endianness,
|dest| {
for v in dest_offsets.iter() {
match to_entry.entry_type {
4u16 => {
let offset = u32::try_from(*v).map_err(|_err| {
Error::InvalidAsset("value out of range".to_string())
})?;
dest.write_u32(offset)?;
}
3u16 => {
let offset = u16::try_from(*v).map_err(|_err| {
Error::InvalidAsset("value out of range".to_string())
})?;
dest.write_u16(offset)?;
}
16u16 => {
let offset = *v;
dest.write_u64(offset)?;
}
_ => {
return Err(Error::InvalidAsset(
"invalid TIFF tile".to_string(),
))
}
}
}
}
);
}
(_, _, _, _) => (),
};
Ok(())
}
fn clone_sub_files<R: Read + Seek + ?Sized>(
&mut self,
tiff_tree: &Arena<ImageFileDirectory>,
page: Token,
asset_reader: &mut R,
) -> Result<HashMap<u16, Vec<u64>>> {
let mut offset_map: HashMap<u16, Vec<u64>> = HashMap::new();
let mut offsets_ifd: Vec<u64> = Vec::new();
let mut offsets_exif: Vec<u64> = Vec::new();
let mut offsets_gps: Vec<u64> = Vec::new();
for n in page.children(tiff_tree) {
let ifd = &n.data;
let mut cloned_ifd = self.clone_ifd_entries(&ifd.entries, asset_reader)?;
self.clone_image_data(&mut cloned_ifd, asset_reader)?;
let sub_ifd_offset = self.write_ifd(&mut cloned_ifd)?;
match ifd.ifd_type {
IfdType::Page => (),
IfdType::Subfile => offsets_ifd.push(sub_ifd_offset),
IfdType::Exif => offsets_exif.push(sub_ifd_offset),
IfdType::Gps => offsets_gps.push(sub_ifd_offset),
};
}
offset_map.insert(SUBFILE_TAG, offsets_ifd);
offset_map.insert(EXIFIFD_TAG, offsets_exif);
offset_map.insert(GPSIFD_TAG, offsets_gps);
Ok(offset_map)
}
pub fn clone_tiff<R: Read + Seek + ?Sized>(
&mut self,
tiff_tree: &mut Arena<ImageFileDirectory>,
tokens: &[Token],
asset_reader: &mut R,
) -> Result<()> {
let mut page_ifd_offsets = Vec::new();
let first_page = tokens
.first()
.ok_or(Error::InvalidAsset("no IFD found".to_string()))?;
let last_page = tokens
.last()
.ok_or(Error::InvalidAsset("no IFD found".to_string()))?;
let last_page_ifd = tiff_tree
.get(*last_page)
.ok_or_else(|| Error::InvalidAsset("TIFF does not have IFD".to_string()))?;
let new_c2pa_entry = self.additional_ifds.remove(&C2PA_TAG);
let needs_end_ifd = last_page == first_page
|| !last_page_ifd.data.entries.contains_key(&C2PA_TAG) && new_c2pa_entry.is_some();
if last_page != first_page
&& last_page_ifd.data.entries.contains_key(&C2PA_TAG)
&& last_page_ifd.data.entries.len() > 1
{
return Err(Error::ValidationRule(
"Last IDF with C2PA manifest contained additional tags, expected 1 tag".to_string(),
));
}
let has_c2pa_ifd =
last_page != first_page || last_page_ifd.data.entries.contains_key(&C2PA_TAG);
for page_token in tokens {
let subfile_offsets = self.clone_sub_files(tiff_tree, *page_token, asset_reader)?;
let page_ifd = tiff_tree
.get(*page_token)
.ok_or_else(|| Error::InvalidAsset("TIFF does not have IFD".to_string()))?;
let mut cloned_ifd = self.clone_ifd_entries(&page_ifd.data.entries, asset_reader)?;
self.clone_image_data(&mut cloned_ifd, asset_reader)?;
if page_token == first_page {
for (tag, new_entry) in &self.additional_ifds {
cloned_ifd.insert(*tag, new_entry.clone());
}
}
if page_token == last_page && has_c2pa_ifd {
if let Some(new_entry) = &new_c2pa_entry {
cloned_ifd.insert(C2PA_TAG, new_entry.clone());
}
}
for t in SUBFILES {
if let Some(offsets) = subfile_offsets.get(&t) {
if offsets.is_empty() {
continue;
}
let e = cloned_ifd
.get_mut(&t)
.ok_or_else(|| Error::InvalidAsset("TIFF does not have IFD".to_string()))?;
let mut adjust_offsets: Vec<u8> = if self.big_tiff {
safe_vec(offsets.len() as u64 * 8, Some(0))?
} else {
safe_vec(offsets.len() as u64 * 4, Some(0))?
};
with_order!(adjust_offsets.as_mut_slice(), self.endianness, |dest| {
for o in offsets {
if self.big_tiff {
dest.write_u64(*o)?;
} else {
let offset_u32 = u32::try_from(*o).map_err(|_err| {
Error::InvalidAsset("value out of range".to_string())
})?;
dest.write_u32(offset_u32)?;
}
}
});
e.value_bytes = adjust_offsets;
}
}
let ifd_offset = self.write_ifd(&mut cloned_ifd)?;
page_ifd_offsets.push((ifd_offset, self.offset()?));
}
if needs_end_ifd {
if let Some(new_entry) = &new_c2pa_entry {
let mut cloned_entry = BTreeMap::new();
cloned_entry.insert(C2PA_TAG, new_entry.clone());
let ifd_offset = self.write_ifd(&mut cloned_entry)?;
page_ifd_offsets.push((ifd_offset, self.offset()?));
}
}
let mut prior_ifd_start = 0u64;
for (index, (offset, _write_point)) in page_ifd_offsets.iter().enumerate() {
if index == 0 {
self.writer.seek(SeekFrom::Start(4))?;
if self.big_tiff {
self.writer.write_u64(*offset)?;
} else {
let offset_u32 = u32::try_from(*offset)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
self.writer.write_u32(offset_u32)?;
}
prior_ifd_start = *offset;
} else {
self.writer.seek(SeekFrom::Start(prior_ifd_start))?;
let prior_ifd = TiffStructure::read_ifd(
&mut self.writer.inner_mut(),
self.endianness,
self.big_tiff,
IfdType::Page,
)?;
self.set_next_ifd_offset(&prior_ifd, *offset)?;
prior_ifd_start = *offset;
}
}
self.writer.flush()?;
Ok(())
}
fn clone_ifd_entries<R: Read + Seek + ?Sized>(
&mut self,
entries: &HashMap<u16, IfdEntry>,
mut asset_reader: &mut R,
) -> Result<BTreeMap<u16, IfdClonedEntry>> {
let mut target_ifd: BTreeMap<u16, IfdClonedEntry> = BTreeMap::new();
for (tag, entry) in entries {
let target_endianness = self.writer.endianness();
let cnt = entry.value_count;
let et = entry.entry_type;
let entry_type = IFDEntryType::from_u16(et).ok_or(Error::UnsupportedType)?;
let data = match entry_type {
IFDEntryType::Byte
| IFDEntryType::Sbyte
| IFDEntryType::Undefined
| IFDEntryType::Ascii => {
let num_bytes = usize::try_from(cnt)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
let mut data = safe_vec(cnt, Some(0u8))?;
if num_bytes <= 4 || self.big_tiff && num_bytes <= 8 {
let offset_bytes = entry.value_offset.to_ne_bytes();
for (i, item) in offset_bytes.iter().take(num_bytes).enumerate() {
data[i] = *item;
}
} else {
asset_reader.seek(SeekFrom::Start(decode_offset(
entry.value_offset,
target_endianness,
self.big_tiff,
)?))?;
asset_reader.read_exact(data.as_mut_slice())?;
}
data
}
IFDEntryType::Short => {
let num_shorts_x2 =
usize::try_from(cnt.checked_mul(2).ok_or_else(|| {
Error::InvalidAsset("value out of range".to_string())
})?)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
let mut data = safe_vec(num_shorts_x2 as u64, Some(0u8))?;
if num_shorts_x2 <= 4 || self.big_tiff && num_shorts_x2 <= 8 {
let offset_bytes = entry.value_offset.to_ne_bytes();
let mut offset_reader = Cursor::new(offset_bytes);
let mut w = Cursor::new(data.as_mut_slice());
for _i in 0..num_shorts_x2 / 2 {
let s = offset_reader.read_u16::<NativeEndian>()?; w.write_u16::<NativeEndian>(s)?; }
} else {
asset_reader.seek(SeekFrom::Start(decode_offset(
entry.value_offset,
target_endianness,
self.big_tiff,
)?))?;
asset_reader.read_exact(data.as_mut_slice())?;
}
data
}
IFDEntryType::Long | IFDEntryType::Ifd => {
let num_longs_x4 =
usize::try_from(cnt.checked_mul(4).ok_or_else(|| {
Error::InvalidAsset("value out of range".to_string())
})?)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
let mut data = safe_vec(num_longs_x4 as u64, Some(0u8))?;
if num_longs_x4 <= 4 || self.big_tiff && num_longs_x4 <= 8 {
let offset_bytes = entry.value_offset.to_ne_bytes();
let mut offset_reader = Cursor::new(offset_bytes);
let mut w = Cursor::new(data.as_mut_slice());
for _i in 0..num_longs_x4 / 4 {
let s = offset_reader.read_u32::<NativeEndian>()?; w.write_u32::<NativeEndian>(s)?; }
} else {
asset_reader.seek(SeekFrom::Start(decode_offset(
entry.value_offset,
target_endianness,
self.big_tiff,
)?))?;
asset_reader.read_exact(data.as_mut_slice())?;
}
data
}
IFDEntryType::Sshort => {
let num_sshorts_x2 =
usize::try_from(cnt.checked_mul(2).ok_or_else(|| {
Error::InvalidAsset("value out of range".to_string())
})?)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
let mut data = safe_vec(num_sshorts_x2 as u64, Some(0u8))?;
if num_sshorts_x2 <= 4 || self.big_tiff && num_sshorts_x2 <= 8 {
let offset_bytes = entry.value_offset.to_ne_bytes();
let mut offset_reader = Cursor::new(offset_bytes);
let mut w = Cursor::new(data.as_mut_slice());
for _i in 0..num_sshorts_x2 / 2 {
let s = offset_reader.read_i16::<NativeEndian>()?; w.write_i16::<NativeEndian>(s)?; }
} else {
asset_reader.seek(SeekFrom::Start(decode_offset(
entry.value_offset,
target_endianness,
self.big_tiff,
)?))?;
asset_reader.read_exact(data.as_mut_slice())?;
}
data
}
IFDEntryType::Slong => {
let num_slongs_x4 =
usize::try_from(cnt.checked_mul(4).ok_or_else(|| {
Error::InvalidAsset("value out of range".to_string())
})?)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
let mut data = safe_vec(num_slongs_x4 as u64, Some(0u8))?;
if num_slongs_x4 <= 4 || self.big_tiff && num_slongs_x4 <= 8 {
let offset_bytes = entry.value_offset.to_ne_bytes();
let mut offset_reader = Cursor::new(offset_bytes);
let mut w = Cursor::new(data.as_mut_slice());
for _i in 0..num_slongs_x4 / 4 {
let s = offset_reader.read_i32::<NativeEndian>()?; w.write_i32::<NativeEndian>(s)?; }
} else {
asset_reader.seek(SeekFrom::Start(decode_offset(
entry.value_offset,
target_endianness,
self.big_tiff,
)?))?;
asset_reader.read_exact(data.as_mut_slice())?;
}
data
}
IFDEntryType::Float => {
let num_floats_x4 =
usize::try_from(cnt.checked_mul(4).ok_or_else(|| {
Error::InvalidAsset("value out of range".to_string())
})?)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
let mut data = safe_vec(num_floats_x4 as u64, Some(0u8))?;
if num_floats_x4 <= 4 || self.big_tiff && num_floats_x4 <= 8 {
let offset_bytes = entry.value_offset.to_ne_bytes();
let mut offset_reader = Cursor::new(offset_bytes);
let mut w = Cursor::new(data.as_mut_slice());
for _i in 0..num_floats_x4 / 4 {
let s = offset_reader.read_f32::<NativeEndian>()?; w.write_f32::<NativeEndian>(s)?; }
} else {
asset_reader.seek(SeekFrom::Start(decode_offset(
entry.value_offset,
target_endianness,
self.big_tiff,
)?))?;
asset_reader.read_exact(data.as_mut_slice())?;
}
data
}
IFDEntryType::Rational
| IFDEntryType::Srational
| IFDEntryType::Slong8
| IFDEntryType::Double
| IFDEntryType::Long8
| IFDEntryType::Ifd8 => {
let num_bytes_8 = cnt
.checked_mul(8)
.ok_or_else(|| Error::InvalidAsset("value out of range".to_string()))?;
asset_reader.seek(SeekFrom::Start(decode_offset(
entry.value_offset,
target_endianness,
self.big_tiff,
)?))?;
asset_reader.read_to_vec(num_bytes_8)?
}
};
target_ifd.insert(
*tag,
IfdClonedEntry {
entry_tag: *tag,
entry_type: entry_type as u16,
value_count: cnt,
value_bytes: data,
},
);
}
Ok(target_ifd)
}
}
fn tiff_clone_with_tags<R: Read + Seek + ?Sized, W: Read + Write + Seek + ?Sized>(
asset_writer: &mut W,
asset_reader: &mut R,
tiff_tags: Vec<IfdClonedEntry>,
) -> Result<()> {
let (mut tiff_tree, page_tokens, endianness, big_tiff) = map_tiff(asset_reader)?;
if let Some(tt) = tiff_tags.iter().find(|t| t.entry_tag == C2PA_TAG) {
if tiff_tags.len() == 1 {
let status = fast_update(
asset_writer,
asset_reader,
&mut tiff_tree,
&page_tokens,
&tt.value_bytes,
big_tiff,
endianness,
)?;
if status {
return Ok(());
}
}
}
let mut bo = ByteOrdered::new(asset_writer, endianness);
let mut tc = TiffCloner::new(endianness, big_tiff, &mut bo)?;
for t in tiff_tags {
tc.add_target_tag(t);
}
tc.clone_tiff(&mut tiff_tree, &page_tokens, asset_reader)?;
Ok(())
}
fn add_required_tags_to_stream(
input_stream: &mut dyn CAIRead,
output_stream: &mut dyn CAIReadWrite,
) -> Result<()> {
let tiff_io = TiffIO {};
match tiff_io.read_cai(input_stream) {
Ok(_) => {
input_stream.rewind()?;
output_stream.rewind()?;
std::io::copy(input_stream, output_stream)?;
Ok(())
}
Err(Error::JumbfNotFound) => {
let some_bytes = vec![0u8; 10];
let tio = TiffIO {};
tio.write_cai(input_stream, output_stream, &some_bytes)
}
Err(e) => Err(e),
}
}
fn get_cai_data<R>(mut asset_reader: &mut R) -> Result<Vec<u8>>
where
R: Read + Seek + ?Sized,
{
let (tiff_tree, page_tokens, e, big_tiff) = map_tiff(asset_reader)?;
let first_page = page_tokens
.first()
.ok_or(Error::InvalidAsset("no IFD".to_string()))?;
let last_page = page_tokens
.last()
.ok_or(Error::InvalidAsset("no IFD".to_string()))?;
let last_ifd = &tiff_tree[*last_page].data;
let cai_ifd_entry = match last_ifd.get_tag(C2PA_TAG) {
Some(entry) => entry,
None => {
let first_ifd = &tiff_tree[*first_page].data;
first_ifd.get_tag(C2PA_TAG).ok_or(Error::JumbfNotFound)?
}
};
if cai_ifd_entry.entry_type != C2PA_FIELD_TYPE {
return Err(Error::InvalidAsset(
"Ifd entry for C2PA must be type UNDEFINED(7)".to_string(),
));
}
let decoded_offset = decode_offset(cai_ifd_entry.value_offset, e, big_tiff)?;
asset_reader.seek(SeekFrom::Start(decoded_offset))?;
let data = asset_reader
.read_to_vec(cai_ifd_entry.value_count)
.map_err(|_err| Error::InvalidAsset("TIFF/DNG out of range".to_string()))?;
Ok(data)
}
fn get_xmp_data<R>(mut asset_reader: &mut R) -> Option<Vec<u8>>
where
R: Read + Seek + ?Sized,
{
let (tiff_tree, page_tokens, e, big_tiff) = map_tiff(asset_reader).ok()?;
let first_page = page_tokens.first()?;
let first_ifd = &tiff_tree[*first_page].data;
let xmp_ifd_entry = first_ifd.get_tag(XMP_TAG)?;
if IFDEntryType::from_u16(xmp_ifd_entry.entry_type)? != IFDEntryType::Byte {
return None;
}
let decoded_offset = decode_offset(xmp_ifd_entry.value_offset, e, big_tiff).ok()?;
asset_reader.seek(SeekFrom::Start(decoded_offset)).ok()?;
asset_reader.read_to_vec(xmp_ifd_entry.value_count).ok()
}
fn fast_update<R: Read + Seek + ?Sized, W: Read + Write + Seek + ?Sized>(
asset_writer: &mut W,
asset_reader: &mut R,
tiff_tree: &mut Arena<ImageFileDirectory>,
tokens: &[Token],
data: &[u8],
big_tiff: bool,
endianness: Endianness,
) -> Result<bool> {
let mut bo = ByteOrdered::new(asset_writer, endianness);
let first_page = tokens
.first()
.ok_or(Error::InvalidAsset("no IFD found".to_string()))?;
let last_page = tokens
.last()
.ok_or(Error::InvalidAsset("no IFD found".to_string()))?;
let last_page_ifd = tiff_tree
.get(*last_page)
.ok_or_else(|| Error::InvalidAsset("TIFF does not have IFD".to_string()))?;
if last_page != first_page
&& last_page_ifd.data.entries.contains_key(&C2PA_TAG)
&& last_page_ifd.data.entries.len() == 1
{
let ifd_offset = last_page_ifd.data.offset;
let manifest_offset = &last_page_ifd.data.entries[&C2PA_TAG].value_offset;
let new_manifest_size = data.len() as u64;
let manifest_size = last_page_ifd.data.entries[&C2PA_TAG].value_count;
let asset_len = stream_len(asset_reader)?;
if (*manifest_offset + manifest_size) != asset_len {
return Ok(false);
}
bo.rewind()?;
asset_reader.rewind()?;
let mut before_manifest = asset_reader.take(*manifest_offset);
std::io::copy(&mut before_manifest, &mut bo)?;
bo.write_all(data)?;
let entry_cnt_size = if big_tiff { 8u64 } else { 2u64 };
let size_offset = ifd_offset
+ entry_cnt_size + 2 + 2;
bo.seek(SeekFrom::Start(size_offset))?;
if big_tiff {
bo.write_u64(new_manifest_size)?;
} else {
let new_manifest_size_u32 = u32::try_from(new_manifest_size)
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
bo.write_u32(new_manifest_size_u32)?;
}
return Ok(true);
}
Ok(false)
}
pub struct TiffIO {}
impl CAIReader for TiffIO {
fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>> {
let cai_data = get_cai_data(asset_reader)?;
Ok(cai_data)
}
fn read_xmp(&self, asset_reader: &mut dyn CAIRead) -> Option<String> {
let xmp_data = get_xmp_data(asset_reader)?;
String::from_utf8(xmp_data).ok()
}
}
impl AssetIO for TiffIO {
fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> {
Some(self)
}
fn read_cai_store(&self, asset_path: &std::path::Path) -> Result<Vec<u8>> {
let mut reader = std::fs::File::open(asset_path)?;
self.read_cai(&mut reader)
}
fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
let mut input_stream = std::fs::OpenOptions::new()
.read(true)
.open(asset_path)
.map_err(Error::IoError)?;
let mut temp_file = tempfile_builder("c2pa_temp")?;
self.write_cai(&mut input_stream, &mut temp_file, store_bytes)?;
rename_or_move(temp_file, asset_path)
}
fn get_object_locations(
&self,
asset_path: &std::path::Path,
) -> Result<Vec<crate::asset_io::HashObjectPositions>> {
let mut input_stream =
std::fs::File::open(asset_path).map_err(|_err| Error::EmbeddingError)?;
self.get_object_locations_from_stream(&mut input_stream)
}
fn remove_cai_store(&self, asset_path: &std::path::Path) -> Result<()> {
let mut input_file = std::fs::File::open(asset_path)?;
let mut temp_file = tempfile_builder("c2pa_temp")?;
self.remove_cai_store_from_stream(&mut input_file, &mut temp_file)?;
rename_or_move(temp_file, asset_path)
}
fn new(_asset_type: &str) -> Self
where
Self: Sized,
{
TiffIO {}
}
fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> {
Box::new(TiffIO::new(asset_type))
}
fn get_reader(&self) -> &dyn CAIReader {
self
}
fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> {
if SUPPORTED_WRITER_TYPES.contains(&asset_type) {
Some(Box::new(TiffIO::new(asset_type)))
} else {
None
}
}
fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> {
Some(self)
}
fn composed_data_ref(&self) -> Option<&dyn ComposedManifestRef> {
Some(self)
}
fn supported_types(&self) -> &[&str] {
&SUPPORTED_TYPES
}
}
impl CAIWriter for TiffIO {
fn write_cai(
&self,
input_stream: &mut dyn CAIRead,
output_stream: &mut dyn CAIReadWrite,
store_bytes: &[u8],
) -> Result<()> {
let l = u64::try_from(store_bytes.len())
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
let entry = IfdClonedEntry {
entry_tag: C2PA_TAG,
entry_type: C2PA_FIELD_TYPE,
value_count: l,
value_bytes: store_bytes.to_vec(),
};
tiff_clone_with_tags(output_stream, input_stream, vec![entry])
}
fn get_object_locations_from_stream(
&self,
input_stream: &mut dyn CAIRead,
) -> Result<Vec<HashObjectPositions>> {
let len = stream_len(input_stream)?;
let vec_cap = usize::try_from(len)
.map_err(|_err| Error::InvalidAsset("value out of range".to_owned()))?;
let output_buf: Vec<u8> = Vec::with_capacity(vec_cap + 100);
let mut output_stream = Cursor::new(output_buf);
add_required_tags_to_stream(input_stream, &mut output_stream)?;
output_stream.rewind()?;
let (ifds, page_tokens, e, big_tiff) = map_tiff(&mut output_stream)?;
let last_page = page_tokens
.last()
.ok_or(Error::InvalidAsset("no IFD found".to_string()))?;
let cai_ifd_entry = match ifds[*last_page].data.get_tag(C2PA_TAG) {
Some(ifd) => ifd,
None => {
return Ok(Vec::new());
}
};
if cai_ifd_entry.entry_type != C2PA_FIELD_TYPE {
return Err(Error::InvalidAsset(
"Ifd entry for C2PA must be type UNKNOWN(7)".to_string(),
));
}
let decoded_offset = decode_offset(cai_ifd_entry.value_offset, e, big_tiff)?;
let manifest_offset = usize::try_from(decoded_offset)
.map_err(|_err| Error::InvalidAsset("TIFF/DNG out of range".to_string()))?;
let manifest_len = usize::try_from(cai_ifd_entry.value_count)
.map_err(|_err| Error::InvalidAsset("TIFF/DNG out of range".to_string()))?;
let entry_cnt_size = if big_tiff { 8u64 } else { 2u64 };
let count_offset = ifds[*last_page].data.offset
+ entry_cnt_size + 2 + 2;
let count_size = if big_tiff { 8 } else { 4 };
Ok(vec![
HashObjectPositions {
offset: manifest_offset,
length: manifest_len,
htype: HashBlockObjectType::Cai,
},
HashObjectPositions {
offset: usize::try_from(count_offset)
.map_err(|_err| Error::InvalidAsset("TIFF/DNG out of range".to_string()))?,
length: count_size,
htype: HashBlockObjectType::OtherExclusion,
},
])
}
fn remove_cai_store_from_stream(
&self,
input_stream: &mut dyn CAIRead,
output_stream: &mut dyn CAIReadWrite,
) -> Result<()> {
let (mut ifds, page_tokens, e, big_tiff) = map_tiff(input_stream)?;
let last_page = page_tokens
.last()
.ok_or(Error::InvalidAsset("no IFD found".to_string()))?;
if ifds[*last_page].data.entries.remove(&C2PA_TAG).is_some() {
let mut bo = ByteOrdered::new(output_stream, e);
let mut tc = TiffCloner::new(e, big_tiff, &mut bo)?;
tc.clone_tiff(&mut ifds, &page_tokens, input_stream)?;
} else {
input_stream.rewind()?;
std::io::copy(input_stream, output_stream)?;
}
Ok(())
}
}
impl AssetPatch for TiffIO {
fn patch_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
let mut asset_io = OpenOptions::new()
.write(true)
.read(true)
.create(false)
.open(asset_path)?;
let (tiff_tree, page_tokens, e, big_tiff) = map_tiff(&mut asset_io)?;
let last_page = page_tokens
.last()
.ok_or(Error::InvalidAsset("no IFD".to_string()))?;
let last_ifd = &tiff_tree[*last_page].data;
let cai_ifd_entry = last_ifd.get_tag(C2PA_TAG).ok_or(Error::JumbfNotFound)?;
if cai_ifd_entry.entry_type != C2PA_FIELD_TYPE {
return Err(Error::InvalidAsset(
"Ifd entry for C2PA must be type UNKNOWN(7)".to_string(),
));
}
let manifest_len: usize = usize::try_from(cai_ifd_entry.value_count)
.map_err(|_err| Error::InvalidAsset("TIFF/DNG out of range".to_string()))?;
if store_bytes.len() == manifest_len {
let decoded_offset = decode_offset(cai_ifd_entry.value_offset, e, big_tiff)?;
asset_io.seek(SeekFrom::Start(decoded_offset))?;
asset_io.write_all(store_bytes)?;
Ok(())
} else {
Err(Error::InvalidAsset(
"patch_cai_store store size mismatch.".to_string(),
))
}
}
}
impl RemoteRefEmbed for TiffIO {
#[allow(unused_variables)]
fn embed_reference(
&self,
asset_path: &Path,
embed_ref: crate::asset_io::RemoteRefEmbedType,
) -> Result<()> {
match embed_ref {
crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => {
let output_buf = Vec::new();
let mut output_stream = Cursor::new(output_buf);
{
let mut source_stream = std::fs::File::open(asset_path)?;
self.embed_reference_to_stream(
&mut source_stream,
&mut output_stream,
RemoteRefEmbedType::Xmp(manifest_uri),
)?;
}
output_stream.rewind()?;
std::fs::write(asset_path, output_stream.into_inner())?;
Ok(())
}
crate::asset_io::RemoteRefEmbedType::StegoS(_) => Err(Error::UnsupportedType),
crate::asset_io::RemoteRefEmbedType::StegoB(_) => Err(Error::UnsupportedType),
crate::asset_io::RemoteRefEmbedType::Watermark(_) => Err(Error::UnsupportedType),
}
}
fn embed_reference_to_stream(
&self,
source_stream: &mut dyn CAIRead,
output_stream: &mut dyn CAIReadWrite,
embed_ref: RemoteRefEmbedType,
) -> Result<()> {
match embed_ref {
crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => {
let xmp = match self.get_reader().read_xmp(source_stream) {
Some(xmp) => add_provenance(&xmp, &manifest_uri)?,
None => {
let xmp = MIN_XMP.to_string();
add_provenance(&xmp, &manifest_uri)?
}
};
let l = u64::try_from(xmp.len())
.map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
let entry = IfdClonedEntry {
entry_tag: XMP_TAG,
entry_type: IFDEntryType::Byte as u16,
value_count: l,
value_bytes: xmp.as_bytes().to_vec(),
};
tiff_clone_with_tags(output_stream, source_stream, vec![entry])
}
crate::asset_io::RemoteRefEmbedType::StegoS(_) => Err(Error::UnsupportedType),
crate::asset_io::RemoteRefEmbedType::StegoB(_) => Err(Error::UnsupportedType),
crate::asset_io::RemoteRefEmbedType::Watermark(_) => Err(Error::UnsupportedType),
}
}
}
impl ComposedManifestRef for TiffIO {
fn compose_manifest(&self, manifest_data: &[u8], _format: &str) -> Result<Vec<u8>> {
Ok(manifest_data.to_vec())
}
}
#[derive(Debug, thiserror::Error)]
pub enum TiffError {
#[error("invalid file signature: {reason}")]
InvalidFileSignature { reason: String },
}
#[cfg(test)]
pub mod tests {
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
use core::panic;
use super::*;
use crate::utils::{io_utils::tempdirectory, test::temp_dir_path};
#[test]
fn test_multipage_read_write_manifest() {
let data = "some data";
let data2 = "some different data";
let source = crate::utils::test::fixture_path("MultiPage.tif");
let temp_dir = tempdirectory().unwrap();
let output = temp_dir_path(&temp_dir, "test.tif");
std::fs::copy(source, &output).unwrap();
let tiff_io = TiffIO {};
tiff_io.save_cai_store(&output, data.as_bytes()).unwrap();
let loaded = tiff_io.read_cai_store(&output).unwrap();
assert_eq!(&loaded, data.as_bytes());
tiff_io.save_cai_store(&output, data2.as_bytes()).unwrap();
let loaded = tiff_io.read_cai_store(&output).unwrap();
assert_eq!(&loaded, data2.as_bytes());
}
#[test]
fn test_read_write_manifest() {
let data = "some data";
let source = crate::utils::test::fixture_path("TUSCANY.TIF");
let temp_dir = tempdirectory().unwrap();
let output = temp_dir_path(&temp_dir, "test.tif");
std::fs::copy(source, &output).unwrap();
let tiff_io = TiffIO {};
tiff_io.save_cai_store(&output, data.as_bytes()).unwrap();
let loaded = tiff_io.read_cai_store(&output).unwrap();
assert_eq!(&loaded, data.as_bytes());
}
#[test]
fn test_read_write_manifest_dng() {
let data = "some data";
let source = crate::utils::test::fixture_path("subfiles.dng");
let temp_dir = tempdirectory().unwrap();
let output = temp_dir_path(&temp_dir, "test.dng");
std::fs::copy(source, &output).unwrap();
let tiff_io = TiffIO {};
tiff_io.save_cai_store(&output, data.as_bytes()).unwrap();
let loaded = tiff_io.read_cai_store(&output).unwrap();
assert_eq!(&loaded, data.as_bytes());
}
#[test]
fn test_write_xmp() {
let data = "some data";
let source = crate::utils::test::fixture_path("TUSCANY.TIF");
let temp_dir = tempdirectory().unwrap();
let output = temp_dir_path(&temp_dir, "test.tif");
std::fs::copy(source, &output).unwrap();
let tiff_io = TiffIO {};
let eh = tiff_io.remote_ref_writer_ref().unwrap();
eh.embed_reference(&output, RemoteRefEmbedType::Xmp(data.to_string()))
.unwrap();
let mut output_stream = std::fs::File::open(&output).unwrap();
let xmp = tiff_io.read_xmp(&mut output_stream).unwrap();
let loaded = crate::utils::xmp_inmemory_utils::extract_provenance(&xmp).unwrap();
assert_eq!(&loaded, data);
}
#[test]
fn test_remove_manifest() {
let data = "some data";
let source = crate::utils::test::fixture_path("TUSCANY.TIF");
let temp_dir = tempdirectory().unwrap();
let output = temp_dir_path(&temp_dir, "test.tif");
std::fs::copy(source, &output).unwrap();
let tiff_io = TiffIO {};
tiff_io.remove_cai_store(&output).unwrap();
tiff_io.save_cai_store(&output, data.as_bytes()).unwrap();
let loaded = tiff_io.read_cai_store(&output).unwrap();
assert_eq!(&loaded, data.as_bytes());
tiff_io.remove_cai_store(&output).unwrap();
match tiff_io.read_cai_store(&output) {
Err(Error::JumbfNotFound) => (),
_ => panic!("should be no C2PA store"),
}
}
#[test]
fn test_get_object_location() {
let data = "some data";
let source = crate::utils::test::fixture_path("TUSCANY.TIF");
let temp_dir = tempdirectory().unwrap();
let output = temp_dir_path(&temp_dir, "test.tif");
std::fs::copy(source, &output).unwrap();
let tiff_io = TiffIO {};
tiff_io.save_cai_store(&output, data.as_bytes()).unwrap();
let loaded = tiff_io.read_cai_store(&output).unwrap();
assert_eq!(&loaded, data.as_bytes());
let mut success = false;
if let Ok(locations) = tiff_io.get_object_locations(&output) {
for op in locations {
if op.htype == HashBlockObjectType::Cai {
let mut of = std::fs::File::open(&output).unwrap();
let mut manifests_buf: Vec<u8> = vec![0u8; op.length];
of.seek(SeekFrom::Start(op.offset as u64)).unwrap();
of.read_exact(manifests_buf.as_mut_slice()).unwrap();
if crate::hash_utils::vec_compare(&manifests_buf, data.as_bytes()) {
success = true;
}
}
}
}
assert!(success);
}
#[test]
fn test_overflow_clone_ifd_entries() {
let data = [
0x49, 0x49, 0x2b, 0x00, 0x08, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49,
0x49, 0x2a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xf9, 0x00,
0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
];
let mut stream = Cursor::new(&data);
let tiff_io = TiffIO {};
let locations = tiff_io.get_object_locations_from_stream(&mut stream);
assert!(matches!(locations, Err(Error::InvalidAsset(_))));
}
#[test]
fn test_overflow_clone_ifd_entries_rational_type() {
let data = [
0x49, 0x49, 0x2b, 0x00, 0x08, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49,
0x49, 0x2a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xf9, 0x00,
0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
];
let mut stream = Cursor::new(&data);
let tiff_io = TiffIO {};
let locations = tiff_io.get_object_locations_from_stream(&mut stream);
assert!(matches!(locations, Err(Error::InvalidAsset(_))));
}
}