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:
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.