commit a09b013e805ccff785c99410ae0aa2a0cdb26dce
parent 486eb585809ddfae3322f1b03808e4e66a8db8c7
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date: Tue, 9 Aug 2022 16:05:56 -0400
Added support for external manifests (#101)
* Treat 'meta' box as standard container
Support for more BMFF container types
Handle bad FourCC names
Add bad signer unit test
* clippy fixes
* External manifest generation
* More detailed unit test
Bug fixes
Slight speed up
* test smaller chunk size for hashing
* Only generate output upon success.
Auto cleanup temp content
* cleanup
* WASM build fixes
* code review fixes
* Fix typo
* Changes based on feedback
* Support Info for constructor
Put remote manifest behind xmp_write feature
Split external manifest into two functions
* One more suggestion
* formatting
* formatting
Diffstat:
5 files changed, 410 insertions(+), 64 deletions(-)
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
@@ -79,13 +79,13 @@ tempfile = "3.1.0"
thiserror = ">= 1.0.20, < 1.0.32"
time = ">= 0.2.23"
twoway = "0.2.1"
+url = "2.2.2"
uuid = { version = "0.8.1", features = ["serde", "v4", "wasm-bindgen"] }
x509-parser = "0.11.0"
x509-certificate = "0.12.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
ring = "0.16.20"
-url = "2.2.2"
ureq = "2.4.0"
image = { version = "0.24.2", optional = true }
instant = "0.1.0"
diff --git a/sdk/src/asset_handlers/c2pa_io.rs b/sdk/src/asset_handlers/c2pa_io.rs
@@ -14,10 +14,9 @@
use std::{fs::File, path::Path};
use crate::{
- asset_io::{AssetIO, CAILoader, CAIRead, HashObjectPositions},
+ asset_io::{AssetIO, CAILoader, CAIRead, HashBlockObjectType, HashObjectPositions},
error::{Error, Result},
};
-
/// Supports working with ".c2pa" files containing only manifest store data
pub struct C2paIO {}
@@ -53,7 +52,13 @@ impl AssetIO for C2paIO {
&self,
_asset_path: &std::path::Path,
) -> Result<Vec<HashObjectPositions>> {
- Ok(Vec::new())
+ let hop = HashObjectPositions {
+ offset: 0,
+ length: 0,
+ htype: HashBlockObjectType::Cai,
+ };
+
+ Ok(vec![hop])
}
}
diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs
@@ -156,6 +156,10 @@ impl fmt::Debug for ClaimAssertion {
/// that hash is signed to produce the claim signature.
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
pub struct Claim {
+ // external manifest
+ #[serde(skip_deserializing, skip_serializing)]
+ remote_manifest: RemoteManifest,
+
// root of CAI store
#[serde(skip_deserializing, skip_serializing)]
update_manifest: bool,
@@ -229,6 +233,20 @@ pub enum AssertionStoreJsonFormat {
OrderedListNoBinary, // list of Assertions as json objects omitting binaries results
}
+/// Remote manifest options. Use 'set_remote_manifest' to generate external manifests.
+#[derive(Clone, Debug, PartialEq)]
+pub enum RemoteManifest {
+ NoRemote, // No external manifest (default)
+ SideCar, // Manifest will be saved as a side car file, output asset is untouched.
+ Remote(String), // Manifest will be saved as a side car file, output asset will contain remote reference
+}
+
+impl Default for RemoteManifest {
+ fn default() -> Self {
+ Self::NoRemote
+ }
+}
+
#[derive(Serialize, Deserialize, Debug)]
pub struct JsonOrderedAssertionData {
label: String,
@@ -247,8 +265,7 @@ impl Claim {
/// Create a new claim.
/// vendor: name used to label the claim (unique instance number is automatically calculated)
/// claim_generator: User agent see c2pa spec for format
- // #[cfg(not(target_arch = "wasm32"))]
- pub fn new(claim_generator: &str, vendor: Option<&str>) -> Self {
+ pub fn new<S: Into<String>>(claim_generator: S, vendor: Option<&str>) -> Self {
let urn = Uuid::new_v4();
let l = match vendor {
Some(v) => format!(
@@ -263,6 +280,7 @@ impl Claim {
};
Claim {
+ remote_manifest: RemoteManifest::NoRemote,
box_prefix: "self#jumbf".to_string(),
root: jumbf::labels::MANIFEST_STORE.to_string(),
signature_val: Vec::new(),
@@ -270,7 +288,7 @@ impl Claim {
label: l,
signature: "".to_string(),
- claim_generator: claim_generator.to_string(),
+ claim_generator: claim_generator.into(),
assertion_store: Vec::new(),
vc_store: Vec::new(),
assertions: Vec::new(),
@@ -288,6 +306,37 @@ impl Claim {
}
}
+ /// Create a new claim with a user supplied GUID.
+ /// user_guid: is user supplied guid conforming the C2PA spec for manifest names
+ /// claim_generator: User agent see c2pa spec for format
+ pub fn new_with_user_guid<S: Into<String>>(claim_generator: S, user_guid: S) -> Self {
+ Claim {
+ remote_manifest: RemoteManifest::NoRemote,
+ box_prefix: "self#jumbf".to_string(),
+ root: jumbf::labels::MANIFEST_STORE.to_string(),
+ signature_val: Vec::new(),
+ ingredients_store: HashMap::new(),
+ label: user_guid.into(), // todo figure out how to validate this
+ signature: "".to_string(),
+
+ claim_generator: claim_generator.into(),
+ assertion_store: Vec::new(),
+ vc_store: Vec::new(),
+ assertions: Vec::new(),
+ original_bytes: None,
+ redacted_assertions: None,
+ alg: Some(BUILD_HASH_ALG.into()),
+ alg_soft: None,
+ claim_generator_hints: None,
+
+ title: None,
+ format: "".to_string(),
+ instance_id: "".to_string(),
+
+ update_manifest: false,
+ }
+ }
+
/// Build a claim and verify its integrity.
pub fn build(&mut self) -> Result<()> {
// A claim must have a signature box.
@@ -381,6 +430,26 @@ impl Claim {
self.update_manifest
}
+ pub fn set_remote_manifest<S: Into<String> + AsRef<str>>(
+ &mut self,
+ remote_url: S,
+ ) -> Result<()> {
+ url::Url::parse(remote_url.as_ref())
+ .map_err(|_e| Error::BadParam("remote url is badly formed".to_string()))?;
+ self.remote_manifest = RemoteManifest::Remote(remote_url.into());
+
+ Ok(())
+ }
+
+ pub fn set_external_manifest(&mut self) {
+ self.remote_manifest = RemoteManifest::SideCar;
+ }
+
+ #[cfg(feature = "file_io")]
+ pub(crate) fn remote_manifest(&self) -> RemoteManifest {
+ self.remote_manifest.clone()
+ }
+
pub(crate) fn set_update_manifest(&mut self, is_update_manifest: bool) {
self.update_manifest = is_update_manifest;
}
diff --git a/sdk/src/store.rs b/sdk/src/store.rs
@@ -27,6 +27,7 @@ use crate::{
assertion::AssertionData,
assertions::{BmffHash, DataHash, DataMap, ExclusionsMap, SubsetMap},
asset_io::{HashBlockObjectType, HashObjectPositions},
+ claim::RemoteManifest,
cose_sign::cose_sign,
cose_validator::verify_cose,
jumbf_io::{
@@ -53,6 +54,8 @@ use crate::{
ManifestStoreReport,
};
+const MANIFEST_STORE_EXT: &str = "c2pa"; // file extension for external manifests
+
/// A `Store` maintains a list of `Claim` structs.
///
/// Typically, this list of `Claim`s represents all of the claims in an asset.
@@ -92,7 +95,7 @@ impl Default for Store {
impl Store {
/// Create a new, empty claims store.
pub fn new() -> Self {
- Self::new_with_label(jumbf::labels::MANIFEST_STORE)
+ Self::new_with_label(MANIFEST_STORE_EXT)
}
/// Create a new, empty claims store with a custom label.
@@ -136,16 +139,14 @@ impl Store {
&self.claims
}
- /// Add a new Claim to this Store. The claim label
- /// may be updated to reflect is position in the Claim Store
- /// if there are conflicting label names. The function
- /// will return the label of the claim used
+ /// Add a new Claim to this Store. The function
+ /// will return the label of the claim.
pub fn commit_claim(&mut self, mut claim: Claim) -> Result<String> {
// verify the claim is valid
claim.build()?;
// load the claim ingredients
- // preparse first to make sure we can load them
+ // parse first to make sure we can load them
let mut ingredient_claims: Vec<Claim> = Vec::new();
for (pc, claims) in claim.claim_ingredient_store() {
let mut valid_pc = false;
@@ -1233,9 +1234,8 @@ impl Store {
calc_hashes: bool,
) -> Result<Vec<DataHash>> {
if block_locations.is_empty() {
- return Err(Error::BadParam(
- "No asset hash locations specified".to_owned(),
- ));
+ let out: Vec<DataHash> = vec![];
+ return Ok(out);
}
let metadata = asset_path.metadata().map_err(crate::error::wrap_io_err)?;
@@ -1273,7 +1273,9 @@ impl Store {
if found_jumbf {
// add exclusion hash for bytes before and after jumbf
let mut dh = DataHash::new("jumbf manifest", alg, None);
- dh.add_exclusion(Exclusion::new(block_start, block_end - block_start));
+ if block_end > block_start {
+ dh.add_exclusion(Exclusion::new(block_start, block_end - block_start));
+ }
if calc_hashes {
dh.gen_hash(asset_path)?;
} else {
@@ -1386,26 +1388,76 @@ impl Store {
Ok(hashes)
}
+
+ // move or copy data from source to dest
+ #[cfg(feature = "file_io")]
+ fn move_or_copy(source: &Path, dest: &Path) -> Result<()> {
+ // copy temp file to asset
+ std::fs::rename(source, dest)
+ // if rename fails, try to copy in case we are on different volumes or output does not exist
+ .or_else(|_| std::fs::copy(source, dest).and(Ok(())))
+ .map_err(Error::IoError)
+ }
+
+ // copy output and possibly the external manifest to final destination
+ #[cfg(feature = "file_io")]
+ fn copy_c2pa_to_output(source: &Path, dest: &Path, remote_type: RemoteManifest) -> Result<()> {
+ match remote_type {
+ crate::claim::RemoteManifest::NoRemote => Store::move_or_copy(source, dest)?,
+ crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => {
+ // make correct path names
+ let source_asset = source;
+ let source_cai = source_asset.with_extension(MANIFEST_STORE_EXT);
+ let dest_cai = dest.with_extension(MANIFEST_STORE_EXT);
+
+ Store::move_or_copy(&source_cai, &dest_cai)?; // copy manifest
+ Store::move_or_copy(source_asset, dest)?; // copy asset
+ }
+ }
+ Ok(())
+ }
+
/// Embed the claims store as jumbf into an asset. Updates XMP with provenance record.
#[cfg(feature = "file_io")]
pub fn save_to_asset(
&mut self,
asset_path: &Path,
signer: &dyn Signer,
- output_path: &Path,
- ) -> Result<()> {
- let jumbf_bytes = self.start_save(asset_path, output_path, signer.reserve_size())?;
+ dest_path: &Path,
+ ) -> Result<Vec<u8>> {
+ // set up temp dir, contents auto deleted
+ let td = tempfile::TempDir::new()?;
+ let temp_path = td.into_path();
+ let temp_file = temp_path.join(
+ dest_path
+ .file_name()
+ .ok_or_else(|| Error::BadParam("invalid destination path".to_string()))?,
+ );
+
+ let jumbf_bytes = self.start_save(asset_path, &temp_file, signer.reserve_size())?;
let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
let sig = self.sign_claim(pc, signer, signer.reserve_size())?;
let sig_placeholder = self.sign_claim_placeholder(pc, signer.reserve_size());
- match self.finish_save(jumbf_bytes, output_path, sig, &sig_placeholder) {
- Ok(v) => {
+ // get correct output path for remote manifest
+ let output_path = match pc.remote_manifest() {
+ crate::claim::RemoteManifest::NoRemote => temp_file.to_path_buf(),
+ crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => {
+ temp_file.with_extension(MANIFEST_STORE_EXT)
+ }
+ };
+
+ match self.finish_save(jumbf_bytes, &output_path, sig, &sig_placeholder) {
+ Ok((s, m)) => {
// save sig so store is up to date
let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
- pc_mut.set_signature_val(v);
- Ok(())
+ pc_mut.set_signature_val(s);
+
+ // copy the correct files upon completion
+ Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?;
+
+ Ok(m)
}
Err(e) => Err(e),
}
@@ -1417,9 +1469,18 @@ impl Store {
&mut self,
asset_path: &Path,
signer: &dyn AsyncSigner,
- output_path: &Path,
- ) -> Result<()> {
- let jumbf_bytes = self.start_save(asset_path, output_path, signer.reserve_size())?;
+ dest_path: &Path,
+ ) -> Result<Vec<u8>> {
+ // set up temp dir, contents auto deleted
+ let td = tempfile::TempDir::new()?;
+ let temp_path = td.into_path();
+ let temp_file = temp_path.join(
+ dest_path
+ .file_name()
+ .ok_or_else(|| Error::BadParam("invalid destination path".to_string()))?,
+ );
+
+ let jumbf_bytes = self.start_save(asset_path, &temp_file, signer.reserve_size())?;
let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
let sig = self
@@ -1427,12 +1488,24 @@ impl Store {
.await?;
let sig_placeholder = self.sign_claim_placeholder(pc, signer.reserve_size());
- match self.finish_save(jumbf_bytes, output_path, sig, &sig_placeholder) {
- Ok(v) => {
+ // get correct output path for remote manifest
+ let output_path = match pc.remote_manifest() {
+ crate::claim::RemoteManifest::NoRemote => temp_file.to_path_buf(),
+ crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => {
+ temp_file.with_extension(MANIFEST_STORE_EXT)
+ }
+ };
+
+ match self.finish_save(jumbf_bytes, &output_path, sig, &sig_placeholder) {
+ Ok((s, m)) => {
// save sig so store is up to date
let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
- pc_mut.set_signature_val(v);
- Ok(())
+ pc_mut.set_signature_val(s);
+
+ // copy the correct files upon completion
+ Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?;
+
+ Ok(m)
}
Err(e) => Err(e),
}
@@ -1444,21 +1517,42 @@ impl Store {
&mut self,
asset_path: &Path,
remote_signer: &dyn crate::signer::RemoteSigner,
- output_path: &Path,
- ) -> Result<()> {
- let jumbf_bytes = self.start_save(asset_path, output_path, remote_signer.reserve_size())?;
+ dest_path: &Path,
+ ) -> Result<Vec<u8>> {
+ // set up temp dir, contents auto deleted
+ let td = tempfile::TempDir::new()?;
+ let temp_path = td.into_path();
+ let temp_file = temp_path.join(
+ dest_path
+ .file_name()
+ .ok_or_else(|| Error::BadParam("invalid destination path".to_string()))?,
+ );
+
+ let jumbf_bytes = self.start_save(asset_path, &temp_file, remote_signer.reserve_size())?;
let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
let sig = remote_signer.sign_remote(&pc.data()?).await?;
let sig_placeholder = self.sign_claim_placeholder(pc, remote_signer.reserve_size());
- match self.finish_save(jumbf_bytes, output_path, sig, &sig_placeholder) {
- Ok(v) => {
+ // get correct output path for remote manifest
+ let output_path = match pc.remote_manifest() {
+ crate::claim::RemoteManifest::NoRemote => temp_file.to_path_buf(),
+ crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => {
+ temp_file.with_extension(MANIFEST_STORE_EXT)
+ }
+ };
+
+ match self.finish_save(jumbf_bytes, &output_path, sig, &sig_placeholder) {
+ Ok((s, m)) => {
// save sig so store is up to date
let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
- pc_mut.set_signature_val(v);
- Ok(())
+ pc_mut.set_signature_val(s);
+
+ // copy the correct files upon completion
+ Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?;
+
+ Ok(m)
}
Err(e) => Err(e),
}
@@ -1468,39 +1562,67 @@ impl Store {
fn start_save(
&mut self,
asset_path: &Path,
- output_path: &Path,
+ dest_path: &Path,
reserve_size: usize,
) -> Result<Vec<u8>> {
// clone the source to working copy if requested
+
get_supported_file_extension(asset_path).ok_or(Error::UnsupportedType)?; // verify extensions
- let ext = get_supported_file_extension(output_path).ok_or(Error::UnsupportedType)?;
- if asset_path != output_path {
- fs::copy(&asset_path, &output_path).map_err(Error::IoError)?;
+ let ext = get_supported_file_extension(dest_path).ok_or(Error::UnsupportedType)?;
+ if asset_path != dest_path {
+ fs::copy(&asset_path, &dest_path).map_err(Error::IoError)?;
}
// update file following the steps outlined in CAI spec
// 1) Add DC provenance XMP
- // update XMP info & add xmp hash to provenance claim
- #[cfg(feature = "xmp_write")]
- if let Some(provenance) = self.provenance_path() {
- embedded_xmp::add_manifest_uri_to_file(output_path, &provenance)?;
+ let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
+ let output_path = if cfg!(feature = "xmp_write") {
+ match pc.remote_manifest() {
+ crate::claim::RemoteManifest::NoRemote => {
+ // update XMP info & add xmp hash to provenance claim
+ #[cfg(feature = "xmp_write")]
+ if let Some(provenance) = self.provenance_path() {
+ embedded_xmp::add_manifest_uri_to_file(dest_path, &provenance)?;
+ } else {
+ return Err(Error::XmpWriteError);
+ }
+ dest_path.to_path_buf()
+ }
+ crate::claim::RemoteManifest::SideCar => {
+ dest_path.with_extension(MANIFEST_STORE_EXT)
+ }
+ crate::claim::RemoteManifest::Remote(url) => {
+ let d = dest_path.with_extension(MANIFEST_STORE_EXT);
+ embedded_xmp::add_manifest_uri_to_file(dest_path, &url)?;
+ d
+ }
+ }
} else {
- return Err(Error::XmpWriteError);
- }
+ // only side car and embedded supported without feature "xmp_write"
+ match pc.remote_manifest() {
+ crate::claim::RemoteManifest::NoRemote => dest_path.to_path_buf(),
+ crate::claim::RemoteManifest::SideCar => {
+ dest_path.with_extension(MANIFEST_STORE_EXT)
+ }
+ crate::claim::RemoteManifest::Remote(_) => {
+ return Err(Error::BadParam("requires 'xmp_writte' feature".to_string()))
+ }
+ }
+ };
+
+ // get the provenance claim changing mutability
+ let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
let is_bmff = is_bmff_format(&ext);
let mut data;
let jumbf_size;
- // get the provenance claim
- let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
-
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(&output_path, pc.alg(), false)?;
for hash in bmff_hashes {
pc.add_assertion(&hash)?;
}
@@ -1511,7 +1633,7 @@ impl Store {
// source and dest the same so save_jumbf_to_file will use the same file since we have already cloned
data = self.to_jumbf_internal(reserve_size)?;
jumbf_size = data.len();
- save_jumbf_to_file(&data, output_path, Some(output_path))?;
+ save_jumbf_to_file(&data, &output_path, Some(&output_path))?;
// generate actual hash values
let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; // reborrow to change mutability
@@ -1521,17 +1643,17 @@ 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(&output_path)?;
pc.update_bmff_hash(bmff_hash)?;
}
}
} else {
// 2) Get hash ranges if needed, do not generate for update manifests
- let mut hash_ranges = object_locations(output_path)?;
+ let mut hash_ranges = object_locations(&output_path)?;
let hashes: Vec<DataHash> = if pc.update_manifest() {
Vec::new()
} else {
- Store::generate_data_hashes(output_path, pc.alg(), &mut hash_ranges, false)?
+ Store::generate_data_hashes(dest_path, pc.alg(), &mut hash_ranges, false)?
};
// add the placeholder data hashes to provenance claim so that the required space is reserved
@@ -1548,23 +1670,22 @@ impl Store {
// source and dest the same so save_jumbf_to_file will use the same file since we have already cloned
data = self.to_jumbf_internal(reserve_size)?;
jumbf_size = data.len();
- save_jumbf_to_file(&data, output_path, Some(output_path))?;
+ save_jumbf_to_file(&data, &output_path, Some(&output_path))?;
// 4) determine final object locations and patch the asset hashes with correct offset
// replace the source with correct asset hashes so that the claim hash will be correct
let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
// get the final hash ranges, but not for update manifests
- let mut new_hash_ranges = object_locations(output_path)?;
+ let mut new_hash_ranges = object_locations(&output_path)?;
let updated_hashes = if pc.update_manifest() {
Vec::new()
} else {
- Store::generate_data_hashes(output_path, pc.alg(), &mut new_hash_ranges, true)?
+ Store::generate_data_hashes(dest_path, pc.alg(), &mut new_hash_ranges, true)?
};
// patch existing claim hash with updated data
- for mut hash in updated_hashes {
- hash.gen_hash(output_path)?; // generate
+ for hash in updated_hashes {
pc.update_data_hash(hash)?;
}
}
@@ -1585,7 +1706,7 @@ impl Store {
output_path: &Path,
sig: Vec<u8>,
sig_placeholder: &[u8],
- ) -> Result<Vec<u8>> {
+ ) -> Result<(Vec<u8>, Vec<u8>)> {
if sig_placeholder.len() != sig.len() {
return Err(Error::CoseSigboxTooSmall);
}
@@ -1596,7 +1717,7 @@ impl Store {
// re-save to file
save_jumbf_to_file(&jumbf_bytes, output_path, Some(output_path))?;
- Ok(sig)
+ Ok((sig, jumbf_bytes))
}
/// Verify Store from an existing asset
@@ -2712,4 +2833,155 @@ pub mod tests {
println!("store = {}", store);
}
+
+ #[test]
+ fn test_external_manifest_sidecar() {
+ // test adding to actual image
+ let ap = fixture_path("libpng-test.png");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png");
+
+ let sidecar = op.with_extension(MANIFEST_STORE_EXT);
+
+ // Create claims store.
+ let mut store = Store::new();
+
+ // Create a new claim.
+ let mut claim = create_test_claim().unwrap();
+
+ // set claim for side car generation
+ claim.set_external_manifest();
+
+ // Do we generate JUMBF?
+ let signer = temp_signer();
+
+ store.commit_claim(claim).unwrap();
+
+ let saved_manifest = store.save_to_asset(&ap, &signer, &op).unwrap();
+
+ assert!(sidecar.exists());
+
+ // load external manifest
+ let loaded_manifest = std::fs::read(sidecar).unwrap();
+
+ // compare returned to external
+ assert_eq!(saved_manifest, loaded_manifest);
+
+ // load the jumbf back into a store
+ let mut validation_log = OneShotStatusTracker::default();
+ let restored_store = Store::from_jumbf(&loaded_manifest, &mut validation_log).unwrap();
+ let pc = restored_store.provenance_claim().unwrap();
+
+ // make sure this manifest goes with this asset
+ for dh_assertion in pc.data_hash_assertions() {
+ if dh_assertion.label_root() == DataHash::LABEL {
+ let dh = DataHash::from_assertion(dh_assertion).unwrap();
+
+ dh.verify_hash(&op.clone(), Some(pc.alg().to_string()))
+ .unwrap();
+ }
+ }
+ }
+
+ #[test]
+ fn test_external_manifest_embedded() {
+ // test adding to actual image
+ let ap = fixture_path("libpng-test.png");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png");
+
+ let sidecar = op.with_extension(MANIFEST_STORE_EXT);
+
+ // Create claims store.
+ let mut store = Store::new();
+
+ // Create a new claim.
+ let mut claim = create_test_claim().unwrap();
+
+ // Do we generate JUMBF?
+ let signer = temp_signer();
+
+ // start with base url
+ let fp = format!("file:/{}", sidecar.to_str().unwrap());
+ let url = url::Url::parse(&fp).unwrap();
+
+ let url_string: String = url.into();
+
+ // set claim for side car with remote manifest embedding generation
+ claim.set_remote_manifest(url_string.clone()).unwrap();
+
+ store.commit_claim(claim).unwrap();
+
+ let saved_manifest = store.save_to_asset(&ap, &signer, &op).unwrap();
+
+ assert!(sidecar.exists());
+
+ // load external manifest
+ let loaded_manifest = std::fs::read(sidecar).unwrap();
+
+ // compare returned to external
+ assert_eq!(saved_manifest, loaded_manifest);
+
+ // load the jumbf back into a store
+ let mut validation_log = OneShotStatusTracker::default();
+ let restored_store = Store::from_jumbf(&loaded_manifest, &mut validation_log).unwrap();
+ let pc = restored_store.provenance_claim().unwrap();
+
+ let mut asset_reader = std::fs::File::open(op.clone()).unwrap();
+ let ext_ref =
+ crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut asset_reader, "png")
+ .provenance
+ .unwrap();
+
+ assert_eq!(ext_ref, url_string);
+
+ // make sure this manifest goes with this asset
+ for dh_assertion in pc.data_hash_assertions() {
+ if dh_assertion.label_root() == DataHash::LABEL {
+ let dh = DataHash::from_assertion(dh_assertion).unwrap();
+
+ dh.verify_hash(&op.clone(), Some(pc.alg().to_string()))
+ .unwrap();
+ }
+ }
+ }
+
+ #[test]
+ fn test_user_guid_external_manifest_embedded() {
+ // test adding to actual image
+ let ap = fixture_path("libpng-test.png");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png");
+
+ let sidecar = op.with_extension(MANIFEST_STORE_EXT);
+
+ // Create claims store.
+ let mut store = Store::new();
+
+ // Create a new claim.
+ let mut claim = Claim::new_with_user_guid("store unit test", "my guid");
+
+ // Do we generate JUMBF?
+ let signer = temp_signer();
+
+ // start with base url
+ let fp = format!("file:/{}", sidecar.to_str().unwrap());
+ let url = url::Url::parse(&fp).unwrap();
+
+ let url_string: String = url.into();
+ // set claim for side car with remote manifest embedding generation
+ claim.set_remote_manifest(url_string).unwrap();
+
+ store.commit_claim(claim).unwrap();
+
+ let saved_manifest = store.save_to_asset(&ap, &signer, &op).unwrap();
+
+ assert!(sidecar.exists());
+
+ // load external manifest
+ let loaded_manifest = std::fs::read(sidecar).unwrap();
+
+ // compare returned to external
+ assert_eq!(saved_manifest, loaded_manifest);
+ }
}
diff --git a/sdk/src/utils/hash_utils.rs b/sdk/src/utils/hash_utils.rs
@@ -29,7 +29,7 @@ use sha2::{Digest, Sha256, Sha384, Sha512};
use crate::{Error, Result};
-const MAX_HASH_BUF: usize = 1024 * 1024 * 1024; // cap memory usage to 1GB
+const MAX_HASH_BUF: usize = 256 * 1024 * 1024; // cap memory usage to 256MB
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Exclusion {