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:
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();