c2pa-rs

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

commit a7d8b9e5f8e1b3d8275030ddb1fc62071decf579
parent 665c5a359b761e3e48ada9c23a57d38dd681ce7c
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Fri, 10 Mar 2023 16:05:15 -0500

(MINOR) Riff support with refactored AssetIO (#203)

* Initial RIFF implementation

* AVI test

* webp support

* Disable XMP default writting

* Fix broken hashing

* Refactor asset handlers to control supported features

* Fix clippy errors

* Moved XMP generation to the asset handlers

* Remove XMP stuff from store since it is now performed by handlders

* XMP restrict some file types
BMFF generation bug for remote manifests

* Code cleanup

* Update version

* format issues

* more formatting fixes

* cleanup

* Bump major number since new error codes are available

* Fix PR comments

* Minor cleanup
Diffstat:
MREADME.md | 1-
Msdk/Cargo.toml | 3++-
Msdk/src/asset_handlers/bmff_io.rs | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msdk/src/asset_handlers/c2pa_io.rs | 30++++++++++++++++++++++++++++--
Msdk/src/asset_handlers/jpeg_io.rs | 93++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msdk/src/asset_handlers/mod.rs | 1+
Msdk/src/asset_handlers/png_io.rs | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Asdk/src/asset_handlers/riff_io.rs | 426+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msdk/src/asset_handlers/tiff_io.rs | 60+++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msdk/src/asset_io.rs | 56++++++++++++++++++++++++++++++++++++++++++++++++++------
Msdk/src/error.rs | 6++++++
Msdk/src/jumbf_io.rs | 255+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msdk/src/store.rs | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Asdk/tests/fixtures/sample1.wav | 0
Asdk/tests/fixtures/sample1.webp | 0
Asdk/tests/fixtures/test.avi | 0
16 files changed, 1246 insertions(+), 158 deletions(-)

diff --git a/README.md b/README.md @@ -61,7 +61,6 @@ NOTE: If you are building for WASM, omit the `file_io` dependency. The Rust SDK crate provides: * `async_signer` enables signing via asynchronous services which require `async` support. -* `bmff` enables handling of ISO base media file formats (BMFF) used for video. 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 @@ -22,7 +22,6 @@ rustdoc-args = ["--cfg", "docsrs"] default = ["add_thumbnails"] add_thumbnails = ["image"] async_signer = ["async-trait", "file_io"] -bmff = [] file_io = ["sign"] serialize_thumbnails = [] xmp_write = ["xmp_toolkit"] @@ -66,11 +65,13 @@ extfmt = "0.1.1" hex = "0.4.3" img-parts = "0.3.0" log = "0.4.8" +lazy_static = "1.4.0" multibase = "0.9.0" multihash = "0.11.4" png_pong = "0.8.2" quick-xml = "0.20.0" range-set = "0.0.9" +riff = "1.0.1" serde = { version = "1.0", features = ["derive"] } serde_bytes = "0.11.5" serde_cbor = "0.11.1" diff --git a/sdk/src/asset_handlers/bmff_io.rs b/sdk/src/asset_handlers/bmff_io.rs @@ -28,7 +28,7 @@ use tempfile::{Builder, NamedTempFile}; use crate::{ assertions::ExclusionsMap, - asset_io::{AssetIO, AssetPatch, CAILoader, CAIRead, HashObjectPositions}, + asset_io::{AssetIO, AssetPatch, CAIRead, CAIReader, HashObjectPositions, RemoteRefEmbed}, error::{Error, Result}, utils::hash_utils::{vec_compare, Exclusion}, }; @@ -37,13 +37,6 @@ 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; // 4 byte type + 4 byte size const HEADER_SIZE_LARGE: u64 = 16; // 4 byte type + 4 byte size + 8 byte large size @@ -65,6 +58,22 @@ const FULL_BOX_TYPES: &[&str; 80] = &[ "txtC", "mime", "uri ", "uriI", "hmhd", "sthd", "vvhd", "medc", ]; +static SUPPORTED_TYPES: [&str; 6] = [ + /*"avif", // disable for now while we test a little more + "heif", + "heic",*/ + "mp4", + "m4a", + "mov", + "application/mp4", + "audio/mp4", + /* + "image/avif", + "image/heic", + "image/heif",*/ + "video/mp4", +]; + // define CAIRead for tempfile impl CAIRead for NamedTempFile {} @@ -839,7 +848,7 @@ fn get_manifest_token( None } -impl CAILoader for BmffIO { +impl CAIReader for BmffIO { fn read_cai(&self, reader: &mut dyn CAIRead) -> Result<Vec<u8>> { let start = reader.stream_position()?; let size = reader.seek(SeekFrom::End(0))?; @@ -942,7 +951,7 @@ impl CAILoader for BmffIO { // Get XMP block fn read_xmp(&self, _asset_reader: &mut dyn CAIRead) -> Option<String> { - None + None // todo: figure out where XMP is stored for supported formats } } @@ -1231,6 +1240,30 @@ impl AssetIO for BmffIO { .or_else(|_| std::fs::copy(temp_file.path(), asset_path).and(Ok(()))) .map_err(Error::IoError) } + + 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 remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { + Some(self) + } + fn supported_types(&self) -> &[&str] { + &SUPPORTED_TYPES + } } impl AssetPatch for BmffIO { @@ -1307,7 +1340,36 @@ impl AssetPatch for BmffIO { } } -#[cfg(feature = "bmff")] +impl RemoteRefEmbed for BmffIO { + #[allow(unused_variables)] + fn embed_reference( + &self, + asset_path: &Path, + embed_ref: crate::asset_io::RemoteRefEmbedType, + ) -> Result<()> { + match embed_ref { + crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => { + #[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) + } + } + } + + #[cfg(not(feature = "xmp_write"))] + { + Err(crate::error::Error::MissingFeature("xmp_write".to_string())) + } + } + 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)] pub mod tests { #![allow(clippy::expect_used)] @@ -1317,14 +1379,16 @@ 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}, - }; + use crate::utils::test::{fixture_path, temp_dir_path}; + #[cfg(not(target_arch = "wasm32"))] #[test] fn test_read_mp4() { + use crate::{ + status_tracker::{report_split_errors, DetailedStatusTracker, StatusTracker}, + store::Store, + }; + let ap = fixture_path("video1.mp4"); let mut log = DetailedStatusTracker::default(); diff --git a/sdk/src/asset_handlers/c2pa_io.rs b/sdk/src/asset_handlers/c2pa_io.rs @@ -14,13 +14,20 @@ use std::{fs::File, path::Path}; use crate::{ - asset_io::{AssetIO, CAILoader, CAIRead, HashBlockObjectType, HashObjectPositions}, + asset_io::{AssetIO, CAIRead, CAIReader, HashBlockObjectType, HashObjectPositions}, error::{Error, Result}, }; + +static SUPPORTED_TYPES: [&str; 3] = [ + "c2pa", + "application/c2pa", + "application/x-c2pa-manifest-store", +]; + /// Supports working with ".c2pa" files containing only manifest store data pub struct C2paIO {} -impl CAILoader for C2paIO { +impl CAIReader for C2paIO { fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>> { let mut cai_data = Vec::new(); // read the whole file @@ -64,6 +71,25 @@ impl AssetIO for C2paIO { fn remove_cai_store(&self, _asset_path: &Path) -> Result<()> { Ok(()) } + + fn new(_asset_type: &str) -> Self + where + Self: Sized, + { + C2paIO {} + } + + fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> { + Box::new(C2paIO::new(asset_type)) + } + + fn get_reader(&self) -> &dyn CAIReader { + self + } + + fn supported_types(&self) -> &[&str] { + &SUPPORTED_TYPES + } } #[cfg(test)] diff --git a/sdk/src/asset_handlers/jpeg_io.rs b/sdk/src/asset_handlers/jpeg_io.rs @@ -21,12 +21,14 @@ use img_parts::{ use crate::{ asset_io::{ - AssetIO, CAILoader, CAIRead, CAIReadWrite, CAIWriter, HashBlockObjectType, - HashObjectPositions, + AssetIO, CAIRead, CAIReadWrite, CAIReader, CAIWriter, HashBlockObjectType, + HashObjectPositions, RemoteRefEmbed, }, error::{Error, Result}, }; +static SUPPORTED_TYPES: [&str; 3] = ["jpg", "jpeg", "image/jpeg"]; + const XMP_SIGNATURE: &[u8] = b"http://ns.adobe.com/xap/1.0/"; const XMP_SIGNATURE_BUFFER_SIZE: usize = XMP_SIGNATURE.len() + 1; // skip null or space char at end @@ -142,7 +144,7 @@ fn delete_cai_segments(jpeg: &mut img_parts::jpeg::Jpeg) -> Result<()> { pub struct JpegIO {} -impl CAILoader for JpegIO { +impl CAIReader for JpegIO { fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>> { let mut buffer: Vec<u8> = Vec::new(); @@ -453,6 +455,59 @@ impl AssetIO for JpegIO { Ok(()) } + + fn new(_asset_type: &str) -> Self + where + Self: Sized, + { + JpegIO {} + } + + fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> { + Box::new(JpegIO::new(asset_type)) + } + + fn get_reader(&self) -> &dyn CAIReader { + self + } + + fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> { + Some(Box::new(JpegIO::new(asset_type))) + } + + fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { + Some(self) + } + + fn supported_types(&self) -> &[&str] { + &SUPPORTED_TYPES + } +} + +impl RemoteRefEmbed for JpegIO { + #[allow(unused_variables)] + fn embed_reference( + &self, + asset_path: &Path, + embed_ref: crate::asset_io::RemoteRefEmbedType, + ) -> Result<()> { + match embed_ref { + crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => { + #[cfg(feature = "xmp_write")] + { + crate::embedded_xmp::add_manifest_uri_to_file(asset_path, &manifest_uri) + } + + #[cfg(not(feature = "xmp_write"))] + { + Err(crate::error::Error::MissingFeature("xmp_write".to_string())) + } + } + 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)] @@ -462,6 +517,7 @@ pub mod tests { use img_parts::Bytes; use super::*; + use crate::asset_io::RemoteRefEmbedType; #[test] fn test_extract_xmp() { @@ -499,4 +555,35 @@ pub mod tests { _ => unreachable!(), } } + + #[test] + fn test_xmp_read_write() { + let source = crate::utils::test::fixture_path("CA.jpg"); + + let temp_dir = tempfile::tempdir().unwrap(); + let output = crate::utils::test::temp_dir_path(&temp_dir, "CA_test.jpg"); + + std::fs::copy(source, &output).unwrap(); + + let test_msg = "this some test xmp data"; + let handler = JpegIO::new(""); + + // write xmp + let assetio_handler = handler.get_handler("jpg"); + + let remote_ref_handler = assetio_handler.remote_ref_writer_ref().unwrap(); + + remote_ref_handler + .embed_reference(&output, RemoteRefEmbedType::Xmp(test_msg.to_string())) + .unwrap(); + + // read back in XMP + let mut file_reader = std::fs::File::open(&output).unwrap(); + let read_xmp = assetio_handler + .get_reader() + .read_xmp(&mut file_reader) + .unwrap(); + + assert!(read_xmp.contains(test_msg)); + } } diff --git a/sdk/src/asset_handlers/mod.rs b/sdk/src/asset_handlers/mod.rs @@ -15,4 +15,5 @@ pub mod bmff_io; pub mod c2pa_io; pub mod jpeg_io; pub mod png_io; +pub mod riff_io; pub mod tiff_io; diff --git a/sdk/src/asset_handlers/png_io.rs b/sdk/src/asset_handlers/png_io.rs @@ -21,7 +21,9 @@ use byteorder::{BigEndian, ReadBytesExt}; use conv::ValueFrom; use crate::{ - asset_io::{AssetIO, CAILoader, CAIRead, HashBlockObjectType, HashObjectPositions}, + asset_io::{ + AssetIO, CAIRead, CAIReader, HashBlockObjectType, HashObjectPositions, RemoteRefEmbed, + }, error::{Error, Result}, }; @@ -32,6 +34,8 @@ const XMP_KEY: &str = "XML:com.adobe.xmp"; const PNG_END: [u8; 4] = *b"IEND"; const PNG_HDR_LEN: u64 = 12; +static SUPPORTED_TYPES: [&str; 2] = ["png", "image/png"]; + #[derive(Clone, Debug)] struct PngChunkPos { pub start: u64, @@ -172,7 +176,7 @@ fn read_string(asset_reader: &mut dyn CAIRead, max_read: u32) -> Result<String> } pub struct PngIO {} -impl CAILoader for PngIO { +impl CAIReader for PngIO { fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>> { let cai_data = get_cai_data(asset_reader)?; Ok(cai_data) @@ -419,6 +423,54 @@ impl AssetIO for PngIO { Ok(()) } + + fn new(_asset_type: &str) -> Self + where + Self: Sized, + { + PngIO {} + } + + fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> { + Box::new(PngIO::new(asset_type)) + } + + fn get_reader(&self) -> &dyn CAIReader { + self + } + fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { + Some(self) + } + + fn supported_types(&self) -> &[&str] { + &SUPPORTED_TYPES + } +} + +impl RemoteRefEmbed for PngIO { + #[allow(unused_variables)] + fn embed_reference( + &self, + asset_path: &Path, + embed_ref: crate::asset_io::RemoteRefEmbedType, + ) -> Result<()> { + match embed_ref { + crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => { + #[cfg(feature = "xmp_write")] + { + crate::embedded_xmp::add_manifest_uri_to_file(asset_path, &manifest_uri) + } + + #[cfg(not(feature = "xmp_write"))] + { + Err(crate::error::Error::MissingFeature("xmp_write".to_string())) + } + } + 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)] diff --git a/sdk/src/asset_handlers/riff_io.rs b/sdk/src/asset_handlers/riff_io.rs @@ -0,0 +1,426 @@ +// Copyright 2023 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::{File, OpenOptions}, + io::{Cursor, Seek, SeekFrom, Write}, + path::Path, +}; + +use conv::ValueFrom; +use riff::*; + +use crate::{ + asset_io::{ + AssetIO, AssetPatch, CAIRead, CAIReader, HashBlockObjectType, HashObjectPositions, + RemoteRefEmbed, + }, + error::{Error, Result}, + jumbf_io::get_file_extension, +}; + +static SUPPORTED_TYPES: [&str; 9] = [ + "avi", + "wav", + "webp", + "image/webp", + "audio/x-wav", + "application/x-troff-msvideo", + "video/avi", + "video/msvideo", + "video/x-msvideo", +]; + +pub struct RiffIO { + #[allow(dead_code)] + riff_format: String, // can be used for specialized RIFF cases +} + +const C2PA_CHUNK_ID: ChunkId = ChunkId { + value: [0x43, 0x32, 0x50, 0x41], +}; // C2PA + +fn read_items<T>(iter: &mut T) -> Vec<T::Item> +where + T: Iterator, +{ + let mut vec: Vec<T::Item> = Vec::new(); + for item in iter { + vec.push(item); + } + vec +} + +fn inject_c2pa<T>(chunk: &Chunk, file: &mut T, data: &[u8], format: &str) -> Result<ChunkContents> +where + T: std::io::Seek + std::io::Read, +{ + let id = chunk.id(); + let is_riff_chunk: bool = id == riff::RIFF_ID; + + if is_riff_chunk || id == riff::LIST_ID { + let chunk_type = chunk.read_type(file).map_err(|_| { + Error::InvalidAsset("RIFF handler could not parse file format {format}".to_string()) + })?; + let mut children = read_items(&mut chunk.iter(file)); + let mut children_contents: Vec<ChunkContents> = Vec::new(); + + if is_riff_chunk { + // remove c2pa manifest store in RIFF chunk + children.retain(|c| c.id() != C2PA_CHUNK_ID); + } + + // for non webp we can place at the front + // add c2pa manifest + if is_riff_chunk && !data.is_empty() && !format.contains("webp") { + children_contents.push(ChunkContents::Data(C2PA_CHUNK_ID, data.to_vec())); + } + + for child in children { + children_contents.push(inject_c2pa(&child, file, data, format)?); + } + + // for non webp we can place at the front + // add c2pa manifest + if is_riff_chunk && !data.is_empty() && format.contains("webp") { + children_contents.push(ChunkContents::Data(C2PA_CHUNK_ID, data.to_vec())); + } + + Ok(ChunkContents::Children(id, chunk_type, children_contents)) + } else if id == riff::SEQT_ID { + let children = read_items(&mut chunk.iter_no_type(file)); + let mut children_contents: Vec<ChunkContents> = Vec::new(); + + for child in children { + children_contents.push(inject_c2pa(&child, file, data, format)?); + } + + Ok(ChunkContents::ChildrenNoType(id, children_contents)) + } else { + let contents = chunk + .read_contents(file) + .map_err(|_| Error::InvalidAsset("RIFF handler could not parse file".to_string()))?; + Ok(ChunkContents::Data(id, contents)) + } +} + +fn get_manifest_pos(reader: &mut dyn CAIRead) -> Option<(u64, u32)> { + let mut asset: Vec<u8> = Vec::new(); + reader.rewind().ok()?; + reader.read_to_end(&mut asset).ok()?; + + let mut chunk_reader = Cursor::new(asset); + + let top_level_chunks = riff::Chunk::read(&mut chunk_reader, 0).ok()?; + + if top_level_chunks.id() == RIFF_ID { + for c in top_level_chunks.iter(&mut chunk_reader) { + if c.id() == C2PA_CHUNK_ID { + return Some((c.offset(), c.len() + 8)); // 8 is len of data chunk header + } + } + } + None +} + +impl CAIReader for RiffIO { + fn read_cai(&self, reader: &mut dyn CAIRead) -> Result<Vec<u8>> { + let mut asset: Vec<u8> = Vec::new(); + reader.rewind()?; + reader.read_to_end(&mut asset)?; + + let mut chunk_reader = Cursor::new(asset); + + let top_level_chunks = riff::Chunk::read(&mut chunk_reader, 0)?; + + if top_level_chunks.id() != RIFF_ID { + return Err(Error::InvalidAsset("Invalid RIFF format".to_string())); + } + + for c in top_level_chunks.iter(&mut chunk_reader) { + if c.id() == C2PA_CHUNK_ID { + let output = c.read_contents(&mut chunk_reader)?; + return Ok(output); + } + } + + Err(Error::JumbfNotFound) + } + + // 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 add_required_chunks(asset_path: &std::path::Path) -> Result<()> { + let mut f = File::open(asset_path)?; + let aio = RiffIO::new(&get_file_extension(asset_path).ok_or(Error::UnsupportedType)?); + + match aio.read_cai(&mut f) { + Ok(_) => Ok(()), + Err(_) => aio.save_cai_store(asset_path, &[1, 2, 3, 4]), // save arbitrary data + } +} + +impl AssetIO for RiffIO { + fn new(riff_format: &str) -> Self { + RiffIO { + riff_format: riff_format.to_string(), + } + } + + fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> { + Box::new(RiffIO::new(asset_type)) + } + + fn get_reader(&self) -> &dyn CAIReader { + self + } + + fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> { + Some(self) + } + + 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 asset = std::fs::read(asset_path)?; + let mut chunk_reader = Cursor::new(asset); + + let top_level_chunks = Chunk::read(&mut chunk_reader, 0)?; + + if top_level_chunks.id() != RIFF_ID { + return Err(Error::InvalidAsset("Invalid RIFF format".to_string())); + } + + // replace/add manifest in memory + let new_contents = inject_c2pa( + &top_level_chunks, + &mut chunk_reader, + store_bytes, + &self.riff_format, + )?; + + // save contents + let mut output = OpenOptions::new() + .read(true) + .write(true) + .open(asset_path) + .map_err(Error::IoError)?; + match new_contents.write(&mut output) { + Ok(_) => Ok(()), + Err(e) => Err(Error::IoError(e)), + } + } + + fn get_object_locations( + &self, + asset_path: &std::path::Path, + ) -> Result<Vec<HashObjectPositions>> { + add_required_chunks(asset_path)?; + + let mut f = std::fs::File::open(asset_path).map_err(|_err| Error::EmbeddingError)?; + + let mut positions: Vec<HashObjectPositions> = Vec::new(); + + let (manifest_pos, manifest_len) = get_manifest_pos(&mut f).ok_or(Error::EmbeddingError)?; + + positions.push(HashObjectPositions { + offset: usize::value_from(manifest_pos) + .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, + length: usize::value_from(manifest_len) + .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, + htype: HashBlockObjectType::Cai, + }); + + // add hash of chunks before cai + positions.push(HashObjectPositions { + offset: 0, + length: usize::value_from(manifest_pos) + .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, + htype: HashBlockObjectType::Other, + }); + + // add position from cai to end + let end = u64::value_from(manifest_pos) + .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))? + + u64::value_from(manifest_len) + .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; + let file_end = f.metadata()?.len(); + positions.push(HashObjectPositions { + offset: usize::value_from(end) + .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, // len of cai + length: usize::value_from(file_end - end) + .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, + htype: HashBlockObjectType::Other, + }); + + Ok(positions) + } + + fn remove_cai_store(&self, asset_path: &Path) -> Result<()> { + self.save_cai_store(asset_path, &[]) + } + + fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { + Some(self) + } + fn supported_types(&self) -> &[&str] { + &SUPPORTED_TYPES + } +} + +impl AssetPatch for RiffIO { + fn patch_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> { + let mut asset = OpenOptions::new() + .write(true) + .read(true) + .create(false) + .open(asset_path)?; + + let (manifest_pos, manifest_len) = + get_manifest_pos(&mut asset).ok_or(Error::EmbeddingError)?; + + if store_bytes.len() + 8 == manifest_len as usize { + asset.seek(SeekFrom::Start(manifest_pos + 8))?; // skip 8 byte chunk data header + asset.write_all(store_bytes)?; + Ok(()) + } else { + Err(Error::InvalidAsset( + "patch_cai_store store size mismatch.".to_string(), + )) + } + } +} + +impl RemoteRefEmbed for RiffIO { + #[allow(unused_variables)] + fn embed_reference( + &self, + asset_path: &Path, + embed_ref: crate::asset_io::RemoteRefEmbedType, + ) -> Result<()> { + match embed_ref { + crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => { + #[cfg(feature = "xmp_write")] + { + match self.riff_format.as_ref() { + "avi" | "wav" => { + crate::embedded_xmp::add_manifest_uri_to_file(asset_path, &manifest_uri) + } + _ => Err(Error::XmpNotSupported), + } + } + + #[cfg(not(feature = "xmp_write"))] + { + Err(crate::error::Error::MissingFeature("xmp_write".to_string())) + } + } + 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)] +pub mod tests { + #![allow(clippy::expect_used)] + #![allow(clippy::panic)] + #![allow(clippy::unwrap_used)] + + use tempfile::tempdir; + + use super::*; + use crate::utils::{ + hash_utils::vec_compare, + test::{fixture_path, temp_dir_path}, + }; + + #[test] + fn test_write_wav() { + let more_data = "some more test data".as_bytes(); + let source = fixture_path("sample1.wav"); + + let mut success = false; + if let Ok(temp_dir) = tempdir() { + let output = temp_dir_path(&temp_dir, "sample1-wav.wav"); + + if let Ok(_size) = std::fs::copy(source, &output) { + let riff_io = RiffIO::new("wav"); + + if let Ok(()) = riff_io.save_cai_store(&output, more_data) { + if let Ok(read_test_data) = riff_io.read_cai_store(&output) { + assert!(vec_compare(more_data, &read_test_data)); + success = true; + } + } + } + } + assert!(success) + } + + #[test] + fn test_patch_write_wav() { + let test_data = "some test data".as_bytes(); + let source = fixture_path("sample1.wav"); + + let mut success = false; + if let Ok(temp_dir) = tempdir() { + let output = temp_dir_path(&temp_dir, "sample1-wav.wav"); + + if let Ok(_size) = std::fs::copy(source, &output) { + let riff_io = RiffIO::new("wav"); + + if let Ok(()) = riff_io.save_cai_store(&output, test_data) { + if let Ok(source_data) = riff_io.read_cai_store(&output) { + // create replacement data of same size + let mut new_data = vec![0u8; source_data.len()]; + new_data[..test_data.len()].copy_from_slice(test_data); + riff_io.patch_cai_store(&output, &new_data).unwrap(); + + let replaced = riff_io.read_cai_store(&output).unwrap(); + + assert_eq!(new_data, replaced); + + success = true; + } + } + } + } + assert!(success) + } + + #[test] + fn test_remove_c2pa() { + let source = fixture_path("sample1.wav"); + + let temp_dir = tempdir().unwrap(); + let output = temp_dir_path(&temp_dir, "sample1-wav.wav"); + + std::fs::copy(source, &output).unwrap(); + let riff_io = RiffIO::new("wav"); + + riff_io.remove_cai_store(&output).unwrap(); + + // read back in asset, JumbfNotFound is expected since it was removed + match riff_io.read_cai_store(&output) { + Err(Error::JumbfNotFound) => (), + _ => unreachable!(), + } + } +} diff --git a/sdk/src/asset_handlers/tiff_io.rs b/sdk/src/asset_handlers/tiff_io.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Adobe. All rights reserved. +// Copyright 2023 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), @@ -15,6 +15,7 @@ use std::{ collections::{BTreeMap, HashMap}, fs::OpenOptions, io::{Cursor, Read, Seek, SeekFrom, Write}, + path::Path, }; use atree::{Arena, Token}; @@ -24,7 +25,10 @@ use conv::ValueFrom; use tempfile::Builder; use crate::{ - asset_io::{AssetIO, AssetPatch, CAILoader, CAIRead, HashBlockObjectType, HashObjectPositions}, + asset_io::{ + AssetIO, AssetPatch, CAIRead, CAIReader, HashBlockObjectType, HashObjectPositions, + RemoteRefEmbed, + }, error::{Error, Result}, }; @@ -45,6 +49,8 @@ const TILEOFFSETS: u16 = 324; const SUBFILES: [u16; 3] = [SUBFILE_TAG, EXIFIFD_TAG, GPSIFD_TAG]; +static SUPPORTED_TYPES: [&str; 4] = ["dng", "tif", "tiff", "image/tiff"]; + // The type of an IFD entry enum IFDEntryType { Byte = 1, // 8-bit unsigned integer @@ -1349,7 +1355,7 @@ where } pub struct TiffIO {} -impl CAILoader for TiffIO { +impl CAIReader for TiffIO { fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>> { let cai_data = get_cai_data(asset_reader)?; Ok(cai_data) @@ -1459,6 +1465,28 @@ impl AssetIO for TiffIO { None => Ok(()), } } + + fn new(_asset_type: &str) -> Self + where + Self: Sized, + { + TiffIO {} + } + + fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> { + Box::new(TiffIO::new(asset_type)) + } + + fn get_reader(&self) -> &dyn CAIReader { + self + } + + fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { + Some(self) + } + fn supported_types(&self) -> &[&str] { + &SUPPORTED_TYPES + } } impl AssetPatch for TiffIO { @@ -1500,6 +1528,32 @@ impl AssetPatch for TiffIO { } } +impl RemoteRefEmbed for TiffIO { + #[allow(unused_variables)] + fn embed_reference( + &self, + asset_path: &Path, + embed_ref: crate::asset_io::RemoteRefEmbedType, + ) -> Result<()> { + match embed_ref { + crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => { + #[cfg(feature = "xmp_write")] + { + crate::embedded_xmp::add_manifest_uri_to_file(asset_path, &manifest_uri) + } + + #[cfg(not(feature = "xmp_write"))] + { + Err(crate::error::Error::MissingFeature("xmp_write".to_string())) + } + } + 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)] pub mod tests { #![allow(clippy::panic)] diff --git a/sdk/src/asset_io.rs b/sdk/src/asset_io.rs @@ -52,7 +52,7 @@ impl CAIReadWrite for std::io::Cursor<&mut [u8]> {} impl CAIReadWrite for std::io::Cursor<Vec<u8>> {} // Interface for in memory CAI reading -pub trait CAILoader { +pub trait CAIReader: Sync + Send { // Return entire CAI block as Vec<u8> fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>>; @@ -60,7 +60,7 @@ pub trait CAILoader { fn read_xmp(&self, asset_reader: &mut dyn CAIRead) -> Option<String>; } -pub trait CAIWriter { +pub trait CAIWriter: Sync + Send { fn write_cai(&self, stream: &mut dyn CAIReadWrite, store_bytes: &[u8]) -> Result<()>; fn get_object_locations_from_stream( @@ -69,7 +69,24 @@ pub trait CAIWriter { ) -> Result<Vec<HashObjectPositions>>; } -pub trait AssetIO { +pub trait AssetIO: Sync + Send { + // create instance of AssetIO handler. The extension type is passed in so + // that format specific customizations can be used during manifest embedding + fn new(asset_type: &str) -> Self + where + Self: Sized; + + // return AssetIO handler for this asset type + fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO>; + + // return streaming reader for this asset type + fn get_reader(&self) -> &dyn CAIReader; + + // return streaming writer if available + fn get_writer(&self, _asset_type: &str) -> Option<Box<dyn CAIWriter>> { + None + } + // Return entire CAI block as Vec<u8> fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>>; @@ -82,13 +99,23 @@ pub trait AssetIO { /// length if the format contains extra header information for example. fn get_object_locations(&self, asset_path: &Path) -> Result<Vec<HashObjectPositions>>; - // Returns [`AssetPatch`] trait if this I/O handler supports patching. + // remove entire C2PA manifest store from asset + fn remove_cai_store(&self, asset_path: &Path) -> Result<()>; + + // list of supported extensions and mime types + fn supported_types(&self) -> &[&str]; + + /// OPTIONAL INTERFACES + + // returns [`AssetPatch`] trait if this I/O handler supports patching. fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> { None } - // Remove entire CAI block from asset - fn remove_cai_store(&self, asset_path: &Path) -> Result<()>; + // returns [`RemoteRefEmbed`] trait if this I/O handler supports remote reference embedding. + fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { + None + } } // `AssetPatch` optimizes output generation for asset_io handlers that @@ -101,3 +128,20 @@ pub trait AssetPatch { // since any other changes will invalidate asset hashes. fn patch_cai_store(&self, asset_path: &Path, store_bytes: &[u8]) -> Result<()>; } + +// Type of remote reference to embed. Some of the listed +// emums are for future uses and experiments. +#[allow(unused_variables)] +pub enum RemoteRefEmbedType { + Xmp(String), + StegoS(String), + StegoB(Vec<u8>), + Watermark(String), +} + +// `RemoteRefEmbed` is used to embed remote references to external manifests. The +// technique used to embed a reference varies bases on the type of embedding. Not +// all embedding choices need be supported. +pub trait RemoteRefEmbed { + fn embed_reference(&self, asset_path: &Path, embed_ref: RemoteRefEmbedType) -> Result<()>; +} diff --git a/sdk/src/error.rs b/sdk/src/error.rs @@ -43,6 +43,9 @@ pub enum Error { #[error("bad parameter: {0}")] BadParam(String), + #[error("required feature missing")] + MissingFeature(String), + /// The attempt to serialize the claim to CBOR failed. #[error("claim could not be converted to CBOR")] ClaimEncoding, @@ -201,6 +204,9 @@ pub enum Error { #[error("XMP write error")] XmpWriteError, + #[error("XMP is not supported")] + XmpNotSupported, + #[error("C2PA provenance not found in XMP")] ProvenanceMissing, diff --git a/sdk/src/jumbf_io.rs b/sdk/src/jumbf_io.rs @@ -12,64 +12,79 @@ // each license. use std::{ + collections::HashMap, fs::{self, File}, io::Cursor, path::{Path, PathBuf}, }; +use lazy_static::lazy_static; + use crate::{ asset_handlers::{ - bmff_io::BmffIO, c2pa_io::C2paIO, jpeg_io::JpegIO, png_io::PngIO, tiff_io::TiffIO, + bmff_io::BmffIO, c2pa_io::C2paIO, jpeg_io::JpegIO, png_io::PngIO, riff_io::RiffIO, + tiff_io::TiffIO, }, - asset_io::{AssetIO, CAILoader, CAIReadWrite, CAIWriter, HashObjectPositions}, + asset_io::{AssetIO, CAIReadWrite, CAIReader, CAIWriter, HashObjectPositions}, error::{Error, Result}, }; -static SUPPORTED_TYPES: [&str; 23] = [ - "avif", - "c2pa", // stand-alone manifest file - "heif", - "heic", - "jpg", - "jpeg", - "mp4", - "m4a", - "mov", - "png", - "tif", - "tiff", - "dng", - "application/mp4", - "audio/mp4", - "image/avif", - "image/heic", - "image/heif", - "image/jpeg", - "image/png", - "video/mp4", - "image/tiff", - "image/dng", -]; +// initialize asset handlers +lazy_static! { + static ref ASSET_HANDLERS: HashMap<String, Box<dyn AssetIO>> = { + let handlers: Vec<Box<dyn AssetIO>> = vec![ + Box::new(C2paIO::new("")), + Box::new(BmffIO::new("")), + Box::new(JpegIO::new("")), + Box::new(PngIO::new("")), + Box::new(RiffIO::new("")), + Box::new(TiffIO::new("")), + ]; + let mut handler_map = HashMap::new(); + + // build handler map + for h in handlers { + // get the supported types add entry for each + for supported_type in h.supported_types() { + handler_map.insert(supported_type.to_string(), h.get_handler(supported_type)); + } + } -#[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", -]; + handler_map + }; +} + +// initialize streaming write handlers +lazy_static! { + static ref CAI_WRITERS: HashMap<String, Box<dyn CAIWriter>> = { + let handlers: Vec<Box<dyn AssetIO>> = vec![ + Box::new(C2paIO::new("")), + Box::new(BmffIO::new("")), + Box::new(JpegIO::new("")), + Box::new(PngIO::new("")), + Box::new(RiffIO::new("")), + Box::new(TiffIO::new("")), + ]; + let mut handler_map = HashMap::new(); + + // build handler map + for h in handlers { + // get the supported types add entry for each + for supported_type in h.supported_types() { + if let Some(writer) = h.get_writer(supported_type) { // get streaming writer if supported + handler_map.insert(supported_type.to_string(), writer); + } + } + } + + handler_map + }; +} #[cfg(feature = "file_io")] pub(crate) fn is_bmff_format(asset_type: &str) -> bool { - BMFF_TYPES.contains(&asset_type) + let bmff_io = BmffIO::new(""); + bmff_io.supported_types().contains(&asset_type) } /// Return jumbf block from in memory asset @@ -110,53 +125,22 @@ pub fn save_jumbf_to_memory( Ok(stream.into_inner()) } -pub fn get_assetio_handler(ext: &str) -> Option<Box<dyn AssetIO>> { +pub fn get_assetio_handler(ext: &str) -> Option<&dyn AssetIO> { 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))), - "tif" | "tiff" | "dng" => Some(Box::new(TiffIO {})), - _ => None, - } + + ASSET_HANDLERS.get(&ext).map(|h| h.as_ref()) } -pub fn get_cailoader_handler(asset_type: &str) -> Option<Box<dyn CAILoader>> { +pub fn get_cailoader_handler(asset_type: &str) -> Option<&dyn CAIReader> { let asset_type = asset_type.to_lowercase(); - match asset_type.as_ref() { - "c2pa" | "application/c2pa" | "application/x-c2pa-manifest-store" => { - 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))) - } - "tif" | "tiff" | "dng" => Some(Box::new(TiffIO {})), - _ => None, - } + + ASSET_HANDLERS.get(&asset_type).map(|h| h.get_reader()) } -pub fn get_caiwriter_handler(asset_type: &str) -> Option<Box<dyn CAIWriter>> { +pub fn get_caiwriter_handler(asset_type: &str) -> Option<&dyn CAIWriter> { let asset_type = asset_type.to_lowercase(); - match asset_type.as_ref() { - // "c2pa" | "application/c2pa" | "application/x-c2pa-manifest-store" => { - // 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, - } + + CAI_WRITERS.get(&asset_type).map(|h| h.as_ref()) } pub fn get_file_extension(path: &Path) -> Option<String> { @@ -170,7 +154,7 @@ pub fn get_file_extension(path: &Path) -> Option<String> { pub fn get_supported_file_extension(path: &Path) -> Option<String> { let ext = get_file_extension(path)?; - if SUPPORTED_TYPES.contains(&ext.as_ref()) { + if ASSET_HANDLERS.get(&ext).is_some() { Some(ext) } else { None @@ -290,3 +274,106 @@ pub fn remove_jumbf_from_file(path: &Path) -> Result<()> { _ => Err(Error::UnsupportedType), } } + +/// returns a list of supported file extensions and mime types +pub fn get_supported_types() -> Vec<String> { + ASSET_HANDLERS.keys().map(|k| k.to_owned()).collect() +} + +#[cfg(test)] +pub mod tests { + #![allow(clippy::panic)] + #![allow(clippy::unwrap_used)] + + use super::*; + #[test] + fn test_get_assetio() { + let handlers: Vec<Box<dyn AssetIO>> = vec![ + Box::new(C2paIO::new("")), + Box::new(BmffIO::new("")), + Box::new(JpegIO::new("")), + Box::new(PngIO::new("")), + Box::new(RiffIO::new("")), + Box::new(TiffIO::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_assetio_handler(supported_type).is_some()); + } + } + } + + #[test] + fn test_get_reader() { + let handlers: Vec<Box<dyn AssetIO>> = vec![ + Box::new(C2paIO::new("")), + Box::new(BmffIO::new("")), + Box::new(JpegIO::new("")), + Box::new(PngIO::new("")), + Box::new(RiffIO::new("")), + Box::new(TiffIO::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_cailoader_handler(supported_type).is_some()); + } + } + } + + #[test] + fn test_get_writer() { + let handlers: Vec<Box<dyn AssetIO>> = vec![Box::new(JpegIO::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_some()); + } + } + } + + #[test] + fn test_no_writer() { + let handlers: Vec<Box<dyn AssetIO>> = vec![ + Box::new(C2paIO::new("")), + Box::new(BmffIO::new("")), + Box::new(PngIO::new("")), + Box::new(RiffIO::new("")), + Box::new(TiffIO::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(); + + assert!(supported.iter().any(|s| s == "jpg")); + assert!(supported.iter().any(|s| s == "jpeg")); + assert!(supported.iter().any(|s| s == "png")); + assert!(supported.iter().any(|s| s == "mov")); + assert!(supported.iter().any(|s| s == "mp4")); + assert!(supported.iter().any(|s| s == "m4a")); + assert!(supported.iter().any(|s| s == "jpg")); + assert!(supported.iter().any(|s| s == "avi")); + assert!(supported.iter().any(|s| s == "webp")); + assert!(supported.iter().any(|s| s == "wav")); + assert!(supported.iter().any(|s| s == "tif")); + assert!(supported.iter().any(|s| s == "tiff")); + assert!(supported.iter().any(|s| s == "dng")); + } +} diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -19,8 +19,6 @@ use std::{fs, path::Path}; use log::error; -#[cfg(all(feature = "xmp_write", feature = "file_io"))] -use crate::embedded_xmp; #[cfg(feature = "async_signer")] use crate::AsyncSigner; use crate::{ @@ -57,10 +55,11 @@ use crate::{ #[cfg(feature = "file_io")] use crate::{ assertions::{BmffHash, DataMap, ExclusionsMap, SubsetMap}, + asset_io::RemoteRefEmbedType, claim::RemoteManifest, jumbf_io::{ - get_file_extension, get_supported_file_extension, is_bmff_format, load_jumbf_from_file, - object_locations, remove_jumbf_from_file, save_jumbf_to_file, + get_assetio_handler, get_file_extension, get_supported_file_extension, is_bmff_format, + load_jumbf_from_file, object_locations, remove_jumbf_from_file, save_jumbf_to_file, }, }; @@ -1846,6 +1845,7 @@ impl Store { reserve_size: usize, ) -> Result<Vec<u8>> { // force generate external manifests for unknown types + let ext = match get_supported_file_extension(dest_path) { Some(ext) => ext, None => { @@ -1865,18 +1865,7 @@ impl Store { // 1) Add DC provenance XMP let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; let output_path = match pc.remote_manifest() { - crate::claim::RemoteManifest::NoRemote => { - // even though this block is protected by the outer cfg!(feature = "xmp_write") - // the class embedded_xmp is not defined so we have to explicitly exclude it from the build - #[cfg(feature = "xmp_write")] - if let Some(provenance) = self.provenance_path() { - // update XMP info & add xmp hash to provenance claim - embedded_xmp::add_manifest_uri_to_file(dest_path, &provenance)?; - } else { - return Err(Error::XmpWriteError); - } - dest_path.to_path_buf() - } + crate::claim::RemoteManifest::NoRemote => dest_path.to_path_buf(), crate::claim::RemoteManifest::SideCar => { // remove any previous c2pa manifest from the asset match remove_jumbf_from_file(dest_path) { @@ -1886,31 +1875,36 @@ impl Store { Err(e) => return Err(e), } } - crate::claim::RemoteManifest::Remote(_url) => { - if cfg!(feature = "xmp_write") { - let d = dest_path.with_extension(MANIFEST_STORE_EXT); - // remove any previous c2pa manifest from the asset - remove_jumbf_from_file(dest_path)?; - // even though this block is protected by the outer cfg!(feature = "xmp_write") - // the class embedded_xmp is not defined so we have to explicitly exclude it from the build - #[cfg(feature = "xmp_write")] - embedded_xmp::add_manifest_uri_to_file(dest_path, &_url)?; - d + crate::claim::RemoteManifest::Remote(url) => { + let d = dest_path.with_extension(MANIFEST_STORE_EXT); + // remove any previous c2pa manifest from the asset + remove_jumbf_from_file(dest_path)?; + + if let Some(h) = get_assetio_handler(&ext) { + if let Some(external_ref_writer) = h.remote_ref_writer_ref() { + external_ref_writer + .embed_reference(dest_path, RemoteRefEmbedType::Xmp(url))?; + } else { + return Err(Error::XmpNotSupported); + } } else { - return Err(Error::BadParam("requires 'xmp_write' feature".to_string())); + return Err(Error::UnsupportedType); } - } - crate::claim::RemoteManifest::EmbedWithRemote(_url) => { - if cfg!(feature = "xmp_write") { - // even though this block is protected by the outer cfg!(feature = "xmp_write") - // the class embedded_xmp is not defined so we have to explicitly exclude it from the build - #[cfg(feature = "xmp_write")] - embedded_xmp::add_manifest_uri_to_file(dest_path, &_url)?; - dest_path.to_path_buf() + d + } + crate::claim::RemoteManifest::EmbedWithRemote(url) => { + if let Some(h) = get_assetio_handler(&ext) { + if let Some(external_ref_writer) = h.remote_ref_writer_ref() { + external_ref_writer + .embed_reference(dest_path, RemoteRefEmbedType::Xmp(url))?; + } else { + return Err(Error::XmpNotSupported); + } } else { - return Err(Error::BadParam("requires 'xmp_write' feature".to_string())); + return Err(Error::UnsupportedType); } + dest_path.to_path_buf() } }; @@ -1925,7 +1919,7 @@ impl Store { 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)?; + let bmff_hashes = Store::generate_bmff_data_hashes(dest_path, pc.alg(), false)?; for hash in bmff_hashes { pc.add_assertion(&hash)?; } @@ -1946,7 +1940,7 @@ impl Store { if !bmff_hashes.is_empty() { let mut bmff_hash = BmffHash::from_assertion(bmff_hashes[0])?; - bmff_hash.gen_hash(&output_path)?; + bmff_hash.gen_hash(dest_path)?; pc.update_bmff_hash(bmff_hash)?; } } @@ -2902,6 +2896,255 @@ pub mod tests { } } + #[test] + #[cfg(feature = "file_io")] + fn test_wav_jumbf_generation() { + let ap = fixture_path("sample1.wav"); + let temp_dir = tempdir().expect("temp dir"); + let op = temp_dir_path(&temp_dir, "ssample1.wav"); + + // Create claims store. + let mut store = Store::new(); + + // Create a new claim. + let claim1 = create_test_claim().unwrap(); + + // Create a new claim. + let mut claim2 = Claim::new("Photoshop", Some("Adobe")); + create_editing_claim(&mut claim2).unwrap(); + + // Create a 3rd party claim + let mut claim_capture = Claim::new("capture", Some("claim_capture")); + create_capture_claim(&mut claim_capture).unwrap(); + + // Do we generate JUMBF? + let signer = temp_signer(); + + // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commmits + store.commit_claim(claim1).unwrap(); + store.save_to_asset(&ap, &signer, &op).unwrap(); + store.commit_claim(claim_capture).unwrap(); + store.save_to_asset(&op, &signer, &op).unwrap(); + store.commit_claim(claim2).unwrap(); + store.save_to_asset(&op, &signer, &op).unwrap(); + + // write to new file + println!("Provenance: {}\n", store.provenance_path().unwrap()); + + let mut report = DetailedStatusTracker::new(); + + // read from new file + let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); + + // can we get by the ingredient data back + let _some_binary_data: Vec<u8> = vec![ + 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, + 0x0b, 0x0e, + ]; + + // dump store and compare to original + for claim in new_store.claims() { + let _restored_json = claim + .to_json(AssertionStoreJsonFormat::OrderedList, false) + .unwrap(); + let _orig_json = store + .get_claim(claim.label()) + .unwrap() + .to_json(AssertionStoreJsonFormat::OrderedList, false) + .unwrap(); + + println!( + "Claim: {} \n{}", + claim.label(), + claim + .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) + .expect("could not restore from json") + ); + + for hashed_uri in claim.assertions() { + let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); + claim + .get_claim_assertion(&label, instance) + .expect("Should find assertion"); + } + } + } + + #[test] + #[cfg(feature = "file_io")] + fn test_avi_jumbf_generation() { + let ap = fixture_path("test.avi"); + let temp_dir = tempdir().expect("temp dir"); + let op = temp_dir_path(&temp_dir, "test.avi"); + + // Create claims store. + let mut store = Store::new(); + + // Create a new claim. + let claim1 = create_test_claim().unwrap(); + + // Create a new claim. + let mut claim2 = Claim::new("Photoshop", Some("Adobe")); + create_editing_claim(&mut claim2).unwrap(); + + // Create a 3rd party claim + let mut claim_capture = Claim::new("capture", Some("claim_capture")); + create_capture_claim(&mut claim_capture).unwrap(); + + // Do we generate JUMBF? + let signer = temp_signer(); + + // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commmits + store.commit_claim(claim1).unwrap(); + store.save_to_asset(&ap, &signer, &op).unwrap(); + store.commit_claim(claim_capture).unwrap(); + store.save_to_asset(&op, &signer, &op).unwrap(); + store.commit_claim(claim2).unwrap(); + store.save_to_asset(&op, &signer, &op).unwrap(); + + // write to new file + println!("Provenance: {}\n", store.provenance_path().unwrap()); + + let mut report = DetailedStatusTracker::new(); + + // read from new file + let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); + + // can we get by the ingredient data back + let _some_binary_data: Vec<u8> = vec![ + 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, + 0x0b, 0x0e, + ]; + + // dump store and compare to original + for claim in new_store.claims() { + let _restored_json = claim + .to_json(AssertionStoreJsonFormat::OrderedList, false) + .unwrap(); + let _orig_json = store + .get_claim(claim.label()) + .unwrap() + .to_json(AssertionStoreJsonFormat::OrderedList, false) + .unwrap(); + + println!( + "Claim: {} \n{}", + claim.label(), + claim + .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) + .expect("could not restore from json") + ); + + for hashed_uri in claim.assertions() { + let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); + claim + .get_claim_assertion(&label, instance) + .expect("Should find assertion"); + } + } + } + + #[test] + #[cfg(feature = "file_io")] + fn test_webp_jumbf_generation() { + let ap = fixture_path("sample1.webp"); + let temp_dir = tempdir().expect("temp dir"); + let op = temp_dir_path(&temp_dir, "sample1.webp"); + + // Create claims store. + let mut store = Store::new(); + + // Create a new claim. + let claim1 = create_test_claim().unwrap(); + + // Create a new claim. + let mut claim2 = Claim::new("Photoshop", Some("Adobe")); + create_editing_claim(&mut claim2).unwrap(); + + // Create a 3rd party claim + let mut claim_capture = Claim::new("capture", Some("claim_capture")); + create_capture_claim(&mut claim_capture).unwrap(); + + // Do we generate JUMBF? + let signer = temp_signer(); + + // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commmits + store.commit_claim(claim1).unwrap(); + store.save_to_asset(&ap, &signer, &op).unwrap(); + store.commit_claim(claim_capture).unwrap(); + store.save_to_asset(&op, &signer, &op).unwrap(); + store.commit_claim(claim2).unwrap(); + store.save_to_asset(&op, &signer, &op).unwrap(); + + // write to new file + println!("Provenance: {}\n", store.provenance_path().unwrap()); + + let mut report = DetailedStatusTracker::new(); + + // read from new file + let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); + + // can we get by the ingredient data back + let _some_binary_data: Vec<u8> = vec![ + 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, + 0x0b, 0x0e, + ]; + + // dump store and compare to original + for claim in new_store.claims() { + let _restored_json = claim + .to_json(AssertionStoreJsonFormat::OrderedList, false) + .unwrap(); + let _orig_json = store + .get_claim(claim.label()) + .unwrap() + .to_json(AssertionStoreJsonFormat::OrderedList, false) + .unwrap(); + + println!( + "Claim: {} \n{}", + claim.label(), + claim + .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) + .expect("could not restore from json") + ); + + for hashed_uri in claim.assertions() { + let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); + claim + .get_claim_assertion(&label, instance) + .expect("Should find assertion"); + } + } + } + + /* enable when we renable HEIC + #[test] + #[cfg(feature = "file_io")] + fn test_no_xmp_err() { + let ap = fixture_path("sample1.heic"); + let temp_dir = tempdir().expect("temp dir"); + let op = temp_dir_path(&temp_dir, "sample1.heic"); + + // Create claims store. + let mut store = Store::new(); + + // Create a new claim. + let mut claim1 = create_test_claim().unwrap(); + + // Do we generate JUMBF? + let signer = temp_signer(); + + // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commmits + claim1.set_remote_manifest("http://somecompany.com/someasset").unwrap(); + store.commit_claim(claim1).unwrap(); + let result = store.save_to_asset(&ap, &signer, &op); + + assert!(result.is_err()); + assert_eq!(format!("{:?}", result.err().unwrap()), format!("{:?}", &Error::XmpNotSupported)); + } + */ + /* todo: disable until we can generate a valid file with no xmp #[test] fn test_manifest_no_xmp() { @@ -3219,7 +3462,6 @@ 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"); @@ -3228,7 +3470,6 @@ pub mod tests { 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/tests/fixtures/sample1.wav b/sdk/tests/fixtures/sample1.wav Binary files differ. diff --git a/sdk/tests/fixtures/sample1.webp b/sdk/tests/fixtures/sample1.webp Binary files differ. diff --git a/sdk/tests/fixtures/test.avi b/sdk/tests/fixtures/test.avi Binary files differ.