commit 0a735a0910b10e085ee36b1e02b15d935774f090
parent 18204cc2ecdc7ba0e18dc2ec619dad281da3dd9d
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date: Tue, 21 Feb 2023 17:54:28 -0500
Bmff v2 (#186)
* Check for more than one manifest store
* Fix grabbing wrong UUID
* Updated to fix playback issue
* Add comment
* Initial code
* BMFF 1.1 hash support
* fix missing file
* Test wasm fix
* Slightly more readable check
* Better variable name
* Fix typos in comments
* Update to incorporate comments
Diffstat:
6 files changed, 203 insertions(+), 96 deletions(-)
diff --git a/sdk/src/assertions/bmff_hash.rs b/sdk/src/assertions/bmff_hash.rs
@@ -30,7 +30,7 @@ use crate::{
Error,
};
-const ASSERTION_CREATION_VERSION: usize = 1;
+const ASSERTION_CREATION_VERSION: usize = 2;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct ExclusionsMap {
@@ -109,8 +109,11 @@ pub struct BmffHash {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<UriT>,
- #[serde(skip_deserializing, skip_serializing)]
+ #[serde(skip)]
pub path: PathBuf,
+
+ #[serde(skip)]
+ bmff_version: usize,
}
impl BmffHash {
@@ -123,6 +126,7 @@ impl BmffHash {
name: Some(name.to_string()),
url,
path: PathBuf::new(),
+ bmff_version: ASSERTION_CREATION_VERSION,
}
}
@@ -159,6 +163,14 @@ impl BmffHash {
self.url.as_ref()
}
+ pub fn bmff_version(&self) -> usize {
+ self.bmff_version
+ }
+
+ fn set_bmff_version(&mut self, version: usize) {
+ self.bmff_version = version;
+ }
+
/// Returns `true` if this is a remote hash.
pub fn is_remote_hash(&self) -> bool {
self.url.is_some()
@@ -200,7 +212,8 @@ impl BmffHash {
// convert BMFF exclusion map to flat exclusion list
let mut data = fs::File::open(asset_path)?;
- let exclusions = bmff_to_jumbf_exclusions(&mut data, bmff_exclusions)?;
+ let exclusions =
+ bmff_to_jumbf_exclusions(&mut data, bmff_exclusions, self.bmff_version > 1)?;
let hash = hash_asset_by_alg(&alg, asset_path, Some(exclusions))?;
@@ -225,7 +238,8 @@ impl BmffHash {
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)?;
+ let exclusions =
+ bmff_to_jumbf_exclusions(&mut data_reader, bmff_exclusions, self.bmff_version > 1)?;
if verify_by_alg(&curr_alg, &self.hash, data, Some(exclusions)) {
Ok(())
@@ -241,7 +255,8 @@ impl BmffHash {
// convert BMFF exclusion map to flat exclusion list
let mut data = fs::File::open(asset_path)?;
- let exclusions = bmff_to_jumbf_exclusions(&mut data, bmff_exclusions)?;
+ let exclusions =
+ bmff_to_jumbf_exclusions(&mut data, bmff_exclusions, self.bmff_version > 1)?;
if verify_asset_by_alg(curr_alg, &self.hash, asset_path, Some(exclusions)) {
Ok(())
@@ -255,13 +270,16 @@ impl AssertionCbor for BmffHash {}
impl AssertionBase for BmffHash {
const LABEL: &'static str = Self::LABEL;
- const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
+ const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION); // todo: this mechanism needs to change since a struct could support different versions
fn to_assertion(&self) -> Result<Assertion> {
Self::to_cbor_assertion(self)
}
fn from_assertion(assertion: &Assertion) -> Result<Self> {
- Self::from_cbor_assertion(assertion)
+ let mut bmff_hash = Self::from_cbor_assertion(assertion)?;
+ bmff_hash.set_bmff_version(assertion.get_ver().unwrap_or(1));
+
+ Ok(bmff_hash)
}
}
diff --git a/sdk/src/assertions/data_hash.rs b/sdk/src/assertions/data_hash.rs
@@ -11,7 +11,10 @@
// specific language governing permissions and limitations under
// each license.
-use std::path::*;
+use std::{
+ io::{Read, Seek},
+ path::*,
+};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
@@ -19,7 +22,6 @@ use serde_bytes::ByteBuf;
use crate::{
assertion::{Assertion, AssertionBase, AssertionCbor},
assertions::labels,
- asset_io::CAIReadWrite,
cbor_types::UriT,
error::{Error, Result},
utils::hash_utils::{hash_stream_by_alg, verify_asset_by_alg, verify_by_alg, Exclusion},
@@ -106,7 +108,10 @@ impl DataHash {
}
/// generate the hash value for the Asset stream using the range from the DataHash
- pub fn gen_hash_from_stream(&mut self, stream: &mut dyn CAIReadWrite) -> Result<()> {
+ pub fn gen_hash_from_stream<R>(&mut self, stream: &mut R) -> Result<()>
+ where
+ R: Read + Seek + ?Sized,
+ {
self.hash = self.hash_from_stream(stream)?;
Ok(())
}
@@ -156,7 +161,10 @@ impl DataHash {
/// generate the asset hash from a stream using the constructed
/// start and length values
- pub fn hash_from_stream(&mut self, stream: &mut dyn CAIReadWrite) -> Result<Vec<u8>> {
+ pub fn hash_from_stream<R>(&mut self, stream: &mut R) -> Result<Vec<u8>>
+ where
+ R: Read + Seek + ?Sized,
+ {
if self.is_remote_hash() {
return Err(Error::BadParam(
"asset hash is remote, not yet supported".to_owned(),
diff --git a/sdk/src/asset_handlers/bmff_io.rs b/sdk/src/asset_handlers/bmff_io.rs
@@ -45,7 +45,8 @@ impl BmffIO {
}
}
-const HEADER_SIZE: u64 = 8;
+const HEADER_SIZE: u64 = 8; // 4 byte type + 4 byte size
+const HEADER_SIZE_LARGE: u64 = 16; // 4 byte type + 4 byte size + 8 byte large size
const C2PA_UUID: [u8; 16] = [
0xD8, 0xFE, 0xC3, 0xD6, 0x1B, 0x0E, 0x48, 0x3C, 0x92, 0x97, 0x58, 0x28, 0x87, 0x7E, 0xC4, 0x81,
@@ -165,6 +166,7 @@ struct BoxHeaderLite {
pub name: BoxType,
pub size: u64,
pub fourcc: String,
+ pub large_size: bool,
}
impl BoxHeaderLite {
@@ -173,6 +175,7 @@ impl BoxHeaderLite {
name,
size,
fourcc: fourcc.to_string(),
+ large_size: false,
}
}
pub fn read<R: Read + ?Sized>(reader: &mut R) -> Result<Self> {
@@ -198,14 +201,16 @@ impl BoxHeaderLite {
Ok(BoxHeaderLite {
name: BoxType::from(typ),
- size: largesize - HEADER_SIZE,
+ size: largesize,
fourcc,
+ large_size: true,
})
} else {
Ok(BoxHeaderLite {
name: BoxType::from(typ),
size: size as u64,
fourcc,
+ large_size: false,
})
}
}
@@ -252,8 +257,12 @@ 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) -> Result<u64> {
- Ok(reader.stream_position()? - HEADER_SIZE)
+fn box_start(reader: &mut dyn CAIRead, is_large: bool) -> Result<u64> {
+ if is_large {
+ Ok(reader.stream_position()? - HEADER_SIZE_LARGE)
+ } else {
+ Ok(reader.stream_position()? - HEADER_SIZE)
+ }
}
fn _skip_bytes(reader: &mut dyn CAIRead, size: u64) -> Result<()> {
@@ -266,12 +275,6 @@ fn skip_bytes_to(reader: &mut dyn CAIRead, pos: u64) -> Result<u64> {
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],
@@ -360,9 +363,30 @@ fn path_from_token(bmff_tree: &mut Arena<BoxInfo>, current_node_token: &Token) -
Ok(path)
}
+fn get_top_level_box_offsets(
+ bmff_tree: &Arena<BoxInfo>,
+ bmff_path_map: &HashMap<String, Vec<Token>>,
+) -> Vec<u64> {
+ let mut tl_offsets = Vec::new();
+
+ for (p, t) in bmff_path_map {
+ // look for top level offsets
+ if p.matches('/').count() == 1 {
+ for token in t {
+ if let Some(box_info) = bmff_tree.get(*token) {
+ tl_offsets.push(box_info.data.offset);
+ }
+ }
+ }
+ }
+
+ tl_offsets
+}
+
pub fn bmff_to_jumbf_exclusions(
reader: &mut dyn CAIRead,
bmff_exclusions: &[ExclusionsMap],
+ bmff_v2: bool,
) -> Result<Vec<Exclusion>> {
let start = reader.stream_position()?;
let size = reader.seek(SeekFrom::End(0))?;
@@ -386,6 +410,10 @@ pub fn bmff_to_jumbf_exclusions(
// build layout of the BMFF structure
build_bmff_tree(reader, size, &mut bmff_tree, &root_token, &mut bmff_map)?;
+ // get top level box offsets
+ let mut tl_offsets = get_top_level_box_offsets(&bmff_tree, &bmff_map);
+ tl_offsets.sort();
+
let mut exclusions = Vec::new();
for bmff_exclusion in bmff_exclusions {
@@ -481,17 +509,37 @@ pub fn bmff_to_jumbf_exclusions(
subset.length as u64
}) as usize,
);
+
exclusions.push(exclusion);
}
} else {
+ // exclude box in its entirty
let exclusion =
Exclusion::new(exclusion_start as usize, exclusion_length as usize);
+
exclusions.push(exclusion);
+
+ // for BMFF V2 hashes we do not add hash offsets for top level boxes
+ // that are completely excluded, so remove from BMFF V2 hash offset calc
+ if let Some(pos) = tl_offsets.iter().position(|x| *x == exclusion_start) {
+ tl_offsets.remove(pos);
+ }
}
}
}
}
+ // add remaining top level offsets to be included when generating BMFF V2 hashes
+ // note: this is technically not an exclusion but a replacement with a new range of bytes to be hashed
+ if bmff_v2 {
+ for tl_start in tl_offsets {
+ let mut exclusion = Exclusion::new(tl_start as usize, 1);
+ exclusion.set_bmff_offset(tl_start);
+
+ exclusions.push(exclusion);
+ }
+ }
+
Ok(exclusions)
}
@@ -637,7 +685,7 @@ pub(crate) fn build_bmff_tree(
// Match and parse the supported atom boxes.
match header.name {
BoxType::UuidBox => {
- let start = box_start(reader)?;
+ let start = box_start(reader, header.large_size)?;
let mut extended_type = [0u8; 16]; // 16 bytes of UUID
reader.read_exact(&mut extended_type)?;
@@ -680,7 +728,7 @@ pub(crate) fn build_bmff_tree(
| BoxType::MfraBox
| BoxType::MetaBox
| BoxType::SchiBox => {
- let start = box_start(reader)?;
+ let start = box_start(reader, header.large_size)?;
let b = if FULL_BOX_TYPES.contains(&header.fourcc.as_str()) {
let (version, flags) = read_box_header_ext(reader)?; // box extensions
@@ -727,7 +775,7 @@ pub(crate) fn build_bmff_tree(
skip_bytes_to(reader, start + s)?;
}
_ => {
- let start = reader.stream_position()? - HEADER_SIZE;
+ let start = box_start(reader, header.large_size)?;
let b = if FULL_BOX_TYPES.contains(&header.fourcc.as_str()) {
let (version, flags) = read_box_header_ext(reader)?; // box extensions
diff --git a/sdk/src/store.rs b/sdk/src/store.rs
@@ -12,7 +12,7 @@
// each license.
#[cfg(feature = "sign")]
-use std::io::SeekFrom;
+use std::io::{Read, Seek, SeekFrom};
use std::{collections::HashMap, io::Cursor};
#[cfg(feature = "file_io")]
use std::{fs, path::Path};
@@ -1337,12 +1337,15 @@ impl Store {
// generate a list of AssetHashes based on the location of objects in the stream
#[cfg(feature = "sign")]
- fn generate_data_hashes_for_stream(
- stream: &mut dyn CAIReadWrite,
+ fn generate_data_hashes_for_stream<R>(
+ stream: &mut R,
alg: &str,
block_locations: &mut Vec<HashObjectPositions>,
calc_hashes: bool,
- ) -> Result<Vec<DataHash>> {
+ ) -> Result<Vec<DataHash>>
+ where
+ R: Read + Seek + ?Sized,
+ {
if block_locations.is_empty() {
let out: Vec<DataHash> = vec![];
return Ok(out);
@@ -1485,6 +1488,17 @@ impl Store {
trun.flags = Some(ByteBuf::from([1, 0, 0]));
exclusions.push(trun);
+ // V2 exclusions
+ // /mdat exclusion
+ let mut mdat = ExclusionsMap::new("/mdat".to_owned());
+ let subset_mdat = SubsetMap {
+ offset: 16,
+ length: 0,
+ };
+ let subset_mdat_vec = vec![subset_mdat];
+ mdat.subset = Some(subset_mdat_vec);
+ exclusions.push(mdat);
+
if calc_hashes {
dh.gen_hash(asset_path)?;
} else {
@@ -2023,8 +2037,6 @@ impl Store {
#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "file_io")]
fn fetch_remote_manifest(url: &str) -> Result<Vec<u8>> {
- use std::io::Read;
-
use conv::ValueFrom;
use ureq::Error as uError;
@@ -3180,6 +3192,8 @@ pub mod tests {
let ap = fixture_path("CA.jpg");
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!("store = {store}");
}
@@ -3194,6 +3208,15 @@ pub mod tests {
#[test]
#[cfg(all(feature = "file_io", feature = "bmff"))]
+ fn test_bmff_legacy() {
+ // test 1.0 bmff hash
+ let ap = fixture_path("legacy.mp4");
+ let mut report = DetailedStatusTracker::new();
+ let store = Store::load_from_asset(&ap, true, &mut report).expect("load_from_asset");
+ println!("store = {store}");
+ }
+ #[test]
+ #[cfg(all(feature = "file_io", feature = "bmff"))]
fn test_bmff_jumbf_generation() {
// test adding to actual image
let ap = fixture_path("video1.mp4");
diff --git a/sdk/src/utils/hash_utils.rs b/sdk/src/utils/hash_utils.rs
@@ -13,12 +13,13 @@
use std::{
fs::File,
- io::{Read, Seek, SeekFrom},
+ io::{Cursor, Read, Seek, SeekFrom},
ops::RangeInclusive,
path::Path,
};
-use log::{debug, warn};
+//use conv::ValueFrom;
+use log::warn;
// multihash versions
use multibase::{decode, encode};
use multihash::{wrap, Code, Multihash, Sha2_256, Sha2_512, Sha3_256, Sha3_384, Sha3_512};
@@ -27,7 +28,7 @@ use serde::{Deserialize, Serialize};
// direct sha functions
use sha2::{Digest, Sha256, Sha384, Sha512};
-use crate::{asset_io::CAIReadWrite, Error, Result};
+use crate::{Error, Result};
const MAX_HASH_BUF: usize = 256 * 1024 * 1024; // cap memory usage to 256MB
@@ -35,11 +36,16 @@ const MAX_HASH_BUF: usize = 256 * 1024 * 1024; // cap memory usage to 256MB
pub struct Exclusion {
start: usize,
length: usize,
+ bmff_offset: Option<u64>, // optional offset position to include in BMFF_V2 hashes in BE format
}
impl Exclusion {
pub fn new(start: usize, length: usize) -> Self {
- Exclusion { start, length }
+ Exclusion {
+ start,
+ length,
+ bmff_offset: None,
+ }
}
/// update the start value
@@ -57,6 +63,16 @@ impl Exclusion {
pub fn length(&self) -> usize {
self.length
}
+
+ // set offset for BMFF_V2 to be hashed in addition to data
+ pub fn set_bmff_offset(&mut self, offset: u64) {
+ self.bmff_offset = Some(offset);
+ }
+
+ // get option offset for BMFF_V2 hash
+ pub fn bmff_offset(&self) -> Option<u64> {
+ self.bmff_offset
+ }
}
/// Compare two byte vectors return true if match, false otherwise
@@ -115,61 +131,9 @@ impl Hasher {
// Return hash bytes for desired hashing algorithm.
pub fn hash_by_alg(alg: &str, data: &[u8], exclusions: Option<Vec<Exclusion>>) -> Vec<u8> {
- use Hasher::*;
- let mut hasher_enum = match alg {
- "sha256" => SHA256(Sha256::new()),
- "sha384" => SHA384(Sha384::new()),
- "sha512" => SHA512(Sha512::new()),
- _ => {
- warn!(
- "Unsupported hashing algorithm: {}, substituting sha256",
- alg
- );
- SHA256(Sha256::new())
- }
- };
-
- match exclusions {
- Some(mut e) if !e.is_empty() => {
- // hash data skipping excluded regions
- // sort the exclusions
- e.sort_by_key(|a| a.start());
+ let mut reader = Cursor::new(data);
- // verify structure of blocks
- let num_blocks = e.len();
- let exclusion_end = e[num_blocks - 1].start() + e[num_blocks - 1].length();
- let data_len = data.len();
- let data_end = data_len - 1;
-
- // if not enough range we will just calc to the end
- if data_len < exclusion_end {
- debug!("the exclusion range exceed the data length");
- return Vec::new();
- }
-
- //build final ranges
- let mut ranges = RangeSet::<[RangeInclusive<usize>; 1]>::from(0..=data_end);
- for exclusion in e {
- let end = exclusion.start() + exclusion.length() - 1;
- ranges.remove_range(exclusion.start()..=end);
- }
-
- // hash the data for ranges
- for r in ranges.into_smallvec() {
- hasher_enum.update(&data[r]);
- }
-
- // return the hash
- Hasher::finalize(hasher_enum)
- }
- _ => {
- // add the data
- hasher_enum.update(data);
-
- // return the hash
- Hasher::finalize(hasher_enum)
- }
- }
+ hash_stream_by_alg(alg, &mut reader, exclusions).unwrap_or(Vec::new())
}
// Return hash bytes for asset using desired hashing algorithm.
@@ -183,11 +147,16 @@ pub fn hash_asset_by_alg(
}
// Return hash bytes for stream using desired hashing algorithm.
-pub fn hash_stream_by_alg(
+pub fn hash_stream_by_alg<R>(
alg: &str,
- data: &mut dyn CAIReadWrite,
+ data: &mut R,
exclusions: Option<Vec<Exclusion>>,
-) -> Result<Vec<u8>> {
+) -> Result<Vec<u8>>
+where
+ R: Read + Seek + ?Sized,
+{
+ let mut bmff_v2_starts: Vec<u64> = Vec::new();
+
use Hasher::*;
let mut hasher_enum = match alg {
"sha256" => SHA256(Sha256::new()),
@@ -224,24 +193,55 @@ pub fn hash_stream_by_alg(
}
//build final ranges
+ let mut ranges_vec: Vec<RangeInclusive<u64>> = Vec::new();
let mut ranges = RangeSet::<[RangeInclusive<u64>; 1]>::from(0..=data_end);
for exclusion in e {
let end = (exclusion.start() + exclusion.length() - 1) as u64;
let exclusion_start = exclusion.start() as u64;
ranges.remove_range(exclusion_start..=end);
+
+ // add new BMFF V2 offset as a new range to be included so that we can
+ // pause to add the offset hash
+ if let Some(offset) = exclusion.bmff_offset() {
+ ranges_vec.push(RangeInclusive::new(offset, offset));
+ bmff_v2_starts.push(offset);
+ }
}
- ranges
+ // merge standard ranges and BMFF V2 ranges into single list
+ if !bmff_v2_starts.is_empty() {
+ // add regularly included ranges
+ for r in ranges.into_smallvec() {
+ ranges_vec.push(r);
+ }
+
+ // sort by start position
+ ranges_vec.sort_by(|a, b| {
+ let a_start = a.start();
+ let b_start = b.start();
+ a_start.cmp(b_start)
+ });
+
+ ranges_vec
+ } else {
+ for r in ranges.into_smallvec() {
+ ranges_vec.push(r);
+ }
+ ranges_vec
+ }
}
_ => {
+ let mut ranges_vec: Vec<RangeInclusive<u64>> = Vec::new();
let data_end = data_len - 1;
- RangeSet::<[RangeInclusive<u64>; 1]>::from(0..=data_end)
+ ranges_vec.push(RangeInclusive::new(0_u64, data_end));
+
+ ranges_vec
}
};
- if cfg!(feature = "no_interleaved_io") {
+ if cfg!(feature = "no_interleaved_io") || cfg!(target_arch = "wasm32") {
// hash the data for ranges
- for r in ranges.into_smallvec() {
+ for r in ranges {
let start = r.start();
let end = r.end();
let mut chunk_left = end - start + 1;
@@ -249,6 +249,11 @@ pub fn hash_stream_by_alg(
// move to start of range
data.seek(SeekFrom::Start(*start))?;
+ // check to see if this range is an BMFF V2 offset to include in the hash
+ if bmff_v2_starts.contains(start) && (end - start) == 0 {
+ hasher_enum.update(&start.to_be_bytes());
+ }
+
loop {
let mut chunk = vec![0u8; std::cmp::min(chunk_left as usize, MAX_HASH_BUF)];
@@ -264,7 +269,7 @@ pub fn hash_stream_by_alg(
}
} else {
// hash the data for ranges
- for r in ranges.into_smallvec() {
+ for r in ranges {
let start = r.start();
let end = r.end();
let mut chunk_left = end - start + 1;
@@ -272,6 +277,11 @@ pub fn hash_stream_by_alg(
// move to start of range
data.seek(SeekFrom::Start(*start))?;
+ // check to see if this range is an BMFF V2 offset to include in the hash
+ if bmff_v2_starts.contains(start) && (end - start) == 0 {
+ hasher_enum.update(&start.to_be_bytes());
+ }
+
let mut chunk = vec![0u8; std::cmp::min(chunk_left as usize, MAX_HASH_BUF)];
data.read_exact(&mut chunk)?;
diff --git a/sdk/tests/fixtures/legacy.mp4 b/sdk/tests/fixtures/legacy.mp4
Binary files differ.