commit 8f1ae2e9f7a843ec32f9ae40e26f56031aa5a3f6
parent 4116c22ca09c10df7131dd7715b1372d024a74bc
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date: Thu, 23 Jun 2022 18:30:06 -0400
Initial BMFF support (#39)
* initial BMFF support
* Disable reading of BMFF for wasm
* clippy fixes
* PR review fixes
* cargo fmt
* Another clippy fix & wasm fix
* Cargo ffmt
* cliippy fixes
* Make sure we handle capitalized file extensions. Allow for MOV and M4A writing
* Fix build issue.
* Minor cleanup, address PR comments
* fix formating
* cleanup some comments
* Proofreading doc comments
* Proofreading
* Proofreading
* Remove commented-out use statement
* Quick tweaks
* Exclude test fixtures from crates.io package
* Remove double space
* fix formatting
* Smaller video sample
* Reduced size of sample MP4. Fixed typo for Co64 box processing.
* Restore limits on bmff tests
* clippy fixes
Co-authored-by: Eric Scouten <scouten@adobe.com>
Diffstat:
10 files changed, 1732 insertions(+), 55 deletions(-)
diff --git a/README.md b/README.md
@@ -49,6 +49,7 @@ c2pa = "0.5.2"
## Crate features
* `async_signer` enables signing via asynchronous services which require `async` support.
+* `bmff` enables handling of BMFF file formats. Currently only MP4, M4A, and MOV are enabled for writing.
* `file_io` enables manifest generation, signing via OpenSSL, and embedding manifests in various file formats.
* `serialize_thumbnails` includes binary thumbnail data in the [Serde](https://serde.rs/) serialization output.
* `xmp_write` enables updating XMP on embed with the `dcterms:provenance` field (requires [xmp_toolkit](https://crates.io/crates/xmp_toolkit)).
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
@@ -10,9 +10,11 @@ keywords = ["xmp", "metadata"]
categories = ["api-bindings"]
edition = "2018"
rust-version = "1.58.0"
+exclude = ["tests/fixtures"]
[features]
async_signer = ["async-trait"]
+bmff = [] # Work in progress support for BMFF-based containers
file_io = ["openssl"]
serialize_thumbnails = []
xmp_write = ["xmp_toolkit"]
@@ -26,6 +28,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
async-trait = { version = "0.1.48", optional = true }
+atree = "0.5.2"
base64 = "0.13.0"
bcder = "0.6.0"
blake3 = "1.0.0"
diff --git a/sdk/src/assertions/bmff_hash.rs b/sdk/src/assertions/bmff_hash.rs
@@ -0,0 +1,254 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::{
+ fs,
+ io::Cursor,
+ path::{Path, PathBuf},
+};
+
+use serde::{Deserialize, Serialize};
+use serde_bytes::ByteBuf;
+
+use crate::{
+ assertion::{Assertion, AssertionBase, AssertionCbor},
+ assertions::labels,
+ asset_handlers::bmff_io::bmff_to_jumbf_exclusions,
+ cbor_types::UriT,
+ error::{wrap_io_err, Result},
+ utils::hash_utils::{hash_by_alg, verify_by_alg},
+ Error,
+};
+
+const ASSERTION_CREATION_VERSION: usize = 1;
+
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+pub struct ExclusionsMap {
+ pub xpath: String,
+ pub length: Option<u32>,
+ pub data: Option<Vec<DataMap>>,
+ pub subset: Option<Vec<SubsetMap>>,
+ pub version: Option<u8>,
+ pub flags: Option<ByteBuf>,
+ pub exact: Option<bool>,
+}
+
+impl ExclusionsMap {
+ pub fn new(xpath: String) -> Self {
+ ExclusionsMap {
+ xpath,
+ length: None,
+ data: None,
+ subset: None,
+ version: None,
+ flags: None,
+ exact: None,
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+pub struct MerkleMap {
+ #[serde(rename = "uniqueId")]
+ pub unique_id: u32,
+
+ #[serde(rename = "localId")]
+ pub local_id: u32,
+
+ pub count: u32,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub alg: Option<String>,
+
+ #[serde(rename = "initHash")]
+ pub init_hash: Vec<u8>,
+
+ pub hashes: Vec<ByteBuf>,
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+pub struct DataMap {
+ pub offset: u32,
+ #[serde(with = "serde_bytes")]
+ pub value: Vec<u8>,
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+pub struct SubsetMap {
+ pub offset: u32,
+ pub length: u32,
+}
+
+/// Helper class to create BmffHash assertion. (These are auto-generated by the SDK.)
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+pub struct BmffHash {
+ exclusions: Vec<ExclusionsMap>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ alg: Option<String>,
+
+ #[serde(with = "serde_bytes")]
+ hash: Vec<u8>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ merkle: Option<Vec<MerkleMap>>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ name: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ url: Option<UriT>,
+
+ #[serde(skip_deserializing, skip_serializing)]
+ pub path: PathBuf,
+}
+
+impl BmffHash {
+ pub fn new(name: &str, alg: &str, url: Option<UriT>) -> Self {
+ BmffHash {
+ exclusions: Vec::new(),
+ alg: Some(alg.to_string()),
+ hash: Vec::new(),
+ merkle: None,
+ name: Some(name.to_string()),
+ url,
+ path: PathBuf::new(),
+ }
+ }
+
+ /// Label prefix for a BMFF hash assertion.
+ ///
+ /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_bmff_based_hash>.
+ pub const LABEL: &'static str = labels::BMFF_HASH;
+
+ pub fn exclusions(&self) -> &[ExclusionsMap] {
+ self.exclusions.as_ref()
+ }
+
+ pub fn exclusions_mut(&mut self) -> &mut Vec<ExclusionsMap> {
+ &mut self.exclusions
+ }
+
+ pub fn alg(&self) -> Option<&String> {
+ self.alg.as_ref()
+ }
+
+ pub fn hash(&self) -> &[u8] {
+ self.hash.as_ref()
+ }
+
+ pub fn set_hash(&mut self, hash: Vec<u8>) {
+ self.hash = hash;
+ }
+
+ pub fn name(&self) -> Option<&String> {
+ self.name.as_ref()
+ }
+
+ pub fn url(&self) -> Option<&UriT> {
+ self.url.as_ref()
+ }
+
+ /// Returns `true` if this is a remote hash.
+ pub fn is_remote_hash(&self) -> bool {
+ self.url.is_some()
+ }
+
+ pub fn set_merkle(&mut self, merkle: Vec<MerkleMap>) {
+ self.merkle = Some(merkle);
+ }
+
+ /// Generate the hash value for the asset using the range from the BmffHash.
+ pub fn gen_hash(&mut self, asset_path: &Path) -> Result<()> {
+ self.hash = self.hash_from_asset(asset_path)?;
+ self.path = PathBuf::from(asset_path);
+ Ok(())
+ }
+
+ /// Generate the hash again.
+ pub fn regen_hash(&mut self) -> Result<()> {
+ let p = self.path.clone();
+ self.hash = self.hash_from_asset(p.as_path())?;
+ Ok(())
+ }
+
+ /// Generate the asset hash from a file asset using the constructed
+ /// start and length values.
+ fn hash_from_asset(&mut self, asset_path: &Path) -> Result<Vec<u8>> {
+ if self.is_remote_hash() {
+ return Err(Error::BadParam(
+ "asset hash is remote, not yet supported".to_owned(),
+ ));
+ }
+
+ let mut data = fs::read(asset_path).map_err(wrap_io_err)?;
+ let mut data_reader = Cursor::new(data);
+
+ let alg = match self.alg {
+ Some(ref a) => a.clone(),
+ None => "sha256".to_string(),
+ };
+
+ let bmff_exclusions = &self.exclusions;
+
+ // convert BMFF exclusion map to flat exclusion list
+ let exclusions = bmff_to_jumbf_exclusions(&mut data_reader, bmff_exclusions)?;
+
+ data = data_reader.into_inner(); // back to buffer
+ let hash = hash_by_alg(&alg, &data, Some(exclusions));
+
+ if hash.is_empty() {
+ Err(Error::BadParam("could not generate data hash".to_string()))
+ } else {
+ Ok(hash)
+ }
+ }
+
+ pub fn verify_in_memory_hash(&self, data: &[u8], alg: Option<String>) -> Result<()> {
+ let curr_alg = match alg {
+ Some(a) => a,
+ None => match self.alg {
+ Some(ref a) => a.clone(),
+ None => "sha256".to_string(),
+ },
+ };
+
+ let bmff_exclusions = &self.exclusions;
+
+ let mut data_reader = Cursor::new(data);
+
+ // convert BMFF exclusion map to flat exclusion list
+ let exclusions = bmff_to_jumbf_exclusions(&mut data_reader, bmff_exclusions)?;
+
+ if verify_by_alg(&curr_alg, &self.hash, data, Some(exclusions)) {
+ Ok(())
+ } else {
+ Err(Error::HashMismatch("Hashes do not match".to_owned()))
+ }
+ }
+}
+
+impl AssertionCbor for BmffHash {}
+
+impl AssertionBase for BmffHash {
+ const LABEL: &'static str = Self::LABEL;
+ const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
+
+ fn to_assertion(&self) -> Result<Assertion> {
+ Self::to_cbor_assertion(self)
+ }
+
+ fn from_assertion(assertion: &Assertion) -> Result<Self> {
+ Self::from_cbor_assertion(assertion)
+ }
+}
diff --git a/sdk/src/assertions/mod.rs b/sdk/src/assertions/mod.rs
@@ -16,6 +16,9 @@
mod actions;
pub use actions::*;
+mod bmff_hash;
+pub use bmff_hash::{BmffHash, DataMap, ExclusionsMap, SubsetMap};
+
#[allow(dead_code)] // will become public later
mod data_hash;
pub(crate) use data_hash::DataHash;
diff --git a/sdk/src/asset_handlers/bmff_io.rs b/sdk/src/asset_handlers/bmff_io.rs
@@ -0,0 +1,1141 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::collections::HashMap;
+use std::convert::From;
+use std::fs::File;
+use std::io::{Read, Seek, SeekFrom, Write};
+use std::path::Path;
+
+use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
+use conv::ValueFrom;
+use serde::{Deserialize, Serialize};
+use serde_bytes::ByteBuf;
+
+use atree::{Arena, Token};
+use tempfile::{Builder, NamedTempFile};
+
+use crate::assertions::ExclusionsMap;
+use crate::asset_io::{AssetIO, CAILoader, CAIRead, HashObjectPositions};
+use crate::error::{Error, Result};
+use crate::utils::hash_utils::{vec_compare, Exclusion};
+
+pub struct BmffIO {
+ #[allow(dead_code)]
+ bmff_format: String, // can be used for specialized BMFF cases
+}
+impl BmffIO {
+ pub fn new(bmff_format: &str) -> Self {
+ BmffIO {
+ bmff_format: bmff_format.to_string(),
+ }
+ }
+}
+
+const HEADER_SIZE: u64 = 8;
+const HEADER_EXT_SIZE: u64 = 4;
+
+const C2PA_UUID: [u8; 16] = [
+ 0xD8, 0xFE, 0xC3, 0xD6, 0x1B, 0x0E, 0x48, 0x3C, 0x92, 0x97, 0x58, 0x28, 0x87, 0x7E, 0xC4, 0x81,
+];
+const MANIFEST: &str = "manifest";
+const MERKLE: &str = "merkle";
+
+// ISO IEC 14496-12_2022 FullBoxes
+const FULL_BOX_TYPES: &[&str; 80] = &[
+ "pdin", "mvhd", "tkhd", "mdhd", "hdlr", "nmhd", "elng", "stsd", "stdp", "stts", "ctts", "cslg",
+ "stss", "stsh", "stdp", "elst", "dref", "stsz", "stz2", "stsc", "stco", "co64", "padb", "subs",
+ "saiz", "saio", "mehd", "trex", "mfhd", "tfhd", "trun", "tfra", "mfro", "tfdt", "leva", "trep",
+ "assp", "sbgp", "sgpd", "csgp", "cprt", "tsel", "kind", "meta", "xml ", "bxml", "iloc", "pitm",
+ "ipro", "infe", "iinf", "iref", "ipma", "schm", "fiin", "fpar", "fecr", "gitn", "fire", "stri",
+ "stsg", "stvi", "csch", "sidx", "ssix", "prft", "srpp", "vmhd", "smhd", "srat", "chnl", "dmix",
+ "txtC", "mime", "uri ", "uriI", "hmhd", "sthd", "vvhd", "medc",
+];
+
+// define CAIRead for tempfile
+impl CAIRead for NamedTempFile {}
+
+macro_rules! boxtype {
+ ($( $name:ident => $value:expr ),*) => {
+ #[derive(Clone, Copy, Debug, PartialEq)]
+ pub enum BoxType {
+ $( $name, )*
+ UnknownBox(u32),
+ }
+
+ impl From<u32> for BoxType {
+ fn from(t: u32) -> BoxType {
+ match t {
+ $( $value => BoxType::$name, )*
+ _ => BoxType::UnknownBox(t),
+ }
+ }
+ }
+
+ impl From<BoxType> for u32 {
+ fn from(t: BoxType) -> u32 {
+ match t {
+ $( BoxType::$name => $value, )*
+ BoxType::UnknownBox(t) => t,
+ }
+ }
+ }
+ }
+}
+
+boxtype! {
+ Empty => 0x0000_0000,
+ UuidBox => 0x75756964,
+ FtypBox => 0x66747970,
+ MvhdBox => 0x6d766864,
+ MfhdBox => 0x6d666864,
+ FreeBox => 0x66726565,
+ MdatBox => 0x6d646174,
+ MoovBox => 0x6d6f6f76,
+ MvexBox => 0x6d766578,
+ MehdBox => 0x6d656864,
+ TrexBox => 0x74726578,
+ EmsgBox => 0x656d7367,
+ MoofBox => 0x6d6f6f66,
+ TkhdBox => 0x746b6864,
+ TfhdBox => 0x74666864,
+ EdtsBox => 0x65647473,
+ MdiaBox => 0x6d646961,
+ ElstBox => 0x656c7374,
+ MdhdBox => 0x6d646864,
+ HdlrBox => 0x68646c72,
+ MinfBox => 0x6d696e66,
+ VmhdBox => 0x766d6864,
+ StblBox => 0x7374626c,
+ StsdBox => 0x73747364,
+ SttsBox => 0x73747473,
+ CttsBox => 0x63747473,
+ StssBox => 0x73747373,
+ StscBox => 0x73747363,
+ StszBox => 0x7374737A,
+ StcoBox => 0x7374636F,
+ Co64Box => 0x636F3634,
+ TrakBox => 0x7472616b,
+ TrafBox => 0x74726166,
+ TrunBox => 0x7472756E,
+ UdtaBox => 0x75647461,
+ DinfBox => 0x64696e66,
+ DrefBox => 0x64726566,
+ UrlBox => 0x75726C20,
+ SmhdBox => 0x736d6864,
+ Avc1Box => 0x61766331,
+ AvcCBox => 0x61766343,
+ Hev1Box => 0x68657631,
+ HvcCBox => 0x68766343,
+ Mp4aBox => 0x6d703461,
+ EsdsBox => 0x65736473,
+ Tx3gBox => 0x74783367,
+ VpccBox => 0x76706343,
+ Vp09Box => 0x76703039,
+ MetaBox => 0x6D657461
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+struct BmffMerkleMap {
+ #[serde(rename = "uniqueId")]
+ unique_id: u32,
+ #[serde(rename = "localId")]
+ local_id: u32,
+ location: u32,
+ hashes: Option<Vec<ByteBuf>>,
+}
+
+struct BoxHeaderLite {
+ pub name: BoxType,
+ pub size: u64,
+ pub fourcc: String,
+}
+
+impl BoxHeaderLite {
+ pub fn new(name: BoxType, size: u64, fourcc: &str) -> Self {
+ Self {
+ name,
+ size,
+ fourcc: fourcc.to_string(),
+ }
+ }
+ pub fn read<R: Read + ?Sized>(reader: &mut R) -> Result<Self> {
+ // Create and read to buf.
+ let mut buf = [0u8; 8]; // 8 bytes for box header.
+ reader.read_exact(&mut buf)?;
+
+ // Get size.
+ let mut s = [0u8; 4];
+ s.clone_from_slice(&buf[0..4]);
+ let size = u32::from_be_bytes(s);
+
+ // Get box type string.
+ let mut t = [0u8; 4];
+ t.clone_from_slice(&buf[4..8]);
+ let fourcc = String::from_utf8(buf[4..8].to_vec())
+ .map_err(|_err| Error::BadParam("value out of range".to_string()))?;
+ let typ = u32::from_be_bytes(t);
+
+ // Get largesize if size is 1
+ if size == 1 {
+ reader.read_exact(&mut buf)?;
+ let largesize = u64::from_be_bytes(buf);
+
+ Ok(BoxHeaderLite {
+ name: BoxType::from(typ),
+ size: largesize - HEADER_SIZE,
+ fourcc,
+ })
+ } else {
+ Ok(BoxHeaderLite {
+ name: BoxType::from(typ),
+ size: size as u64,
+ fourcc,
+ })
+ }
+ }
+
+ pub fn write<W: Write>(&self, writer: &mut W) -> Result<u64> {
+ if self.size > u32::MAX as u64 {
+ writer.write_u32::<BigEndian>(1)?;
+ writer.write_u32::<BigEndian>(self.name.into())?;
+ writer.write_u64::<BigEndian>(self.size)?;
+ Ok(16)
+ } else {
+ writer.write_u32::<BigEndian>(self.size as u32)?;
+ writer.write_u32::<BigEndian>(self.name.into())?;
+ Ok(8)
+ }
+ }
+}
+
+fn write_box_uuid_extension<W: Write>(w: &mut W, uuid: &[u8; 16]) -> Result<u64> {
+ w.write_all(uuid)?;
+ Ok(16)
+}
+
+#[derive(Debug, PartialEq)]
+pub(crate) struct BoxInfo {
+ path: String,
+ parent: Option<Token>,
+ offset: u64,
+ size: u64,
+ box_type: BoxType,
+ user_type: Option<Vec<u8>>,
+ version: Option<u8>,
+ flags: Option<u32>,
+}
+
+fn read_box_header_ext(reader: &mut dyn CAIRead) -> Result<(u8, u32)> {
+ let version = reader.read_u8()?;
+ let flags = reader.read_u24::<BigEndian>()?;
+ Ok((version, flags))
+}
+fn write_box_header_ext<W: Write>(w: &mut W, v: u8, f: u32) -> Result<u64> {
+ w.write_u8(v)?;
+ w.write_u24::<BigEndian>(f)?;
+ Ok(4)
+}
+
+fn box_start(reader: &mut dyn CAIRead) -> Result<u64> {
+ Ok(reader.seek(SeekFrom::Current(0))? - HEADER_SIZE)
+}
+
+fn skip_bytes(reader: &mut dyn CAIRead, size: u64) -> Result<()> {
+ reader.seek(SeekFrom::Current(size as i64))?;
+ Ok(())
+}
+
+fn skip_bytes_to(reader: &mut dyn CAIRead, pos: u64) -> Result<u64> {
+ let pos = reader.seek(SeekFrom::Start(pos))?;
+ Ok(pos)
+}
+
+fn _skip_box(reader: &mut dyn CAIRead, size: u64) -> Result<()> {
+ let start = box_start(reader)?;
+ skip_bytes_to(reader, start + size)?;
+ Ok(())
+}
+
+fn write_c2pa_box<W: Write>(
+ w: &mut W,
+ data: &[u8],
+ is_manifest: bool,
+ merkle_data: &[u8],
+) -> Result<()> {
+ let purpose_size = if is_manifest {
+ MANIFEST.len() + 1
+ } else {
+ MERKLE.len() + 1
+ };
+ let merkle_size = if is_manifest { 8 } else { merkle_data.len() };
+ let size = 8 + 16 + 4 + purpose_size + merkle_size + data.len(); // header + UUID + version/flags + data + zero terminated purpose + merkle data
+ let bh = BoxHeaderLite::new(BoxType::UuidBox, size as u64, "uuid");
+
+ // write out header
+ bh.write(w)?;
+
+ // write out c2pa extension UUID
+ write_box_uuid_extension(w, &C2PA_UUID)?;
+
+ // write out version and flags
+ let version: u8 = 0;
+ let flags: u32 = 0;
+ write_box_header_ext(w, version, flags)?;
+
+ // write purpose
+ if is_manifest {
+ w.write_all(MANIFEST.as_bytes())?;
+ w.write_u8(0)?;
+
+ // write no merkle flag
+ w.write_u64::<BigEndian>(0)?;
+ } else {
+ w.write_all(MERKLE.as_bytes())?;
+ w.write_u8(0)?;
+
+ // write merkle cbor
+ w.write_all(merkle_data)?;
+ }
+
+ // 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()));
+ }
+
+ let zeros = vec![0u8; size - 8];
+ let bh = BoxHeaderLite::new(BoxType::FreeBox, size as u64, "free");
+
+ // write out header
+ bh.write(w)?;
+
+ // write out header
+ w.write_all(&zeros)?;
+
+ Ok(())
+}
+
+fn add_token_to_cache(bmff_path_map: &mut HashMap<String, Vec<Token>>, path: String, token: Token) {
+ if let Some(token_list) = bmff_path_map.get_mut(&path) {
+ token_list.push(token);
+ } else {
+ let token_list = vec![token];
+ bmff_path_map.insert(path, token_list);
+ }
+}
+
+fn path_from_token(bmff_tree: &mut Arena<BoxInfo>, current_node_token: &Token) -> Result<String> {
+ let ancestors = current_node_token.ancestors(bmff_tree);
+ let mut path = bmff_tree[*current_node_token].data.path.clone();
+
+ for parent in ancestors {
+ path = format!("{}/{}", parent.data.path, path);
+ }
+
+ if path.is_empty() {
+ path = "/".to_string();
+ }
+
+ Ok(path)
+}
+
+pub fn bmff_to_jumbf_exclusions(
+ reader: &mut dyn CAIRead,
+ bmff_exclusions: &[ExclusionsMap],
+) -> Result<Vec<Exclusion>> {
+ let start = reader.seek(SeekFrom::Current(0))?;
+ let size = reader.seek(SeekFrom::End(0))?;
+ reader.seek(SeekFrom::Start(start))?;
+
+ // 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(reader, size, &mut bmff_tree, &root_token, &mut bmff_map)?;
+
+ let mut exclusions = Vec::new();
+
+ for bmff_exclusion in bmff_exclusions {
+ if let Some(box_token_list) = bmff_map.get(&bmff_exclusion.xpath) {
+ for box_token in box_token_list {
+ let box_info = &bmff_tree[*box_token].data;
+
+ let box_start = box_info.offset;
+ let box_length = box_info.size;
+
+ let exclusion_start = box_start;
+ let exclusion_length = box_length;
+
+ // adjust exclusion bounds as needed
+
+ // check the length
+ if let Some(desired_length) = bmff_exclusion.length {
+ if desired_length as u64 != box_length {
+ continue;
+ }
+ }
+
+ // check the version
+ if let Some(desired_version) = bmff_exclusion.version {
+ if let Some(box_version) = box_info.version {
+ if desired_version != box_version {
+ continue;
+ }
+ }
+ }
+
+ // check the flags
+ if let Some(desired_flag_bytes) = &bmff_exclusion.flags {
+ let mut temp_bytes = [0u8; 4];
+ if desired_flag_bytes.len() >= 3 {
+ temp_bytes[0] = desired_flag_bytes[0];
+ temp_bytes[1] = desired_flag_bytes[1];
+ temp_bytes[2] = desired_flag_bytes[2];
+ }
+ let desired_flags = u32::from_be_bytes(temp_bytes);
+
+ if let Some(box_flags) = box_info.flags {
+ let exact = if let Some(is_exact) = bmff_exclusion.exact {
+ is_exact
+ } else {
+ true
+ };
+
+ if exact {
+ if desired_flags != box_flags {
+ continue;
+ }
+ } else {
+ // bitwise match
+ if (desired_flags | box_flags) != desired_flags {
+ continue;
+ }
+ }
+ }
+ }
+
+ // check data match
+ if let Some(data_map_vec) = &bmff_exclusion.data {
+ let mut should_add = true;
+
+ for data_map in data_map_vec {
+ // move to the start of exclusion
+ skip_bytes_to(reader, box_start + data_map.offset as u64)?;
+
+ // match the data
+ let mut buf = vec![0u8; data_map.value.len()];
+ reader.read_exact(&mut buf)?;
+
+ // does not match so skip
+ if !vec_compare(&data_map.value, &buf) {
+ should_add = false;
+ break;
+ }
+ }
+ if !should_add {
+ continue;
+ }
+ }
+
+ // reduce range if desired
+ if let Some(subset_vec) = &bmff_exclusion.subset {
+ for subset in subset_vec {
+ let exclusion = Exclusion::new(
+ (exclusion_start + subset.offset as u64) as usize,
+ (if subset.length == 0 {
+ exclusion_length - subset.offset as u64
+ } else {
+ subset.length as u64
+ }) as usize,
+ );
+ exclusions.push(exclusion);
+ }
+ } else {
+ let exclusion =
+ Exclusion::new(exclusion_start as usize, exclusion_length as usize);
+ exclusions.push(exclusion);
+ }
+ }
+ }
+ }
+
+ Ok(exclusions)
+}
+
+// `stco` and `co64` elements contain absolute file offsets so they need to be adjusted based on whether content was added or removed.
+fn adjust_stco_and_co64<W: Write + CAIRead>(
+ output: &mut W,
+ bmff_tree: &Arena<BoxInfo>,
+ bmff_path_map: &HashMap<String, Vec<Token>>,
+ adjust: i32,
+) -> Result<()> {
+ let start_pos = output.seek(SeekFrom::Current(0))?; // save starting point
+
+ // handle 32 bit offsets
+ if let Some(stco_list) = bmff_path_map.get("/moov/trak/mdia/minf/stbl/stco") {
+ for stco_token in stco_list {
+ let stco_box_info = &bmff_tree[*stco_token].data;
+ if stco_box_info.box_type != BoxType::StcoBox {
+ return Err(Error::BadParam("Bad BMFF".to_string()));
+ }
+
+ // read stco box and patch
+ output.seek(SeekFrom::Start(stco_box_info.offset))?;
+
+ // read header
+ let header = BoxHeaderLite::read(output)
+ .map_err(|_err| Error::BadParam("Bad BMFF".to_string()))?;
+ if header.name != BoxType::StcoBox {
+ return Err(Error::BadParam("Bad BMFF".to_string()));
+ }
+
+ // read extended header
+ let (_version, _flags) = read_box_header_ext(output)?; // box extensions
+
+ // get count of offsets
+ let entry_count = output.read_u32::<BigEndian>()?;
+
+ // read and patch offsets
+ let entry_start_pos = output.seek(SeekFrom::Current(0))?;
+ let mut entries: Vec<u32> = Vec::new();
+ for _e in 0..entry_count {
+ let offset = output.read_u32::<BigEndian>()?;
+ let new_offset = if adjust < 0 {
+ offset - adjust.abs() as u32
+ } else {
+ offset + adjust as u32
+ };
+ entries.push(new_offset);
+ }
+
+ // write updated offsets
+ output.seek(SeekFrom::Start(entry_start_pos))?;
+ for e in entries {
+ output.write_u32::<BigEndian>(e)?;
+ }
+ }
+ }
+
+ // handle 64 offsets
+ if let Some(co64_list) = bmff_path_map.get("/moov/trak/mdia/minf/stbl/co64") {
+ for co64_token in co64_list {
+ let co64_box_info = &bmff_tree[*co64_token].data;
+ if co64_box_info.box_type != BoxType::Co64Box {
+ return Err(Error::BadParam("Bad BMFF".to_string()));
+ }
+
+ // read co64 box and patch
+ output.seek(SeekFrom::Start(co64_box_info.offset))?;
+
+ // read header
+ let header = BoxHeaderLite::read(output)
+ .map_err(|_err| Error::BadParam("Bad BMFF".to_string()))?;
+ if header.name != BoxType::Co64Box {
+ return Err(Error::BadParam("Bad BMFF".to_string()));
+ }
+
+ // read extended header
+ let (_version, _flags) = read_box_header_ext(output)?; // box extensions
+
+ // get count of offsets
+ let entry_count = output.read_u32::<BigEndian>()?;
+
+ // read and patch offsets
+ let entry_start_pos = output.seek(SeekFrom::Current(0))?;
+ let mut entries: Vec<u64> = Vec::new();
+ for _e in 0..entry_count {
+ let offset = output.read_u64::<BigEndian>()?;
+ let new_offset = if adjust < 0 {
+ offset - adjust.abs() as u64
+ } else {
+ offset + adjust as u64
+ };
+ entries.push(new_offset);
+ }
+
+ // write updated offsets
+ output.seek(SeekFrom::Start(entry_start_pos))?;
+ for e in entries {
+ output.write_u64::<BigEndian>(e)?;
+ }
+ }
+ }
+
+ // restore seek point
+ output.seek(SeekFrom::Start(start_pos))?;
+ output.flush()?;
+
+ Ok(())
+}
+
+pub(crate) fn build_bmff_tree(
+ reader: &mut dyn CAIRead,
+ end: u64,
+ bmff_tree: &mut Arena<BoxInfo>,
+ current_node: &Token,
+ bmff_path_map: &mut HashMap<String, Vec<Token>>,
+) -> Result<()> {
+ let start = reader.seek(SeekFrom::Current(0))?;
+
+ let mut current = start;
+ while current < end {
+ // Get box header.
+ let header =
+ BoxHeaderLite::read(reader).map_err(|_err| Error::BadParam("Bad BMFF".to_string()))?;
+
+ // Break if size zero BoxHeader
+ let s = header.size;
+ if s == 0 {
+ break;
+ }
+
+ // Match and parse the supported atom boxes.
+ match header.name {
+ BoxType::UuidBox => {
+ let start = box_start(reader)?;
+
+ let mut extended_type = [0u8; 16]; // 16 bytes of UUID
+ reader.read_exact(&mut extended_type)?;
+
+ let (version, flags) = read_box_header_ext(reader)?;
+
+ let b = BoxInfo {
+ path: header.fourcc.clone(),
+ offset: start,
+ size: s,
+ box_type: BoxType::UuidBox,
+ parent: Some(*current_node),
+ user_type: Some(extended_type.to_vec()),
+ version: Some(version),
+ flags: Some(flags),
+ };
+
+ let new_token = current_node.append(bmff_tree, b);
+
+ let path = path_from_token(bmff_tree, &new_token)?;
+ add_token_to_cache(bmff_path_map, path, new_token);
+
+ // position seek pointer
+ skip_bytes_to(reader, start + s)?;
+ }
+ // container box types
+ BoxType::MoovBox
+ | BoxType::TrakBox
+ | BoxType::MdiaBox
+ | BoxType::MinfBox
+ | BoxType::StblBox
+ | BoxType::MoofBox
+ | BoxType::TrafBox => {
+ let start = box_start(reader)?;
+
+ let b = BoxInfo {
+ path: header.fourcc.clone(),
+ offset: start,
+ size: s,
+ box_type: header.name,
+ parent: Some(*current_node),
+ user_type: None,
+ version: None,
+ flags: None,
+ };
+
+ let new_token = bmff_tree.new_node(b);
+ current_node
+ .append_node(bmff_tree, new_token)
+ .map_err(|_err| Error::BadParam("Bad BMFF Graph".to_string()))?;
+
+ let path = path_from_token(bmff_tree, &new_token)?;
+ add_token_to_cache(bmff_path_map, path, new_token);
+
+ // consume all sub-boxes
+ let mut current = reader.seek(SeekFrom::Current(0))?;
+ let end = start + s;
+ while current < end {
+ build_bmff_tree(reader, end, bmff_tree, &new_token, bmff_path_map)?;
+ current = reader.seek(SeekFrom::Current(0))?;
+ }
+
+ // position seek pointer
+ skip_bytes_to(reader, start + s)?;
+ }
+ // handle the meta box since it is explicitly listed in the spec
+ BoxType::MetaBox => {
+ let start = box_start(reader)?;
+
+ let b = BoxInfo {
+ path: header.fourcc.clone(),
+ offset: start,
+ size: s,
+ box_type: header.name,
+ parent: Some(*current_node),
+ user_type: None,
+ version: None,
+ flags: None,
+ };
+
+ let new_token = current_node.append(bmff_tree, b);
+
+ let path = path_from_token(bmff_tree, &new_token)?;
+ add_token_to_cache(bmff_path_map, path, new_token);
+
+ // parse 'hdlr' box
+ let _handler = {
+ let header = BoxHeaderLite::read(reader)
+ .map_err(|_err| Error::BadParam("Bad BMFF".to_string()))?;
+
+ // Break if size zero BoxHeader, which can result in dead-loop.
+ let handler_size = header.size;
+
+ let start = box_start(reader)?;
+
+ let (_version, _flags) = read_box_header_ext(reader)?;
+
+ let _pre_defined = reader.read_u32::<BigEndian>()?; // pre-defined
+ let _handler = reader.read_u32::<BigEndian>()?;
+
+ skip_bytes(reader, 12)?; // reserved
+
+ let buf_size = handler_size - HEADER_SIZE - HEADER_EXT_SIZE - 20 - 1;
+ let mut buf = vec![0u8; buf_size as usize];
+ reader.read_exact(&mut buf)?;
+
+ let handler_string = match String::from_utf8(buf) {
+ Ok(t) => {
+ if t.len() != buf_size as usize {
+ return Err(Error::BadParam(
+ "string size does not match buffer".to_string(),
+ ));
+ }
+ t
+ }
+ _ => String::from("null"),
+ };
+
+ skip_bytes_to(reader, start + handler_size)?;
+
+ handler_string
+ };
+
+ // consume enclosed boxes
+ let end = current + s;
+ build_bmff_tree(reader, end, bmff_tree, &new_token, bmff_path_map)?;
+
+ // position seek pointer
+ skip_bytes_to(reader, start + s)?;
+ }
+ _ => {
+ let start = reader.seek(SeekFrom::Current(0))? - HEADER_SIZE;
+
+ let b = if FULL_BOX_TYPES.contains(&header.fourcc.as_str()) {
+ let (version, flags) = read_box_header_ext(reader)?; // box extensions
+ BoxInfo {
+ path: header.fourcc.clone(),
+ offset: start,
+ size: s,
+ box_type: header.name,
+ parent: Some(*current_node),
+ user_type: None,
+ version: Some(version),
+ flags: Some(flags),
+ }
+ } else {
+ BoxInfo {
+ path: header.fourcc.clone(),
+ offset: start,
+ size: s,
+ box_type: header.name,
+ parent: Some(*current_node),
+ user_type: None,
+ version: None,
+ flags: None,
+ }
+ };
+
+ let new_token = current_node.append(bmff_tree, b);
+
+ let path = path_from_token(bmff_tree, &new_token)?;
+ add_token_to_cache(bmff_path_map, path, new_token);
+
+ // position seek pointer
+ skip_bytes_to(reader, start + s)?;
+ }
+ }
+ current = reader.seek(SeekFrom::Current(0))?;
+ }
+
+ Ok(())
+}
+
+impl CAILoader for BmffIO {
+ fn read_cai(&self, reader: &mut dyn CAIRead) -> Result<Vec<u8>> {
+ let start = reader.seek(SeekFrom::Current(0))?;
+ let size = reader.seek(SeekFrom::End(0))?;
+ reader.seek(SeekFrom::Start(start))?;
+
+ // 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(reader, size, &mut bmff_tree, &root_token, &mut bmff_map)?;
+
+ // grab top level (for now) C2PA box
+ if let Some(uuid_list) = bmff_map.get("/uuid") {
+ let uuid_token = &uuid_list[0];
+ let box_info = &bmff_tree[*uuid_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) {
+ let mut 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)?;
+
+ // Fullbox => 8 bits for version 24 bits for flags
+ let (_version, _flags) = read_box_header_ext(reader)?;
+ data_len -= 4;
+
+ // get the purpose
+ let mut purpose = Vec::with_capacity(64);
+ loop {
+ let mut buf = [0; 1];
+ reader.read_exact(&mut buf)?;
+ data_len -= 1;
+ if buf[0] == 0x00 {
+ break;
+ } else {
+ purpose.push(buf[0]);
+ }
+ }
+
+ // is the purpose manifest?
+ if vec_compare(&purpose, MANIFEST.as_bytes()) {
+ // offset to first aux uuid with purpose merkle
+ let mut buf = [0u8; 8];
+ reader.read_exact(&mut buf)?;
+ data_len -= 8;
+
+ // offset to first aux uuid
+ let offset = u64::from_be_bytes(buf);
+
+ // if no offset this contains the manifest
+ if offset == 0 {
+ let mut buf = vec![0u8; data_len as usize];
+ reader.read_exact(&mut buf)?;
+
+ return Ok(buf);
+ } else {
+ // handle aux uuids
+ let mut buf = vec![0u8; data_len as usize];
+ reader.read_exact(&mut buf)?;
+
+ let _mm: BmffMerkleMap = serde_cbor::from_slice(&buf)?;
+ }
+ } else if vec_compare(&purpose, MERKLE.as_bytes()) {
+ // handle merkle boxes not yet handled
+ return Err(Error::UnsupportedType);
+ }
+ }
+ }
+ }
+ }
+
+ Err(Error::JumbfNotFound)
+ }
+
+ // Get XMP block
+ fn read_xmp(&self, _asset_reader: &mut dyn CAIRead) -> Option<String> {
+ None
+ }
+}
+
+impl AssetIO for BmffIO {
+ fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> {
+ let mut f = File::open(asset_path)?;
+ self.read_cai(&mut f)
+ }
+ 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.seek(SeekFrom::Start(0))?;
+
+ // create root node
+ let root_box = BoxInfo {
+ path: "".to_string(),
+ offset: 0,
+ size: size as u64,
+ 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(
+ &mut input,
+ size as u64,
+ &mut bmff_tree,
+ &root_token,
+ &mut bmff_map,
+ )?;
+
+ // get position to insert c2pa
+ let (c2pa_start, c2pa_length) = if let Some(uuid_tokens) = bmff_map.get("/uuid") {
+ let uuid_info = &bmff_tree[uuid_tokens[0]].data;
+
+ // is this a C2PA manifest
+ let is_c2pa = if let Some(uuid) = &uuid_info.user_type {
+ // make sure it is a C2PA box
+ vec_compare(&C2PA_UUID, uuid)
+ } else {
+ false
+ };
+
+ if is_c2pa {
+ (uuid_info.offset, Some(uuid_info.size))
+ } else {
+ (0, None)
+ }
+ } else {
+ // start after ftyp
+ let ftyp_token = bmff_map.get("/ftyp").ok_or(Error::UnsupportedType)?; // todo check ftyps to make sure we supprt any special format requirements
+ let ftyp_info = &bmff_tree[ftyp_token[0]].data;
+ ((ftyp_info.offset + ftyp_info.size), None)
+ };
+
+ let mut new_c2pa_box: Vec<u8> = Vec::with_capacity(store_bytes.len() * 2);
+ let merkle_data: &[u8] = &[]; // not yet supported
+ 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::BadParam("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label
+
+ let end = usize::value_from(c2pa_start + c2pa_length)
+ .map_err(|_err| Error::BadParam("value out of range".to_string()))?;
+
+ (start, end)
+ } else {
+ // insert new c2pa
+ let end = usize::value_from(c2pa_start)
+ .map_err(|_err| Error::BadParam("value out of range".to_string()))?;
+
+ (end, end)
+ };
+
+ // write content before ContentProvenanceBox
+ input.seek(SeekFrom::Start(0))?;
+ let mut b = vec![0u8; start];
+ input.read_exact(&mut b)?;
+ temp_file.write_all(&b)?;
+
+ // write ContentProvenanceBox
+ temp_file.write_all(&new_c2pa_box)?;
+
+ // calc offset adjustments
+ let offset_adjust: i32 = if end == 0 {
+ new_c2pa_box_size as i32
+ } else {
+ // value could be negative is box is truncated
+ let existing_c2pa_box_size = end - start;
+ let pad_size: i32 = new_c2pa_box_size as i32 - existing_c2pa_box_size as i32;
+ pad_size
+ };
+
+ // 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()?;
+
+ // 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: size as u64,
+ 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.seek(SeekFrom::Start(0))?;
+ build_bmff_tree(
+ &mut temp_file,
+ size as u64,
+ &mut output_bmff_tree,
+ &root_token,
+ &mut output_bmff_map,
+ )?;
+
+ // adjust based on current layyout
+ adjust_stco_and_co64(
+ &mut temp_file,
+ &output_bmff_tree,
+ &output_bmff_map,
+ offset_adjust,
+ )?;
+ }
+ _ => (), // todo: handle more patching cases as necessary
+ }
+
+ std::fs::copy(temp_file.path(), asset_path)?;
+
+ Ok(())
+ }
+
+ fn get_object_locations(
+ &self,
+ _asset_path: &std::path::Path,
+ ) -> Result<Vec<HashObjectPositions>> {
+ let vec: Vec<HashObjectPositions> = Vec::new();
+ Ok(vec)
+ }
+}
+
+#[cfg(feature = "bmff")]
+#[cfg(test)]
+pub mod tests {
+ use tempfile::tempdir;
+
+ use super::*;
+ use crate::{
+ status_tracker::{report_split_errors, DetailedStatusTracker, StatusTracker},
+ store::Store,
+ utils::test::{fixture_path, temp_dir_path},
+ };
+
+ #[test]
+ fn test_read_mp4() {
+ let ap = fixture_path("video1.mp4");
+
+ let mut log = DetailedStatusTracker::default();
+ let store = Store::load_from_asset(&ap, true, &mut log);
+
+ let errors = report_split_errors(log.get_log_mut());
+ assert!(errors.is_empty());
+
+ if let Ok(s) = store {
+ print!("Store: \n{}", s);
+ }
+ }
+
+ #[test]
+ fn test_truncated_c2pa_write_mp4() {
+ let test_data = "some test data".as_bytes();
+ let source = fixture_path("video1.mp4");
+
+ let mut success = false;
+ if let Ok(temp_dir) = tempdir() {
+ let output = temp_dir_path(&temp_dir, "mp4_test.mp4");
+
+ if let Ok(_size) = std::fs::copy(&source, &output) {
+ let bmff = BmffIO::new("mp4");
+
+ //let test_data = bmff.read_cai_store(&source).unwrap();
+ if let Ok(()) = bmff.save_cai_store(&output, test_data) {
+ if let Ok(read_test_data) = bmff.read_cai_store(&output) {
+ assert!(vec_compare(test_data, &read_test_data));
+ success = true;
+ }
+ }
+ }
+ }
+ assert!(success)
+ }
+
+ #[test]
+ fn test_expanded_c2pa_write_mp4() {
+ let mut more_data = "some more test data".as_bytes().to_vec();
+ let source = fixture_path("video1.mp4");
+
+ let mut success = false;
+ if let Ok(temp_dir) = tempdir() {
+ let output = temp_dir_path(&temp_dir, "mp4_test.mp4");
+
+ if let Ok(_size) = std::fs::copy(&source, &output) {
+ let bmff = BmffIO::new("mp4");
+
+ if let Ok(mut test_data) = bmff.read_cai_store(&source) {
+ test_data.append(&mut more_data);
+ if let Ok(()) = bmff.save_cai_store(&output, &test_data) {
+ if let Ok(read_test_data) = bmff.read_cai_store(&output) {
+ assert!(vec_compare(&test_data, &read_test_data));
+ success = true;
+ }
+ }
+ }
+ }
+ }
+ assert!(success)
+ }
+}
diff --git a/sdk/src/asset_handlers/mod.rs b/sdk/src/asset_handlers/mod.rs
@@ -11,6 +11,7 @@
// specific language governing permissions and limitations under
// each license.
+pub mod bmff_io;
pub mod c2pa_io;
pub mod jpeg_io;
pub mod png_io;
diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs
@@ -22,7 +22,7 @@ use crate::assertion::{
get_thumbnail_image_type, get_thumbnail_instance, get_thumbnail_type, Assertion, AssertionBase,
AssertionData,
};
-use crate::assertions::{self, labels, DataHash};
+use crate::assertions::{self, labels, BmffHash, DataHash};
use crate::cose_validator::{get_signing_info, verify_cose, verify_cose_async};
use crate::hashed_uri::HashedUri;
use crate::jumbf::{
@@ -609,6 +609,46 @@ impl Claim {
}
}
+ // crate private function to allow for patching a BMFF hash with final contents
+ #[cfg(feature = "file_io")]
+ pub(crate) fn update_bmff_hash(&mut self, bmff_hash: BmffHash) -> Result<()> {
+ let replacement_assertion = bmff_hash.to_assertion()?;
+
+ match self.assertion_store.iter_mut().find(|assertion| {
+ // is this a BMFFHash Assertion
+ Assertion::assertions_eq(&replacement_assertion, assertion.assertion())
+ }) {
+ Some(ref mut bmff_assertion) => {
+ let original_hash = bmff_assertion.hash().to_vec();
+
+ let replacement_hash = Claim::calc_box_hash(
+ &bmff_assertion.label(),
+ &replacement_assertion,
+ bmff_assertion.salt().clone(),
+ bmff_assertion.hash_alg(),
+ )?;
+ bmff_assertion.update_assertion(replacement_assertion, replacement_hash)?;
+
+ // fix up hashed uri
+ match self.assertions.iter_mut().find_map(|f| {
+ if f.url().contains(&bmff_assertion.label())
+ && vec_compare(&f.hash(), &original_hash)
+ {
+ // replace with newly updated hash
+ f.update_hash(bmff_assertion.hash().to_vec());
+ Some(f)
+ } else {
+ None
+ }
+ }) {
+ Some(_) => Ok(()),
+ None => Err(Error::NotFound),
+ }
+ }
+ None => Err(Error::NotFound),
+ }
+ }
+
/// Not ready for use!!!!!
/// Redact an assertion from a prior claim.
/// This will remove the assertion from the JUMBF
@@ -760,6 +800,7 @@ impl Claim {
verified: Result<ValidationInfo>,
validation_log: &mut impl StatusTracker,
) -> Result<()> {
+ const UNNAMED: &str = "unnamed";
let default_str = |s: &String| s.clone();
match verified {
@@ -916,10 +957,45 @@ impl Claim {
}
for dh_assertion in claim.data_hash_assertions() {
- let dh = DataHash::from_assertion(&dh_assertion)?;
- let name = dh.name.as_ref().map_or("unnamed".to_string(), default_str);
- if !dh.is_remote_hash() {
- // only verify local hashes here
+ if dh_assertion.label_root() == DataHash::LABEL {
+ let dh = DataHash::from_assertion(dh_assertion)?;
+ let name = dh.name.as_ref().map_or(UNNAMED.to_string(), default_str);
+ if !dh.is_remote_hash() {
+ // only verify local hashes here
+ match dh.verify_in_memory_hash(asset_bytes, Some(claim.alg().to_string())) {
+ Ok(_a) => {
+ let log_item = log_item!(
+ claim.assertion_uri(&dh_assertion.label()),
+ "data hash valid",
+ "verify_internal"
+ )
+ .validation_status(validation_status::ASSERTION_DATAHASH_MATCH);
+ validation_log.log_silent(log_item);
+
+ continue;
+ }
+ Err(e) => {
+ let log_item = log_item!(
+ claim.assertion_uri(&dh_assertion.label()),
+ format!("asset hash error, name: {}, error: {}", name, e),
+ "verify_internal"
+ )
+ .error(Error::HashMismatch(format!("Asset hash failure: {}", e)))
+ .validation_status(validation_status::ASSERTION_DATAHASH_MISMATCH);
+
+ validation_log.log(
+ log_item,
+ Some(Error::HashMismatch(format!("Asset hash failure: {}", e))),
+ )?;
+ }
+ }
+ }
+ } else {
+ // handle BMFF data hashes
+ let dh = BmffHash::from_assertion(dh_assertion)?;
+
+ let name = dh.name().map_or("unnamed".to_string(), default_str);
+
match dh.verify_in_memory_hash(asset_bytes, Some(claim.alg().to_string())) {
Ok(_a) => {
let log_item = log_item!(
@@ -967,7 +1043,7 @@ impl Claim {
}
/// Return list of data hash assertions
- pub fn data_hash_assertions(&self) -> Vec<Assertion> {
+ pub fn data_hash_assertions(&self) -> Vec<&Assertion> {
let dummy_data = AssertionData::Cbor(Vec::new());
let dummy_hash = Assertion::new(DataHash::LABEL, None, dummy_data);
let mut data_hashes = self.assertions_by_type(&dummy_hash);
@@ -980,10 +1056,16 @@ impl Claim {
data_hashes
}
+ pub fn bmff_hash_assertions(&self) -> Vec<&Assertion> {
+ // add in an BMFF hashes
+ let dummy_bmff_data = AssertionData::Cbor(Vec::new());
+ let dummy_bmff_hash = Assertion::new(assertions::labels::BMFF_HASH, None, dummy_bmff_data);
+ self.assertions_by_type(&dummy_bmff_hash)
+ }
/// Return list of ingredient assertions. This function
/// is only useful on commited or loaded claims since ingredients
/// are resolved at commit time.
- pub fn ingredient_assertions(&self) -> Vec<Assertion> {
+ pub fn ingredient_assertions(&self) -> Vec<&Assertion> {
let dummy_data = AssertionData::Cbor(Vec::new());
let dummy_ingredient = Assertion::new(labels::INGREDIENT, None, dummy_data);
self.assertions_by_type(&dummy_ingredient)
@@ -1050,12 +1132,12 @@ impl Claim {
.collect()
}
- pub fn assertions_by_type(&self, assertion_proto: &Assertion) -> Vec<Assertion> {
+ pub fn assertions_by_type(&self, assertion_proto: &Assertion) -> Vec<&Assertion> {
self.assertion_store
.iter()
.filter_map(|x| {
if Assertion::assertions_eq(assertion_proto, x.assertion()) {
- Some(x.assertion.clone())
+ Some(&x.assertion)
} else {
None
}
diff --git a/sdk/src/jumbf_io.rs b/sdk/src/jumbf_io.rs
@@ -12,26 +12,59 @@
// each license.
use crate::{
- asset_handlers::{c2pa_io::C2paIO, jpeg_io::JpegIO, png_io::PngIO},
+ asset_handlers::{bmff_io::BmffIO, c2pa_io::C2paIO, jpeg_io::JpegIO, png_io::PngIO},
asset_io::{AssetIO, CAILoader, HashObjectPositions},
error::{Error, Result},
};
use std::{
- fs,
+ fs::{self, File},
io::Cursor,
path::{Path, PathBuf},
};
-static SUPPORTED_TYPES: &[&str; 6] = &[
+static SUPPORTED_TYPES: [&str; 18] = [
+ "avif",
"c2pa", // stand-alone manifest file
+ "heif",
+ "heic",
"jpg",
"jpeg",
+ "mp4",
+ "m4a",
+ "mov",
"png",
+ "application/mp4",
+ "audio/mp4",
+ "image/avif",
+ "image/heic",
+ "image/heif",
"image/jpeg",
"image/png",
+ "video/mp4",
];
+#[cfg(feature = "file_io")]
+static BMFF_TYPES: [&str; 12] = [
+ "avif",
+ "heif",
+ "heic",
+ "mp4",
+ "m4a",
+ "mov",
+ "application/mp4",
+ "audio/mp4",
+ "image/avif",
+ "image/heic",
+ "image/heif",
+ "video/mp4",
+];
+
+#[cfg(feature = "file_io")]
+pub(crate) fn is_bmff_format(asset_type: &str) -> bool {
+ BMFF_TYPES.contains(&asset_type)
+}
+
/// Return jumbf block from in memory asset
pub fn load_jumbf_from_memory(asset_type: &str, data: &[u8]) -> Result<Vec<u8>> {
let mut buf_reader = Cursor::new(data);
@@ -47,19 +80,28 @@ pub fn load_jumbf_from_memory(asset_type: &str, data: &[u8]) -> Result<Vec<u8>>
}
pub fn get_assetio_handler(ext: &str) -> Option<Box<dyn AssetIO>> {
- match ext {
+ let ext = ext.to_lowercase();
+ match ext.as_ref() {
"c2pa" => Some(Box::new(C2paIO {})),
"jpg" | "jpeg" => Some(Box::new(JpegIO {})),
"png" => Some(Box::new(PngIO {})),
+ "mp4" | "m4a" | "mov" if cfg!(feature = "bmff") => Some(Box::new(BmffIO::new(&ext))),
_ => None,
}
}
pub fn get_cailoader_handler(asset_type: &str) -> Option<Box<dyn CAILoader>> {
- match asset_type {
+ let asset_type = asset_type.to_lowercase();
+ match asset_type.as_ref() {
"c2pa" | "application/c2pa" => Some(Box::new(C2paIO {})),
"jpg" | "jpeg" | "image/jpeg" => Some(Box::new(JpegIO {})),
"png" | "image/png" => Some(Box::new(PngIO {})),
+ "avif" | "heif" | "heic" | "mp4" | "m4a" | "application/mp4" | "audio/mp4"
+ | "image/avif" | "image/heic" | "image/heif" | "video/mp4"
+ if cfg!(feature = "bmff") && !cfg!(target_arch = "wasm32") =>
+ {
+ Some(Box::new(BmffIO::new(&asset_type)))
+ }
_ => None,
}
}
@@ -144,8 +186,11 @@ pub fn update_file_jumbf(
pub fn load_jumbf_from_file(in_path: &Path) -> Result<Vec<u8>> {
let ext = get_file_extension(in_path).ok_or(Error::UnsupportedType)?;
- match get_assetio_handler(&ext) {
- Some(asset_handler) => asset_handler.read_cai_store(in_path),
+ match get_cailoader_handler(&ext) {
+ Some(asset_handler) => {
+ let mut f = File::open(in_path)?;
+ asset_handler.read_cai(&mut f)
+ }
_ => Err(Error::UnsupportedType),
}
}
diff --git a/sdk/src/store.rs b/sdk/src/store.rs
@@ -27,12 +27,13 @@ use crate::{
#[cfg(feature = "file_io")]
use crate::{
assertion::AssertionData,
- assertions::DataHash,
+ assertions::{BmffHash, DataHash, DataMap, ExclusionsMap, SubsetMap},
asset_io::{HashBlockObjectType, HashObjectPositions},
cose_sign::cose_sign,
cose_validator::verify_cose,
jumbf_io::{
- get_supported_file_extension, load_jumbf_from_file, object_locations, save_jumbf_to_file,
+ get_supported_file_extension, is_bmff_format, load_jumbf_from_file, object_locations,
+ save_jumbf_to_file,
},
utils::{
hash_utils::{hash256, Exclusion},
@@ -989,7 +990,7 @@ impl Store {
// walk the ingredients
for i in claim.ingredient_assertions() {
- let ingredient_assertion = Ingredient::from_assertion(&i)?;
+ let ingredient_assertion = Ingredient::from_assertion(i)?;
// is this an ingredient
if let Some(ref c2pa_manifest) = &ingredient_assertion.c2pa_manifest {
@@ -1097,7 +1098,7 @@ impl Store {
) -> Result<()> {
// walk the ingredients
for i in claim.ingredient_assertions() {
- let ingredient_assertion = Ingredient::from_assertion(&i)?;
+ let ingredient_assertion = Ingredient::from_assertion(i)?;
// is this an ingredient
if let Some(ref c2pa_manifest) = &ingredient_assertion.c2pa_manifest {
@@ -1262,6 +1263,102 @@ impl Store {
Ok(hashes)
}
+ #[cfg(feature = "file_io")]
+ fn generate_bmff_data_hashes(
+ asset_path: &Path,
+ alg: &str,
+ calc_hashes: bool,
+ ) -> Result<Vec<BmffHash>> {
+ use serde_bytes::ByteBuf;
+
+ // The spec has mandatory BMFF exclusion ranges for certain atoms.
+ // The function makes sure those are included.
+
+ let mut hashes: Vec<BmffHash> = Vec::new();
+
+ let mut dh = BmffHash::new("jumbf manifest", alg, None);
+ let exclusions = dh.exclusions_mut();
+
+ // jumbf exclusion
+ let mut uuid = ExclusionsMap::new("/uuid".to_owned());
+ let data = DataMap {
+ offset: 8,
+ value: vec![
+ 216, 254, 195, 214, 27, 14, 72, 60, 146, 151, 88, 40, 135, 126, 196, 129,
+ ], // C2PA identifier
+ };
+ let data_vec = vec![data];
+ uuid.data = Some(data_vec);
+ exclusions.push(uuid);
+
+ // ftyp exclusion
+ let ftyp = ExclusionsMap::new("/ftyp".to_owned());
+ exclusions.push(ftyp);
+
+ // meta/iloc exclusion
+ let iloc = ExclusionsMap::new("/meta/iloc".to_owned());
+ exclusions.push(iloc);
+
+ // /mfra/tfra exclusion
+ let tfra = ExclusionsMap::new("/mfra/tfra".to_owned());
+ exclusions.push(tfra);
+
+ // /moov/trak/mdia/minf/stbl/stco exclusion
+ let mut stco = ExclusionsMap::new("/moov/trak/mdia/minf/stbl/stco".to_owned());
+ let subset_stco = SubsetMap {
+ offset: 16,
+ length: 0,
+ };
+ let subset_stco_vec = vec![subset_stco];
+ stco.subset = Some(subset_stco_vec);
+ exclusions.push(stco);
+
+ // /moov/trak/mdia/minf/stbl/co64 exclusion
+ let mut co64 = ExclusionsMap::new("/moov/trak/mdia/minf/stbl/co64".to_owned());
+ let subset_co64 = SubsetMap {
+ offset: 16,
+ length: 0,
+ };
+ let subset_co64_vec = vec![subset_co64];
+ co64.subset = Some(subset_co64_vec);
+ exclusions.push(co64);
+
+ // /moof/traf/tfhd exclusion
+ let mut tfhd = ExclusionsMap::new("/moof/traf/tfhd".to_owned());
+ let subset_tfhd = SubsetMap {
+ offset: 16,
+ length: 8,
+ };
+ let subset_tfhd_vec = vec![subset_tfhd];
+ tfhd.subset = Some(subset_tfhd_vec);
+ tfhd.flags = Some(ByteBuf::from([1, 0, 0]));
+ exclusions.push(tfhd);
+
+ // /moof/traf/trun exclusion
+ let mut trun = ExclusionsMap::new("/moof/traf/trun".to_owned());
+ let subset_trun = SubsetMap {
+ offset: 16,
+ length: 4,
+ };
+ let subset_trun_vec = vec![subset_trun];
+ trun.subset = Some(subset_trun_vec);
+ trun.flags = Some(ByteBuf::from([1, 0, 0]));
+ exclusions.push(trun);
+
+ if calc_hashes {
+ dh.gen_hash(asset_path)?;
+ } else {
+ match alg {
+ "sha256" => dh.set_hash([0u8; 32].to_vec()),
+ "sha384" => dh.set_hash([0u8; 48].to_vec()),
+ "sha512" => dh.set_hash([0u8; 64].to_vec()),
+ _ => return Err(Error::UnsupportedType),
+ }
+ }
+ hashes.push(dh);
+
+ Ok(hashes)
+ }
/// Embed the claims store as jumbf into an asset. Updates XMP with provenance record.
#[cfg(feature = "file_io")]
pub fn save_to_asset(
@@ -1321,7 +1418,7 @@ impl Store {
) -> Result<Vec<u8>> {
// clone the source to working copy if requested
get_supported_file_extension(asset_path).ok_or(Error::UnsupportedType)?; // verify extensions
- let _ext = get_supported_file_extension(output_path).ok_or(Error::UnsupportedType)?;
+ let ext = get_supported_file_extension(output_path).ok_or(Error::UnsupportedType)?;
if asset_path != output_path {
fs::copy(&asset_path, &output_path).map_err(Error::IoError)?;
}
@@ -1338,49 +1435,84 @@ impl Store {
return Err(Error::XmpWriteError);
}
+ let is_bmff = is_bmff_format(&ext);
+
+ let mut data;
+ let jumbf_size;
+
// get the provenance claim
let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
- // 2) Get hash ranges if needed, do not generate for update manifests
- let mut hash_ranges = object_locations(output_path)?;
- let hashes: Vec<DataHash> = if pc.update_manifest() {
- Vec::new()
- } else {
- Store::generate_data_hashes(output_path, pc.alg(), &mut hash_ranges, false)?
- };
-
- // add the placeholder data hashes to provenance claim so that the required space is reserved
- for mut hash in hashes {
- // add padding to account for possible cbor expansion of final DataHash
- let padding: Vec<u8> = vec![0x0; 10];
- hash.add_padding(padding);
+ if is_bmff {
+ // 2) Get hash ranges if needed, do not generate for update manifests
+ if !pc.update_manifest() {
+ let bmff_hashes = Store::generate_bmff_data_hashes(output_path, pc.alg(), false)?;
+ for hash in bmff_hashes {
+ pc.add_assertion(&hash)?;
+ }
+ }
- pc.add_assertion(&hash)?;
- }
+ // 3) Generate in memory CAI jumbf block
+ // and write preliminary jumbf store to file
+ // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned
+ data = self.to_jumbf_internal(reserve_size)?;
+ jumbf_size = data.len();
+ save_jumbf_to_file(&data, output_path, Some(output_path))?;
- // 3) Generate in memory CAI jumbf block
- // and write preliminary jumbf store to file
- // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned
- let mut data = self.to_jumbf_internal(reserve_size)?;
- let jumbf_size = data.len();
- save_jumbf_to_file(&data, output_path, Some(output_path))?;
+ // generate actual hash values
+ let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; // reborrow to change mutability
- // 4) determine final object locations and patch the asset hashes with correct offset
- // replace the source with correct asset hashes so that the claim hash will be correct
- let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
+ if !pc.update_manifest() {
+ let bmff_hashes = pc.bmff_hash_assertions();
- // get the final hash ranges, but not for update manifests
- let mut new_hash_ranges = object_locations(output_path)?;
- let updated_hashes = if pc.update_manifest() {
- Vec::new()
+ if !bmff_hashes.is_empty() {
+ let mut bmff_hash = BmffHash::from_assertion(bmff_hashes[0])?;
+ bmff_hash.gen_hash(output_path)?;
+ pc.update_bmff_hash(bmff_hash)?;
+ }
+ }
} else {
- Store::generate_data_hashes(output_path, pc.alg(), &mut new_hash_ranges, true)?
- };
+ // 2) Get hash ranges if needed, do not generate for update manifests
+ let mut hash_ranges = object_locations(output_path)?;
+ let hashes: Vec<DataHash> = if pc.update_manifest() {
+ Vec::new()
+ } else {
+ Store::generate_data_hashes(output_path, pc.alg(), &mut hash_ranges, false)?
+ };
+
+ // add the placeholder data hashes to provenance claim so that the required space is reserved
+ for mut hash in hashes {
+ // add padding to account for possible cbor expansion of final DataHash
+ let padding: Vec<u8> = vec![0x0; 10];
+ hash.add_padding(padding);
+
+ pc.add_assertion(&hash)?;
+ }
- // patch existing claim hash with updated data
- for mut hash in updated_hashes {
- hash.gen_hash(output_path)?; // generate
- pc.update_data_hash(hash)?;
+ // 3) Generate in memory CAI jumbf block
+ // and write preliminary jumbf store to file
+ // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned
+ data = self.to_jumbf_internal(reserve_size)?;
+ jumbf_size = data.len();
+ save_jumbf_to_file(&data, output_path, Some(output_path))?;
+
+ // 4) determine final object locations and patch the asset hashes with correct offset
+ // replace the source with correct asset hashes so that the claim hash will be correct
+ let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
+
+ // get the final hash ranges, but not for update manifests
+ let mut new_hash_ranges = object_locations(output_path)?;
+ let updated_hashes = if pc.update_manifest() {
+ Vec::new()
+ } else {
+ Store::generate_data_hashes(output_path, pc.alg(), &mut new_hash_ranges, true)?
+ };
+
+ // patch existing claim hash with updated data
+ for mut hash in updated_hashes {
+ hash.gen_hash(output_path)?; // generate
+ pc.update_data_hash(hash)?;
+ }
}
// regenerate the jumbf because the cbor changed
@@ -2445,4 +2577,19 @@ pub mod tests {
let store = Store::load_from_asset(&ap, true, &mut report).expect("load_from_asset");
println!("store = {}", store);
}
+
+ #[test]
+ #[cfg(feature = "bmff")]
+ fn test_bmff() {
+ let ap = fixture_path("video1.mp4");
+ let mut report = DetailedStatusTracker::new();
+ let store = Store::load_from_asset(&ap, true, &mut report).expect("load_from_asset");
+
+ let errors = report_split_errors(report.get_log_mut());
+
+ println!("Error report for {}: {:?}", ap.as_display(), errors);
+ assert!(errors.is_empty());
+
+ println!("store = {}", store);
+ }
}
diff --git a/sdk/tests/fixtures/video1.mp4 b/sdk/tests/fixtures/video1.mp4
Binary files differ.