c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

commit 7321899891c05d4a239b8598ea83b1c653612c90
parent 3de41524ce32bb45f1dd100f8ddc6bb009aa10e2
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Thu, 28 Mar 2024 20:26:10 -0400

Streaming write support for BMFF (#435)

* Streaming write support for BMFF

* remove obsolete unit test

* Added XMP support to BMFF

* fix some comment text

* another comment fix

* Fix for broken remove when no manifest exists
Diffstat:
Msdk/src/asset_handlers/bmff_io.rs | 547++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msdk/src/jumbf_io.rs | 14--------------
2 files changed, 388 insertions(+), 173 deletions(-)

diff --git a/sdk/src/asset_handlers/bmff_io.rs b/sdk/src/asset_handlers/bmff_io.rs @@ -15,7 +15,7 @@ use std::{ cmp::min, collections::HashMap, fs::{File, OpenOptions}, - io::{Read, Seek, SeekFrom, Write}, + io::{Cursor, Read, Seek, SeekFrom, Write}, path::Path, }; @@ -27,11 +27,14 @@ use tempfile::Builder; use crate::{ assertions::{BmffMerkleMap, ExclusionsMap}, asset_io::{ - rename_or_copy, AssetIO, AssetPatch, CAIRead, CAIReadWrite, CAIReader, HashObjectPositions, - RemoteRefEmbed, RemoteRefEmbedType, + rename_or_copy, AssetIO, AssetPatch, CAIRead, CAIReadWrite, CAIReader, CAIWriter, + HashObjectPositions, RemoteRefEmbed, RemoteRefEmbedType, }, error::{Error, Result}, - utils::hash_utils::{vec_compare, HashRange}, + utils::{ + hash_utils::{vec_compare, HashRange}, + xmp_inmemory_utils::{add_provenance, MIN_XMP}, + }, }; pub struct BmffIO { @@ -45,6 +48,9 @@ const HEADER_SIZE_LARGE: u64 = 16; // 4 byte type + 4 byte size + 8 byte large s const C2PA_UUID: [u8; 16] = [ 0xd8, 0xfe, 0xc3, 0xd6, 0x1b, 0x0e, 0x48, 0x3c, 0x92, 0x97, 0x58, 0x28, 0x87, 0x7e, 0xc4, 0x81, ]; +const XMP_UUID: [u8; 16] = [ + 0xbe, 0x7a, 0xcf, 0xcb, 0x97, 0xa9, 0x42, 0xe8, 0x9c, 0x71, 0x99, 0x94, 0x91, 0xe3, 0xaf, 0xac, +]; const MANIFEST: &str = "manifest"; const MERKLE: &str = "merkle"; @@ -251,7 +257,7 @@ pub(crate) struct BoxInfoLite { pub size: u64, } -fn read_box_header_ext(reader: &mut dyn CAIRead) -> Result<(u8, u32)> { +fn read_box_header_ext<R: Read + Seek + ?Sized>(reader: &mut R) -> Result<(u8, u32)> { let version = reader.read_u8()?; let flags = reader.read_u24::<BigEndian>()?; Ok((version, flags)) @@ -262,7 +268,7 @@ fn write_box_header_ext<W: Write>(w: &mut W, v: u8, f: u32) -> Result<u64> { Ok(4) } -fn box_start(reader: &mut dyn CAIRead, is_large: bool) -> Result<u64> { +fn box_start<R: Read + Seek + ?Sized>(reader: &mut R, is_large: bool) -> Result<u64> { if is_large { Ok(reader.stream_position()? - HEADER_SIZE_LARGE) } else { @@ -270,12 +276,12 @@ fn box_start(reader: &mut dyn CAIRead, is_large: bool) -> Result<u64> { } } -fn _skip_bytes(reader: &mut dyn CAIRead, size: u64) -> Result<()> { +fn _skip_bytes<R: Read + Seek + ?Sized>(reader: &mut R, size: u64) -> Result<()> { reader.seek(SeekFrom::Current(size as i64))?; Ok(()) } -fn skip_bytes_to(reader: &mut dyn CAIRead, pos: u64) -> Result<u64> { +fn skip_bytes_to<R: Read + Seek + ?Sized>(reader: &mut R, pos: u64) -> Result<u64> { let pos = reader.seek(SeekFrom::Start(pos))?; Ok(pos) } @@ -327,6 +333,22 @@ fn write_c2pa_box<W: Write>( Ok(()) } +fn write_xmp_box<W: Write>(w: &mut W, data: &[u8]) -> Result<()> { + let size = 8 + 16 + 4 + data.len(); // header + UUID + data + let bh = BoxHeaderLite::new(BoxType::UuidBox, size as u64, "uuid"); + + // write out header + bh.write(w)?; + + // write out XMP extension UUID + write_box_uuid_extension(w, &XMP_UUID)?; + + // write out data + w.write_all(data)?; + + Ok(()) +} + fn _write_free_box<W: Write>(w: &mut W, size: usize) -> Result<()> { if size < 8 { return Err(Error::BadParam("cannot adjust free space".to_string())); @@ -573,7 +595,7 @@ pub fn bmff_to_jumbf_exclusions( // `iloc`, `stco` and `co64` elements contain absolute file offsets so they need to be adjusted based on whether content was added or removed. // todo: when fragment support is added adjust these (/moof/iloc, /moof/mfro, /moof/traf/saio, /sidx) -fn adjust_known_offsets<W: Write + CAIRead>( +fn adjust_known_offsets<W: Write + CAIRead + ?Sized>( output: &mut W, bmff_tree: &Arena<BoxInfo>, bmff_path_map: &HashMap<String, Vec<Token>>, @@ -907,8 +929,8 @@ fn adjust_known_offsets<W: Write + CAIRead>( Ok(()) } -pub(crate) fn build_bmff_tree( - reader: &mut dyn CAIRead, +pub(crate) fn build_bmff_tree<R: Read + Seek + ?Sized>( + reader: &mut R, end: u64, bmff_tree: &mut Arena<BoxInfo>, current_node: &Token, @@ -1063,9 +1085,10 @@ pub(crate) fn build_bmff_tree( Ok(()) } -fn get_manifest_token( +fn get_uuid_token( bmff_tree: &Arena<BoxInfo>, bmff_map: &HashMap<String, Vec<Token>>, + uuid: &[u8; 16], ) -> Option<Token> { if let Some(uuid_list) = bmff_map.get("/uuid") { for uuid_token in uuid_list { @@ -1073,9 +1096,9 @@ fn get_manifest_token( // make sure it is UUID box if box_info.data.box_type == BoxType::UuidBox { - if let Some(uuid) = &box_info.data.user_type { - // make sure it is a C2PA ContentProvenanceBox box - if vec_compare(&C2PA_UUID, uuid) { + if let Some(found_uuid) = &box_info.data.user_type { + // make sure uuids match + if vec_compare(uuid, found_uuid) { return Some(*uuid_token); } } @@ -1089,6 +1112,7 @@ pub(crate) struct C2PABmffBoxes { pub manifest_bytes: Option<Vec<u8>>, pub bmff_merkle: Vec<BmffMerkleMap>, pub box_infos: Vec<BoxInfoLite>, + pub xmp: Option<String>, } pub(crate) fn read_bmff_c2pa_boxes(reader: &mut dyn CAIRead) -> Result<C2PABmffBoxes> { @@ -1114,6 +1138,7 @@ pub(crate) fn read_bmff_c2pa_boxes(reader: &mut dyn CAIRead) -> Result<C2PABmffB build_bmff_tree(reader, size, &mut bmff_tree, &root_token, &mut bmff_map)?; let mut output: Option<Vec<u8>> = None; + let mut xmp: Option<String> = None; let mut _first_aux_uuid = 0; let mut merkle_boxes: Vec<BmffMerkleMap> = Vec::new(); @@ -1195,6 +1220,18 @@ pub(crate) fn read_bmff_c2pa_boxes(reader: &mut dyn CAIRead) -> Result<C2PABmffB let mm: BmffMerkleMap = serde_cbor::from_slice(&merkle)?; merkle_boxes.push(mm); } + } else if vec_compare(&XMP_UUID, uuid) { + let data_len = box_info.data.size - HEADER_SIZE - 16 /*UUID*/; + + // set reader to start of box contents + skip_bytes_to(reader, box_info.data.offset + HEADER_SIZE + 16)?; + + let mut xmp_vec = vec![0u8; data_len as usize]; + reader.read_exact(&mut xmp_vec)?; + + if let Ok(xmp_string) = String::from_utf8(xmp_vec) { + xmp = Some(xmp_string); + } } } } @@ -1209,6 +1246,7 @@ pub(crate) fn read_bmff_c2pa_boxes(reader: &mut dyn CAIRead) -> Result<C2PABmffB manifest_bytes: output, bmff_merkle: merkle_boxes, box_infos, + xmp, }) } @@ -1220,8 +1258,10 @@ impl CAIReader for BmffIO { } // Get XMP block - fn read_xmp(&self, _asset_reader: &mut dyn CAIRead) -> Option<String> { - None // todo: figure out where XMP is stored for supported formats + fn read_xmp(&self, reader: &mut dyn CAIRead) -> Option<String> { + let c2pa_boxes = read_bmff_c2pa_boxes(reader).ok()?; + + c2pa_boxes.xmp } } @@ -1236,9 +1276,83 @@ impl AssetIO for BmffIO { } fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> { - let mut input = File::open(asset_path)?; - let size = input.seek(SeekFrom::End(0))?; - input.rewind()?; + let mut input_stream = std::fs::OpenOptions::new() + .read(true) + .open(asset_path) + .map_err(Error::IoError)?; + + let mut temp_file = Builder::new() + .prefix("c2pa_temp") + .rand_bytes(5) + .tempfile()?; + + self.write_cai(&mut input_stream, &mut temp_file, store_bytes)?; + + // copy temp file to asset + rename_or_copy(temp_file, asset_path) + } + + fn get_object_locations( + &self, + _asset_path: &std::path::Path, + ) -> Result<Vec<HashObjectPositions>> { + let vec: Vec<HashObjectPositions> = Vec::new(); + Ok(vec) + } + + fn remove_cai_store(&self, asset_path: &Path) -> Result<()> { + let mut input_file = std::fs::File::open(asset_path)?; + + let mut temp_file = Builder::new() + .prefix("c2pa_temp") + .rand_bytes(5) + .tempfile()?; + + self.remove_cai_store_from_stream(&mut input_file, &mut temp_file)?; + + // copy temp file to asset + rename_or_copy(temp_file, asset_path) + } + + fn new(asset_type: &str) -> Self + where + Self: Sized, + { + BmffIO { + bmff_format: asset_type.to_string(), + } + } + + fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> { + Box::new(BmffIO::new(asset_type)) + } + + fn get_reader(&self) -> &dyn CAIReader { + self + } + + fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> { + Some(Box::new(BmffIO::new(asset_type))) + } + + fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { + Some(self) + } + + fn supported_types(&self) -> &[&str] { + &SUPPORTED_TYPES + } +} + +impl CAIWriter for BmffIO { + fn write_cai( + &self, + input_stream: &mut dyn CAIRead, + output_stream: &mut dyn CAIReadWrite, + store_bytes: &[u8], + ) -> Result<()> { + let size = input_stream.seek(SeekFrom::End(0))?; + input_stream.rewind()?; // create root node let root_box = BoxInfo { @@ -1256,7 +1370,13 @@ impl AssetIO for BmffIO { let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new(); // build layout of the BMFF structure - build_bmff_tree(&mut input, size, &mut bmff_tree, &root_token, &mut bmff_map)?; + build_bmff_tree( + input_stream, + size, + &mut bmff_tree, + &root_token, + &mut bmff_map, + )?; // get ftyp location // start after ftyp @@ -1267,7 +1387,7 @@ impl AssetIO for BmffIO { // get position to insert c2pa let (c2pa_start, c2pa_length) = - if let Some(c2pa_token) = get_manifest_token(&bmff_tree, &bmff_map) { + if let Some(c2pa_token) = get_uuid_token(&bmff_tree, &bmff_map, &C2PA_UUID) { let uuid_info = &bmff_tree[c2pa_token].data; (uuid_info.offset, Some(uuid_info.size)) @@ -1280,11 +1400,6 @@ impl AssetIO for BmffIO { write_c2pa_box(&mut new_c2pa_box, store_bytes, true, merkle_data)?; let new_c2pa_box_size = new_c2pa_box.len(); - let mut temp_file = Builder::new() - .prefix("c2pa_temp") - .rand_bytes(5) - .tempfile()?; - let (start, end) = if let Some(c2pa_length) = c2pa_length { let start = usize::value_from(c2pa_start) .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label @@ -1302,13 +1417,12 @@ impl AssetIO for BmffIO { }; // write content before ContentProvenanceBox - input.rewind()?; - let mut b = vec![0u8; start]; - input.read_exact(&mut b)?; - temp_file.write_all(&b)?; + input_stream.rewind()?; + let mut before_manifest = input_stream.take(start as u64); + std::io::copy(&mut before_manifest, output_stream)?; // write ContentProvenanceBox - temp_file.write_all(&new_c2pa_box)?; + output_stream.write_all(&new_c2pa_box)?; // calc offset adjustments let offset_adjust: i32 = if end == 0 { @@ -1321,18 +1435,8 @@ impl AssetIO for BmffIO { }; // write content after ContentProvenanceBox - input.seek(SeekFrom::Start(end as u64))?; - let mut chunk = vec![0u8; 1024 * 1024]; - loop { - let len = match input.read(&mut chunk) { - Ok(0) => break, - Ok(len) => len, - Err(e) => return Err(Error::IoError(e)), - }; - - temp_file.write_all(&chunk[0..len])?; - } - temp_file.flush()?; + input_stream.seek(SeekFrom::Start(end as u64))?; + std::io::copy(input_stream, output_stream)?; // Manipulating the UUID box means we may need some patch offsets if they are file absolute offsets. @@ -1352,10 +1456,10 @@ impl AssetIO for BmffIO { let (mut output_bmff_tree, root_token) = Arena::with_data(root_box); let mut output_bmff_map: HashMap<String, Vec<Token>> = HashMap::new(); - let size = temp_file.seek(SeekFrom::End(0))?; - temp_file.rewind()?; + let size = output_stream.seek(SeekFrom::End(0))?; + output_stream.rewind()?; build_bmff_tree( - &mut temp_file, + output_stream, size, &mut output_bmff_tree, &root_token, @@ -1363,29 +1467,30 @@ impl AssetIO for BmffIO { )?; // adjust offsets based on current layout + output_stream.rewind()?; adjust_known_offsets( - &mut temp_file, + output_stream, &output_bmff_tree, &output_bmff_map, offset_adjust, - )?; - - // copy temp file to asset - rename_or_copy(temp_file, asset_path) + ) } - fn get_object_locations( + fn get_object_locations_from_stream( &self, - _asset_path: &std::path::Path, + _input_stream: &mut dyn CAIRead, ) -> Result<Vec<HashObjectPositions>> { let vec: Vec<HashObjectPositions> = Vec::new(); Ok(vec) } - fn remove_cai_store(&self, asset_path: &Path) -> Result<()> { - let mut input = File::open(asset_path)?; - let size = input.seek(SeekFrom::End(0))?; - input.rewind()?; + fn remove_cai_store_from_stream( + &self, + input_stream: &mut dyn CAIRead, + output_stream: &mut dyn CAIReadWrite, + ) -> Result<()> { + let size = input_stream.seek(SeekFrom::End(0))?; + input_stream.rewind()?; // create root node let root_box = BoxInfo { @@ -1403,23 +1508,26 @@ impl AssetIO for BmffIO { let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new(); // build layout of the BMFF structure - build_bmff_tree(&mut input, size, &mut bmff_tree, &root_token, &mut bmff_map)?; + build_bmff_tree( + input_stream, + size, + &mut bmff_tree, + &root_token, + &mut bmff_map, + )?; // get position of c2pa manifest let (c2pa_start, c2pa_length) = - if let Some(c2pa_token) = get_manifest_token(&bmff_tree, &bmff_map) { + if let Some(c2pa_token) = get_uuid_token(&bmff_tree, &bmff_map, &C2PA_UUID) { let uuid_info = &bmff_tree[c2pa_token].data; (uuid_info.offset, Some(uuid_info.size)) } else { - return Ok(()); // no box to remove + input_stream.rewind()?; + std::io::copy(input_stream, output_stream)?; + return Ok(()); // no box to remove, propagate source to output }; - let mut temp_file = Builder::new() - .prefix("c2pa_temp") - .rand_bytes(5) - .tempfile()?; - let (start, end) = if let Some(c2pa_length) = c2pa_length { let start = usize::value_from(c2pa_start) .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label @@ -1433,10 +1541,9 @@ impl AssetIO for BmffIO { }; // write content before ContentProvenanceBox - input.rewind()?; - let mut b = vec![0u8; start]; - input.read_exact(&mut b)?; - temp_file.write_all(&b)?; + input_stream.rewind()?; + let mut before_manifest = input_stream.take(start as u64); + std::io::copy(&mut before_manifest, output_stream)?; // calc offset adjustments // value will be negative since the box is truncated @@ -1445,86 +1552,45 @@ impl AssetIO for BmffIO { let offset_adjust = new_c2pa_box_size - existing_c2pa_box_size as i32; // write content after ContentProvenanceBox - input.seek(SeekFrom::Start(end as u64))?; - let mut chunk = vec![0u8; 1024 * 1024]; - loop { - let len = match input.read(&mut chunk) { - Ok(0) => break, - Ok(len) => len, - Err(e) => return Err(Error::IoError(e)), - }; - - temp_file.write_all(&chunk[0..len])?; - } - temp_file.flush()?; + input_stream.seek(SeekFrom::Start(end as u64))?; + std::io::copy(input_stream, output_stream)?; // Manipulating the UUID box means we may need some patch offsets if they are file absolute offsets. - match self.bmff_format.as_ref() { - "m4a" | "mp4" | "mov" => { - // create root node - let root_box = BoxInfo { - path: "".to_string(), - offset: 0, - size, - box_type: BoxType::Empty, - parent: None, - user_type: None, - version: None, - flags: None, - }; - - // rebuild box layout for output file - let (mut output_bmff_tree, root_token) = Arena::with_data(root_box); - let mut output_bmff_map: HashMap<String, Vec<Token>> = HashMap::new(); - - let size = temp_file.seek(SeekFrom::End(0))?; - temp_file.rewind()?; - build_bmff_tree( - &mut temp_file, - size, - &mut output_bmff_tree, - &root_token, - &mut output_bmff_map, - )?; - // adjust based on current layout - adjust_known_offsets( - &mut temp_file, - &output_bmff_tree, - &output_bmff_map, - offset_adjust, - )?; - } - _ => (), // todo: handle more patching cases as necessary - } - - // copy temp file to asset - rename_or_copy(temp_file, asset_path) - } - - fn new(asset_type: &str) -> Self - where - Self: Sized, - { - BmffIO { - bmff_format: asset_type.to_string(), - } - } - - fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> { - Box::new(BmffIO::new(asset_type)) - } + // create root node + let root_box = BoxInfo { + path: "".to_string(), + offset: 0, + size, + box_type: BoxType::Empty, + parent: None, + user_type: None, + version: None, + flags: None, + }; - fn get_reader(&self) -> &dyn CAIReader { - self - } + // map box layout of current output file + let (mut output_bmff_tree, root_token) = Arena::with_data(root_box); + let mut output_bmff_map: HashMap<String, Vec<Token>> = HashMap::new(); - fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { - Some(self) - } + let size = output_stream.seek(SeekFrom::End(0))?; + output_stream.rewind()?; + build_bmff_tree( + output_stream, + size, + &mut output_bmff_tree, + &root_token, + &mut output_bmff_map, + )?; - fn supported_types(&self) -> &[&str] { - &SUPPORTED_TYPES + // adjust offsets based on current layout + output_stream.rewind()?; + adjust_known_offsets( + output_stream, + &output_bmff_tree, + &output_bmff_map, + offset_adjust, + ) } } @@ -1611,20 +1677,22 @@ impl RemoteRefEmbed for BmffIO { ) -> Result<()> { match embed_ref { crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => { - #[cfg(feature = "xmp_write")] - { - match self.bmff_format.as_ref() { - "heic" | "avif" => Err(Error::XmpNotSupported), - _ => { - crate::embedded_xmp::add_manifest_uri_to_file(asset_path, &manifest_uri) - } - } - } + let output_buf = Vec::new(); + let mut output_stream = Cursor::new(output_buf); - #[cfg(not(feature = "xmp_write"))] + // block so that source file is closed after embed { - Err(crate::error::Error::MissingFeature("xmp_write".to_string())) + 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), + )?; } + + // write will replace exisiting contents + 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), @@ -1634,11 +1702,147 @@ impl RemoteRefEmbed for BmffIO { fn embed_reference_to_stream( &self, - _source_stream: &mut dyn CAIRead, - _output_stream: &mut dyn CAIReadWrite, - _embed_ref: RemoteRefEmbedType, + input_stream: &mut dyn CAIRead, + output_stream: &mut dyn CAIReadWrite, + embed_ref: RemoteRefEmbedType, ) -> Result<()> { - Err(Error::UnsupportedType) + match embed_ref { + crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => { + let xmp = match self.get_reader().read_xmp(input_stream) { + Some(xmp) => add_provenance(&xmp, &manifest_uri)?, + None => { + let xmp = format!("http://ns.adobe.com/xap/1.0/\0 {}", MIN_XMP); + add_provenance(&xmp, &manifest_uri)? + } + }; + + let size = input_stream.seek(SeekFrom::End(0))?; + input_stream.rewind()?; + + // create root node + let root_box = BoxInfo { + path: "".to_string(), + offset: 0, + size, + box_type: BoxType::Empty, + parent: None, + user_type: None, + version: None, + flags: None, + }; + + let (mut bmff_tree, root_token) = Arena::with_data(root_box); + let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new(); + + // build layout of the BMFF structure + build_bmff_tree( + input_stream, + size, + &mut bmff_tree, + &root_token, + &mut bmff_map, + )?; + + // get ftyp location + // start after ftyp + let ftyp_token = bmff_map.get("/ftyp").ok_or(Error::UnsupportedType)?; // todo check ftyps to make sure we support any special format requirements + let ftyp_info = &bmff_tree[ftyp_token[0]].data; + let ftyp_offset = ftyp_info.offset; + let ftyp_size = ftyp_info.size; + + // get position to insert xmp + let (xmp_start, xmp_length) = + if let Some(c2pa_token) = get_uuid_token(&bmff_tree, &bmff_map, &XMP_UUID) { + let uuid_info = &bmff_tree[c2pa_token].data; + + (uuid_info.offset, Some(uuid_info.size)) + } else { + ((ftyp_offset + ftyp_size), None) + }; + + let mut new_xmp_box: Vec<u8> = Vec::with_capacity(xmp.len() * 2); + write_xmp_box(&mut new_xmp_box, xmp.as_bytes())?; + let new_xmp_box_size = new_xmp_box.len(); + + let (start, end) = if let Some(xmp_length) = xmp_length { + let start = usize::value_from(xmp_start) + .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label + + let end = usize::value_from(xmp_start + xmp_length) + .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; + + (start, end) + } else { + // insert new c2pa + let end = usize::value_from(xmp_start) + .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; + + (end, end) + }; + + // write content before XMP box + input_stream.rewind()?; + let mut before_manifest = input_stream.take(start as u64); + std::io::copy(&mut before_manifest, output_stream)?; + + // write ContentProvenanceBox + output_stream.write_all(&new_xmp_box)?; + + // calc offset adjustments + let offset_adjust: i32 = if end == 0 { + new_xmp_box_size as i32 + } else { + // value could be negative if box is truncated + let existing_xmp_box_size = end - start; + let pad_size: i32 = new_xmp_box_size as i32 - existing_xmp_box_size as i32; + pad_size + }; + + // write content after XMP box + input_stream.seek(SeekFrom::Start(end as u64))?; + std::io::copy(input_stream, output_stream)?; + + // Manipulating the UUID box means we may need some patch offsets if they are file absolute offsets. + + // create root node + let root_box = BoxInfo { + path: "".to_string(), + offset: 0, + size, + box_type: BoxType::Empty, + parent: None, + user_type: None, + version: None, + flags: None, + }; + + // map box layout of current output file + let (mut output_bmff_tree, root_token) = Arena::with_data(root_box); + let mut output_bmff_map: HashMap<String, Vec<Token>> = HashMap::new(); + + let size = output_stream.seek(SeekFrom::End(0))?; + output_stream.rewind()?; + build_bmff_tree( + output_stream, + size, + &mut output_bmff_tree, + &root_token, + &mut output_bmff_map, + )?; + + // adjust offsets based on current layout + output_stream.rewind()?; + adjust_known_offsets( + output_stream, + &output_bmff_tree, + &output_bmff_map, + offset_adjust, + ) + } + crate::asset_io::RemoteRefEmbedType::StegoS(_) => Err(Error::UnsupportedType), + crate::asset_io::RemoteRefEmbedType::StegoB(_) => Err(Error::UnsupportedType), + crate::asset_io::RemoteRefEmbedType::Watermark(_) => Err(Error::UnsupportedType), + } } } #[cfg(test)] @@ -1675,6 +1879,31 @@ pub mod tests { } #[test] + fn test_xmp_write() { + let data = "some test data"; + let source = fixture_path("video1.mp4"); + + let temp_dir = tempdir().unwrap(); + let output = temp_dir_path(&temp_dir, "video1-out.mp4"); + + std::fs::copy(source, &output).unwrap(); + + let bmff = BmffIO::new("mp4"); + + let eh = bmff.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 = bmff.get_reader().read_xmp(&mut output_stream).unwrap(); + + let loaded = crate::utils::xmp_inmemory_utils::extract_provenance(&xmp).unwrap(); + + assert_eq!(&loaded, data); + } + + #[test] fn test_truncated_c2pa_write_mp4() { let test_data = "some test data".as_bytes(); let source = fixture_path("video1.mp4"); diff --git a/sdk/src/jumbf_io.rs b/sdk/src/jumbf_io.rs @@ -376,20 +376,6 @@ pub mod tests { } #[test] - fn test_no_writer() { - let handlers: Vec<Box<dyn AssetIO>> = - vec![Box::new(C2paIO::new("")), Box::new(BmffIO::new(""))]; - - // build handler map - for h in handlers { - // get the supported types add entry for each - for supported_type in h.supported_types() { - assert!(get_caiwriter_handler(supported_type).is_none()); - } - } - } - - #[test] fn test_get_supported_list() { let supported = get_supported_types();