From 8af63c77f8019128462bea765b86cfc517867b7d Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Fri, 22 Aug 2025 19:39:01 -0700 Subject: [PATCH 01/26] feat: adds Ingredient_assertion::from_stream and claim::thumbnaiI() updates unit test to use this. --- Cargo.lock | 100 +++++++++++++++---------------- sdk/src/assertions/ingredient.rs | 54 +++++++++++++++++ sdk/src/claim.rs | 12 ++++ sdk/src/store.rs | 59 +++--------------- 4 files changed, 125 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 232e13433..5766a6da8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,7 +53,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -74,7 +74,7 @@ checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -214,7 +214,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "synstructure", ] @@ -226,7 +226,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -310,7 +310,7 @@ checksum = "ddf3728566eefa873833159754f5732fb0951d3649e6e5b891cc70d56dd41673" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -392,7 +392,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -455,7 +455,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -814,7 +814,7 @@ name = "c2pa_macros" version = "0.59.1" dependencies = [ "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -861,16 +861,16 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.105", + "syn 2.0.106", "tempfile", "toml", ] [[package]] name = "cc" -version = "1.2.32" +version = "1.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" dependencies = [ "shlex", ] @@ -969,7 +969,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1204,7 +1204,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1234,7 +1234,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1245,7 +1245,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1308,7 +1308,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1358,7 +1358,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1722,7 +1722,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -2489,7 +2489,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -2780,7 +2780,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -2984,7 +2984,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -3173,7 +3173,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -3655,7 +3655,7 @@ checksum = "7a16f695759320ec969864fe5e7aa8e2a9c5665469565b100b8c8c1a36cd5484" dependencies = [ "proc-macro2", "rasn-derive-impl", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -3668,7 +3668,7 @@ dependencies = [ "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "uuid", ] @@ -3748,7 +3748,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4075,7 +4075,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4183,7 +4183,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4194,7 +4194,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4270,7 +4270,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4368,7 +4368,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4421,7 +4421,7 @@ checksum = "3cc4068497ae43896d41174586dcdc2153a1af2c82856fb308bfaaddc28e5549" dependencies = [ "iref", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4440,7 +4440,7 @@ dependencies = [ "quote", "serde", "sha2", - "syn 2.0.105", + "syn 2.0.106", "thiserror 1.0.69", ] @@ -4481,9 +4481,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.105" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -4507,7 +4507,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4603,7 +4603,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4614,7 +4614,7 @@ checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4721,7 +4721,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4870,7 +4870,7 @@ checksum = "d856e22ead1fb79b9fc3cec63300086f680924f2f7b0e2701f6835a28b9c4425" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -5080,7 +5080,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -5115,7 +5115,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5150,7 +5150,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -5249,7 +5249,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -5260,7 +5260,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -5502,7 +5502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225ac858e4405bdf164d92d070422c0b3b9b81f9b0b68836841f4d1bafc446b3" dependencies = [ "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -5576,7 +5576,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "synstructure", ] @@ -5597,7 +5597,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -5617,7 +5617,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "synstructure", ] @@ -5638,7 +5638,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -5671,7 +5671,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs index d3ab0c6ff..86095ce2b 100644 --- a/sdk/src/assertions/ingredient.rs +++ b/sdk/src/assertions/ingredient.rs @@ -11,6 +11,8 @@ // specific language governing permissions and limitations under // each license. +use std::io::{Read, Seek}; + #[cfg(feature = "json_schema")] use schemars::JsonSchema; use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; @@ -520,6 +522,58 @@ impl Ingredient { ingredient_map.end() } + + /// Create a new Ingredient assertion from a stream + /// You must specify the relationship and format. + /// This will return both the new Ingredient and the associated Store. + pub(crate) fn from_stream( + relationship: Relationship, + format: &str, + mut stream: impl Read + Seek + Send, + ) -> Result<(Self, crate::store::Store)> { + use crate::{ + jumbf::labels::{to_manifest_uri, to_signature_uri}, + status_tracker::StatusTracker, + store::Store, + }; + let mut validation_log = StatusTracker::default(); + let store = Store::from_stream(format, &mut stream, true, &mut validation_log)?; + + if let Some(claim) = store.provenance_claim() { + let mut ingredient = Self::new_v3(relationship); + + ingredient.title = claim.title().cloned(); + ingredient.format = claim.format().map(|f| f.to_string()); + ingredient.instance_id = Some(claim.instance_id().to_string()); + + let hashes = store.get_manifest_box_hashes(claim); + + ingredient.active_manifest = Some(HashedUri::new( + to_manifest_uri(claim.label()), + Some(claim.alg().to_owned()), + hashes.manifest_box_hash.as_ref(), + )); + ingredient.claim_signature = Some(HashedUri::new( + to_signature_uri(claim.label()), + Some(claim.alg().to_owned()), + hashes.signature_box_hash.as_ref(), + )); + + ingredient.validation_results = + Some(ValidationResults::from_store(&store, &mut validation_log)); + + if ingredient + .validation_results + .as_ref() + .map(|r| r.validation_state()) + == Some(crate::ValidationState::Valid) + { + ingredient.thumbnail = claim.thumbnail(); + } + return Ok((ingredient, store)); + } + Ok((Self::new_v3(relationship), store)) + } } fn to_decoding_err(label: &str, version: usize, field: &str) -> Error { diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs index d3109ef96..f268775b1 100644 --- a/sdk/src/claim.rs +++ b/sdk/src/claim.rs @@ -3953,6 +3953,18 @@ impl Claim { } } + // Returns a HashedUri to the claim thumbnail assertion, if it exists. + pub fn thumbnail(&self) -> Option { + self.assertions() + .iter() + .find(|hashed_uri| hashed_uri.url().contains(CLAIM_THUMBNAIL)) + .map(|t| { + // convert to absolute + let url = crate::jumbf::labels::to_absolute_uri(self.label(), &t.url()); + HashedUri::new(url, t.alg(), &t.hash()) + }) + } + /// Checks whether or not ocsp values are present in claim pub fn has_ocsp_vals(&self) -> bool { if !self.certificate_status_assertions().is_empty() { diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 779a7933b..23f803650 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -6388,9 +6388,9 @@ pub mod tests { #[cfg(feature = "v1_api")] fn test_update_manifest_v2() { use crate::{ - hashed_uri::HashedUri, jumbf::labels::to_signature_uri, - jumbf_io::load_jumbf_from_memory, utils::test::create_test_store_v1, - ClaimGeneratorInfo, ValidationResults, + assertions::{Action, Actions, Ingredient, Relationship}, + utils::test::create_test_store_v1, + ClaimGeneratorInfo, }; let signer = test_signer(SigningAlg::Ps256); @@ -6408,60 +6408,19 @@ pub mod tests { .save_to_asset(ap.as_path(), signer.as_ref(), op.as_path()) .unwrap(); - let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError); - // read back in - let ingredient_vec = std::fs::read(op.as_path()).unwrap(); - let restored_store = - Store::load_from_memory("jpg", &ingredient_vec, true, &mut report).unwrap(); - let pc = restored_store.provenance_claim().unwrap(); + // now read back as a stream and create an ingredient assertion + let mut stream = std::fs::File::open(op.as_path()).expect("could not open output file"); - // should be a regular manifest - assert!(!pc.update_manifest()); + let (ingredient_assertion, mut new_store) = + Ingredient::from_stream(Relationship::ParentOf, "image/jpeg", &mut stream).unwrap(); - // create a new update manifest + // create a new update manifest let mut claim = Claim::new("adobe unit test", Some("update_manifest_vendor"), 2); // ClaimGeneratorInfo is mandatory in Claim V2 let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test"); claim.add_claim_generator_info(cgi); - let mut new_store = Store::load_ingredient_to_claim( - &mut claim, - &load_jumbf_from_memory("jpg", &ingredient_vec).unwrap(), - None, - ) - .unwrap(); - - let ingredient_hashes = new_store.get_manifest_box_hashes(pc); - let parent_hashed_uri = HashedUri::new( - restored_store.provenance_path().unwrap(), - Some(pc.alg().to_string()), - &ingredient_hashes.manifest_box_hash, - ); - let signature_hashed_uri = HashedUri::new( - to_signature_uri(pc.label()), - Some(pc.alg().to_string()), - &ingredient_hashes.signature_box_hash, - ); - - let validation_results = ValidationResults::from_store(&restored_store, &report); - - let ingredient = Ingredient::new_v3(Relationship::ParentOf) - .set_active_manifests_and_signature_from_hashed_uri( - Some(parent_hashed_uri), - Some(signature_hashed_uri), - ) // mandatory for v3 - .set_validation_results(Some(validation_results)); // mandatory for v3 - - claim.add_assertion(&ingredient).unwrap(); - - // create mandatory opened action (optional for update manifest) - let ingredient = claim.ingredient_assertions()[0]; - let ingregient_uri = to_assertion_uri(claim.label(), &ingredient.label()); - let ingredient_hashed_uri = HashedUri::new( - ingregient_uri, - Some(claim.alg().to_owned()), - ingredient.hash(), - ); + let ingredient_hashed_uri = claim.add_assertion(&ingredient_assertion).unwrap(); let opened = Action::new("c2pa.opened") .set_parameter("ingredients", vec![ingredient_hashed_uri]) From df816c0200b01c56634bb9bb1bf29120e57360f7 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Fri, 22 Aug 2025 19:47:29 -0700 Subject: [PATCH 02/26] feat: content_credential concept --- sdk/src/content_credential.rs | 221 ++++++++++++++++++++++++++++++++++ sdk/src/lib.rs | 2 + 2 files changed, 223 insertions(+) create mode 100644 sdk/src/content_credential.rs diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs new file mode 100644 index 000000000..3cbcfee0a --- /dev/null +++ b/sdk/src/content_credential.rs @@ -0,0 +1,221 @@ +use std::{ + collections::HashMap, + io::{Read, Seek, Write}, +}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::{ + claim::Claim, + crypto::base64, + manifest::{Manifest, StoreOptions}, + manifest_store_report::ManifestStoreReport, + settings::Settings, + store::Store, + validation_status::ValidationStatus, + Error, Result, ValidationResults, +}; + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct StandardStoreReport { + #[serde(skip_serializing_if = "Option::is_none")] + /// A label for the active (most recent) manifest in the store + active_manifest: Option, + + /// A HashMap of Manifests + manifests: HashMap, + + /// ValidationStatus generated when loading the ManifestStore from an asset + validation_results: ValidationResults, +} + +impl StandardStoreReport { + fn from_store(store: &Store, validation_results: &ValidationResults) -> Result { + let mut validation_results = validation_results.clone(); + let active_manifest = store.provenance_label(); + let mut manifests = HashMap::new(); + let mut options = StoreOptions::default(); + + for claim in store.claims() { + let manifest_label = claim.label(); + let result = Manifest::from_store(&store, manifest_label, &mut options); + + match result { + Ok(manifest) => { + manifests.insert(manifest_label.to_owned(), manifest); + } + Err(e) => { + validation_results.add_status(ValidationStatus::from_error(&e)); + return Err(e); + } + }; + } + Ok(Self { + active_manifest, + manifests, + validation_results, + }) + } +} + +/// Experimental optimized Content Credential StructureSett +pub struct ContentCredential { + claim: Claim, + store: Store, + validation_results: ValidationResults, +} + +impl ContentCredential { + pub fn new(_settings: Settings) -> Self { + ContentCredential { + claim: Claim::new("", None, 2), + store: Store::new(), + validation_results: ValidationResults::default(), + } + } + + pub fn with_stream( + &mut self, + format: &str, + mut stream: impl Read + Seek + Send, + ) -> Result<&Self> { + use crate::assertions::{c2pa_action, Action, Actions, Ingredient, Relationship}; + //let verify = get_settings_value::("verify.verify_after_reading")?; // defaults to true + + let (ingredient_assertion, store) = + Ingredient::from_stream(Relationship::ParentOf, format, &mut stream)?; + + // add the ingredient assertion and get it's uri + let ingredient_hashed_uri = self.claim.add_assertion(&ingredient_assertion)?; + + // create an action associated with the ingredient + let opened = Action::new(c2pa_action::OPENED) + .set_parameter("ingredients", vec![ingredient_hashed_uri])?; + let actions = Actions::new().add_action(opened); + + self.claim.add_assertion(&actions)?; + + // capture the store and validation results from the assertion + self.store = store; + if let Some(results) = ingredient_assertion.validation_results { + self.validation_results = results; + } + Ok(self) + } + + fn set_claim_generator_info(&mut self) -> Result<&Self> { + self.claim + .add_claim_generator_info(crate::ClaimGeneratorInfo::default()); + Ok(self) + } + + pub fn save_to_stream( + &mut self, + format: &str, + source: &mut R, + dest: &mut W, + ) -> Result> + where + R: Read + Seek + Send, + W: Write + Read + Seek + Send, + { + let signer = Settings::signer()?; + self.set_claim_generator_info()?; + self.store.commit_claim(self.claim.clone())?; + self.store + .save_to_stream(format, source, dest, &signer) + .map_err(|e| e.into()) + } + + /// replace byte arrays with base64 encoded strings + fn hash_to_b64(mut value: Value) -> Value { + use std::collections::VecDeque; + + let mut queue = VecDeque::new(); + queue.push_back(&mut value); + + while let Some(current) = queue.pop_front() { + match current { + Value::Object(obj) => { + for (_, v) in obj.iter_mut() { + if let Value::Array(hash_arr) = v { + if !hash_arr.is_empty() && hash_arr.iter().all(|x| x.is_number()) { + // Pre-allocate with capacity to avoid reallocations + let mut hash_bytes = Vec::with_capacity(hash_arr.len()); + // Convert numbers to bytes safely + for n in hash_arr.iter() { + if let Some(num) = n.as_u64() { + hash_bytes.push(num as u8); + } + } + *v = Value::String(base64::encode(&hash_bytes)); + } + } + queue.push_back(v); + } + } + Value::Array(arr) => { + for v in arr.iter_mut() { + queue.push_back(v); + } + } + _ => {} + } + } + value + } + + pub fn value(&self) -> Result { + let report = StandardStoreReport::from_store(&self.store, &self.validation_results)?; + let json = serde_json::to_value(report).map_err(Error::JsonError)?; + Ok(Self::hash_to_b64(json)) + } + + pub fn detailed_value(&self) -> Result { + let report = + ManifestStoreReport::from_store_with_results(&self.store, &self.validation_results)?; + let json = serde_json::to_value(report).map_err(Error::JsonError)?; + Ok(Self::hash_to_b64(json)) + } +} + +impl std::fmt::Display for ContentCredential { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let value = self.detailed_value().map_err(|_| std::fmt::Error)?; + f.write_str( + serde_json::to_string_pretty(&value) + .map_err(|_| std::fmt::Error)? + .as_str(), + ) + } +} + +impl std::fmt::Debug for ContentCredential { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let value = self.detailed_value().map_err(|_| std::fmt::Error)?; + f.write_str( + serde_json::to_string_pretty(&value) + .map_err(|_| std::fmt::Error)? + .as_str(), + ) + } +} + +#[test] +fn test_content_credential_new() -> Result<()> { + const IMAGE_WITH_MANIFEST: &[u8] = include_bytes!("../tests/fixtures/CA.jpg"); + //let settings = Settings::default(); + let mut cursor = std::io::Cursor::new(IMAGE_WITH_MANIFEST); + let mut cr = ContentCredential::new(Settings::default()); + cr.with_stream("image/jpeg", &mut cursor).unwrap(); + println!("{}", cr); + + cursor.set_position(0); + cr.save_to_stream( + "image/jpeg", + &mut cursor, + &mut std::io::Cursor::new(Vec::new()), + )?; + Ok(()) +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index e7e957af7..6cf2b835a 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -97,6 +97,8 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// The assertions module contains the definitions for the assertions that are part of the C2PA specification. pub mod assertions; +pub mod content_credential; +pub use content_credential::ContentCredential; /// The cose_sign module contains the definitions for the COSE signing algorithms. pub mod cose_sign; From 3d0a53c2e2e3bbad004c531f6ce52a8a416ff0af Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Tue, 2 Sep 2025 20:43:01 -0700 Subject: [PATCH 03/26] chore: cleanup for unit tests --- sdk/src/assertion_input.rs | 81 ++++++++++++++++++++++++++++++++ sdk/src/assertions/ingredient.rs | 2 +- sdk/src/content_credential.rs | 8 ++-- sdk/src/store.rs | 2 +- 4 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 sdk/src/assertion_input.rs diff --git a/sdk/src/assertion_input.rs b/sdk/src/assertion_input.rs new file mode 100644 index 000000000..3abb903ed --- /dev/null +++ b/sdk/src/assertion_input.rs @@ -0,0 +1,81 @@ +// In assertion_input.rs +use crate::Result; +//use serde::{Serialize}; +use serde_json::Value as JsonValue; +use serde_cbor::Value as CborValue; +use crate::assertion::AssertionBase; + +pub trait AssertionInput { + fn to_assertion_with_label(self, label: &str) -> Result>; +} + +// Implementation for JSON values +impl AssertionInput for JsonValue { + fn to_assertion_with_label(self, label: &str) -> Result> { + try_deserialize_known_json_assertion(label, &self) + } +} + +// Implementation for CBOR values +impl AssertionInput for CborValue { + fn to_assertion_with_label(self, label: &str) -> Result> { + try_deserialize_known_cbor_assertion(label, &self) + } +} + +// Implementation for strings +impl AssertionInput for &str { + fn to_assertion_with_label(self, label: &str) -> Result> { + let json_value: JsonValue = serde_json::from_str(self)?; + json_value.to_assertion_with_label(label) + } +} + +impl AssertionInput for String { + fn to_assertion_with_label(self, label: &str) -> Result> { + self.as_str().to_assertion_with_label(label) + } +} + + +// Helper functions - return concrete Assertion +fn try_deserialize_known_json_assertion(label: &str, value: &JsonValue) -> Result> { + match label { + crate::assertions::Actions::LABEL => { + let actions: crate::assertions::Actions = serde_json::from_value(value.clone())?; + Ok(Box::new(actions)) // Convert to concrete Assertion + }, + // crate::assertions::Ingredient::LABEL => { + // let ingredient: crate::assertions::Ingredient = serde_json::from_value(value.clone())?; + // ingredient.to_assertion() // Convert to concrete Assertion + // }, + // Add more known assertion types here as needed + _ => { + // Create a User assertion for unknown types + let json_str = serde_json::to_string(value)?; + let user_json = crate::assertions::User::new(label, &json_str); + Ok(Box::new(user_json)) + } + } +} + +fn try_deserialize_known_cbor_assertion(label: &str, value: &CborValue) -> Result> { + let cbor_bytes = serde_cbor::to_vec(value)?; + + match label { + crate::assertions::Actions::LABEL => { + let actions: crate::assertions::Actions = serde_cbor::from_slice(&cbor_bytes)?; + Ok(Box::new(actions)) // Convert to concrete Assertion + }, + // crate::assertions::Ingredient::LABEL => { + // let ingredient: crate::assertions::Ingredient = serde_cbor::from_slice(&cbor_bytes)?; + // ingredient.to_assertion() // Convert to concrete Assertion + // }, + // Add more known assertion types here as needed + _ => { + // Create a UserCbor assertion for unknown types + let user_cbor = crate::assertions::UserCbor::new(label, cbor_bytes); + Ok(Box::new(user_cbor)) + } + } +} \ No newline at end of file diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs index 86095ce2b..ef46366e4 100644 --- a/sdk/src/assertions/ingredient.rs +++ b/sdk/src/assertions/ingredient.rs @@ -560,7 +560,7 @@ impl Ingredient { )); ingredient.validation_results = - Some(ValidationResults::from_store(&store, &mut validation_log)); + Some(ValidationResults::from_store(&store, &validation_log)); if ingredient .validation_results diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index 3cbcfee0a..90fb06f30 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -39,7 +39,7 @@ impl StandardStoreReport { for claim in store.claims() { let manifest_label = claim.label(); - let result = Manifest::from_store(&store, manifest_label, &mut options); + let result = Manifest::from_store(store, manifest_label, &mut options); match result { Ok(manifest) => { @@ -123,9 +123,7 @@ impl ContentCredential { let signer = Settings::signer()?; self.set_claim_generator_info()?; self.store.commit_claim(self.claim.clone())?; - self.store - .save_to_stream(format, source, dest, &signer) - .map_err(|e| e.into()) + self.store.save_to_stream(format, source, dest, &signer) } /// replace byte arrays with base64 encoded strings @@ -208,7 +206,7 @@ fn test_content_credential_new() -> Result<()> { //let settings = Settings::default(); let mut cursor = std::io::Cursor::new(IMAGE_WITH_MANIFEST); let mut cr = ContentCredential::new(Settings::default()); - cr.with_stream("image/jpeg", &mut cursor).unwrap(); + cr.with_stream("image/jpeg", &mut cursor)?; println!("{}", cr); cursor.set_position(0); diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 2d735c770..1909e5a63 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -6397,7 +6397,7 @@ pub mod tests { let (ingredient_assertion, mut new_store) = Ingredient::from_stream(Relationship::ParentOf, "image/jpeg", &mut stream).unwrap(); - // create a new update manifest + // create a new update manifest let mut claim = Claim::new("adobe unit test", Some("update_manifest_vendor"), 2); // ClaimGeneratorInfo is mandatory in Claim V2 let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test"); From 203b9fe10f82a16323d6f91d7d6b5085443c9599 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Tue, 2 Sep 2025 21:38:49 -0700 Subject: [PATCH 04/26] feat: remove validation_results and get it from the parent_ingredient. --- sdk/src/content_credential.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index 90fb06f30..61e34a4b8 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::{ + assertion::AssertionBase, + assertions::{Ingredient, Relationship}, claim::Claim, crypto::base64, manifest::{Manifest, StoreOptions}, @@ -25,9 +27,6 @@ pub struct StandardStoreReport { /// A HashMap of Manifests manifests: HashMap, - - /// ValidationStatus generated when loading the ManifestStore from an asset - validation_results: ValidationResults, } impl StandardStoreReport { @@ -54,7 +53,6 @@ impl StandardStoreReport { Ok(Self { active_manifest, manifests, - validation_results, }) } } @@ -75,6 +73,17 @@ impl ContentCredential { } } + fn parent_ingredient(&self) -> Option { + for i in self.claim.ingredient_assertions() { + if let Ok(ingredient) = Ingredient::from_assertion(i.assertion()) { + if ingredient.relationship == Relationship::ParentOf { + return Some(ingredient); + } + } + } + None + } + pub fn with_stream( &mut self, format: &str, @@ -165,14 +174,15 @@ impl ContentCredential { } pub fn value(&self) -> Result { - let report = StandardStoreReport::from_store(&self.store, &self.validation_results)?; + let results = self.parent_ingredient().and_then(|i| i.validation_results); + let report = StandardStoreReport::from_store(&self.store, &results.unwrap())?; let json = serde_json::to_value(report).map_err(Error::JsonError)?; Ok(Self::hash_to_b64(json)) } pub fn detailed_value(&self) -> Result { - let report = - ManifestStoreReport::from_store_with_results(&self.store, &self.validation_results)?; + let results = self.parent_ingredient().and_then(|i| i.validation_results); + let report = ManifestStoreReport::from_store_with_results(&self.store, &results.unwrap())?; let json = serde_json::to_value(report).map_err(Error::JsonError)?; Ok(Self::hash_to_b64(json)) } @@ -180,7 +190,7 @@ impl ContentCredential { impl std::fmt::Display for ContentCredential { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let value = self.detailed_value().map_err(|_| std::fmt::Error)?; + let value = self.value().map_err(|_| std::fmt::Error)?; f.write_str( serde_json::to_string_pretty(&value) .map_err(|_| std::fmt::Error)? From a4a6161db1789b218d817b026fe88243d3ba6f1d Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Tue, 2 Sep 2025 21:46:38 -0700 Subject: [PATCH 05/26] chore more validation_status removal --- sdk/src/content_credential.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index 61e34a4b8..7c3d3cc71 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -61,7 +61,6 @@ impl StandardStoreReport { pub struct ContentCredential { claim: Claim, store: Store, - validation_results: ValidationResults, } impl ContentCredential { @@ -69,7 +68,6 @@ impl ContentCredential { ContentCredential { claim: Claim::new("", None, 2), store: Store::new(), - validation_results: ValidationResults::default(), } } @@ -107,9 +105,6 @@ impl ContentCredential { // capture the store and validation results from the assertion self.store = store; - if let Some(results) = ingredient_assertion.validation_results { - self.validation_results = results; - } Ok(self) } @@ -174,15 +169,25 @@ impl ContentCredential { } pub fn value(&self) -> Result { - let results = self.parent_ingredient().and_then(|i| i.validation_results); - let report = StandardStoreReport::from_store(&self.store, &results.unwrap())?; + let results = self + .parent_ingredient() + .and_then(|i| i.validation_results) + .ok_or(Error::ClaimMissing { + label: "Parent Ingredient missing".to_string(), + })?; + let report = StandardStoreReport::from_store(&self.store, &results)?; let json = serde_json::to_value(report).map_err(Error::JsonError)?; Ok(Self::hash_to_b64(json)) } pub fn detailed_value(&self) -> Result { - let results = self.parent_ingredient().and_then(|i| i.validation_results); - let report = ManifestStoreReport::from_store_with_results(&self.store, &results.unwrap())?; + let results = self + .parent_ingredient() + .and_then(|i| i.validation_results) + .ok_or(Error::ClaimMissing { + label: "Parent Ingredient missing".to_string(), + })?; + let report = ManifestStoreReport::from_store_with_results(&self.store, &results)?; let json = serde_json::to_value(report).map_err(Error::JsonError)?; Ok(Self::hash_to_b64(json)) } From d0ece81eb6c9af57a29e4baf800e99f9e832db80 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Sun, 28 Sep 2025 14:40:28 -0700 Subject: [PATCH 06/26] fix: Reader to_folder now writes c2pa_data.c2pa and c2patool doesn't write a duplicate manifest.json file. --- cli/src/main.rs | 3 +-- sdk/src/reader.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 959226d85..43db629f2 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -877,14 +877,13 @@ fn main() -> Result<()> { let mut reader = Reader::from_file(&args.path).map_err(special_errs)?; validate_cawg(&mut reader)?; reader.to_folder(&output)?; - let report = reader.to_string(); + let _report = reader.to_string(); if args.detailed { // for a detailed report first call the above to generate the thumbnails // then call this to add the detailed report let detailed = format!("{reader:#?}"); File::create(output.join("detailed.json"))?.write_all(&detailed.into_bytes())?; } - File::create(output.join("manifest_store.json"))?.write_all(&report.into_bytes())?; println!("Manifest report written to the directory {:?}", &output); } } else if args.ingredient { diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index 4852bd380..d156b1701 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -660,7 +660,9 @@ impl Reader { #[cfg(feature = "file_io")] pub fn to_folder>(&self, path: P) -> Result<()> { std::fs::create_dir_all(&path)?; - std::fs::write(path.as_ref().join("manifest.json"), self.json())?; + std::fs::write(path.as_ref().join("manifest_store.json"), self.json())?; + let c2pa_data = self.store.to_jumbf_internal(0)?; + std::fs::write(path.as_ref().join("manifest_data.c2pa"), c2pa_data)?; for manifest in self.manifests.values() { let resources = manifest.resources(); for (uri, data) in resources.resources() { @@ -997,7 +999,9 @@ pub mod tests { assert_eq!(reader.validation_status(), None); let temp_dir = tempdirectory().unwrap(); reader.to_folder(temp_dir.path())?; - let path = temp_dir_path(&temp_dir, "manifest.json"); + let path = temp_dir_path(&temp_dir, "manifest_store.json"); + assert!(path.exists()); + let path = temp_dir_path(&temp_dir, "manifest_data.c2pa"); assert!(path.exists()); #[cfg(target_os = "wasi")] crate::utils::io_utils::wasm_remove_dir_all(temp_dir)?; From 05f5d0c0e5bfc02fab7737bbe170c400a92cfda7 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Sun, 28 Sep 2025 14:42:04 -0700 Subject: [PATCH 07/26] feat: add cc from_stream add_assertion and validation_results to reports. adds unit test for from_stream and c2p2.created. --- sdk/src/assertions/ingredient.rs | 31 +++++++---- sdk/src/content_credential.rs | 94 +++++++++++++++++++++++++------- 2 files changed, 95 insertions(+), 30 deletions(-) diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs index ef46366e4..207e0ccd0 100644 --- a/sdk/src/assertions/ingredient.rs +++ b/sdk/src/assertions/ingredient.rs @@ -24,6 +24,9 @@ use crate::{ cbor_types::map_cbor_to_type, error::Result, hashed_uri::HashedUri, + jumbf::labels::{to_manifest_uri, to_signature_uri}, + status_tracker::StatusTracker, + store::Store, validation_results::ValidationResults, validation_status::ValidationStatus, Error, @@ -530,15 +533,22 @@ impl Ingredient { relationship: Relationship, format: &str, mut stream: impl Read + Seek + Send, - ) -> Result<(Self, crate::store::Store)> { - use crate::{ - jumbf::labels::{to_manifest_uri, to_signature_uri}, - status_tracker::StatusTracker, - store::Store, - }; + ) -> Result<(Self, Store)> { let mut validation_log = StatusTracker::default(); - let store = Store::from_stream(format, &mut stream, true, &mut validation_log)?; + let store: Store = Store::from_stream(format, &mut stream, true, &mut validation_log)?; + let validation_results = ValidationResults::from_store(&store, &validation_log); + let ingredient = + Self::from_store_and_validation_results(relationship, &store, &validation_results)?; + Ok((ingredient, store)) + } + /// Create a new Ingredient assertion from a Store and ValidationResults. + /// You must specify the relationship. + pub(crate) fn from_store_and_validation_results( + relationship: Relationship, + store: &Store, + validation_results: &ValidationResults, + ) -> Result { if let Some(claim) = store.provenance_claim() { let mut ingredient = Self::new_v3(relationship); @@ -559,8 +569,7 @@ impl Ingredient { hashes.signature_box_hash.as_ref(), )); - ingredient.validation_results = - Some(ValidationResults::from_store(&store, &validation_log)); + ingredient.validation_results = Some(validation_results.clone()); if ingredient .validation_results @@ -570,9 +579,9 @@ impl Ingredient { { ingredient.thumbnail = claim.thumbnail(); } - return Ok((ingredient, store)); + return Ok(ingredient); } - Ok((Self::new_v3(relationship), store)) + Ok(Self::new_v3(relationship)) } } diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index 4199a99da..974d54c55 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -8,12 +8,12 @@ use serde_json::Value; use crate::{ assertion::AssertionBase, - assertions::{Ingredient, Relationship}, + assertions::{c2pa_action, Action, Actions, Ingredient, Relationship}, claim::Claim, crypto::base64, manifest::{Manifest, StoreOptions}, manifest_store_report::ManifestStoreReport, - settings::Settings, + settings::{self, Settings}, store::Store, validation_status::ValidationStatus, Error, Result, ValidationResults, @@ -27,6 +27,8 @@ pub struct StandardStoreReport { /// A HashMap of Manifests manifests: HashMap, + + validation_results: ValidationResults, } impl StandardStoreReport { @@ -53,6 +55,7 @@ impl StandardStoreReport { Ok(Self { active_manifest, manifests, + validation_results, }) } } @@ -64,13 +67,27 @@ pub struct ContentCredential { } impl ContentCredential { - pub fn new(_settings: Settings) -> Self { + pub fn new(_settings: &Settings) -> Self { + let vendor = + settings::get_settings_value::>("builder.vendor").unwrap_or(None); + let claim = Claim::new("", vendor.as_deref(), 2); ContentCredential { - claim: Claim::new("", None, 2), + claim, store: Store::new(), } } + pub fn from_stream( + settings: &Settings, + format: &str, + mut stream: impl Read + Seek + Send, + ) -> Result { + let mut cc = Self::new(settings); + let (_, store) = cc.with_stream_impl(Relationship::ParentOf, format, &mut stream)?; + cc.store = store; // replaces the empty store + Ok(cc) + } + fn parent_ingredient(&self) -> Option { for i in self.claim.ingredient_assertions() { if let Ok(ingredient) = Ingredient::from_assertion(i.assertion()) { @@ -82,16 +99,31 @@ impl ContentCredential { None } - pub fn with_stream( + pub fn add_assertion(&mut self, assertion: &impl AssertionBase) -> Result<&Self> { + self.claim.add_assertion(assertion)?; + Ok(self) + } + + pub fn add_ingredient_from_stream( &mut self, format: &str, mut stream: impl Read + Seek + Send, ) -> Result<&Self> { - use crate::assertions::{c2pa_action, Action, Actions, Ingredient, Relationship}; + Ok(self + .with_stream_impl(Relationship::ComponentOf, format, &mut stream)? + .0) + } + + fn with_stream_impl( + &mut self, + relationship: Relationship, + format: &str, + mut stream: impl Read + Seek + Send, + ) -> Result<(&Self, Store)> { //let verify = get_settings_value::("verify.verify_after_reading")?; // defaults to true let (ingredient_assertion, store) = - Ingredient::from_stream(Relationship::ParentOf, format, &mut stream)?; + Ingredient::from_stream(relationship, format, &mut stream)?; // add the ingredient assertion and get it's uri let ingredient_hashed_uri = self.claim.add_assertion(&ingredient_assertion)?; @@ -104,8 +136,7 @@ impl ContentCredential { self.claim.add_assertion(&actions)?; // capture the store and validation results from the assertion - self.store = store; - Ok(self) + Ok((self, store)) } fn set_claim_generator_info(&mut self) -> Result<&Self> { @@ -216,19 +247,44 @@ impl std::fmt::Debug for ContentCredential { } #[test] -fn test_content_credential_new() -> Result<()> { +fn test_content_credential_from_stream() -> Result<()> { const IMAGE_WITH_MANIFEST: &[u8] = include_bytes!("../tests/fixtures/CA.jpg"); //let settings = Settings::default(); - let mut cursor = std::io::Cursor::new(IMAGE_WITH_MANIFEST); - let mut cr = ContentCredential::new(Settings::default()); - cr.with_stream("image/jpeg", &mut cursor)?; + let mut source = std::io::Cursor::new(IMAGE_WITH_MANIFEST); + let settings = Settings::default(); + let mut cr = ContentCredential::from_stream(&settings, "image/jpeg", &mut source)?; println!("{cr}"); - cursor.set_position(0); - cr.save_to_stream( - "image/jpeg", - &mut cursor, - &mut std::io::Cursor::new(Vec::new()), - )?; + source.set_position(0); + let mut dest = std::io::Cursor::new(Vec::new()); + cr.save_to_stream("image/jpeg", &mut source, &mut dest)?; + + dest.set_position(0); + let cr2 = ContentCredential::from_stream(&settings, "image/jpeg", &mut dest)?; + println!("{cr2}"); + Ok(()) +} + +#[test] +fn test_content_credential_created() -> Result<()> { + const IMAGE_WITH_MANIFEST: &[u8] = include_bytes!("../tests/fixtures/CA.jpg"); + //let settings = Settings::default(); + let mut source = std::io::Cursor::new(IMAGE_WITH_MANIFEST); + let settings = Settings::default(); + let mut cr = ContentCredential::new(&settings); + let action = crate::assertions::Actions::new().add_action( + crate::assertions::Action::new(crate::assertions::c2pa_action::CREATED) + .set_source_type(crate::DigitalSourceType::Empty) + .set_parameter("note", "Created by test_content_credential_created")?, + ); + cr.add_assertion(&action)?; + + source.set_position(0); + let mut dest = std::io::Cursor::new(Vec::new()); + cr.save_to_stream("image/jpeg", &mut source, &mut dest)?; + + dest.set_position(0); + let cr = ContentCredential::from_stream(&settings, "image/jpeg", &mut dest)?; + println!("{cr}"); Ok(()) } From ae1a4bd77263f961375ac86139e5aaf018e19901 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Sun, 28 Sep 2025 19:14:30 -0700 Subject: [PATCH 08/26] feat: Adds claim.add_action and action, add_action_checked() --- sdk/src/assertions/actions.rs | 30 ++++++++++++++++++++++- sdk/src/claim.rs | 39 ++++++++++++++++++++++++++++-- sdk/src/content_credential.rs | 45 +++++++++++++++++++++++++---------- sdk/src/reader.rs | 2 +- 4 files changed, 100 insertions(+), 16 deletions(-) diff --git a/sdk/src/assertions/actions.rs b/sdk/src/assertions/actions.rs index 5886cbe02..4e842b65d 100644 --- a/sdk/src/assertions/actions.rs +++ b/sdk/src/assertions/actions.rs @@ -19,7 +19,7 @@ use serde_cbor::Value; use crate::{ assertion::{Assertion, AssertionBase, AssertionCbor}, assertions::{labels, region_of_interest::RegionOfInterest, Actor, AssertionMetadata}, - error::Result, + error::{Error, Result}, resource_store::UriOrResource, utils::cbor_types::DateT, ClaimGeneratorInfo, HashedUri, @@ -847,6 +847,34 @@ impl Actions { self } + /// Adds an [`Action`] to this assertion's list of actions. + pub fn add_action_checked(mut self, action: Action) -> Result { + let action_name = action.action(); + if action_name.is_empty() { + return Err(Error::AssertionSpecificError( + "Action must have a non-empty action label".to_string(), + )); + } + if V2_DEPRECATED_ACTIONS.contains(&action_name) { + return Err(Error::VersionCompatibility(format!( + "Action '{action_name}' is deprecated in C2PA v2" + ))); + } + if action_name == c2pa_action::OPENED || action_name == c2pa_action::CREATED { + let existing_action = self.actions.iter().find(|a| a.action() == action_name); + if existing_action.is_some() { + return Err(Error::AssertionSpecificError( + "Only one 'c2pa.opened' action is allowed".to_string(), + )); + } + // always insert as first action + self.actions.insert(0, action); + return Ok(self); + } + self.actions.push(action); + Ok(self) + } + /// Sets [`AssertionMetadata`] for the action. pub fn add_metadata(mut self, metadata: AssertionMetadata) -> Self { self.metadata = Some(metadata); diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs index 094b8ca0a..942086082 100644 --- a/sdk/src/claim.rs +++ b/sdk/src/claim.rs @@ -35,8 +35,8 @@ use crate::{ self, ACTIONS, ASSERTION_METADATA, ASSERTION_STORE, BMFF_HASH, CLAIM_THUMBNAIL, DATABOX_STORE, METADATA_LABEL_REGEX, }, - Actions, AssertionMetadata, AssetType, BmffHash, BoxHash, DataBox, DataHash, Ingredient, - Metadata, Relationship, V2_DEPRECATED_ACTIONS, + Action, Actions, AssertionMetadata, AssetType, BmffHash, BoxHash, DataBox, DataHash, + Ingredient, Metadata, Relationship, V2_DEPRECATED_ACTIONS, }, asset_io::CAIRead, cbor_types::map_cbor_to_type, @@ -1158,6 +1158,28 @@ impl Claim { self.update_manifest = is_update_manifest; } + /// Adds an action to the claim. + /// + /// If an Actions assertion already exists, the action is added to it. + /// If not, a new Actions assertion is created and added to the claim. + /// If multiple exist, this will update the first one found. + pub fn add_action(&mut self, action: Action) -> Result<&mut Self> { + match self.get_assertion(labels::ACTIONS, 0) { + None => { + let actions = Actions::new().add_action_checked(action)?; + self.add_assertion(&actions)?; + } + Some(a) => { + let actions = Actions::from_assertion(a)?; + + let actions = actions.add_action_checked(action)?; + + self.replace_assertion(actions.to_assertion()?)?; + } + }; + Ok(self) + } + pub fn add_claim_generator_info(&mut self, info: ClaimGeneratorInfo) -> &mut Self { match self.claim_generator_info.as_mut() { Some(cgi) => cgi.push(info), @@ -3795,6 +3817,19 @@ impl Claim { iter.next() } + // /// returns first instance of an assertion whose label and instance match + // pub fn get_assertion_mut(&self, assertion_label: &str, instance: usize) -> Option<&Assertion> { + // let mut iter = self.claim_assertion_store().iter().filter_map(|ca| { + // if ca.label_raw() == assertion_label && ca.instance() == instance { + // Some(ca.assertion()) + // } else { + // None + // } + // }); + + // iter.next() + // } + /// returns instance of an assertion whose label and instance match pub fn get_claim_assertion( &self, diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index 974d54c55..b5bb4aefb 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -99,9 +99,8 @@ impl ContentCredential { None } - pub fn add_assertion(&mut self, assertion: &impl AssertionBase) -> Result<&Self> { - self.claim.add_assertion(assertion)?; - Ok(self) + pub fn add_assertion(&mut self, assertion: &impl AssertionBase) -> Result { + self.claim.add_assertion(assertion) } pub fn add_ingredient_from_stream( @@ -120,28 +119,37 @@ impl ContentCredential { format: &str, mut stream: impl Read + Seek + Send, ) -> Result<(&Self, Store)> { - //let verify = get_settings_value::("verify.verify_after_reading")?; // defaults to true + // create an action associated with the ingredient + let action_label = if relationship == Relationship::ParentOf { + c2pa_action::OPENED + } else { + c2pa_action::PLACED + }; let (ingredient_assertion, store) = Ingredient::from_stream(relationship, format, &mut stream)?; // add the ingredient assertion and get it's uri - let ingredient_hashed_uri = self.claim.add_assertion(&ingredient_assertion)?; + let ingredient_hashed_uri = self.add_assertion(&ingredient_assertion)?; - // create an action associated with the ingredient - let opened = Action::new(c2pa_action::OPENED) - .set_parameter("ingredients", vec![ingredient_hashed_uri])?; - let actions = Actions::new().add_action(opened); + // todo add to exiting actions and check fo + let action = + Action::new(action_label).set_parameter("ingredients", vec![ingredient_hashed_uri])?; - self.claim.add_assertion(&actions)?; + let actions = Actions::new().add_action_checked(action)?; + + self.add_assertion(&actions)?; // capture the store and validation results from the assertion Ok((self, store)) } fn set_claim_generator_info(&mut self) -> Result<&Self> { - self.claim - .add_claim_generator_info(crate::ClaimGeneratorInfo::default()); + if self.claim.claim_generator_info().is_none() { + // only set if not already set + self.claim + .add_claim_generator_info(crate::ClaimGeneratorInfo::default()); + } Ok(self) } @@ -288,3 +296,16 @@ fn test_content_credential_created() -> Result<()> { println!("{cr}"); Ok(()) } + +// #[test] +// fn test_from_reader() -> Result<()> { +// const IMAGE_WITH_MANIFEST: &[u8] = include_bytes!("../tests/fixtures/CA.jpg"); +// let mut source = std::io::Cursor::new(IMAGE_WITH_MANIFEST); +// let settings = Settings::default(); +// let reader = crate::Reader::from_stream("image/jpeg", &mut source)?; +// let reader_str = reader.to_string(); +// let cr = ContentCredential::from_reader(&reader)?; +// let cr = cr.to_string(); +// assert_eq!(reader_str, cr); +// Ok(()) +// } diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index d156b1701..e9c203ce1 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -95,7 +95,7 @@ pub struct Reader { #[serde(skip)] /// We keep this around so we can generate a detailed report if needed - store: Store, + pub(crate) store: Store, #[serde(skip)] /// Map to hold post-validation assertion values for reports From c1238e562bfbdcbd4f23f75171fa74f38b610ef4 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Mon, 20 Oct 2025 09:27:15 -0700 Subject: [PATCH 09/26] feat: add create and add_action method add_ingredient_from_stream always set position to zero inside read methods --- sdk/src/assertions/actions.rs | 24 +++- sdk/src/assertions/ingredient.rs | 19 ++- sdk/src/claim.rs | 2 +- sdk/src/content_credential.rs | 198 +++++++++++++++++++------------ sdk/src/manifest.rs | 1 + sdk/src/settings/builder.rs | 2 + 6 files changed, 169 insertions(+), 77 deletions(-) diff --git a/sdk/src/assertions/actions.rs b/sdk/src/assertions/actions.rs index eaebd3e0a..30f6b6e51 100644 --- a/sdk/src/assertions/actions.rs +++ b/sdk/src/assertions/actions.rs @@ -655,6 +655,27 @@ impl Action { Ok(self) } + /// Adds an ingredient HashedUri to the action. + pub(crate) fn add_ingredient(mut self, ingredient: HashedUri) -> Result { + match &mut self.parameters { + Some(params) => match &mut params.ingredients { + Some(ingredients) => { + ingredients.push(ingredient); + } + None => { + params.ingredients = Some(vec![ingredient]); + } + }, + None => { + self.parameters = Some(ActionParameters { + ingredients: Some(vec![ingredient]), + ..Default::default() + }); + } + } + Ok(self) + } + /// Extracts ingredient IDs from the action /// There are many deprecated ways to specify ingredient IDs /// priority: parameters.ingredientIds, parameters.org.cai.ingredientIds, parameters.instanceId, instanceId. @@ -770,6 +791,7 @@ impl Actions { /// /// See . pub const LABEL: &'static str = labels::ACTIONS; + pub const LABEL_VERSIONED: &'static str = "c2pa.actions.v2"; pub const VERSION: Option = Some(ASSERTION_CREATION_VERSION); /// Creates a new [`Actions`] assertion struct. @@ -898,7 +920,7 @@ impl AssertionBase for Actions { } fn label(&self) -> &str { - "c2pa.actions.v2" + Self::LABEL_VERSIONED } fn to_assertion(&self) -> Result { diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs index 207e0ccd0..3767d0a18 100644 --- a/sdk/src/assertions/ingredient.rs +++ b/sdk/src/assertions/ingredient.rs @@ -25,6 +25,7 @@ use crate::{ error::Result, hashed_uri::HashedUri, jumbf::labels::{to_manifest_uri, to_signature_uri}, + settings::Settings, status_tracker::StatusTracker, store::Store, validation_results::ValidationResults, @@ -533,9 +534,25 @@ impl Ingredient { relationship: Relationship, format: &str, mut stream: impl Read + Seek + Send, + settings: &Settings, ) -> Result<(Self, Store)> { let mut validation_log = StatusTracker::default(); - let store: Store = Store::from_stream(format, &mut stream, true, &mut validation_log)?; + + // // Try to get xmp info, if this fails all XmpInfo fields will be None. + // let xmp_info = XmpInfo::from_source(stream, &format); + + // let id = if let Some(id) = xmp_info.instance_id { + // id + // } else { + // default_instance_id() + // }; + + // let mut ingredient = Self::new(title.into(), format, id); + + // ingredient.document_id = xmp_info.document_id; // use document id if one exists + // ingredient.provenance = xmp_info.provenance; + let store: Store = + Store::from_stream(format, &mut stream, true, &mut validation_log, settings)?; let validation_results = ValidationResults::from_store(&store, &validation_log); let ingredient = Self::from_store_and_validation_results(relationship, &store, &validation_results)?; diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs index 2c698f7e9..1d81ed201 100644 --- a/sdk/src/claim.rs +++ b/sdk/src/claim.rs @@ -1164,7 +1164,7 @@ impl Claim { /// If not, a new Actions assertion is created and added to the claim. /// If multiple exist, this will update the first one found. pub fn add_action(&mut self, action: Action) -> Result<&mut Self> { - match self.get_assertion(labels::ACTIONS, 0) { + match self.get_assertion(Actions::LABEL_VERSIONED, 0) { None => { let actions = Actions::new().add_action_checked(action)?; self.add_assertion(&actions)?; diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index b5bb4aefb..ac42efa43 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -1,3 +1,16 @@ +// Copyright 2025 Adobe. All rights reserved. +// This file is licensed to you under the Apache License, +// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +// or the MIT license (http://opensource.org/licenses/MIT), +// at your option. + +// Unless required by applicable law or agreed to in writing, +// this software is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or +// implied. See the LICENSE-MIT and LICENSE-APACHE files for the +// specific language governing permissions and limitations under +// each license. + use std::{ collections::HashMap, io::{Read, Seek, Write}, @@ -13,10 +26,11 @@ use crate::{ crypto::base64, manifest::{Manifest, StoreOptions}, manifest_store_report::ManifestStoreReport, - settings::{self, Settings}, + settings::Settings, + status_tracker::StatusTracker, store::Store, validation_status::ValidationStatus, - Error, Result, ValidationResults, + ClaimGeneratorInfo, DigitalSourceType, Error, HashedUri, Result, ValidationResults, }; #[derive(Serialize, Deserialize, Debug, Default)] @@ -33,14 +47,22 @@ pub struct StandardStoreReport { impl StandardStoreReport { fn from_store(store: &Store, validation_results: &ValidationResults) -> Result { + let settings = Settings::default(); let mut validation_results = validation_results.clone(); let active_manifest = store.provenance_label(); let mut manifests = HashMap::new(); + let mut validation_log = StatusTracker::default(); let mut options = StoreOptions::default(); for claim in store.claims() { let manifest_label = claim.label(); - let result = Manifest::from_store(store, manifest_label, &mut options); + let result = Manifest::from_store( + store, + manifest_label, + &mut options, + &mut validation_log, + &settings, + ); match result { Ok(manifest) => { @@ -60,29 +82,44 @@ impl StandardStoreReport { } } -/// Experimental optimized Content Credential StructureSett +/// Experimental optimized Content Credential Structure pub struct ContentCredential { claim: Claim, store: Store, + settings: Settings, } impl ContentCredential { - pub fn new(_settings: &Settings) -> Self { - let vendor = - settings::get_settings_value::>("builder.vendor").unwrap_or(None); - let claim = Claim::new("", vendor.as_deref(), 2); + pub fn new(settings: &Settings) -> Self { + let mut claim = Claim::new("", settings.builder.vendor.as_deref(), 2); + claim.instance_id = uuid::Uuid::new_v4().to_string(); ContentCredential { claim, store: Store::new(), + settings: settings.clone(), } } + /// Use this for a content credential that is being created from scratch + pub fn create(source_type: DigitalSourceType, settings: &Settings) -> Result { + let mut cc = Self::new(settings); + + let actions = Actions::new() + .add_action(Action::new(c2pa_action::CREATED).set_source_type(source_type)); + + cc.add_assertion(&actions)?; + + Ok(cc) + } + + /// creates a content credential from an existing stream pub fn from_stream( settings: &Settings, format: &str, mut stream: impl Read + Seek + Send, ) -> Result { let mut cc = Self::new(settings); + stream.rewind()?; let (_, store) = cc.with_stream_impl(Relationship::ParentOf, format, &mut stream)?; cc.store = store; // replaces the empty store Ok(cc) @@ -99,10 +136,18 @@ impl ContentCredential { None } - pub fn add_assertion(&mut self, assertion: &impl AssertionBase) -> Result { + pub fn add_assertion(&mut self, assertion: &impl AssertionBase) -> Result { self.claim.add_assertion(assertion) } + pub fn add_action(&mut self, action: Action) -> Result<()> { + self.claim.add_action(action)?; + Ok(()) + } + + /// This is used to add component ingredients from a stream + /// + /// Parent ingredients are created using from_stream. pub fn add_ingredient_from_stream( &mut self, format: &str, @@ -113,6 +158,7 @@ impl ContentCredential { .0) } + /// internal implementation to add ingredient from stream fn with_stream_impl( &mut self, relationship: Relationship, @@ -127,32 +173,37 @@ impl ContentCredential { }; let (ingredient_assertion, store) = - Ingredient::from_stream(relationship, format, &mut stream)?; + Ingredient::from_stream(relationship, format, &mut stream, &self.settings)?; + + let manifest_bytes = store.to_jumbf_internal(0)?; + Store::load_ingredient_to_claim(&mut self.claim, &manifest_bytes, None, &self.settings)?; // add the ingredient assertion and get it's uri let ingredient_hashed_uri = self.add_assertion(&ingredient_assertion)?; // todo add to exiting actions and check fo - let action = - Action::new(action_label).set_parameter("ingredients", vec![ingredient_hashed_uri])?; + let action = Action::new(action_label).add_ingredient(ingredient_hashed_uri)?; - let actions = Actions::new().add_action_checked(action)?; + //let actions = Actions::new().add_action_checked(action)?; + self.claim.add_action(action)?; - self.add_assertion(&actions)?; + //self.add_assertion(&action)?; // capture the store and validation results from the assertion Ok((self, store)) } - fn set_claim_generator_info(&mut self) -> Result<&Self> { + /// sets the default claim generator info if not already set + fn set_default_claim_generator_info(&mut self) -> Result<&Self> { if self.claim.claim_generator_info().is_none() { // only set if not already set self.claim - .add_claim_generator_info(crate::ClaimGeneratorInfo::default()); + .add_claim_generator_info(ClaimGeneratorInfo::default()); } Ok(self) } + /// signs and saves the content credential to the destination stream pub fn save_to_stream( &mut self, format: &str, @@ -164,12 +215,14 @@ impl ContentCredential { W: Write + Read + Seek + Send, { let signer = Settings::signer()?; - self.set_claim_generator_info()?; + self.set_default_claim_generator_info()?; self.store.commit_claim(self.claim.clone())?; - self.store.save_to_stream(format, source, dest, &signer) + source.rewind()?; // always reset source to start + self.store + .save_to_stream(format, source, dest, &signer, &self.settings) } - /// replace byte arrays with base64 encoded strings + /// replace byte arrays with base64 encoded strings for more readable output fn hash_to_b64(mut value: Value) -> Value { use std::collections::VecDeque; @@ -207,7 +260,9 @@ impl ContentCredential { value } - pub fn value(&self) -> Result { + /// Generates a value similar to the C2PA Reader output + pub fn reader_value(&self) -> Result { + // get the validation results from the parent ingredient let results = self .parent_ingredient() .and_then(|i| i.validation_results) @@ -219,6 +274,7 @@ impl ContentCredential { Ok(Self::hash_to_b64(json)) } + /// generates a value similar to the C2PA Reader detailed output pub fn detailed_value(&self) -> Result { let results = self .parent_ingredient() @@ -234,7 +290,7 @@ impl ContentCredential { impl std::fmt::Display for ContentCredential { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let value = self.value().map_err(|_| std::fmt::Error)?; + let value = self.reader_value().map_err(|_| std::fmt::Error)?; f.write_str( serde_json::to_string_pretty(&value) .map_err(|_| std::fmt::Error)? @@ -254,58 +310,52 @@ impl std::fmt::Debug for ContentCredential { } } -#[test] -fn test_content_credential_from_stream() -> Result<()> { - const IMAGE_WITH_MANIFEST: &[u8] = include_bytes!("../tests/fixtures/CA.jpg"); - //let settings = Settings::default(); - let mut source = std::io::Cursor::new(IMAGE_WITH_MANIFEST); - let settings = Settings::default(); - let mut cr = ContentCredential::from_stream(&settings, "image/jpeg", &mut source)?; - println!("{cr}"); - - source.set_position(0); - let mut dest = std::io::Cursor::new(Vec::new()); - cr.save_to_stream("image/jpeg", &mut source, &mut dest)?; - - dest.set_position(0); - let cr2 = ContentCredential::from_stream(&settings, "image/jpeg", &mut dest)?; - println!("{cr2}"); - Ok(()) -} +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::test::*; -#[test] -fn test_content_credential_created() -> Result<()> { - const IMAGE_WITH_MANIFEST: &[u8] = include_bytes!("../tests/fixtures/CA.jpg"); - //let settings = Settings::default(); - let mut source = std::io::Cursor::new(IMAGE_WITH_MANIFEST); - let settings = Settings::default(); - let mut cr = ContentCredential::new(&settings); - let action = crate::assertions::Actions::new().add_action( - crate::assertions::Action::new(crate::assertions::c2pa_action::CREATED) - .set_source_type(crate::DigitalSourceType::Empty) - .set_parameter("note", "Created by test_content_credential_created")?, - ); - cr.add_assertion(&action)?; - - source.set_position(0); - let mut dest = std::io::Cursor::new(Vec::new()); - cr.save_to_stream("image/jpeg", &mut source, &mut dest)?; - - dest.set_position(0); - let cr = ContentCredential::from_stream(&settings, "image/jpeg", &mut dest)?; - println!("{cr}"); - Ok(()) -} + #[test] + fn test_content_credential_created() -> Result<()> { + let (format, mut source, mut dest) = create_test_streams(CA_JPEG); + let settings = Settings::default(); + + let mut cr = ContentCredential::create(DigitalSourceType::Empty, &settings)?; -// #[test] -// fn test_from_reader() -> Result<()> { -// const IMAGE_WITH_MANIFEST: &[u8] = include_bytes!("../tests/fixtures/CA.jpg"); -// let mut source = std::io::Cursor::new(IMAGE_WITH_MANIFEST); -// let settings = Settings::default(); -// let reader = crate::Reader::from_stream("image/jpeg", &mut source)?; -// let reader_str = reader.to_string(); -// let cr = ContentCredential::from_reader(&reader)?; -// let cr = cr.to_string(); -// assert_eq!(reader_str, cr); -// Ok(()) -// } + cr.save_to_stream(format, &mut source, &mut dest)?; + + let cr = ContentCredential::from_stream(&settings, format, &mut dest)?; + println!("{cr}"); + Ok(()) + } + + #[test] + fn test_content_credential_from_stream() -> Result<()> { + let (format, mut source, mut dest) = create_test_streams(CA_JPEG); + let settings = Settings::default(); + + let mut cr = ContentCredential::from_stream(&settings, format, &mut source)?; + println!("{cr}"); + + cr.save_to_stream(format, &mut source, &mut dest)?; + + let cr2 = ContentCredential::from_stream(&settings, format, &mut dest)?; + println!("{cr2}"); + Ok(()) + } + + #[test] + fn test_add_ingredient_from_stream() -> Result<()> { + let (format, mut source, mut dest) = create_test_streams(CA_JPEG); + let settings = Settings::default(); + + let mut cr = ContentCredential::create(DigitalSourceType::Empty, &settings)?; + cr.add_ingredient_from_stream(format, &mut source)?; + + cr.save_to_stream(format, &mut source, &mut dest)?; + + let cr = ContentCredential::from_stream(&settings, format, &mut dest)?; + println!("{cr:?}"); + Ok(()) + } +} diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs index e9f378744..6017010f6 100644 --- a/sdk/src/manifest.rs +++ b/sdk/src/manifest.rs @@ -92,6 +92,7 @@ pub struct Manifest { /// A List of ingredients #[serde(default = "default_vec::")] + #[serde(skip_serializing_if = "Vec::is_empty")] ingredients: Vec, /// A List of verified credentials diff --git a/sdk/src/settings/builder.rs b/sdk/src/settings/builder.rs index 0c582d284..9feae8365 100644 --- a/sdk/src/settings/builder.rs +++ b/sdk/src/settings/builder.rs @@ -393,6 +393,8 @@ impl SettingsValidate for ActionsSettings { #[allow(unused)] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)] pub(crate) struct BuilderSettings { + /// The name of the vendor creating the content credential. + pub vendor: Option, /// Claim generator info that is automatically added to the builder. /// /// Note that this information will prepend any claim generator info From 951d0c9539cd23cd3ac96915bbfd4c78b6ed7602 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Fri, 31 Oct 2025 14:46:24 -0700 Subject: [PATCH 10/26] fmt --- sdk/src/manifest.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs index f55e60b29..cdc75380c 100644 --- a/sdk/src/manifest.rs +++ b/sdk/src/manifest.rs @@ -92,7 +92,6 @@ pub struct Manifest { /// A List of ingredients #[serde(default = "default_vec::")] - #[serde(skip_serializing_if = "Vec::is_empty")] pub(crate) ingredients: Vec, From 1027277ec4b0a0ee87277aed272867d316c0ddc9 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Sat, 1 Nov 2025 13:46:53 -0700 Subject: [PATCH 11/26] asset.rs early work --- sdk/src/asset.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ sdk/src/lib.rs | 1 + 2 files changed, 67 insertions(+) create mode 100644 sdk/src/asset.rs diff --git a/sdk/src/asset.rs b/sdk/src/asset.rs new file mode 100644 index 000000000..51c33959d --- /dev/null +++ b/sdk/src/asset.rs @@ -0,0 +1,66 @@ +use std::io::{BufReader, Read, Seek, Write}; + +use crate::{ + Error, + Result, + asset_io::CAIRead, + jumbf_io::{get_cailoader_handler, get_caiwriter_handler}, +}; + +pub struct Asset<'a> { + stream: Box, + xmp: Option, + manifest: Option>, +} + +impl<'a> Asset<'a> { + pub fn from_stream(stream: impl Read + Seek + Send + 'a, format: &str) -> Result { + let cailoader_handler = get_cailoader_handler(format).ok_or(Error::UnsupportedType)?; + let mut stream = BufReader::new(stream); + let xmp = cailoader_handler.read_xmp(&mut stream); + stream.rewind()?; + let manifest = cailoader_handler.read_cai(&mut stream)?; + Ok(Asset { stream: Box::new(stream), xmp, manifest: Some(manifest) }) + } + + pub fn to_stream(&mut + self, mut output: impl Read + Write + Seek + Send, format: &str) -> Result<()> { + let caiwriter_handler = get_caiwriter_handler(format).ok_or(Error::UnsupportedType)?; + if let Some(manifest) = &self.manifest { + caiwriter_handler.write_cai(&mut self.stream, &mut output, manifest)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + settings::Settings, + content_credential::ContentCredential, + }; + + use super::*; + use std::fs::File; + use std::io::BufReader; + #[test] + fn test_asset_from_stream() { + let settings = Settings::default(); + let file = File::open("tests/fixtures/C.jpg").unwrap(); + let mut reader = BufReader::new(file); + let mut asset = Asset::from_stream(&mut reader, "image/jpeg").unwrap(); + if let Some(xmp) = &asset.xmp { + println!("xmp: {xmp}"); + } + assert!(asset.manifest.is_some()); + if let Some(manifest) = &asset.manifest { + let cc = ContentCredential::from_stream(&settings,"application/c2pa",std::io::Cursor::new(manifest)).unwrap(); + println!("manifest: {:?}", cc); + } + let mut output = Cursor::new(Vec::new()); + Asset::to_stream(&mut asset, &mut output, "image/jpeg").unwrap(); + + let cc = ContentCredential::from_stream(&settings,"image/jpeg", &mut output).unwrap(); + println!("manifest: {:?}", cc); + } +} \ No newline at end of file diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 3d6442232..ce00d0c4d 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -179,6 +179,7 @@ pub use validation_results::{ValidationResults, ValidationState}; // Internal modules pub(crate) mod assertion; +pub(crate) mod asset; pub(crate) mod asset_handlers; pub(crate) mod asset_io; pub(crate) mod builder; From 18c06278f4b28dd3a36b0893a8e79c0f37c4f335 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Sat, 1 Nov 2025 13:47:57 -0700 Subject: [PATCH 12/26] allow claim thumbnail for ingredient if valid or trusted. --- sdk/src/assertions/ingredient.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs index d66ec9a10..609813594 100644 --- a/sdk/src/assertions/ingredient.rs +++ b/sdk/src/assertions/ingredient.rs @@ -592,7 +592,7 @@ impl Ingredient { .validation_results .as_ref() .map(|r| r.validation_state()) - == Some(crate::ValidationState::Valid) + != Some(crate::ValidationState::Invalid) { ingredient.thumbnail = claim.thumbnail(); } From a9565b81bd4703b5f2a52b80d1862713003aa2b3 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Mon, 3 Nov 2025 16:39:40 -0800 Subject: [PATCH 13/26] use shared b64 hash function --- docs/content_credentials.md | 10 +------ sdk/src/assertions/actions.rs | 6 ++--- sdk/src/asset.rs | 39 +++++++++++++++++---------- sdk/src/content_credential.rs | 50 +++++------------------------------ 4 files changed, 35 insertions(+), 70 deletions(-) diff --git a/docs/content_credentials.md b/docs/content_credentials.md index 8e07d7de0..6340aae49 100644 --- a/docs/content_credentials.md +++ b/docs/content_credentials.md @@ -52,14 +52,6 @@ For Builder archives - Sign and save a manifest, either embedded or sidecar. If - Add a captured .c2pa archived ingredient using add_ingredient_from_stream. This will use the parent ingredient in the archive as the ingredient added. - - -### Questions - -- Should we allow any Reader to be converted to a Builder, or only those with the same claim_generator? Maybe there is some other flag. - -- Can we save a .c2pa file that does not need to validate against an asset? This should probably be an option, but lets think about it. - ### Test cases 1) Validate an ingredient without a manifest, store in Builder and save. @@ -78,7 +70,7 @@ A future lower level API will wrap the Claim and Store structures providing a si ## Asset objects (not directly related to the above) -An Asset object creates a persistent layer over the asset_io traits. Currently we parse entire asset every time we need to access information about it. We have separate passes for XMP, JUMBF, Offset/box generation & etc.. +An Asset object creates a semi-persistent layer over the asset_io traits. Currently we parse entire asset every time we need to access information about it. We have separate passes for XMP, JUMBF, Offset/box generation & etc.. - The details of file i/o, in memory, streamed, or remote web access are handled here. - This will parse the asset, extract XMP, and C2PA data and allow diff --git a/sdk/src/assertions/actions.rs b/sdk/src/assertions/actions.rs index a2bd77d5e..94a24af7c 100644 --- a/sdk/src/assertions/actions.rs +++ b/sdk/src/assertions/actions.rs @@ -883,9 +883,9 @@ impl Actions { if action_name == c2pa_action::OPENED || action_name == c2pa_action::CREATED { let existing_action = self.actions.iter().find(|a| a.action() == action_name); if existing_action.is_some() { - return Err(Error::AssertionSpecificError( - "Only one 'c2pa.opened' action is allowed".to_string(), - )); + return Err(Error::AssertionSpecificError(format!( + "Only one '{action_name}' action is allowed" + ))); } // always insert as first action self.actions.insert(0, action); diff --git a/sdk/src/asset.rs b/sdk/src/asset.rs index 51c33959d..203eb7da9 100644 --- a/sdk/src/asset.rs +++ b/sdk/src/asset.rs @@ -1,10 +1,9 @@ use std::io::{BufReader, Read, Seek, Write}; use crate::{ - Error, - Result, asset_io::CAIRead, jumbf_io::{get_cailoader_handler, get_caiwriter_handler}, + Error, Result, }; pub struct Asset<'a> { @@ -20,11 +19,18 @@ impl<'a> Asset<'a> { let xmp = cailoader_handler.read_xmp(&mut stream); stream.rewind()?; let manifest = cailoader_handler.read_cai(&mut stream)?; - Ok(Asset { stream: Box::new(stream), xmp, manifest: Some(manifest) }) + Ok(Asset { + stream: Box::new(stream), + xmp, + manifest: Some(manifest), + }) } - pub fn to_stream(&mut - self, mut output: impl Read + Write + Seek + Send, format: &str) -> Result<()> { + pub fn to_stream( + &mut self, + mut output: impl Read + Write + Seek + Send, + format: &str, + ) -> Result<()> { let caiwriter_handler = get_caiwriter_handler(format).ok_or(Error::UnsupportedType)?; if let Some(manifest) = &self.manifest { caiwriter_handler.write_cai(&mut self.stream, &mut output, manifest)?; @@ -34,15 +40,15 @@ impl<'a> Asset<'a> { } #[cfg(test)] +#[allow(clippy::unwrap_in_result)] mod tests { - use crate::{ - settings::Settings, - content_credential::ContentCredential, + use std::{ + fs::File, + io::{BufReader, Cursor}, }; use super::*; - use std::fs::File; - use std::io::BufReader; + use crate::{content_credential::ContentCredential, settings::Settings}; #[test] fn test_asset_from_stream() { let settings = Settings::default(); @@ -51,16 +57,21 @@ mod tests { let mut asset = Asset::from_stream(&mut reader, "image/jpeg").unwrap(); if let Some(xmp) = &asset.xmp { println!("xmp: {xmp}"); - } + } assert!(asset.manifest.is_some()); if let Some(manifest) = &asset.manifest { - let cc = ContentCredential::from_stream(&settings,"application/c2pa",std::io::Cursor::new(manifest)).unwrap(); + let cc = ContentCredential::from_stream( + &settings, + "application/c2pa", + std::io::Cursor::new(manifest), + ) + .unwrap(); println!("manifest: {:?}", cc); } let mut output = Cursor::new(Vec::new()); Asset::to_stream(&mut asset, &mut output, "image/jpeg").unwrap(); - let cc = ContentCredential::from_stream(&settings,"image/jpeg", &mut output).unwrap(); + let cc = ContentCredential::from_stream(&settings, "image/jpeg", &mut output).unwrap(); println!("manifest: {:?}", cc); } -} \ No newline at end of file +} diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index ac42efa43..10fbacfd8 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -23,16 +23,17 @@ use crate::{ assertion::AssertionBase, assertions::{c2pa_action, Action, Actions, Ingredient, Relationship}, claim::Claim, - crypto::base64, manifest::{Manifest, StoreOptions}, manifest_store_report::ManifestStoreReport, settings::Settings, status_tracker::StatusTracker, store::Store, + utils::hash_utils::hash_to_b64, validation_status::ValidationStatus, ClaimGeneratorInfo, DigitalSourceType, Error, HashedUri, Result, ValidationResults, }; +/// This Generates the standard Reader output format for a Store #[derive(Serialize, Deserialize, Debug, Default)] pub struct StandardStoreReport { #[serde(skip_serializing_if = "Option::is_none")] @@ -136,10 +137,12 @@ impl ContentCredential { None } + /// adds an assertion to the content credential's claim pub fn add_assertion(&mut self, assertion: &impl AssertionBase) -> Result { self.claim.add_assertion(assertion) } + /// create a manifest store report from the store and validation results pub fn add_action(&mut self, action: Action) -> Result<()> { self.claim.add_action(action)?; Ok(()) @@ -184,11 +187,8 @@ impl ContentCredential { // todo add to exiting actions and check fo let action = Action::new(action_label).add_ingredient(ingredient_hashed_uri)?; - //let actions = Actions::new().add_action_checked(action)?; self.claim.add_action(action)?; - //self.add_assertion(&action)?; - // capture the store and validation results from the assertion Ok((self, store)) } @@ -222,44 +222,6 @@ impl ContentCredential { .save_to_stream(format, source, dest, &signer, &self.settings) } - /// replace byte arrays with base64 encoded strings for more readable output - fn hash_to_b64(mut value: Value) -> Value { - use std::collections::VecDeque; - - let mut queue = VecDeque::new(); - queue.push_back(&mut value); - - while let Some(current) = queue.pop_front() { - match current { - Value::Object(obj) => { - for (_, v) in obj.iter_mut() { - if let Value::Array(hash_arr) = v { - if !hash_arr.is_empty() && hash_arr.iter().all(|x| x.is_number()) { - // Pre-allocate with capacity to avoid reallocations - let mut hash_bytes = Vec::with_capacity(hash_arr.len()); - // Convert numbers to bytes safely - for n in hash_arr.iter() { - if let Some(num) = n.as_u64() { - hash_bytes.push(num as u8); - } - } - *v = Value::String(base64::encode(&hash_bytes)); - } - } - queue.push_back(v); - } - } - Value::Array(arr) => { - for v in arr.iter_mut() { - queue.push_back(v); - } - } - _ => {} - } - } - value - } - /// Generates a value similar to the C2PA Reader output pub fn reader_value(&self) -> Result { // get the validation results from the parent ingredient @@ -271,7 +233,7 @@ impl ContentCredential { })?; let report = StandardStoreReport::from_store(&self.store, &results)?; let json = serde_json::to_value(report).map_err(Error::JsonError)?; - Ok(Self::hash_to_b64(json)) + Ok(hash_to_b64(json)) } /// generates a value similar to the C2PA Reader detailed output @@ -284,7 +246,7 @@ impl ContentCredential { })?; let report = ManifestStoreReport::from_store_with_results(&self.store, &results)?; let json = serde_json::to_value(report).map_err(Error::JsonError)?; - Ok(Self::hash_to_b64(json)) + Ok(hash_to_b64(json)) } } From 0df42b244e9ee5d28c4fbf9e57f8bc59a807ff3a Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Mon, 3 Nov 2025 16:39:45 -0800 Subject: [PATCH 14/26] fmt --- sdk/src/asset.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/asset.rs b/sdk/src/asset.rs index 203eb7da9..855f22fa2 100644 --- a/sdk/src/asset.rs +++ b/sdk/src/asset.rs @@ -40,8 +40,8 @@ impl<'a> Asset<'a> { } #[cfg(test)] -#[allow(clippy::unwrap_in_result)] mod tests { + #![allow(clippy::unwrap_used)] use std::{ fs::File, io::{BufReader, Cursor}, From fceb94cb5eb60b6c9c7c9dee17614d18f39f30ce Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Fri, 14 Nov 2025 15:00:27 -0800 Subject: [PATCH 15/26] wip with Context to contain settings and resolvers. --- sdk/src/assertions/ingredient.rs | 5 +- sdk/src/asset.rs | 11 +++ sdk/src/content_credential.rs | 118 +++++++++++++++++-------------- sdk/src/context.rs | 80 +++++++++++++++++++++ sdk/src/lib.rs | 2 + sdk/src/reader.rs | 36 ++++++++++ sdk/src/store.rs | 6 +- 7 files changed, 198 insertions(+), 60 deletions(-) create mode 100644 sdk/src/context.rs diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs index 609813594..03e9501f6 100644 --- a/sdk/src/assertions/ingredient.rs +++ b/sdk/src/assertions/ingredient.rs @@ -22,6 +22,7 @@ use crate::{ assertion::{Assertion, AssertionBase, AssertionDecodeError, AssertionDecodeErrorCause}, assertions::{labels, AssertionMetadata, ReviewRating}, cbor_types::map_cbor_to_type, + context::Context, error::Result, hashed_uri::HashedUri, jumbf::labels::{to_manifest_uri, to_signature_uri}, @@ -534,7 +535,7 @@ impl Ingredient { relationship: Relationship, format: &str, mut stream: impl Read + Seek + Send, - settings: &Settings, + context: &Context, ) -> Result<(Self, Store)> { let mut validation_log = StatusTracker::default(); @@ -552,7 +553,7 @@ impl Ingredient { // ingredient.document_id = xmp_info.document_id; // use document id if one exists // ingredient.provenance = xmp_info.provenance; let store: Store = - Store::from_stream(format, &mut stream, true, &mut validation_log, settings)?; + Store::from_stream(format, &mut stream, true, &mut validation_log, context.resolver(), context.settings())?; let validation_results = ValidationResults::from_store(&store, &validation_log); let ingredient = Self::from_store_and_validation_results(relationship, &store, &validation_results)?; diff --git a/sdk/src/asset.rs b/sdk/src/asset.rs index 855f22fa2..bb5ae7b89 100644 --- a/sdk/src/asset.rs +++ b/sdk/src/asset.rs @@ -7,6 +7,8 @@ use crate::{ }; pub struct Asset<'a> { + name: Option, + format: String, stream: Box, xmp: Option, manifest: Option>, @@ -20,12 +22,21 @@ impl<'a> Asset<'a> { stream.rewind()?; let manifest = cailoader_handler.read_cai(&mut stream)?; Ok(Asset { + name: None, + format: format.to_string(), stream: Box::new(stream), xmp, manifest: Some(manifest), }) } + pub fn with_manifest(mut self, manifest: Vec) -> Self { + self.manifest = Some(manifest); + self + } + + + pub fn to_stream( &mut self, mut output: impl Read + Write + Seek + Send, diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index 10fbacfd8..5f63db34d 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -23,6 +23,7 @@ use crate::{ assertion::AssertionBase, assertions::{c2pa_action, Action, Actions, Ingredient, Relationship}, claim::Claim, + context::Context, manifest::{Manifest, StoreOptions}, manifest_store_report::ManifestStoreReport, settings::Settings, @@ -84,46 +85,44 @@ impl StandardStoreReport { } /// Experimental optimized Content Credential Structure -pub struct ContentCredential { +pub struct ContentCredential<'a> { claim: Claim, store: Store, - settings: Settings, + context: &'a Context, } -impl ContentCredential { - pub fn new(settings: &Settings) -> Self { - let mut claim = Claim::new("", settings.builder.vendor.as_deref(), 2); +impl<'a> ContentCredential<'a> { + pub fn new(context: &'a Context) -> Self { + let mut claim = Claim::new("", context.settings().builder.vendor.as_deref(), 2); claim.instance_id = uuid::Uuid::new_v4().to_string(); ContentCredential { claim, store: Store::new(), - settings: settings.clone(), + context, } } /// Use this for a content credential that is being created from scratch - pub fn create(source_type: DigitalSourceType, settings: &Settings) -> Result { - let mut cc = Self::new(settings); + pub fn create(mut self, source_type: DigitalSourceType) -> Result { let actions = Actions::new() .add_action(Action::new(c2pa_action::CREATED).set_source_type(source_type)); - cc.add_assertion(&actions)?; + self.add_assertion(&actions)?; - Ok(cc) + Ok(self) } /// creates a content credential from an existing stream pub fn from_stream( - settings: &Settings, + context: &'a Context, format: &str, mut stream: impl Read + Seek + Send, ) -> Result { - let mut cc = Self::new(settings); + let mut cr = Self::new(context); stream.rewind()?; - let (_, store) = cc.with_stream_impl(Relationship::ParentOf, format, &mut stream)?; - cc.store = store; // replaces the empty store - Ok(cc) + cr.add_ingredient_from_stream(Relationship::ParentOf, format, &mut stream); + Ok(cr) } fn parent_ingredient(&self) -> Option { @@ -148,26 +147,37 @@ impl ContentCredential { Ok(()) } - /// This is used to add component ingredients from a stream - /// - /// Parent ingredients are created using from_stream. - pub fn add_ingredient_from_stream( - &mut self, - format: &str, - mut stream: impl Read + Seek + Send, - ) -> Result<&Self> { - Ok(self - .with_stream_impl(Relationship::ComponentOf, format, &mut stream)? - .0) - } + // /// This is used to add component ingredients from a stream + // /// + // /// Parent ingredients are created using from_stream. + // pub fn add_ingredient_from_stream( + // &mut self, + // format: &str, + // mut stream: impl Read + Seek + Send, + // ) -> Result<&Self> { + // Ok(self + // .add_ingredient(Relationship::ComponentOf, format, &mut stream)? + // .0) + // } /// internal implementation to add ingredient from stream - fn with_stream_impl( + fn add_ingredient_from_stream( &mut self, relationship: Relationship, format: &str, mut stream: impl Read + Seek + Send, - ) -> Result<(&Self, Store)> { + ) -> Result<&Self> { + + let (ingredient_assertion, store) = + Ingredient::from_stream(relationship.clone(), format, &mut stream, self.context)?; + + // todo: allow passing store to load_ingredient_to_claim to avoid this conversion + let manifest_bytes = store.to_jumbf_internal(0)?; + Store::load_ingredient_to_claim(&mut self.claim, &manifest_bytes, None, self.context.settings())?; + + // add the ingredient assertion and get it's uri + let ingredient_uri = self.add_assertion(&ingredient_assertion)?; + // create an action associated with the ingredient let action_label = if relationship == Relationship::ParentOf { c2pa_action::OPENED @@ -175,22 +185,16 @@ impl ContentCredential { c2pa_action::PLACED }; - let (ingredient_assertion, store) = - Ingredient::from_stream(relationship, format, &mut stream, &self.settings)?; - - let manifest_bytes = store.to_jumbf_internal(0)?; - Store::load_ingredient_to_claim(&mut self.claim, &manifest_bytes, None, &self.settings)?; - - // add the ingredient assertion and get it's uri - let ingredient_hashed_uri = self.add_assertion(&ingredient_assertion)?; + let action = Action::new(action_label).add_ingredient(ingredient_uri)?; - // todo add to exiting actions and check fo - let action = Action::new(action_label).add_ingredient(ingredient_hashed_uri)?; + self.add_action(action)?; - self.claim.add_action(action)?; + if relationship == Relationship::ParentOf { + // we must replace the store for parent ingredients + self.store = store; + } - // capture the store and validation results from the assertion - Ok((self, store)) + Ok(self) } /// sets the default claim generator info if not already set @@ -218,8 +222,9 @@ impl ContentCredential { self.set_default_claim_generator_info()?; self.store.commit_claim(self.claim.clone())?; source.rewind()?; // always reset source to start + let resolver = self.context.resolver(); self.store - .save_to_stream(format, source, dest, &signer, &self.settings) + .save_to_stream(format, source, dest, &signer, resolver, self.context.settings()) } /// Generates a value similar to the C2PA Reader output @@ -250,7 +255,7 @@ impl ContentCredential { } } -impl std::fmt::Display for ContentCredential { +impl std::fmt::Display for ContentCredential<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let value = self.reader_value().map_err(|_| std::fmt::Error)?; f.write_str( @@ -261,7 +266,7 @@ impl std::fmt::Display for ContentCredential { } } -impl std::fmt::Debug for ContentCredential { +impl std::fmt::Debug for ContentCredential<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let value = self.detailed_value().map_err(|_| std::fmt::Error)?; f.write_str( @@ -280,13 +285,14 @@ mod tests { #[test] fn test_content_credential_created() -> Result<()> { let (format, mut source, mut dest) = create_test_streams(CA_JPEG); - let settings = Settings::default(); + let context = Context::new(); - let mut cr = ContentCredential::create(DigitalSourceType::Empty, &settings)?; + let mut cr = ContentCredential::new(&context); + cr.create(DigitalSourceType::Empty)?; cr.save_to_stream(format, &mut source, &mut dest)?; - let cr = ContentCredential::from_stream(&settings, format, &mut dest)?; + let cr = ContentCredential::from_stream(&context, format, &mut dest)?; println!("{cr}"); Ok(()) } @@ -294,14 +300,15 @@ mod tests { #[test] fn test_content_credential_from_stream() -> Result<()> { let (format, mut source, mut dest) = create_test_streams(CA_JPEG); - let settings = Settings::default(); + let context = Context::new(); - let mut cr = ContentCredential::from_stream(&settings, format, &mut source)?; + let mut cr = context.content_credential(); + cr.add_ingredient_from_stream(Relationship::ParentOf,format, &mut source)?; println!("{cr}"); cr.save_to_stream(format, &mut source, &mut dest)?; - let cr2 = ContentCredential::from_stream(&settings, format, &mut dest)?; + let cr2 = ContentCredential::from_stream(&context, format, &mut dest)?; println!("{cr2}"); Ok(()) } @@ -309,14 +316,15 @@ mod tests { #[test] fn test_add_ingredient_from_stream() -> Result<()> { let (format, mut source, mut dest) = create_test_streams(CA_JPEG); - let settings = Settings::default(); + let context = Context::new(); - let mut cr = ContentCredential::create(DigitalSourceType::Empty, &settings)?; - cr.add_ingredient_from_stream(format, &mut source)?; + let mut cr = ContentCredential::new(&context); + cr.create(DigitalSourceType::Empty)?; + cr.add_ingredient_from_stream(Relationship::ParentOf,format, &mut source)?; cr.save_to_stream(format, &mut source, &mut dest)?; - let cr = ContentCredential::from_stream(&settings, format, &mut dest)?; + let cr = ContentCredential::from_stream(&context, format, &mut dest)?; println!("{cr:?}"); Ok(()) } diff --git a/sdk/src/context.rs b/sdk/src/context.rs new file mode 100644 index 000000000..c66ef2d04 --- /dev/null +++ b/sdk/src/context.rs @@ -0,0 +1,80 @@ + +use std::cell::OnceCell; +use crate::{ + content_credential::ContentCredential, + settings::Settings, + http::{SyncGenericResolver, AsyncGenericResolver, SyncHttpResolver, AsyncHttpResolver}, +}; + +pub enum HttpResolver { + Sync(Box), + Async(Box), +} + +pub struct Context { + settings: Settings, + http_resolver: OnceCell>, + http_resolver_async: OnceCell>, +} + +impl Default for Context { + fn default() -> Self { + Self { + settings: Settings::default(), + http_resolver: OnceCell::new(), + http_resolver_async: OnceCell::new(), + } + } +} + +impl Context { + pub fn new() -> Self { + Self::default() + } + + pub fn with_settings(mut self, settings: Settings) -> Self { + self.settings = settings; + self + } + + pub fn with_resolver( + self, + resolver: T, + ) -> Self { + let _ = self.http_resolver.set(Box::new(resolver)); + self + } + + pub fn with_resolver_async( + self, + resolver: T, + ) -> Self { + let _ = self.http_resolver_async.set(Box::new(resolver)); + self + } + + pub fn settings(&self) -> &Settings { + &self.settings + } + + pub fn settings_mut(&mut self) -> &mut Settings { + &mut self.settings + } + + pub fn resolver(&self) ->&dyn SyncHttpResolver { + self.http_resolver + .get_or_init(|| Box::new(SyncGenericResolver::new())) + .as_ref() + } + + pub fn resolver_async(&self) -> &dyn AsyncHttpResolver { + self.http_resolver_async + .get_or_init(|| Box::new(AsyncGenericResolver::new())) + .as_ref() + } + + pub fn content_credential(&self) -> ContentCredential { + ContentCredential::new(&self) + } + +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 1752bec4a..a1850ca99 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -127,6 +127,8 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// The assertions module contains the definitions for the assertions that are part of the C2PA specification. pub mod assertions; +pub mod context; +pub use context::Context; pub mod content_credential; pub use content_credential::ContentCredential; /// The cose_sign module contains the definitions for the COSE signing algorithms. diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index 23ac22473..74eb73f5c 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -32,7 +32,9 @@ use serde_with::skip_serializing_none; #[cfg(feature = "file_io")] use crate::utils::io_utils::uri_to_path; use crate::{ + asset::Asset, claim::Claim, + context::Context, dynamic_assertion::PartialClaim, error::{Error, Result}, http::{AsyncGenericResolver, SyncGenericResolver}, @@ -109,6 +111,40 @@ type ValidationFn = dyn Fn(&str, &crate::ManifestAssertion, &mut StatusTracker) -> Option; impl Reader { + + #[async_generic] + pub fn from_asset<'a>(context: &'a Context, asset: &'a mut Asset<'a>) -> Result { + let settings = context.settings(); + let http_resolver = if _sync { + SyncGenericResolver::new() + } else { + AsyncGenericResolver::new() + }; + let resolver = context.resolver(); + + let mut validation_log = StatusTracker::default(); + //asset.rewind()?; + let store = if _sync { + Store::from_asset(context, + asset, + &mut validation_log, + ) + } else { + Store::from_asset_async( + context, + asset, + &mut validation_log, + ) + .await + }?; + if _sync { + Self::from_store(store, &mut validation_log, context.settings()) + } else { + Self::from_store_async(store, &mut validation_log, context.settings()).await + } + } + + /// Create a manifest store [`Reader`] from a stream. A Reader is used to validate C2PA data from an asset. /// /// # Arguments diff --git a/sdk/src/store.rs b/sdk/src/store.rs index c1cd24a5e..4489aa466 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -2995,7 +2995,7 @@ impl Store { input_stream: &mut dyn CAIRead, output_stream: &mut dyn CAIReadWrite, signer: &dyn Signer, - http_resolver: &impl SyncHttpResolver, + http_resolver: &(impl SyncHttpResolver + ?Sized), settings: &Settings, ) -> Result> { let dynamic_assertions = signer.dynamic_assertions(); @@ -3505,13 +3505,13 @@ impl Store { #[async_generic(async_signature( asset_type: &str, stream: &mut dyn CAIRead, - http_resolver: &impl AsyncHttpResolver, + http_resolver: &(impl AsyncHttpResolver + ?Sized), settings: &Settings ))] pub fn load_jumbf_from_stream( asset_type: &str, stream: &mut dyn CAIRead, - http_resolver: &impl SyncHttpResolver, + http_resolver: &(impl SyncHttpResolver + ?Sized), settings: &Settings, ) -> Result<(Vec, Option)> { match load_jumbf_from_stream(asset_type, stream) { From 5b06a020e5824b6ee7fea2c77b5916defbb87a57 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Fri, 14 Nov 2025 19:48:12 -0800 Subject: [PATCH 16/26] use context in most of the sdk, instead of settings and resolvers. --- sdk/src/assertions/ingredient.rs | 4 +- sdk/src/asset.rs | 18 +- sdk/src/asset_handlers/c2pa_io.rs | 9 +- sdk/src/builder.rs | 82 +- sdk/src/claim.rs | 40 +- sdk/src/content_credential.rs | 19 +- sdk/src/context.rs | 25 +- sdk/src/crypto/cose/ocsp.rs | 41 +- sdk/src/crypto/ocsp/fetch.rs | 23 +- sdk/src/ingredient.rs | 255 ++---- sdk/src/reader.rs | 174 +--- sdk/src/store.rs | 1422 ++++++++--------------------- sdk/src/utils/test.rs | 6 +- 13 files changed, 658 insertions(+), 1460 deletions(-) diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs index 03e9501f6..7a4df45b8 100644 --- a/sdk/src/assertions/ingredient.rs +++ b/sdk/src/assertions/ingredient.rs @@ -26,7 +26,6 @@ use crate::{ error::Result, hashed_uri::HashedUri, jumbf::labels::{to_manifest_uri, to_signature_uri}, - settings::Settings, status_tracker::StatusTracker, store::Store, validation_results::ValidationResults, @@ -552,8 +551,7 @@ impl Ingredient { // ingredient.document_id = xmp_info.document_id; // use document id if one exists // ingredient.provenance = xmp_info.provenance; - let store: Store = - Store::from_stream(format, &mut stream, true, &mut validation_log, context.resolver(), context.settings())?; + let store: Store = Store::from_stream(format, &mut stream, &mut validation_log, context)?; let validation_results = ValidationResults::from_store(&store, &validation_log); let ingredient = Self::from_store_and_validation_results(relationship, &store, &validation_results)?; diff --git a/sdk/src/asset.rs b/sdk/src/asset.rs index bb5ae7b89..4e213e3fd 100644 --- a/sdk/src/asset.rs +++ b/sdk/src/asset.rs @@ -6,6 +6,7 @@ use crate::{ Error, Result, }; +#[allow(dead_code)] pub struct Asset<'a> { name: Option, format: String, @@ -15,6 +16,7 @@ pub struct Asset<'a> { } impl<'a> Asset<'a> { + #[allow(dead_code)] pub fn from_stream(stream: impl Read + Seek + Send + 'a, format: &str) -> Result { let cailoader_handler = get_cailoader_handler(format).ok_or(Error::UnsupportedType)?; let mut stream = BufReader::new(stream); @@ -30,14 +32,14 @@ impl<'a> Asset<'a> { }) } + #[allow(dead_code)] pub fn with_manifest(mut self, manifest: Vec) -> Self { self.manifest = Some(manifest); self } - - - pub fn to_stream( + #[allow(dead_code)] + pub fn write_stream( &mut self, mut output: impl Read + Write + Seek + Send, format: &str, @@ -59,10 +61,10 @@ mod tests { }; use super::*; - use crate::{content_credential::ContentCredential, settings::Settings}; + use crate::content_credential::ContentCredential; #[test] fn test_asset_from_stream() { - let settings = Settings::default(); + let context = crate::context::Context::new(); let file = File::open("tests/fixtures/C.jpg").unwrap(); let mut reader = BufReader::new(file); let mut asset = Asset::from_stream(&mut reader, "image/jpeg").unwrap(); @@ -72,7 +74,7 @@ mod tests { assert!(asset.manifest.is_some()); if let Some(manifest) = &asset.manifest { let cc = ContentCredential::from_stream( - &settings, + &context, "application/c2pa", std::io::Cursor::new(manifest), ) @@ -80,9 +82,9 @@ mod tests { println!("manifest: {:?}", cc); } let mut output = Cursor::new(Vec::new()); - Asset::to_stream(&mut asset, &mut output, "image/jpeg").unwrap(); + Asset::write_stream(&mut asset, &mut output, "image/jpeg").unwrap(); - let cc = ContentCredential::from_stream(&settings, "image/jpeg", &mut output).unwrap(); + let cc = ContentCredential::from_stream(&context, "image/jpeg", &mut output).unwrap(); println!("manifest: {:?}", cc); } } diff --git a/sdk/src/asset_handlers/c2pa_io.rs b/sdk/src/asset_handlers/c2pa_io.rs index 862a90f8e..76343ba96 100644 --- a/sdk/src/asset_handlers/c2pa_io.rs +++ b/sdk/src/asset_handlers/c2pa_io.rs @@ -176,8 +176,6 @@ pub mod tests { use super::{AssetIO, C2paIO, CAIReader, CAIWriter}; use crate::{ crypto::raw_signature::SigningAlg, - http::SyncGenericResolver, - settings::Settings, status_tracker::{ErrorBehavior, StatusTracker}, store::Store, utils::{ @@ -189,8 +187,7 @@ pub mod tests { #[test] fn c2pa_io_parse() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let path = fixture_path("C.jpg"); @@ -213,10 +210,8 @@ pub mod tests { &manifest, "image/jpeg", &stream, - true, &mut StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError), - &http_resolver, - &settings, + &context, ) .expect("loading store"); diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index 783eb5123..b18c86988 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -36,8 +36,8 @@ use crate::{ UserCbor, }, claim::Claim, + context::Context, error::{Error, Result}, - http::{AsyncGenericResolver, SyncGenericResolver}, jumbf_io, resource_store::{ResourceRef, ResourceResolver, ResourceStore}, settings::{self, Settings}, @@ -637,8 +637,6 @@ impl Builder { T: Into, R: Read + Seek + Send, { - let settings = crate::settings::get_settings().unwrap_or_default(); - let ingredient: Ingredient = Ingredient::from_json(&ingredient_json.into())?; if format == "c2pa" || format == "application/c2pa" { @@ -652,11 +650,12 @@ impl Builder { .ok_or(Error::IngredientNotFound); } + let context = Context::new(); let ingredient = if _sync { - ingredient.with_stream(format, stream, &SyncGenericResolver::new(), &settings)? + ingredient.with_stream(format, stream, &context)? } else { ingredient - .with_stream_async(format, stream, &AsyncGenericResolver::new(), &settings) + .with_stream_async(format, stream, &context) .await? }; @@ -852,9 +851,9 @@ impl Builder { /// # Errors /// * Returns an [`Error`] if the archive cannot be written. pub fn to_archive(&mut self, mut stream: impl Write + Seek) -> Result<()> { - let settings = crate::settings::get_settings().unwrap_or_default(); - if let Some(true) = settings.builder.generate_c2pa_archive { - let c2pa_data = self.working_store_sign(&settings)?; + let context = Context::new(); + if let Some(true) = context.settings().builder.generate_c2pa_archive { + let c2pa_data = self.working_store_sign(&context)?; stream.write_all(&c2pa_data)?; } else { return self.old_to_archive(stream); @@ -882,7 +881,9 @@ impl Builder { // but we need to disable validation since we are not signing yet // so we will read the store directly here //crate::Reader::from_stream("application/c2pa", stream).and_then(|r| r.into_builder()) - let settings = crate::settings::get_settings().unwrap_or_default(); + let mut settings = settings::get_settings().unwrap_or_default(); + settings.verify.verify_after_reading = false; + let context = Context::new().with_settings(settings); let mut validation_log = crate::status_tracker::StatusTracker::default(); stream.rewind()?; // Ensure stream is at the start @@ -890,18 +891,16 @@ impl Builder { let store = Store::from_stream( "application/c2pa", &mut stream, - false, &mut validation_log, - &SyncGenericResolver::new(), - &settings, + &context, )?; - let reader = Reader::from_store(store, &mut validation_log, &settings)?; + let reader = Reader::from_store(store, &mut validation_log, &context)?; reader.into_builder() }) } // Convert a Manifest into a Claim - fn to_claim(&self, settings: &Settings) -> Result { + fn to_claim(&self, context: &Context) -> Result { // utility function to add created or gathered assertions fn add_assertion( claim: &mut Claim, @@ -920,7 +919,7 @@ impl Builder { // add the default claim generator info for this library if claim_generator_info.is_empty() { - let claim_generator_info_settings = &settings.builder.claim_generator_info; + let claim_generator_info_settings = &context.settings().builder.claim_generator_info; match claim_generator_info_settings { Some(claim_generator_info_settings) => { claim_generator_info.push(claim_generator_info_settings.clone().try_into()?); @@ -1030,7 +1029,7 @@ impl Builder { &mut claim, definition.redactions.clone(), Some(&self.resources), - settings, + context, )?; if !id.is_empty() { ingredient_map.insert(id, (ingredient.relationship(), uri)); @@ -1360,10 +1359,10 @@ impl Builder { } // Convert a Manifest into a Store - fn to_store(&self, settings: &Settings) -> Result { - let claim = self.to_claim(settings)?; + fn to_store(&self, context: &Context) -> Result { + let claim = self.to_claim(context)?; - let mut store = Store::with_settings(settings); + let mut store = Store::with_context(context); // if this can be an update manifest, then set the update_manifest flag if self.intent() == Some(BuilderIntent::Update) { @@ -1473,7 +1472,7 @@ impl Builder { reserve_size: usize, format: &str, ) -> Result> { - let settings = crate::settings::get_settings().unwrap_or_default(); + let context = crate::context::Context::new(); let dh: Result = self.find_assertion(DataHash::LABEL); if dh.is_err() { @@ -1485,7 +1484,7 @@ impl Builder { } self.definition.format = format.to_string(); self.definition.instance_id = format!("xmp:iid:{}", Uuid::new_v4()); - let mut store = self.to_store(&settings)?; + let mut store = self.to_store(&context)?; let placeholder = store.get_data_hashed_manifest_placeholder(reserve_size, format)?; Ok(placeholder) } @@ -1517,15 +1516,15 @@ impl Builder { data_hash: &DataHash, format: &str, ) -> Result> { - let settings = crate::settings::get_settings().unwrap_or_default(); + let context = Context::new(); - let mut store = self.to_store(&settings)?; + let mut store = self.to_store(&context)?; if _sync { - store.get_data_hashed_embeddable_manifest(data_hash, signer, format, None, &settings) + store.get_data_hashed_embeddable_manifest(data_hash, signer, format, None, &context) } else { store .get_data_hashed_embeddable_manifest_async( - data_hash, signer, format, None, &settings, + data_hash, signer, format, None, &context, ) .await } @@ -1551,16 +1550,16 @@ impl Builder { signer: &dyn Signer, format: &str, ) -> Result> { - let settings = crate::settings::get_settings().unwrap_or_default(); + let context = Context::new(); self.definition.instance_id = format!("xmp:iid:{}", Uuid::new_v4()); - let mut store = self.to_store(&settings)?; + let mut store = self.to_store(&context)?; let bytes = if _sync { - store.get_box_hashed_embeddable_manifest(signer, &settings) + store.get_box_hashed_embeddable_manifest(signer, &context) } else { store - .get_box_hashed_embeddable_manifest_async(signer, &settings) + .get_box_hashed_embeddable_manifest_async(signer, &context) .await }?; // get composed version for embedding to JPEG @@ -1597,11 +1596,7 @@ impl Builder { W: Write + Read + Seek + Send, { let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = if _sync { - SyncGenericResolver::new() - } else { - AsyncGenericResolver::new() - }; + let context = Context::new().with_settings(settings); let format = format_to_mime(format); self.definition.format.clone_from(&format); @@ -1618,17 +1613,17 @@ impl Builder { // generate thumbnail if we don't already have one #[cfg(feature = "add_thumbnails")] - self.maybe_add_thumbnail(&format, source, &settings)?; + self.maybe_add_thumbnail(&format, source, context.settings())?; // convert the manifest to a store - let mut store = self.to_store(&settings)?; + let mut store = self.to_store(&context)?; // sign and write our store to to the output image file if _sync { - store.save_to_stream(&format, source, dest, signer, &http_resolver, &settings) + store.save_to_stream(&format, source, dest, signer, &context) } else { store - .save_to_stream_async(&format, source, dest, signer, &http_resolver, &settings) + .save_to_stream_async(&format, source, dest, signer, &context) .await } } @@ -1682,8 +1677,6 @@ impl Builder { fragment_paths: &Vec, output_path: P, ) -> Result<()> { - let settings = crate::settings::get_settings().unwrap_or_default(); - if !output_path.as_ref().exists() { // ensure the path exists std::fs::create_dir_all(output_path.as_ref())?; @@ -1706,7 +1699,8 @@ impl Builder { } // convert the manifest to a store - let mut store = self.to_store(&settings)?; + let context = Context::new(); + let mut store = self.to_store(&context)?; // sign and write our store to DASH content store.save_to_bmff_fragmented( @@ -1714,7 +1708,7 @@ impl Builder { fragment_paths, output_path.as_ref(), signer, - &settings, + &context, ) } @@ -1806,7 +1800,7 @@ impl Builder { /// The working store is signed with a BoxHash over an empty string /// And is returned as a Vec of the c2pa_manifest bytes /// This works as an archive of the store that can be read back to restore the Builder state - fn working_store_sign(&self, settings: &Settings) -> Result> { + fn working_store_sign(&self, context: &Context) -> Result> { // first we need to generate a BoxHash over an empty string let mut empty_asset = std::io::Cursor::new(""); let boxes = jumbf_io::get_assetio_handler("application/c2pa") @@ -1817,7 +1811,7 @@ impl Builder { let box_hash = BoxHash { boxes }; // then convert the builder to a claim and add the box hash assertion - let mut claim = self.to_claim(settings)?; + let mut claim = self.to_claim(context)?; claim.add_assertion(&box_hash)?; // now commit and sign it. The signing will allow us to detect tampering. diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs index 58c092e19..6879f8ea3 100644 --- a/sdk/src/claim.rs +++ b/sdk/src/claim.rs @@ -40,6 +40,7 @@ use crate::{ }, asset_io::CAIRead, cbor_types::map_cbor_to_type, + context::Context, cose_validator::{ get_signing_cert_serial_num, get_signing_info, get_signing_info_async, verify_cose, verify_cose_async, @@ -55,7 +56,6 @@ use crate::{ }, error::{Error, Result}, hashed_uri::HashedUri, - http::{AsyncHttpResolver, SyncHttpResolver}, jumbf::{ self, boxes::{ @@ -1846,7 +1846,6 @@ impl Claim { /// Verify claim signature, assertion store and asset hashes /// claim - claim to be verified /// asset_bytes - reference to bytes of the asset - #[allow(clippy::too_many_arguments)] pub(crate) async fn verify_claim_async( claim: &Claim, asset_data: &mut ClaimAssetData<'_>, @@ -1854,9 +1853,9 @@ impl Claim { cert_check: bool, ctp: &CertificateTrustPolicy, validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result<()> { + let settings = context.settings(); // Parse COSE signed data (signature) and validate it. let sig = claim.signature_val().clone(); let additional_bytes: Vec = Vec::new(); @@ -1906,8 +1905,7 @@ impl Claim { svi.certificate_statuses.get(&certificate_serial_num), svi.timestamps.get(claim.label()), validation_log, - http_resolver, - settings, + context, ) .await?; @@ -1940,14 +1938,13 @@ impl Claim { cert_check: bool, ctp: &CertificateTrustPolicy, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result<()> { // Parse COSE signed data (signature) and validate it. let sig = claim.signature_val(); let additional_bytes: Vec = Vec::new(); - let mut adjusted_settings = settings.clone(); + let mut adjusted_settings = context.settings().clone(); if claim.version() == 1 { adjusted_settings.verify.verify_timestamp_trust = false; } @@ -2008,8 +2005,7 @@ impl Claim { svi.certificate_statuses.get(&certificate_serial_num), svi.timestamps.get(claim.label()), validation_log, - http_resolver, - &adjusted_settings, + context, )?; let verified = verify_cose( @@ -3993,16 +3989,7 @@ impl Claim { } #[allow(dead_code, clippy::too_many_arguments)] -#[async_generic(async_signature( - sign1: &coset::CoseSign1, - data: &[u8], - ctp: &CertificateTrustPolicy, - ocsp_responses: Option<&Vec>>, - tst_info: Option<&TstInfo>, - validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, -))] +#[async_generic] pub(crate) fn check_ocsp_status( sign1: &coset::CoseSign1, data: &[u8], @@ -4010,12 +3997,11 @@ pub(crate) fn check_ocsp_status( ocsp_responses: Option<&Vec>>, tst_info: Option<&TstInfo>, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { // Moved here instead of c2pa-crypto because of the dependency on settings. - let fetch_policy = if settings.verify.ocsp_fetch { + let fetch_policy = if context.settings().verify.ocsp_fetch { OcspFetchPolicy::FetchAllowed } else { OcspFetchPolicy::DoNotFetch @@ -4030,8 +4016,7 @@ pub(crate) fn check_ocsp_status( ocsp_responses, tst_info, validation_log, - http_resolver, - settings, + context, )?) } else { Ok(crate::crypto::cose::check_ocsp_status_async( @@ -4042,8 +4027,7 @@ pub(crate) fn check_ocsp_status( ocsp_responses, tst_info, validation_log, - http_resolver, - settings, + context, ) .await?) } diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index 5f63db34d..ea9aae29a 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -104,7 +104,6 @@ impl<'a> ContentCredential<'a> { /// Use this for a content credential that is being created from scratch pub fn create(mut self, source_type: DigitalSourceType) -> Result { - let actions = Actions::new() .add_action(Action::new(c2pa_action::CREATED).set_source_type(source_type)); @@ -121,7 +120,7 @@ impl<'a> ContentCredential<'a> { ) -> Result { let mut cr = Self::new(context); stream.rewind()?; - cr.add_ingredient_from_stream(Relationship::ParentOf, format, &mut stream); + cr.add_ingredient_from_stream(Relationship::ParentOf, format, &mut stream)?; Ok(cr) } @@ -167,13 +166,12 @@ impl<'a> ContentCredential<'a> { format: &str, mut stream: impl Read + Seek + Send, ) -> Result<&Self> { - let (ingredient_assertion, store) = Ingredient::from_stream(relationship.clone(), format, &mut stream, self.context)?; // todo: allow passing store to load_ingredient_to_claim to avoid this conversion let manifest_bytes = store.to_jumbf_internal(0)?; - Store::load_ingredient_to_claim(&mut self.claim, &manifest_bytes, None, self.context.settings())?; + Store::load_ingredient_to_claim(&mut self.claim, &manifest_bytes, None, self.context)?; // add the ingredient assertion and get it's uri let ingredient_uri = self.add_assertion(&ingredient_assertion)?; @@ -222,9 +220,8 @@ impl<'a> ContentCredential<'a> { self.set_default_claim_generator_info()?; self.store.commit_claim(self.claim.clone())?; source.rewind()?; // always reset source to start - let resolver = self.context.resolver(); self.store - .save_to_stream(format, source, dest, &signer, resolver, self.context.settings()) + .save_to_stream(format, source, dest, &signer, self.context) } /// Generates a value similar to the C2PA Reader output @@ -287,8 +284,7 @@ mod tests { let (format, mut source, mut dest) = create_test_streams(CA_JPEG); let context = Context::new(); - let mut cr = ContentCredential::new(&context); - cr.create(DigitalSourceType::Empty)?; + let mut cr = ContentCredential::new(&context).create(DigitalSourceType::Empty)?; cr.save_to_stream(format, &mut source, &mut dest)?; @@ -303,7 +299,7 @@ mod tests { let context = Context::new(); let mut cr = context.content_credential(); - cr.add_ingredient_from_stream(Relationship::ParentOf,format, &mut source)?; + cr.add_ingredient_from_stream(Relationship::ParentOf, format, &mut source)?; println!("{cr}"); cr.save_to_stream(format, &mut source, &mut dest)?; @@ -318,9 +314,8 @@ mod tests { let (format, mut source, mut dest) = create_test_streams(CA_JPEG); let context = Context::new(); - let mut cr = ContentCredential::new(&context); - cr.create(DigitalSourceType::Empty)?; - cr.add_ingredient_from_stream(Relationship::ParentOf,format, &mut source)?; + let mut cr = ContentCredential::new(&context).create(DigitalSourceType::Empty)?; + cr.add_ingredient_from_stream(Relationship::ParentOf, format, &mut source)?; cr.save_to_stream(format, &mut source, &mut dest)?; diff --git a/sdk/src/context.rs b/sdk/src/context.rs index c66ef2d04..b74f2d87b 100644 --- a/sdk/src/context.rs +++ b/sdk/src/context.rs @@ -1,9 +1,9 @@ - use std::cell::OnceCell; + use crate::{ content_credential::ContentCredential, + http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver}, settings::Settings, - http::{SyncGenericResolver, AsyncGenericResolver, SyncHttpResolver, AsyncHttpResolver}, }; pub enum HttpResolver { @@ -12,7 +12,7 @@ pub enum HttpResolver { } pub struct Context { - settings: Settings, + settings: Settings, http_resolver: OnceCell>, http_resolver_async: OnceCell>, } @@ -20,7 +20,7 @@ pub struct Context { impl Default for Context { fn default() -> Self { Self { - settings: Settings::default(), + settings: crate::settings::get_settings().unwrap_or_default(), http_resolver: OnceCell::new(), http_resolver_async: OnceCell::new(), } @@ -37,18 +37,12 @@ impl Context { self } - pub fn with_resolver( - self, - resolver: T, - ) -> Self { + pub fn with_resolver(self, resolver: T) -> Self { let _ = self.http_resolver.set(Box::new(resolver)); self } - pub fn with_resolver_async( - self, - resolver: T, - ) -> Self { + pub fn with_resolver_async(self, resolver: T) -> Self { let _ = self.http_resolver_async.set(Box::new(resolver)); self } @@ -61,7 +55,7 @@ impl Context { &mut self.settings } - pub fn resolver(&self) ->&dyn SyncHttpResolver { + pub fn resolver(&self) -> &dyn SyncHttpResolver { self.http_resolver .get_or_init(|| Box::new(SyncGenericResolver::new())) .as_ref() @@ -73,8 +67,7 @@ impl Context { .as_ref() } - pub fn content_credential(&self) -> ContentCredential { - ContentCredential::new(&self) + pub fn content_credential(&self) -> ContentCredential<'_> { + ContentCredential::new(self) } - } diff --git a/sdk/src/crypto/cose/ocsp.rs b/sdk/src/crypto/cose/ocsp.rs index b65809829..7681e146a 100644 --- a/sdk/src/crypto/cose/ocsp.rs +++ b/sdk/src/crypto/cose/ocsp.rs @@ -17,6 +17,7 @@ use ciborium::value::Value; use coset::{CoseSign1, Label}; use crate::{ + context::Context, crypto::{ asn1::rfc3161::TstInfo, cose::{ @@ -25,7 +26,6 @@ use crate::{ }, ocsp::OcspResponse, }, - http::{AsyncHttpResolver, SyncHttpResolver}, log_item, settings::Settings, status_tracker::StatusTracker, @@ -42,8 +42,7 @@ use crate::{ ocsp_responses: Option<&Vec>>, tst_info: Option<&TstInfo>, validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, + context: &Context, ))] #[allow(clippy::too_many_arguments)] pub fn check_ocsp_status( @@ -54,10 +53,10 @@ pub fn check_ocsp_status( ocsp_responses: Option<&Vec>>, tst_info: Option<&TstInfo>, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { - if settings + if context + .settings() .builder .certificate_status_should_override .unwrap_or(false) @@ -72,7 +71,7 @@ pub fn check_ocsp_status( ocsp_response_ders, tst_info, validation_log, - settings, + context.settings(), ) } else { process_ocsp_responses_async( @@ -82,7 +81,7 @@ pub fn check_ocsp_status( ocsp_response_ders, tst_info, validation_log, - settings, + context.settings(), ) .await }; @@ -100,7 +99,7 @@ pub fn check_ocsp_status( ctp, tst_info, validation_log, - settings, + context.settings(), ) } else { check_stapled_ocsp_response_async( @@ -110,7 +109,7 @@ pub fn check_ocsp_status( ctp, tst_info, validation_log, - settings, + context.settings(), ) .await } @@ -125,8 +124,7 @@ pub fn check_ocsp_status( ctp, tst_info, validation_log, - http_resolver, - settings, + context, ) } else { fetch_and_check_ocsp_response_async( @@ -135,8 +133,7 @@ pub fn check_ocsp_status( ctp, tst_info, validation_log, - http_resolver, - settings, + context, ) .await } @@ -152,7 +149,7 @@ pub fn check_ocsp_status( ocsp_response_ders, tst_info, validation_log, - settings, + context.settings(), ) } else { process_ocsp_responses_async( @@ -162,7 +159,7 @@ pub fn check_ocsp_status( ocsp_response_ders, tst_info, validation_log, - settings, + context.settings(), ) .await } @@ -333,8 +330,7 @@ fn check_stapled_ocsp_response( ctp: &CertificateTrustPolicy, tst_info: Option<&TstInfo>, validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, + context: &crate::context::Context, ))] pub(crate) fn fetch_and_check_ocsp_response( sign1: &CoseSign1, @@ -342,15 +338,14 @@ pub(crate) fn fetch_and_check_ocsp_response( ctp: &CertificateTrustPolicy, tst_info: Option<&TstInfo>, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &crate::context::Context, ) -> Result { let certs = cert_chain_from_sign1(sign1)?; let ocsp_der = if _sync { - crate::crypto::ocsp::fetch_ocsp_response(&certs, http_resolver) + crate::crypto::ocsp::fetch_ocsp_response(&certs, context) } else { - crate::crypto::ocsp::fetch_ocsp_response_async(&certs, http_resolver).await + crate::crypto::ocsp::fetch_ocsp_response_async(&certs, context).await }; let Some(ocsp_response_der) = ocsp_der else { @@ -360,7 +355,7 @@ pub(crate) fn fetch_and_check_ocsp_response( // use supplied override time if provided let signing_time: Option> = match tst_info { Some(tst_info) => Some(tst_info.gen_time.clone().into()), - None => validate_cose_tst_info(sign1, data, ctp, validation_log, settings) + None => validate_cose_tst_info(sign1, data, ctp, validation_log, context.settings()) .ok() .map(|tst_info| tst_info.gen_time.clone().into()), }; diff --git a/sdk/src/crypto/ocsp/fetch.rs b/sdk/src/crypto/ocsp/fetch.rs index e54b18bd4..243023b0f 100644 --- a/sdk/src/crypto/ocsp/fetch.rs +++ b/sdk/src/crypto/ocsp/fetch.rs @@ -23,10 +23,7 @@ use x509_parser::{ prelude::*, }; -use crate::{ - crypto::base64, - http::{AsyncHttpResolver, SyncHttpResolver}, -}; +use crate::{context::Context, crypto::base64}; const AD_OCSP_OID: Oid<'static> = oid!(1.3.6 .1 .5 .5 .7 .48 .1); const AUTHORITY_INFO_ACCESS_OID: Oid<'static> = oid!(1.3.6 .1 .5 .5 .7 .1 .1); @@ -145,14 +142,8 @@ fn process_ocsp_responders(certs: &[Vec]) -> Option> { /// /// Checks for an OCSP responder in the end-entity certificate. If found, it /// will attempt to retrieve the raw DER-encoded OCSP response. -#[async_generic(async_signature( - certs: &[Vec], - http_resolver: &impl AsyncHttpResolver, -))] -pub(crate) fn fetch_ocsp_response( - certs: &[Vec], - http_resolver: &impl SyncHttpResolver, -) -> Option> { +#[async_generic] +pub(crate) fn fetch_ocsp_response(certs: &[Vec], context: &Context) -> Option> { let requests = process_ocsp_responders(certs)?; for request_data in requests { let req_url = request_data.url.join(&request_data.request_str).ok()?; @@ -165,9 +156,13 @@ pub(crate) fn fetch_ocsp_response( let request = request.body(Vec::new()).ok()?; let response = if _sync { - http_resolver.http_resolve(request).ok()? + context.resolver().http_resolve(request).ok()? } else { - http_resolver.http_resolve_async(request).await.ok()? + context + .resolver_async() + .http_resolve_async(request) + .await + .ok()? }; if response.status() == 200 { diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs index 6f1aa71aa..e26a11f50 100644 --- a/sdk/src/ingredient.rs +++ b/sdk/src/ingredient.rs @@ -32,17 +32,16 @@ use crate::{ }, asset_io::CAIRead, claim::{Claim, ClaimAssetData}, + context::Context, crypto::base64, error::{Error, Result}, hashed_uri::HashedUri, - http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver}, jumbf::{ self, labels::{assertion_label_from_uri, manifest_label_from_uri}, }, log_item, resource_store::{ResourceRef, ResourceStore}, - settings::Settings, status_tracker::StatusTracker, store::Store, utils::{ @@ -733,8 +732,8 @@ impl Ingredient { options: &dyn IngredientOptions, ) -> Result { let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = SyncGenericResolver::new(); - Self::from_file_impl(path.as_ref(), options, &http_resolver, &settings) + let context = Context::new().with_settings(settings); + Self::from_file_impl(path.as_ref(), options, &context) } // Internal implementation to avoid code bloat. @@ -742,8 +741,7 @@ impl Ingredient { fn from_file_impl( path: &Path, options: &dyn IngredientOptions, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { #[cfg(feature = "diagnostics")] let _t = crate::utils::time_it::TimeIt::new("Ingredient:from_file_with_options"); @@ -774,25 +772,15 @@ impl Ingredient { let mut validation_log = StatusTracker::default(); // retrieve the manifest bytes from embedded, sidecar or remote and convert to store if found - let (result, manifest_bytes) = - match Store::load_jumbf_from_path(path, http_resolver, settings) { - Ok(manifest_bytes) => { - ( - // generate a store from the buffer and then validate from the asset path - Store::from_jumbf_with_settings( - &manifest_bytes, - &mut validation_log, - settings, - ) + let (result, manifest_bytes) = match Store::load_jumbf_from_path(path, context) { + Ok(manifest_bytes) => { + ( + // generate a store from the buffer and then validate from the asset path + Store::from_jumbf_with_context(&manifest_bytes, &mut validation_log, context) .and_then(|mut store| { // verify the store store - .verify_from_path( - path, - &mut validation_log, - http_resolver, - settings, - ) + .verify_from_path(path, &mut validation_log, context) .map(|_| store) }) .inspect_err(|e| { @@ -800,11 +788,11 @@ impl Ingredient { log_item!("asset", "error loading file", "Ingredient::from_file") .failure_no_throw(&mut validation_log, e); }), - Some(manifest_bytes), - ) - } - Err(err) => (Err(err), None), - }; + Some(manifest_bytes), + ) + } + Err(err) => (Err(err), None), + }; // set validation status from result and log ingredient.update_validation_status(result, manifest_bytes, &validation_log)?; @@ -819,7 +807,7 @@ impl Ingredient { ingredient.maybe_add_thumbnail( &format, &mut std::io::BufReader::new(std::fs::File::open(path)?), - settings, + context, )?; } } @@ -841,10 +829,10 @@ impl Ingredient { /// This does not set title or hash. /// Thumbnail will be set only if one can be retrieved from a previous valid manifest. pub fn from_stream(format: &str, stream: &mut dyn CAIRead) -> Result { - let settings = crate::settings::get_settings().unwrap_or_default(); + let context = Context::new(); let ingredient = Self::from_stream_info(stream, format, "untitled"); stream.rewind()?; - ingredient.add_stream_internal(format, stream, &SyncGenericResolver::new(), &settings) + ingredient.add_stream_internal(format, stream, &context) } /// Create an Ingredient from JSON. @@ -859,19 +847,12 @@ impl Ingredient { /// Sets thumbnail if not defined and a valid claim thumbnail is found or add_thumbnails is enabled. /// Instance_id, document_id, and provenance will be overridden if found in the stream. /// Format will be overridden only if it is the default (application/octet-stream). - #[async_generic(async_signature( - mut self, - format: S, - stream: &mut dyn CAIRead, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, - ))] + #[async_generic] pub(crate) fn with_stream>( mut self, format: S, stream: &mut dyn CAIRead, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { let format = format.into(); @@ -903,27 +884,20 @@ impl Ingredient { stream.rewind()?; if _sync { - self.add_stream_internal(&format, stream, http_resolver, settings) + self.add_stream_internal(&format, stream, context) } else { - self.add_stream_internal_async(&format, stream, http_resolver, settings) + self.add_stream_internal_async(&format, stream, context) .await } } // Internal implementation to avoid code bloat. - #[async_generic(async_signature( - mut self, - format: &str, - stream: &mut dyn CAIRead, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, - ))] + #[async_generic] fn add_stream_internal( mut self, format: &str, stream: &mut dyn CAIRead, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { let mut validation_log = StatusTracker::default(); @@ -931,9 +905,9 @@ impl Ingredient { let jumbf_result = match self.manifest_data() { Some(data) => Ok(data.into_owned()), None => if _sync { - Store::load_jumbf_from_stream(format, stream, http_resolver, settings) + Store::load_jumbf_from_stream(format, stream, context) } else { - Store::load_jumbf_from_stream_async(format, stream, http_resolver, settings).await + Store::load_jumbf_from_stream_async(format, stream, context).await } .map(|(manifest_bytes, _)| manifest_bytes), }; @@ -946,20 +920,16 @@ impl Ingredient { &manifest_bytes, format, &mut *stream, - true, &mut validation_log, - http_resolver, - settings, + context, ) } else { Store::from_manifest_data_and_stream_async( &manifest_bytes, format, &mut *stream, - true, &mut validation_log, - http_resolver, - settings, + context, ) .await }; @@ -970,23 +940,13 @@ impl Ingredient { // Fetch ocsp responses and store it with the ingredient if let Ok(ref mut store) = result { - let labels = store.get_manifest_labels_for_ocsp(settings); + let labels = store.get_manifest_labels_for_ocsp(context.settings()); let ocsp_response_ders = if _sync { - store.get_ocsp_response_ders( - labels, - &mut validation_log, - http_resolver, - settings, - )? + store.get_ocsp_response_ders(labels, &mut validation_log, context)? } else { store - .get_ocsp_response_ders_async( - labels, - &mut validation_log, - http_resolver, - settings, - ) + .get_ocsp_response_ders_async(labels, &mut validation_log, context) .await? }; @@ -1003,7 +963,7 @@ impl Ingredient { // create a thumbnail if we don't already have a manifest with a thumb we can use #[cfg(feature = "add_thumbnails")] - self.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), settings)?; + self.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), context)?; Ok(self) } @@ -1022,16 +982,14 @@ impl Ingredient { /// This does not set title or hash. /// Thumbnail will be set only if one can be retrieved from a previous valid manifest. pub async fn from_stream_async(format: &str, stream: &mut dyn CAIRead) -> Result { - let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = AsyncGenericResolver::new(); - Self::from_stream_async_with_settings(format, stream, &http_resolver, &settings).await + let context = Context::new(); + Self::from_stream_async_with_settings(format, stream, &context).await } pub(crate) async fn from_stream_async_with_settings( format: &str, stream: &mut dyn CAIRead, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { let mut ingredient = Self::from_stream_info(stream, format, "untitled"); stream.rewind()?; @@ -1039,57 +997,50 @@ impl Ingredient { let mut validation_log = StatusTracker::default(); // retrieve the manifest bytes from embedded, sidecar or remote and convert to store if found - let (result, manifest_bytes) = match Store::load_jumbf_from_stream_async( - format, - stream, - &AsyncGenericResolver::new(), - settings, - ) - .await - { - Ok((manifest_bytes, _)) => { - ( - // generate a store from the buffer and then validate from the asset path - match Store::from_jumbf_with_settings( - &manifest_bytes, - &mut validation_log, - settings, - ) { - Ok(store) => { - // verify the store - Store::verify_store_async( - &store, - &mut ClaimAssetData::Stream(stream, format), - &mut validation_log, - http_resolver, - settings, - ) - .await - .map(|_| store) - } - Err(e) => { - log_item!( - "asset", - "error loading asset", - "Ingredient::from_stream_async" - ) - .failure_no_throw(&mut validation_log, &e); + let (result, manifest_bytes) = + match Store::load_jumbf_from_stream_async(format, stream, context).await { + Ok((manifest_bytes, _)) => { + ( + // generate a store from the buffer and then validate from the asset path + match Store::from_jumbf_with_context( + &manifest_bytes, + &mut validation_log, + context, + ) { + Ok(store) => { + // verify the store + Store::verify_store_async( + &store, + &mut ClaimAssetData::Stream(stream, format), + &mut validation_log, + context, + ) + .await + .map(|_| store) + } + Err(e) => { + log_item!( + "asset", + "error loading asset", + "Ingredient::from_stream_async" + ) + .failure_no_throw(&mut validation_log, &e); - Err(e) - } - }, - Some(manifest_bytes), - ) - } - Err(err) => (Err(err), None), - }; + Err(e) + } + }, + Some(manifest_bytes), + ) + } + Err(err) => (Err(err), None), + }; // set validation status from result and log ingredient.update_validation_status(result, manifest_bytes, &validation_log)?; // create a thumbnail if we don't already have a manifest with a thumb we can use #[cfg(feature = "add_thumbnails")] - ingredient.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), settings)?; + ingredient.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), context)?; Ok(ingredient) } @@ -1249,7 +1200,7 @@ impl Ingredient { claim: &mut Claim, redactions: Option>, resources: Option<&ResourceStore>, // use alternate resource store (for Builder model) - settings: &Settings, + context: &Context, ) -> Result { let mut thumbnail = None; // for Builder model, ingredient resources may be in the manifest @@ -1270,7 +1221,7 @@ impl Ingredient { // have Store check and load ingredients and add them to a claim let ingredient_store = - Store::load_ingredient_to_claim(claim, &manifest_data, redactions, settings)?; + Store::load_ingredient_to_claim(claim, &manifest_data, redactions, context)?; let ingredient_active_claim = ingredient_store .provenance_claim() @@ -1488,48 +1439,43 @@ impl Ingredient { format: &str, stream: &mut dyn CAIRead, ) -> Result { - let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = AsyncGenericResolver::new(); + let context = Context::new(); let mut ingredient = Self::from_stream_info(stream, format, "untitled"); let mut validation_log = StatusTracker::default(); let manifest_bytes: Vec = manifest_bytes.into(); // generate a store from the buffer and then validate from the asset path - let result = match Store::from_jumbf_with_settings( - &manifest_bytes, - &mut validation_log, - &settings, - ) { - Ok(store) => { - // verify the store - stream.rewind()?; - - Store::verify_store_async( - &store, - &mut ClaimAssetData::Stream(stream, format), - &mut validation_log, - &http_resolver, - &settings, - ) - .await - .map(|_| store) - } - Err(e) => { - // add a log entry for the error so we act like verify - log_item!("asset", "error loading file", "Ingredient::from_file") - .failure_no_throw(&mut validation_log, &e); + let result = + match Store::from_jumbf_with_context(&manifest_bytes, &mut validation_log, &context) { + Ok(store) => { + // verify the store + stream.rewind()?; + + Store::verify_store_async( + &store, + &mut ClaimAssetData::Stream(stream, format), + &mut validation_log, + &context, + ) + .await + .map(|_| store) + } + Err(e) => { + // add a log entry for the error so we act like verify + log_item!("asset", "error loading file", "Ingredient::from_file") + .failure_no_throw(&mut validation_log, &e); - Err(e) - } - }; + Err(e) + } + }; // set validation status from result and log ingredient.update_validation_status(result, Some(manifest_bytes), &validation_log)?; // create a thumbnail if we don't already have a manifest with a thumb we can use #[cfg(feature = "add_thumbnails")] - ingredient.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), &settings)?; + ingredient.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), &context)?; Ok(ingredient) } @@ -1543,11 +1489,12 @@ impl Ingredient { &mut self, format: &str, stream: &mut R, - settings: &Settings, + context: &Context, ) -> Result<()> where R: std::io::BufRead + std::io::Seek, { + let settings = context.settings(); let auto_thumbnail = settings.builder.thumbnail.enabled; if self.thumbnail.is_none() && auto_thumbnail { @@ -1923,7 +1870,6 @@ mod tests_file_io { const NO_MANIFEST_JPEG: &str = "earth_apollo17.jpg"; const MANIFEST_JPEG: &str = "C.jpg"; const BAD_SIGNATURE_JPEG: &str = "E-sig-CA.jpg"; - const PRERELEASE_JPEG: &str = "prerelease.jpg"; fn stats(ingredient: &Ingredient) -> usize { let thumb_size = ingredient.thumbnail_bytes().map_or(0, |i| i.len()); @@ -2083,6 +2029,7 @@ mod tests_file_io { #[test] #[cfg(all(feature = "file_io", feature = "add_thumbnails"))] fn test_jpg_prerelease() { + const PRERELEASE_JPEG: &str = "prerelease.jpg"; let ap = fixture_path(PRERELEASE_JPEG); let ingredient = Ingredient::from_file(ap).expect("from_file"); stats(&ingredient); diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index 74eb73f5c..c8894d624 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -32,17 +32,14 @@ use serde_with::skip_serializing_none; #[cfg(feature = "file_io")] use crate::utils::io_utils::uri_to_path; use crate::{ - asset::Asset, claim::Claim, context::Context, dynamic_assertion::PartialClaim, error::{Error, Result}, - http::{AsyncGenericResolver, SyncGenericResolver}, jumbf::labels::{manifest_label_from_uri, to_absolute_uri, to_relative_uri}, jumbf_io, log_item, manifest::StoreOptions, manifest_store_report::ManifestStoreReport, - settings::Settings, status_tracker::StatusTracker, store::Store, utils::hash_utils::hash_to_b64, @@ -111,40 +108,31 @@ type ValidationFn = dyn Fn(&str, &crate::ManifestAssertion, &mut StatusTracker) -> Option; impl Reader { + // #[async_generic] + // pub fn from_asset<'a>(context: &'a Context, asset: &'a mut Asset<'a>) -> Result { + + // let mut validation_log = StatusTracker::default(); + // //asset.rewind()?; + // let store = if _sync { + // Store::from_asset(context, + // asset, + // &mut validation_log, + // ) + // } else { + // Store::from_asset_async( + // context, + // asset, + // &mut validation_log, + // ) + // .await + // }?; + // if _sync { + // Self::from_store(store, &mut validation_log, context) + // } else { + // Self::from_store_async(store, &mut validation_log, context).await + // } + // } - #[async_generic] - pub fn from_asset<'a>(context: &'a Context, asset: &'a mut Asset<'a>) -> Result { - let settings = context.settings(); - let http_resolver = if _sync { - SyncGenericResolver::new() - } else { - AsyncGenericResolver::new() - }; - let resolver = context.resolver(); - - let mut validation_log = StatusTracker::default(); - //asset.rewind()?; - let store = if _sync { - Store::from_asset(context, - asset, - &mut validation_log, - ) - } else { - Store::from_asset_async( - context, - asset, - &mut validation_log, - ) - .await - }?; - if _sync { - Self::from_store(store, &mut validation_log, context.settings()) - } else { - Self::from_store_async(store, &mut validation_log, context.settings()).await - } - } - - /// Create a manifest store [`Reader`] from a stream. A Reader is used to validate C2PA data from an asset. /// /// # Arguments @@ -175,84 +163,40 @@ impl Reader { #[async_generic] #[cfg(not(target_arch = "wasm32"))] pub fn from_stream(format: &str, mut stream: impl Read + Seek + Send) -> Result { - let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = if _sync { - SyncGenericResolver::new() - } else { - AsyncGenericResolver::new() - }; - // TODO: passing verify is redundant with settings - let verify = settings.verify.verify_after_reading; + let context = Context::new(); let mut validation_log = StatusTracker::default(); stream.rewind()?; // Ensure stream is at the start let store = if _sync { - Store::from_stream( - format, - stream, - verify, - &mut validation_log, - &http_resolver, - &settings, - ) + Store::from_stream(format, stream, &mut validation_log, &context) } else { - Store::from_stream_async( - format, - stream, - verify, - &mut validation_log, - &http_resolver, - &settings, - ) - .await + Store::from_stream_async(format, stream, &mut validation_log, &context).await }?; if _sync { - Self::from_store(store, &mut validation_log, &settings) + Self::from_store(store, &mut validation_log, &context) } else { - Self::from_store_async(store, &mut validation_log, &settings).await + Self::from_store_async(store, &mut validation_log, &context).await } } #[async_generic] #[cfg(target_arch = "wasm32")] pub fn from_stream(format: &str, mut stream: impl Read + Seek) -> Result { - let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = if _sync { - SyncGenericResolver::new() - } else { - AsyncGenericResolver::new() - }; - // TODO: passing verify is redundant with settings - let verify = settings.verify.verify_after_reading; + let context = Context::new(); let mut validation_log = StatusTracker::default(); let store = if _sync { - Store::from_stream( - format, - &mut stream, - verify, - &mut validation_log, - &http_resolver, - &settings, - ) + Store::from_stream(format, &mut stream, &mut validation_log, &context) } else { - Store::from_stream_async( - format, - &mut stream, - verify, - &mut validation_log, - &http_resolver, - &settings, - ) - .await + Store::from_stream_async(format, &mut stream, &mut validation_log, &context).await }?; if _sync { - Self::from_store(store, &mut validation_log, &settings) + Self::from_store(store, &mut validation_log, &context) } else { - Self::from_store_async(store, &mut validation_log, &settings).await + Self::from_store_async(store, &mut validation_log, &context).await } } @@ -344,41 +288,30 @@ impl Reader { format: &str, stream: impl Read + Seek + Send, ) -> Result { - let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = if _sync { - SyncGenericResolver::new() - } else { - AsyncGenericResolver::new() - }; + let context = Context::new(); let mut validation_log = StatusTracker::default(); - let verify = settings.verify.verify_after_reading; - let store = if _sync { Store::from_manifest_data_and_stream( c2pa_data, format, stream, - verify, &mut validation_log, - &http_resolver, - &settings, + &context, ) } else { Store::from_manifest_data_and_stream_async( c2pa_data, format, stream, - verify, &mut validation_log, - &http_resolver, - &settings, + &context, ) .await }?; - Self::from_store(store, &mut validation_log, &settings) + Self::from_store(store, &mut validation_log, &context) } /// Create a [`Reader`] from an initial segment and a fragment stream. @@ -398,12 +331,7 @@ impl Reader { mut stream: impl Read + Seek + Send, mut fragment: impl Read + Seek + Send, ) -> Result { - let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = if _sync { - SyncGenericResolver::new() - } else { - AsyncGenericResolver::new() - }; + let context = Context::new(); let mut validation_log = StatusTracker::default(); @@ -413,8 +341,7 @@ impl Reader { &mut stream, &mut fragment, &mut validation_log, - &http_resolver, - &settings, + &context, ) } else { Store::load_fragment_from_stream_async( @@ -422,13 +349,12 @@ impl Reader { &mut stream, &mut fragment, &mut validation_log, - &http_resolver, - &settings, + &context, ) .await }?; - Self::from_store(store, &mut validation_log, &settings) + Self::from_store(store, &mut validation_log, &context) } /// Loads a [`Reader`]` from an initial segment and fragments. This @@ -439,10 +365,8 @@ impl Reader { path: P, fragments: &Vec, ) -> Result { - let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = SyncGenericResolver::new(); + let context = Context::new(); - let verify = settings.verify.verify_after_reading; let mut validation_log = StatusTracker::default(); let asset_type = jumbf_io::get_supported_file_extension(path.as_ref()) @@ -454,12 +378,10 @@ impl Reader { &asset_type, &mut init_segment, fragments, - verify, &mut validation_log, - &http_resolver, - &settings, + &context, ) { - Ok(store) => Self::from_store(store, &mut validation_log, &settings), + Ok(store) => Self::from_store(store, &mut validation_log, &context), Err(e) => Err(e), } } @@ -784,7 +706,7 @@ impl Reader { pub(crate) fn from_store( store: Store, validation_log: &mut StatusTracker, - settings: &Settings, + context: &Context, ) -> Result { let active_manifest = store.provenance_label(); let mut manifests = HashMap::new(); @@ -798,7 +720,7 @@ impl Reader { manifest_label, &mut options, validation_log, - settings, + context.settings(), ) } else { Manifest::from_store_async( @@ -806,7 +728,7 @@ impl Reader { manifest_label, &mut options, validation_log, - settings, + context.settings(), ) .await }; diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 4489aa466..0cb429423 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -41,6 +41,7 @@ use crate::{ check_ocsp_status, check_ocsp_status_async, Claim, ClaimAssertion, ClaimAssetData, RemoteManifest, }, + context::Context, cose_sign::{cose_sign, cose_sign_async}, cose_validator::{verify_cose, verify_cose_async}, crypto::{ @@ -90,6 +91,7 @@ use crate::{ }; const MANIFEST_STORE_EXT: &str = "c2pa"; // file extension for external manifests +#[cfg(feature = "fetch_remote_manifests")] const DEFAULT_MANIFEST_RESPONSE_SIZE: usize = 10 * 1024 * 1024; // 10 MB pub(crate) struct ManifestHashes { @@ -132,8 +134,7 @@ struct ManifestInfo<'a> { impl Default for Store { fn default() -> Self { - let settings = crate::settings::get_settings().unwrap_or_default(); - Self::with_settings(&settings) + Self::with_context(&Context::new()) } } @@ -153,8 +154,9 @@ impl Store { } /// Create a new, empty claims store with the specified settings. - pub fn with_settings(settings: &Settings) -> Self { + pub fn with_context(context: &Context) -> Self { let mut store = Store::new(); + let settings = context.settings(); // load the trust handler settings, don't worry about status as these are checked during setting generation if let Some(ta) = &settings.trust.trust_anchors { @@ -540,16 +542,8 @@ impl Store { // Currently only called from manifest_store behind a feature flag but this is allowable // anywhere so allow dead code here for future uses to compile #[allow(dead_code)] - #[async_generic(async_signature( - &self, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, - ))] - pub fn get_ocsp_status( - &self, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, - ) -> Option { + #[async_generic] + pub fn get_ocsp_status(&self, context: &Context) -> Option { let claim = self .provenance_claim() .ok_or(Error::ProvenanceMissing) @@ -569,8 +563,7 @@ impl Store { None, None, &mut validation_log, - http_resolver, - settings, + context, ) } else { check_ocsp_status_async( @@ -580,8 +573,7 @@ impl Store { None, None, &mut validation_log, - http_resolver, - settings, + context, ) .await }; @@ -1194,12 +1186,12 @@ impl Store { } #[inline] - pub fn from_jumbf_with_settings( + pub fn from_jumbf_with_context( buffer: &[u8], validation_log: &mut StatusTracker, - settings: &Settings, + context: &Context, ) -> Result { - Self::from_jumbf_impl(Store::with_settings(settings), buffer, validation_log) + Self::from_jumbf_impl(Store::with_context(context), buffer, validation_log) } fn from_jumbf_impl( @@ -1559,9 +1551,9 @@ impl Store { svi: &StoreValidationInfo, asset_data: &mut ClaimAssetData<'_>, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result<()> { + let settings = context.settings(); // walk the ingredients for i in claim.ingredient_assertions() { // allow for zero out ingredient assertions @@ -1737,8 +1729,7 @@ impl Store { check_ingredient_trust, &store.ctp, validation_log, - http_resolver, - settings, + context, )?; // recurse nested ingredients @@ -1748,8 +1739,7 @@ impl Store { svi, asset_data, validation_log, - http_resolver, - settings, + context, )?; } else { log_item!(label.clone(), "ingredient not found", "ingredient_checks") @@ -1783,9 +1773,9 @@ impl Store { svi: &StoreValidationInfo<'_>, asset_data: &mut ClaimAssetData<'_>, validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result<()> { + let settings = context.settings(); // walk the ingredients for i in claim.ingredient_assertions() { // allow for zero out ingredient assertions @@ -1961,8 +1951,7 @@ impl Store { check_ingredient_trust, &store.ctp, validation_log, - http_resolver, - settings, + context, ) .await?; @@ -1973,8 +1962,7 @@ impl Store { svi, asset_data, validation_log, - http_resolver, - settings, + context, )) .await?; } else { @@ -2148,16 +2136,14 @@ impl Store { store: &Store, asset_data: &mut ClaimAssetData<'_>, validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, + context: &Context, ))] pub fn verify_store( store: &Store, asset_data: &mut ClaimAssetData<'_>, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result<()> { let claim = match store.provenance_claim() { Some(c) => c, @@ -2171,7 +2157,12 @@ impl Store { }; // get info needed to complete validation - let svi = store.get_store_validation_info(claim, asset_data, validation_log, settings)?; + let svi = store.get_store_validation_info( + claim, + asset_data, + validation_log, + context.settings(), + )?; if _sync { // verify the provenance claim @@ -2182,19 +2173,10 @@ impl Store { true, &store.ctp, validation_log, - http_resolver, - settings, + context, )?; - Store::ingredient_checks( - store, - claim, - &svi, - asset_data, - validation_log, - http_resolver, - settings, - )?; + Store::ingredient_checks(store, claim, &svi, asset_data, validation_log, context)?; } else { Claim::verify_claim_async( claim, @@ -2203,21 +2185,12 @@ impl Store { true, &store.ctp, validation_log, - http_resolver, - settings, + context, ) .await?; - Store::ingredient_checks_async( - store, - claim, - &svi, - asset_data, - validation_log, - http_resolver, - settings, - ) - .await?; + Store::ingredient_checks_async(store, claim, &svi, asset_data, validation_log, context) + .await?; } Ok(()) @@ -2582,14 +2555,14 @@ impl Store { signer: &dyn Signer, format: &str, asset_reader: Option<&mut dyn CAIRead>, - settings: &Settings, + context: &Context, ) -> Result> { let mut jumbf_bytes = self.prep_embeddable_store(signer.reserve_size(), dh, asset_reader)?; // sign contents let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; - let sig = self.sign_claim(pc, signer, signer.reserve_size(), settings)?; + let sig = self.sign_claim(pc, signer, signer.reserve_size(), context.settings())?; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); @@ -2613,7 +2586,7 @@ impl Store { signer: &dyn AsyncSigner, format: &str, asset_reader: Option<&mut dyn CAIRead>, - settings: &Settings, + context: &Context, ) -> Result> { let mut jumbf_bytes = self.prep_embeddable_store(signer.reserve_size(), dh, asset_reader)?; @@ -2621,7 +2594,7 @@ impl Store { // sign contents let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; let sig = self - .sign_claim_async(pc, signer, signer.reserve_size(), settings) + .sign_claim_async(pc, signer, signer.reserve_size(), context.settings()) .await?; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); @@ -2634,7 +2607,7 @@ impl Store { pub fn get_box_hashed_embeddable_manifest( &mut self, signer: &dyn Signer, - settings: &Settings, + context: &Context, ) -> Result> { let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; @@ -2653,7 +2626,7 @@ impl Store { let mut jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?; // sign contents - let sig = self.sign_claim(pc, signer, signer.reserve_size(), settings)?; + let sig = self.sign_claim(pc, signer, signer.reserve_size(), context.settings())?; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); if sig_placeholder.len() != sig.len() { @@ -2671,7 +2644,7 @@ impl Store { pub async fn get_box_hashed_embeddable_manifest_async( &mut self, signer: &dyn AsyncSigner, - settings: &Settings, + context: &Context, ) -> Result> { let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; @@ -2691,7 +2664,7 @@ impl Store { // sign contents let sig = self - .sign_claim_async(pc, signer, signer.reserve_size(), settings) + .sign_claim_async(pc, signer, signer.reserve_size(), context.settings()) .await?; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); @@ -2878,7 +2851,7 @@ impl Store { fragments: &Vec, output_path: &Path, signer: &dyn Signer, - settings: &Settings, + context: &Context, ) -> Result<()> { match get_supported_file_extension(asset_path) { Some(ext) => { @@ -2903,15 +2876,14 @@ impl Store { let jumbf = self.to_jumbf(signer)?; // use temp store so mulitple calls across renditions will work (the Store is not finalized this way) - let mut temp_store = - Store::from_jumbf_with_settings(&jumbf, &mut validation_log, settings)?; + let mut temp_store = Store::from_jumbf_with_context(&jumbf, &mut validation_log, context)?; let mut jumbf_bytes = temp_store.start_save_bmff_fragmented( asset_path, fragments, output_path, signer.reserve_size(), - settings, + context.settings(), )?; let mut preliminary_claim = PartialClaim::default(); @@ -2960,7 +2932,7 @@ impl Store { // sign the claim let pc = temp_store.provenance_claim().ok_or(Error::ClaimEncoding)?; - let sig = temp_store.sign_claim(pc, signer, signer.reserve_size(), settings)?; + let sig = temp_store.sign_claim(pc, signer, signer.reserve_size(), context.settings())?; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); match temp_store.finish_save(jumbf_bytes, &dest_path, sig, &sig_placeholder) { @@ -2986,8 +2958,7 @@ impl Store { input_stream: &mut dyn CAIRead, output_stream: &mut dyn CAIReadWrite, signer: &dyn AsyncSigner, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, + context: &Context, ))] pub(crate) fn save_to_stream( &mut self, @@ -2995,9 +2966,9 @@ impl Store { input_stream: &mut dyn CAIRead, output_stream: &mut dyn CAIReadWrite, signer: &dyn Signer, - http_resolver: &(impl SyncHttpResolver + ?Sized), - settings: &Settings, + context: &Context, ) -> Result> { + let settings = context.settings(); let dynamic_assertions = signer.dynamic_assertions(); let da_uris = if _sync { @@ -3105,16 +3076,14 @@ impl Store { self, &mut crate::claim::ClaimAssetData::Stream(output_stream, format), &mut validation_log, - http_resolver, - settings, + context, )?; } else { Store::verify_store_async( self, &mut crate::claim::ClaimAssetData::Stream(output_stream, format), &mut validation_log, - http_resolver, - settings, + context, ) .await?; } @@ -3396,32 +3365,27 @@ impl Store { &mut self, asset_path: &'_ Path, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result<()> { Store::verify_store( self, &mut ClaimAssetData::Path(asset_path), validation_log, - http_resolver, - settings, + context, ) } // fetch remote manifest if possible #[cfg(feature = "fetch_remote_manifests")] - #[async_generic(async_signature( - url: &str, - http_resolver: &impl AsyncHttpResolver - ))] - fn fetch_remote_manifest(url: &str, http_resolver: &impl SyncHttpResolver) -> Result> { + #[async_generic] + fn fetch_remote_manifest(url: &str, context: &Context) -> Result> { //const MANIFEST_CONTENT_TYPE: &str = "application/x-c2pa-manifest-store"; // todo verify once these are served let request = http::Request::get(url).body(Vec::new())?; let response = if _sync { - http_resolver.http_resolve(request) + context.resolver().http_resolve(request) } else { - http_resolver.http_resolve_async(request).await + context.resolver_async().http_resolve_async(request).await }; match response { @@ -3460,26 +3424,18 @@ impl Store { } /// Handles remote manifests when file_io/fetch_remote_manifests feature is enabled - #[async_generic(async_signature( - ext_ref: &str, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings - ))] - fn handle_remote_manifest( - ext_ref: &str, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, - ) -> Result> { + #[async_generic] + fn handle_remote_manifest(ext_ref: &str, _context: &Context) -> Result> { // verify provenance path is remote url if Store::is_valid_remote_url(ext_ref) { #[cfg(feature = "fetch_remote_manifests")] { // Everything except browser wasm if fetch_remote_manifests is enabled - if settings.verify.remote_manifest_fetch { + if _context.settings().verify.remote_manifest_fetch { if _sync { - Store::fetch_remote_manifest(ext_ref, http_resolver) + Store::fetch_remote_manifest(ext_ref, _context) } else { - Store::fetch_remote_manifest_async(ext_ref, http_resolver).await + Store::fetch_remote_manifest_async(ext_ref, _context).await } } else { Err(Error::RemoteManifestUrl(ext_ref.to_owned())) @@ -3505,14 +3461,12 @@ impl Store { #[async_generic(async_signature( asset_type: &str, stream: &mut dyn CAIRead, - http_resolver: &(impl AsyncHttpResolver + ?Sized), - settings: &Settings + context: &Context ))] pub fn load_jumbf_from_stream( asset_type: &str, stream: &mut dyn CAIRead, - http_resolver: &(impl SyncHttpResolver + ?Sized), - settings: &Settings, + context: &Context, ) -> Result<(Vec, Option)> { match load_jumbf_from_stream(asset_type, stream) { Ok(manifest_bytes) => Ok((manifest_bytes, None)), @@ -3523,10 +3477,9 @@ impl Store { .provenance { let jumbf = if _sync { - Store::handle_remote_manifest(&ext_ref, http_resolver, settings)? + Store::handle_remote_manifest(&ext_ref, context)? } else { - Store::handle_remote_manifest_async(&ext_ref, http_resolver, settings) - .await? + Store::handle_remote_manifest_async(&ext_ref, context).await? }; Ok((jumbf, Some(ext_ref))) } else { @@ -3544,11 +3497,7 @@ impl Store { /// in_path - path to source file /// validation_log - optional vec to contain addition info about the asset #[cfg(feature = "file_io")] - pub fn load_jumbf_from_path( - in_path: &Path, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, - ) -> Result> { + pub fn load_jumbf_from_path(in_path: &Path, context: &Context) -> Result> { let external_manifest = in_path.with_extension(MANIFEST_STORE_EXT); let external_exists = external_manifest.exists(); @@ -3574,7 +3523,7 @@ impl Store { ) .provenance { - Store::handle_remote_manifest(&ext_ref, http_resolver, settings) + Store::handle_remote_manifest(&ext_ref, context) } else { Err(Error::JumbfNotFound) } @@ -3612,27 +3561,18 @@ impl Store { } /// Load store from a stream - #[async_generic(async_signature( - format: &str, - mut stream: impl Read + Seek + Send, - verify: bool, - validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, - ))] + #[async_generic] #[cfg(not(target_arch = "wasm32"))] pub fn from_stream( format: &str, mut stream: impl Read + Seek + Send, - verify: bool, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { let (manifest_bytes, remote_url) = if _sync { - Store::load_jumbf_from_stream(format, &mut stream, http_resolver, settings) + Store::load_jumbf_from_stream(format, &mut stream, context) } else { - Store::load_jumbf_from_stream_async(format, &mut stream, http_resolver, settings).await + Store::load_jumbf_from_stream_async(format, &mut stream, context).await } .inspect_err(|e| { log_item!("asset", "error loading file", "load_from_asset") @@ -3644,20 +3584,16 @@ impl Store { &manifest_bytes, format, &mut stream, - verify, validation_log, - http_resolver, - settings, + context, ) } else { Self::from_manifest_data_and_stream_async( &manifest_bytes, format, &mut stream, - verify, validation_log, - http_resolver, - settings, + context, ) .await }; @@ -3673,27 +3609,18 @@ impl Store { } /// Load store from a stream - #[async_generic(async_signature( - format: &str, - mut stream: impl Read + Seek, - verify: bool, - validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, - ))] + #[async_generic] #[cfg(target_arch = "wasm32")] pub fn from_stream( format: &str, mut stream: impl Read + Seek, - verify: bool, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { let (manifest_bytes, remote_url) = if _sync { - Store::load_jumbf_from_stream(format, &mut stream, http_resolver, settings) + Store::load_jumbf_from_stream(format, &mut stream, context) } else { - Store::load_jumbf_from_stream_async(format, &mut stream, http_resolver, settings).await + Store::load_jumbf_from_stream_async(format, &mut stream, context).await } .inspect_err(|e| { log_item!("asset", "error loading file", "load_from_asset") @@ -3705,20 +3632,16 @@ impl Store { &manifest_bytes, format, &mut stream, - verify, validation_log, - http_resolver, - settings, + context, ) } else { Self::from_manifest_data_and_stream_async( &manifest_bytes, format, &mut stream, - verify, validation_log, - http_resolver, - settings, + context, ) .await }; @@ -3734,107 +3657,60 @@ impl Store { } /// Load store from a manifest data and stream - #[async_generic(async_signature( - c2pa_data: &[u8], - format: &str, - mut stream: impl Read + Seek + Send, - verify: bool, - validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, - ))] + #[async_generic] #[cfg(not(target_arch = "wasm32"))] pub fn from_manifest_data_and_stream( c2pa_data: &[u8], format: &str, mut stream: impl Read + Seek + Send, - verify: bool, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { stream.rewind()?; // First we convert the JUMBF into a usable store. - let store = Store::from_jumbf_with_settings(c2pa_data, validation_log, settings) + let store = Store::from_jumbf_with_context(c2pa_data, validation_log, context) .inspect_err(|e| { log_item!("asset", "error loading file", "load_from_asset") .failure_no_throw(validation_log, e); })?; - if verify { + if context.settings().verify.verify_after_reading { stream.rewind()?; let mut asset_data = ClaimAssetData::Stream(&mut stream, format); if _sync { - Store::verify_store( - &store, - &mut asset_data, - validation_log, - http_resolver, - settings, - ) + Store::verify_store(&store, &mut asset_data, validation_log, context) } else { - Store::verify_store_async( - &store, - &mut asset_data, - validation_log, - http_resolver, - settings, - ) - .await + Store::verify_store_async(&store, &mut asset_data, validation_log, context).await }?; } Ok(store) } /// Load store from a manifest data and stream - #[async_generic(async_signature( - c2pa_data: &[u8], - format: &str, - mut stream: impl Read + Seek, - verify: bool, - validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, - ))] + #[async_generic] #[cfg(target_arch = "wasm32")] pub fn from_manifest_data_and_stream( c2pa_data: &[u8], format: &str, mut stream: impl Read + Seek, - verify: bool, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { // first we convert the JUMBF into a usable store - let store = Store::from_jumbf_with_settings(c2pa_data, validation_log, settings) + let store = Store::from_jumbf_with_context(c2pa_data, validation_log, context) .inspect_err(|e| { log_item!("asset", "error loading file", "load_from_asset") .failure_no_throw(validation_log, e); })?; - //let verify = get_settings_value::("verify.verify_after_reading")?; // defaults to true - + let verify = context.settings().verify.verify_after_reading; if verify { let mut asset_data = ClaimAssetData::Stream(&mut stream, format); if _sync { - Store::verify_store( - &store, - &mut asset_data, - validation_log, - http_resolver, - settings, - ) + Store::verify_store(&store, &mut asset_data, validation_log, context) } else { - Store::verify_store_async( - &store, - &mut asset_data, - validation_log, - http_resolver, - settings, - ) - .await + Store::verify_store_async(&store, &mut asset_data, validation_log, context).await }?; } Ok(store) @@ -3851,19 +3727,11 @@ impl Store { asset_type: &str, init_segment: &mut dyn CAIRead, fragments: &Vec, - verify: bool, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { - let store = Self::from_stream( - asset_type, - &mut *init_segment, - verify, - validation_log, - http_resolver, - settings, - )?; + let verify = context.settings().verify.verify_after_reading; + let store = Self::from_stream(asset_type, &mut *init_segment, validation_log, context)?; // verify the store if verify { @@ -3873,8 +3741,7 @@ impl Store { &store, &mut ClaimAssetData::StreamFragments(init_segment, fragments, asset_type), validation_log, - http_resolver, - settings, + context, )?; } @@ -3892,47 +3759,32 @@ impl Store { mut stream: impl Read + Seek + Send, mut fragment: impl Read + Seek + Send, validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, + context: &Context, ))] pub fn load_fragment_from_stream( format: &str, mut stream: impl Read + Seek + Send, mut fragment: impl Read + Seek + Send, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result { let manifest_bytes = if _sync { - Store::load_jumbf_from_stream(format, &mut stream, http_resolver, settings)?.0 + Store::load_jumbf_from_stream(format, &mut stream, context)?.0 } else { - Store::load_jumbf_from_stream_async(format, &mut stream, http_resolver, settings) + Store::load_jumbf_from_stream_async(format, &mut stream, context) .await? .0 }; - let store = Store::from_jumbf_with_settings(&manifest_bytes, validation_log, settings)?; - let verify = settings.verify.verify_after_reading; + let store = Store::from_jumbf_with_context(&manifest_bytes, validation_log, context)?; + let verify = context.settings().verify.verify_after_reading; if verify { let mut fragment = ClaimAssetData::StreamFragment(&mut stream, &mut fragment, format); if _sync { - Store::verify_store( - &store, - &mut fragment, - validation_log, - http_resolver, - settings, - ) + Store::verify_store(&store, &mut fragment, validation_log, context) } else { - Store::verify_store_async( - &store, - &mut fragment, - validation_log, - http_resolver, - settings, - ) - .await + Store::verify_store_async(&store, &mut fragment, validation_log, context).await }?; }; Ok(store) @@ -4100,7 +3952,7 @@ impl Store { claim: &mut Claim, data: &[u8], redactions: Option>, - settings: &Settings, + context: &Context, ) -> Result { // constants for ingredient conflict reasons const CONFLICTING_MANIFEST: usize = 1; // Conflicts with another C2PA Manifest @@ -4109,7 +3961,7 @@ impl Store { let mut to_remove_from_incoming = Vec::new(); let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError); - let i_store = Store::from_jumbf_with_settings(data, &mut report, settings)?; + let i_store = Store::from_jumbf_with_context(data, &mut report, context)?; let empty_store = Store::default(); @@ -4134,7 +3986,10 @@ impl Store { // resolve conflicts // for 2.x perform ingredients conflict handling by making new label if needed - let skip_resolution = settings.verify.skip_ingredient_conflict_resolution; + let skip_resolution = context + .settings() + .verify + .skip_ingredient_conflict_resolution; if claim.version() > 1 && !skip_resolution { // if the hashes match then the values are OK to add so remove form conflict list @@ -4256,7 +4111,7 @@ impl Store { } // make necessary changes to the incoming store - let mut i_store_mut = Store::from_jumbf_with_settings(data, &mut report, settings)?; + let mut i_store_mut = Store::from_jumbf_with_context(data, &mut report, context)?; let mut final_redactions = Vec::new(); if let Some(mut redactions) = redactions { final_redactions.append(&mut redactions); @@ -4294,19 +4149,17 @@ impl Store { &self, manifest_labels: Vec, validation_log: &mut StatusTracker, - http_resolver: &impl AsyncHttpResolver, - settings: &Settings, + context: &Context, ))] pub fn get_ocsp_response_ders( &self, manifest_labels: Vec, validation_log: &mut StatusTracker, - http_resolver: &impl SyncHttpResolver, - settings: &Settings, + context: &Context, ) -> Result)>> { let mut oscp_response_ders = Vec::new(); - let mut adjusted_settings = settings.clone(); + let mut adjusted_settings = context.settings().clone(); let original_trust_val = adjusted_settings.verify.verify_timestamp_trust; for manifest_label in manifest_labels { @@ -4327,8 +4180,7 @@ impl Store { &self.ctp, None, validation_log, - http_resolver, - settings, + context, )? .ocsp_der } else { @@ -4338,8 +4190,7 @@ impl Store { &self.ctp, None, validation_log, - http_resolver, - settings, + context, ) .await? .ocsp_der @@ -4470,7 +4321,6 @@ pub mod tests { assertions::{Action, Actions, Uuid}, claim::AssertionStoreJsonFormat, crypto::raw_signature::SigningAlg, - http::{AsyncGenericResolver, SyncGenericResolver}, settings::Settings, status_tracker::{LogItem, StatusTracker}, utils::{ @@ -4512,14 +4362,13 @@ pub mod tests { #[test] fn test_jumbf_generation() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // ClaimGeneratorInfo is mandatory in Claim V2 let cgi = ClaimGeneratorInfo::new("claim_v1_unit_test"); @@ -4538,8 +4387,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -4547,15 +4395,8 @@ pub mod tests { // read from new stream output_stream.rewind().unwrap(); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); // should not have any assert!(!report.has_any_error()); @@ -4588,14 +4429,13 @@ pub mod tests { #[test] fn test_claim_v2_generation() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // ClaimGeneratorInfo is mandatory in Claim V2 let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test"); @@ -4614,8 +4454,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -4623,15 +4462,8 @@ pub mod tests { // read from new stream output_stream.rewind().unwrap(); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); // should not have any assert!(!report.has_any_error()); @@ -4666,13 +4498,13 @@ pub mod tests { fn test_bad_claim_v2_generation() { let mut settings = Settings::default(); settings.verify.verify_after_sign = false; - let http_resolver = SyncGenericResolver::new(); + let context = Context::new().with_settings(settings); let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // ClaimGeneratorInfo is mandatory in Claim V2 let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test"); @@ -4695,8 +4527,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -4704,14 +4535,7 @@ pub mod tests { // read from new stream output_stream.rewind().unwrap(); - let _new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ); + let _new_store = Store::from_stream(format, &mut output_stream, &mut report, &context); // should have action errors assert!(report.has_any_error()); @@ -4724,15 +4548,14 @@ pub mod tests { #[cfg(feature = "file_io")] #[ignore = "we need to make this work again"] fn test_unknown_asset_type_generation() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); // test adding to actual image let (_format, mut input_stream, mut output_stream) = create_test_streams("unsupported_type.txt"); let format = "text/plain"; // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -4756,8 +4579,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -4766,10 +4588,8 @@ pub mod tests { let new_store = Store::from_stream( format, &mut output_stream, - true, &mut StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -4804,8 +4624,7 @@ pub mod tests { #[test] #[cfg(feature = "file_io")] fn test_detects_unverifiable_signature() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); struct BadSigner {} @@ -4831,7 +4650,7 @@ pub mod tests { let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg"); - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); let claim = create_test_claim().unwrap(); @@ -4850,8 +4669,7 @@ pub mod tests { &mut input_stream, &mut output_stream, &signer, - &http_resolver, - &settings, + &context, ) .unwrap_err(); } @@ -4861,14 +4679,13 @@ pub mod tests { fn test_sign_with_expired_cert() { use crate::{create_signer, crypto::raw_signature::SigningAlg}; - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = Context::new(); // test adding to actual image let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg"); - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); let claim = create_test_claim().unwrap(); @@ -4884,8 +4701,7 @@ pub mod tests { &mut input_stream, &mut output_stream, &signer, - &http_resolver, - &settings, + &context, ); assert!(r.is_err()); assert_eq!( @@ -4897,8 +4713,9 @@ pub mod tests { #[test] #[cfg(feature = "file_io")] fn test_jumbf_replacement_generation() { + let context = Context::new(); // Create claims store. - let mut store = Store::with_settings(&Settings::default()); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -4932,8 +4749,7 @@ pub mod tests { #[c2pa_test_async] //#[ignore] // this is not generating the expected error. Needs investigation. async fn test_jumbf_generation_async() -> Result<()> { - let settings = Settings::default(); - let http_resolver = AsyncGenericResolver::new(); + let context = crate::context::Context::new(); // Verify after sign is causing UnreferencedManifest errors here, since the manifests don't reference each other. //no_verify_after_sign(); @@ -4944,7 +4760,7 @@ pub mod tests { create_test_streams("earth_apollo17.jpg"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = crate::utils::test::create_test_claim()?; @@ -4970,8 +4786,7 @@ pub mod tests { &mut input_stream, &mut output_stream, &signer, - &http_resolver, - &settings, + &context, ) .await?; store.commit_claim(claim_capture)?; @@ -4983,8 +4798,7 @@ pub mod tests { &mut output_stream, &mut temp_stream, &signer, - &http_resolver, - &settings, + &context, ) .await?; store.commit_claim(claim2)?; @@ -4996,8 +4810,7 @@ pub mod tests { &mut temp_stream, &mut output_stream, &signer, - &http_resolver, - &settings, + &context, ) .await .unwrap(); @@ -5021,15 +4834,8 @@ pub mod tests { let mut report = StatusTracker::default(); output_stream.rewind()?; - let _new_store = Store::from_stream_async( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .await?; + let _new_store = + Store::from_stream_async(format, &mut output_stream, &mut report, &context).await?; assert!(!report.has_any_error()); Ok(()) @@ -5039,13 +4845,13 @@ pub mod tests { fn test_png_jumbf_generation() { let mut settings = Settings::default(); settings.verify.verify_after_sign = false; - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new().with_settings(settings); // test adding to actual image let (format, mut input_stream, mut output_stream) = create_test_streams("libpng-test.png"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -5069,8 +4875,7 @@ pub mod tests { &mut input_stream, &mut output_stream, &signer, - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -5083,8 +4888,7 @@ pub mod tests { &mut output_stream, &mut temp_stream, &signer, - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -5097,8 +4901,7 @@ pub mod tests { &mut temp_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -5109,15 +4912,8 @@ pub mod tests { // read from new stream output_stream.rewind().unwrap(); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); // can we get by the ingredient data back let _some_binary_data: Vec = vec![ @@ -5324,13 +5120,12 @@ pub mod tests { */ #[test] fn test_wav_jumbf_generation() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.wav"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -5354,8 +5149,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); store.commit_claim(claim_capture).unwrap(); @@ -5367,8 +5161,7 @@ pub mod tests { &mut output_stream, &mut temp_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); store.commit_claim(claim2).unwrap(); @@ -5379,8 +5172,7 @@ pub mod tests { &mut temp_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -5397,15 +5189,8 @@ pub mod tests { // read from new stream output_stream.rewind().unwrap(); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); // dump store and compare to original for claim in new_store.claims() { @@ -5437,13 +5222,12 @@ pub mod tests { #[test] fn test_avi_jumbf_generation() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("test.avi"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -5467,8 +5251,7 @@ pub mod tests { &mut input_stream, &mut output_stream, &signer, - &http_resolver, - &settings, + &context, ) .unwrap(); store.commit_claim(claim_capture).unwrap(); @@ -5480,8 +5263,7 @@ pub mod tests { &mut output_stream, &mut temp_stream, &signer, - &http_resolver, - &settings, + &context, ) .unwrap(); store.commit_claim(claim2).unwrap(); @@ -5493,8 +5275,7 @@ pub mod tests { &mut temp_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -5511,15 +5292,8 @@ pub mod tests { // read from new stream output_stream.rewind().unwrap(); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); // dump store and compare to original for claim in new_store.claims() { @@ -5551,13 +5325,12 @@ pub mod tests { #[test] fn test_webp_jumbf_generation() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.webp"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -5581,8 +5354,7 @@ pub mod tests { &mut input_stream, &mut output_stream, &signer, - &http_resolver, - &settings, + &context, ) .unwrap(); store.commit_claim(claim_capture).unwrap(); @@ -5594,8 +5366,7 @@ pub mod tests { &mut output_stream, &mut temp_stream, &signer, - &http_resolver, - &settings, + &context, ) .unwrap(); store.commit_claim(claim2).unwrap(); @@ -5607,8 +5378,7 @@ pub mod tests { &mut temp_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -5625,15 +5395,8 @@ pub mod tests { // read from new stream output_stream.rewind().unwrap(); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); // dump store and compare to original for claim in new_store.claims() { @@ -5665,13 +5428,12 @@ pub mod tests { #[test] fn test_heic() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.heic"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -5687,8 +5449,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -5696,15 +5457,8 @@ pub mod tests { // read from new stream output_stream.rewind().unwrap(); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); // dump store and compare to original for claim in new_store.claims() { @@ -5727,13 +5481,12 @@ pub mod tests { #[test] fn test_avif() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.avif"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -5749,8 +5502,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -5758,15 +5510,8 @@ pub mod tests { // read from new stream output_stream.rewind().unwrap(); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); // dump store and compare to original for claim in new_store.claims() { @@ -5789,13 +5534,12 @@ pub mod tests { #[test] fn test_heif() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.heif"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -5811,8 +5555,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -5820,15 +5563,8 @@ pub mod tests { // read from new stream output_stream.rewind().unwrap(); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); // dump store and compare to original for claim in new_store.claims() { @@ -5860,16 +5596,10 @@ pub mod tests { #[test] #[ignore = "This is not generating the expected error. Needs investigation."] fn test_manifest_bad_sig() { + let context = Context::new(); let (format, mut input_stream, _output_stream) = create_test_streams("CIE-sig-CA.jpg"); let tracker = &mut StatusTracker::default(); - let result = Store::from_stream( - format, - &mut input_stream, - true, - tracker, - &SyncGenericResolver::new(), - &Settings::default(), - ); + let result = Store::from_stream(format, &mut input_stream, tracker, &context); assert!(result.is_ok()); println!("Error report: {tracker:?}"); assert!(tracker.has_error(Error::AssertionInvalidRedaction)); @@ -5877,17 +5607,11 @@ pub mod tests { #[test] fn test_unsupported_type_without_external_manifest() { + let context = Context::new(); let (format, mut input_stream, _output_stream) = create_test_streams("unsupported_type.txt"); let mut report = StatusTracker::default(); - let result = Store::from_stream( - format, - &mut input_stream, - true, - &mut report, - &SyncGenericResolver::new(), - &Settings::default(), - ); + let result = Store::from_stream(format, &mut input_stream, &mut report, &context); assert!(matches!(result, Err(Error::UnsupportedType))); println!("Error report: {report:?}"); assert!(!report.logged_items().is_empty()); @@ -5900,14 +5624,7 @@ pub mod tests { // test bad jumbf let (format, mut input_stream, _output_stream) = create_test_streams("prerelease.jpg"); let mut report = StatusTracker::default(); - let _r = Store::from_stream( - format, - &mut input_stream, - true, - &mut report, - &SyncGenericResolver::new(), - &Settings::default(), - ); + let _r = Store::from_stream(format, &mut input_stream, &mut report, &Context::new()); // error report println!("Error report: {report:?}"); @@ -5921,15 +5638,7 @@ pub mod tests { // test bad jumbf let (format, mut input_stream, _output_stream) = create_test_streams("XCA.jpg"); let mut report = StatusTracker::default(); - Store::from_stream( - format, - &mut input_stream, - true, - &mut report, - &SyncGenericResolver::new(), - &Settings::default(), - ) - .unwrap(); + Store::from_stream(format, &mut input_stream, &mut report, &Context::new()).unwrap(); // error report println!("Error report: {report:?}"); @@ -5961,14 +5670,7 @@ pub mod tests { fn test_old_manifest() { let (format, mut input_stream, _output_stream) = create_test_streams("prerelease.jpg"); let mut report = StatusTracker::default(); - let _r = Store::from_stream( - format, - &mut input_stream, - true, - &mut report, - &SyncGenericResolver::new(), - &Settings::default(), - ); + let _r = Store::from_stream(format, &mut input_stream, &mut report, &Context::new()); println!("Error report: {report:?}"); @@ -5986,8 +5688,7 @@ pub mod tests { fn test_verifiable_credentials() { use crate::utils::test::create_test_store_v1; - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg"); @@ -6004,8 +5705,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6014,10 +5714,8 @@ pub mod tests { let restored_store = Store::from_stream( format, &mut output_stream, - true, &mut StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6038,8 +5736,7 @@ pub mod tests { fn test_data_box_creation() { use crate::utils::test::create_test_store_v1; - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg"); @@ -6056,8 +5753,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6066,10 +5762,8 @@ pub mod tests { let restored_store = Store::from_stream( format, &mut output_stream, - true, &mut StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6105,14 +5799,7 @@ pub mod tests { let mut patched_stream = std::io::Cursor::new(data); let mut report = StatusTracker::default(); - let _r = Store::from_stream( - format, - &mut patched_stream, - true, - &mut report, - &SyncGenericResolver::new(), - &Settings::default(), - ); // errs are in report + let _r = Store::from_stream(format, &mut patched_stream, &mut report, &Context::new()); // errs are in report println!("report: {report:?}"); report } @@ -6121,8 +5808,7 @@ pub mod tests { fn test_update_manifest_v1() { use crate::{hashed_uri::HashedUri, utils::test::create_test_store_v1}; - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg"); @@ -6138,23 +5824,15 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError); // read back in output_stream.rewind().unwrap(); - let restored_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let restored_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); let pc = restored_store.provenance_claim().unwrap(); // should be a regular manifest @@ -6167,7 +5845,7 @@ pub mod tests { &mut claim, &load_jumbf_from_stream(format, &mut output_stream).unwrap(), None, - &settings, + &context, ) .unwrap(); @@ -6193,22 +5871,14 @@ pub mod tests { &mut output_stream, &mut output_stream2, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); // read back in store with update manifest output_stream2.rewind().unwrap(); - let um_store = Store::from_stream( - format, - &mut output_stream2, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let um_store = + Store::from_stream(format, &mut output_stream2, &mut report, &context).unwrap(); let um = um_store.provenance_claim().unwrap(); @@ -6227,8 +5897,7 @@ pub mod tests { utils::test::create_test_store_v1, ClaimGeneratorInfo, ValidationResults, }; - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let signer = test_signer(SigningAlg::Ps256); @@ -6246,8 +5915,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6256,15 +5924,8 @@ pub mod tests { output_stream.rewind().unwrap(); let ingredient_vec = output_stream.get_ref().clone(); let mut ingredient_stream = Cursor::new(ingredient_vec); - let restored_store = Store::from_stream( - format, - &mut ingredient_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let restored_store = + Store::from_stream(format, &mut ingredient_stream, &mut report, &context).unwrap(); let pc = restored_store.provenance_claim().unwrap(); // should be a regular manifest @@ -6277,15 +5938,10 @@ pub mod tests { claim.add_claim_generator_info(cgi); ingredient_stream.rewind().unwrap(); - let (manifest_bytes, _) = Store::load_jumbf_from_stream( - format, - &mut ingredient_stream, - &http_resolver, - &settings, - ) - .unwrap(); + let (manifest_bytes, _) = + Store::load_jumbf_from_stream(format, &mut ingredient_stream, &context).unwrap(); let mut new_store = - Store::load_ingredient_to_claim(&mut claim, &manifest_bytes, None, &settings).unwrap(); + Store::load_ingredient_to_claim(&mut claim, &manifest_bytes, None, &context).unwrap(); let ingredient_hashes = new_store.get_manifest_box_hashes(pc); let parent_hashed_uri = HashedUri::new( @@ -6346,8 +6002,7 @@ pub mod tests { &mut output_stream, &mut output_stream2, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6355,15 +6010,8 @@ pub mod tests { // read back in store with update manifest output_stream2.rewind().unwrap(); - let um_store = Store::from_stream( - format, - &mut output_stream2, - true, - &mut um_report, - &http_resolver, - &settings, - ) - .unwrap(); + let um_store = + Store::from_stream(format, &mut output_stream2, &mut um_report, &context).unwrap(); let um = um_store.provenance_claim().unwrap(); @@ -6382,8 +6030,7 @@ pub mod tests { ValidationResults, }; - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let signer = test_signer(SigningAlg::Ps256); @@ -6393,15 +6040,8 @@ pub mod tests { let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError); // read in the store - let mut store = Store::from_stream( - format, - &mut input_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let mut store = + Store::from_stream(format, &mut input_stream, &mut report, &context).unwrap(); let pc = store.provenance_claim().unwrap(); // create a new update manifest @@ -6458,8 +6098,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6467,15 +6106,8 @@ pub mod tests { // read back in store with update manifest output_stream.rewind().unwrap(); - let mut um_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut um_report, - &http_resolver, - &settings, - ) - .unwrap(); + let mut um_store = + Store::from_stream(format, &mut output_stream, &mut um_report, &context).unwrap(); let um = um_store.provenance_claim().unwrap(); @@ -6545,8 +6177,7 @@ pub mod tests { &mut output_stream, &mut output_stream2, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6555,15 +6186,9 @@ pub mod tests { // read back in store with update manifest output_stream2.rewind().unwrap(); - let collapsed_store = Store::from_stream( - format, - &mut output_stream2, - true, - &mut collapsed_report, - &http_resolver, - &settings, - ) - .unwrap(); + let collapsed_store = + Store::from_stream(format, &mut output_stream2, &mut collapsed_report, &context) + .unwrap(); let cm = collapsed_store.provenance_claim().unwrap(); assert!(!cm.update_manifest()); @@ -6579,15 +6204,8 @@ pub mod tests { let (format, mut input_stream, _output_stream) = create_test_streams("update_manifest.jpg"); let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError); - let restored_store = Store::from_stream( - format, - &mut input_stream, - true, - &mut report, - &SyncGenericResolver::new(), - &Settings::default(), - ) - .unwrap(); + let restored_store = + Store::from_stream(format, &mut input_stream, &mut report, &Context::new()).unwrap(); let pc = restored_store.provenance_claim().unwrap(); // should be an update manifest @@ -6601,8 +6219,7 @@ pub mod tests { utils::test::create_test_store_v1, ClaimGeneratorInfo, ValidationResults, }; - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let signer = test_signer(SigningAlg::Ps256); @@ -6620,8 +6237,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6630,15 +6246,8 @@ pub mod tests { output_stream.rewind().unwrap(); let ingredient_vec = output_stream.get_ref().clone(); let mut ingredient_stream = Cursor::new(ingredient_vec.clone()); - let restored_store = Store::from_stream( - format, - &mut ingredient_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let restored_store = + Store::from_stream(format, &mut ingredient_stream, &mut report, &context).unwrap(); let pc = restored_store.provenance_claim().unwrap(); // should be a regular manifest @@ -6656,15 +6265,14 @@ pub mod tests { let (manifest_bytes, _) = Store::load_jumbf_from_stream( format, &mut Cursor::new(ingredient_vec.clone()), - &http_resolver, - &settings, + &context, ) .unwrap(); let mut redacted_store = Store::load_ingredient_to_claim( &mut claim, &manifest_bytes, Some(vec![redacted_uri]), - &settings, + &context, ) .unwrap(); @@ -6718,8 +6326,7 @@ pub mod tests { &mut output_stream, &mut output_stream2, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6727,15 +6334,8 @@ pub mod tests { // read back in store with update manifest output_stream2.rewind().unwrap(); - let um_store = Store::from_stream( - format, - &mut output_stream2, - true, - &mut um_report, - &http_resolver, - &settings, - ) - .unwrap(); + let um_store = + Store::from_stream(format, &mut output_stream2, &mut um_report, &context).unwrap(); let um = um_store.provenance_claim().unwrap(); @@ -6754,24 +6354,19 @@ pub mod tests { // load ingredient with redaction output_stream2.rewind().unwrap(); let (redacted_manifest_bytes, _) = - Store::load_jumbf_from_stream(format, &mut output_stream2, &http_resolver, &settings) - .unwrap(); - Store::load_ingredient_to_claim(&mut new_claim, &redacted_manifest_bytes, None, &settings) + Store::load_jumbf_from_stream(format, &mut output_stream2, &context).unwrap(); + Store::load_ingredient_to_claim(&mut new_claim, &redacted_manifest_bytes, None, &context) .unwrap(); // load original ingredient without redaction - let (original_manifest_bytes, _) = Store::load_jumbf_from_stream( - format, - &mut Cursor::new(ingredient_vec), - &http_resolver, - &settings, - ) - .unwrap(); + let (original_manifest_bytes, _) = + Store::load_jumbf_from_stream(format, &mut Cursor::new(ingredient_vec), &context) + .unwrap(); let _conflict_store = Store::load_ingredient_to_claim( &mut new_claim, &original_manifest_bytes, None, - &settings, + &context, ) .unwrap(); @@ -6789,8 +6384,7 @@ pub mod tests { utils::test::create_test_store_v1, ClaimGeneratorInfo, ValidationResults, }; - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = Context::new(); let signer = test_signer(SigningAlg::Ps256); @@ -6808,8 +6402,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6818,15 +6411,8 @@ pub mod tests { output_stream.rewind().unwrap(); let ingredient_vec = output_stream.get_ref().clone(); let mut ingredient_stream = Cursor::new(ingredient_vec.clone()); - let restored_store = Store::from_stream( - format, - &mut ingredient_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let restored_store = + Store::from_stream(format, &mut ingredient_stream, &mut report, &context).unwrap(); let pc = restored_store.provenance_claim().unwrap(); @@ -6845,8 +6431,7 @@ pub mod tests { let (manifest_bytes, _) = Store::load_jumbf_from_stream( format, &mut Cursor::new(ingredient_vec.clone()), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6854,7 +6439,7 @@ pub mod tests { &mut claim, &manifest_bytes, Some(vec![redacted_uri]), - &settings, + &context, ) .unwrap(); @@ -6908,8 +6493,7 @@ pub mod tests { &mut output_stream, &mut output_stream2, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -6917,15 +6501,8 @@ pub mod tests { // read back in store with update manifest output_stream2.rewind().unwrap(); - let um_store = Store::from_stream( - format, - &mut output_stream2, - true, - &mut um_report, - &http_resolver, - &settings, - ) - .unwrap(); + let um_store = + Store::from_stream(format, &mut output_stream2, &mut um_report, &context).unwrap(); let um = um_store.provenance_claim().unwrap(); @@ -6942,14 +6519,10 @@ pub mod tests { new_claim.add_claim_generator_info(cgi); // load original ingredient without redaction - let (original_manifest_bytes, _) = Store::load_jumbf_from_stream( - format, - &mut Cursor::new(ingredient_vec), - &http_resolver, - &settings, - ) - .unwrap(); - Store::load_ingredient_to_claim(&mut new_claim, &original_manifest_bytes, None, &settings) + let (original_manifest_bytes, _) = + Store::load_jumbf_from_stream(format, &mut Cursor::new(ingredient_vec), &context) + .unwrap(); + Store::load_ingredient_to_claim(&mut new_claim, &original_manifest_bytes, None, &context) .unwrap(); // the confict_store is adjusted to remove the conflicting claim @@ -6962,9 +6535,8 @@ pub mod tests { output_stream2.rewind().unwrap(); let (redacted_manifest_bytes, _) = - Store::load_jumbf_from_stream(format, &mut output_stream2, &http_resolver, &settings) - .unwrap(); - Store::load_ingredient_to_claim(&mut new_claim, &redacted_manifest_bytes, None, &settings) + Store::load_jumbf_from_stream(format, &mut output_stream2, &context).unwrap(); + Store::load_ingredient_to_claim(&mut new_claim, &redacted_manifest_bytes, None, &context) .unwrap(); // the confict_store is adjusted to remove the conflicting claim @@ -6982,8 +6554,7 @@ pub mod tests { utils::test::create_test_store_v1, ClaimGeneratorInfo, ValidationResults, }; - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = Context::new(); let signer = test_signer(SigningAlg::Ps256); @@ -7001,23 +6572,15 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError); // read back in output_stream.rewind().unwrap(); - let restored_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let restored_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); let pc = restored_store.provenance_claim().unwrap(); // should be a regular manifest @@ -7038,7 +6601,7 @@ pub mod tests { &mut claim, &ingredient_vec, Some(vec![redacted_uri]), - &settings, + &context, ) .unwrap(); @@ -7092,8 +6655,7 @@ pub mod tests { &mut output_stream, &mut op_output, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -7101,15 +6663,8 @@ pub mod tests { // read back in store with update manifest op_output.rewind().unwrap(); - let um_store = Store::from_stream( - format, - &mut op_output, - true, - &mut um_report, - &http_resolver, - &settings, - ) - .unwrap(); + let um_store = + Store::from_stream(format, &mut op_output, &mut um_report, &context).unwrap(); let um = um_store.provenance_claim().unwrap(); @@ -7131,7 +6686,7 @@ pub mod tests { &mut claim2, &ingredient_vec, Some(vec![redacted_uri2]), - &settings, + &context, ) .unwrap(); @@ -7185,8 +6740,7 @@ pub mod tests { &mut output_stream, &mut op2_output, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -7202,7 +6756,7 @@ pub mod tests { &mut new_claim, &load_jumbf_from_stream(format, &mut op_output).unwrap(), None, - &settings, + &context, ) .unwrap(); @@ -7212,7 +6766,7 @@ pub mod tests { &mut new_claim, &load_jumbf_from_stream(format, &mut op2_output).unwrap(), None, - &settings, + &context, ) .unwrap(); @@ -7276,18 +6830,12 @@ pub mod tests { #[test] fn test_display() { + let context = Context::new(); let (format, mut input_stream, _output_stream) = create_test_streams("CA.jpg"); let mut report = StatusTracker::default(); - let store = Store::from_stream( - format, - &mut input_stream, - true, - &mut report, - &SyncGenericResolver::new(), - &Settings::default(), - ) - .expect("from_stream"); + let store = Store::from_stream(format, &mut input_stream, &mut report, &context) + .expect("from_stream"); assert!(!report.has_any_error()); println!("store = {store}"); @@ -7297,14 +6845,7 @@ pub mod tests { fn test_no_alg() { let (format, mut input_stream, _output_stream) = create_test_streams("no_alg.jpg"); let mut report = StatusTracker::default(); - let _store = Store::from_stream( - format, - &mut input_stream, - true, - &mut report, - &SyncGenericResolver::new(), - &Settings::default(), - ); + let _store = Store::from_stream(format, &mut input_stream, &mut report, &Context::new()); assert!(report.has_status(ALGORITHM_UNSUPPORTED)); } @@ -7328,37 +6869,26 @@ pub mod tests { #[test] fn test_legacy_ingredient_hash() { + let context = Context::new(); // test 1.0 ingredient hash - let (format, mut input_stream, _output_stream) = + let (format, input_stream, _output_stream) = create_test_streams("legacy_ingredient_hash.jpg"); let mut report = StatusTracker::default(); - let store = Store::from_stream( - format, - &mut input_stream, - true, - &mut report, - &SyncGenericResolver::new(), - &Settings::default(), - ) - .expect("from_stream"); + let store = + Store::from_stream(format, input_stream, &mut report, &context).expect("from_stream"); println!("store = {store}"); } #[test] #[ignore = "this test is not generating the expected errors - the test image cert has expired"] fn test_bmff_legacy() { - crate::settings::set_settings_value("verify.verify_trust", false).unwrap(); + let mut settings = Settings::default(); + settings.verify.verify_trust = false; + let context = Context::new().with_settings(settings); let (format, mut input_stream, _output_stream) = create_test_streams("legacy.mp4"); // test 1.0 bmff hash let mut report = StatusTracker::default(); - let store = Store::from_stream( - format, - &mut input_stream, - true, - &mut report, - &SyncGenericResolver::new(), - &Settings::default(), - ); + let store = Store::from_stream(format, &mut input_stream, &mut report, &context); println!("store = {report:#?}"); // expect action error assert!(store.is_err()); @@ -7370,6 +6900,7 @@ pub mod tests { #[test] fn test_bmff_fragments() { + let context = Context::new(); let init_stream_path = fixture_path("dashinit.mp4"); let segment_stream_path = fixture_path("dash1.m4s"); @@ -7384,8 +6915,7 @@ pub mod tests { init_stream, segment_stream, &mut report, - &SyncGenericResolver::new(), - &Settings::default(), + &context, ) .expect("load_from_asset"); println!("store = {store}"); @@ -7393,14 +6923,13 @@ pub mod tests { #[test] fn test_bmff_jumbf_generation() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); // test adding to actual image let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -7415,8 +6944,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -7424,15 +6952,8 @@ pub mod tests { // can we read back in output_stream.set_position(0); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); assert!(!report.has_any_error()); @@ -7441,14 +6962,13 @@ pub mod tests { #[test] fn test_bmff_jumbf_generation_claim_v1() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); // test adding to actual image let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = crate::utils::test::create_test_claim_v1().unwrap(); @@ -7463,8 +6983,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -7472,15 +6991,8 @@ pub mod tests { // can we read back in output_stream.set_position(0); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); assert!(!report.has_any_error()); @@ -7489,8 +7001,7 @@ pub mod tests { #[test] fn test_jumbf_generation_with_bmffv3_fixed_block_size() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); // test adding to actual image let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4"); @@ -7499,7 +7010,7 @@ pub mod tests { crate::settings::set_settings_value("core.merkle_tree_chunk_size_in_kb", 1).unwrap(); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -7514,8 +7025,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -7523,15 +7033,8 @@ pub mod tests { // can we read back in output_stream.set_position(0); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); assert!(!report.has_any_error()); @@ -7540,8 +7043,7 @@ pub mod tests { #[test] fn test_jumbf_generation_with_bmffv3_fixed_block_size_no_proof() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4"); @@ -7550,7 +7052,7 @@ pub mod tests { crate::settings::set_settings_value("core.merkle_tree_max_proofs", 0).unwrap(); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -7565,8 +7067,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -7574,15 +7075,8 @@ pub mod tests { // can we read back in output_stream.set_position(0); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); assert!(!report.has_any_error()); @@ -7591,8 +7085,7 @@ pub mod tests { #[test] fn test_jumbf_generation_with_bmffv3_fixed_block_size_stream() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); // test adding to actual image let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4"); @@ -7601,7 +7094,7 @@ pub mod tests { crate::settings::set_settings_value("core.merkle_tree_chunk_size_in_kb", 1).unwrap(); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -7616,8 +7109,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -7625,15 +7117,8 @@ pub mod tests { // can we read back in output_stream.set_position(0); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); assert!(!report.has_any_error()); @@ -7642,14 +7127,15 @@ pub mod tests { #[test] fn test_bmff_jumbf_stream_generation() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let mut settings = Settings::default(); + settings.verify.verify_after_reading = false; + let context = crate::context::Context::new().with_settings(settings); // test adding to actual image let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -7665,8 +7151,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -7675,22 +7160,19 @@ pub mod tests { output_stream.set_position(0); let (manifest_bytes, _) = - Store::load_jumbf_from_stream(format, &mut input_stream, &http_resolver, &settings) - .unwrap(); + Store::load_jumbf_from_stream(format, &mut input_stream, &context).unwrap(); let _new_store = { Store::from_manifest_data_and_stream( &manifest_bytes, format, &mut output_stream, - false, &mut report, - &http_resolver, - &settings, + &context, ) .unwrap() }; - println!("report = {report:?}"); + println!("report = {report:#?}"); assert!(!report.has_any_error()); } @@ -7698,19 +7180,13 @@ pub mod tests { #[cfg(not(target_arch = "wasm32"))] // TODO: Can run on Wasm once https://github.com/contentauth/c2pa-rs/pull/1325 lands fn test_removed_jumbf() { + let context = Context::new(); let (format, mut input_stream, _output_stream) = create_test_streams("no_manifest.jpg"); let mut report = StatusTracker::default(); // can we read back in - let result = Store::from_stream( - format, - &mut input_stream, - true, - &mut report, - &SyncGenericResolver::new(), - &Settings::default(), - ); + let result = Store::from_stream(format, &mut input_stream, &mut report, &context); assert!(result.is_err()); assert!(matches!(result, Err(Error::JumbfNotFound))); @@ -7762,11 +7238,10 @@ pub mod tests { use crate::utils::test::{run_file_test, TestFileSetup}; run_file_test(file_name, |setup: &TestFileSetup| { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let mut claim = create_test_claim().unwrap(); @@ -7788,8 +7263,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -7813,10 +7287,8 @@ pub mod tests { &saved_manifest, format, &mut validation_stream, - true, &mut validation_log, - &http_resolver, - &settings, + &context, ) .unwrap(); }); @@ -7839,14 +7311,13 @@ pub mod tests { #[test] fn test_user_guid_external_manifest_embedded() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); // Create test streams from fixture let (format, mut input_stream, mut output_stream) = create_test_streams("libpng-test.png"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let mut claim = create_test_claim().unwrap(); @@ -7871,8 +7342,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -7896,25 +7366,22 @@ pub mod tests { &saved_manifest, format, &mut output_stream, - true, &mut validation_log, - &http_resolver, - &settings, + &context, ) .unwrap(); } #[c2pa_test_async] async fn test_jumbf_generation_stream() { - let settings = Settings::default(); - let http_resolver = AsyncGenericResolver::new(); + let context = crate::context::Context::new(); let file_buffer = include_bytes!("../tests/fixtures/earth_apollo17.jpg").to_vec(); // convert buffer to cursor with Read/Write/Seek capability let mut buf_io = Cursor::new(file_buffer); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -7932,8 +7399,7 @@ pub mod tests { &mut buf_io, &mut result_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .await .unwrap(); @@ -7943,16 +7409,10 @@ pub mod tests { // make sure we can read from new file let mut report = StatusTracker::default(); - let _new_store = Store::from_stream_async( - "image/jpeg", - &mut result_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .await - .unwrap(); + let _new_store = + Store::from_stream_async("image/jpeg", &mut result_stream, &mut report, &context) + .await + .unwrap(); assert!(!report.has_any_error()); // std::fs::write("target/test.jpg", result).unwrap(); @@ -7960,14 +7420,13 @@ pub mod tests { #[test] fn test_tiff_jumbf_generation() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); // Create test streams from fixture let (format, mut input_stream, mut output_stream) = create_test_streams("TUSCANY.TIF"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -7983,8 +7442,7 @@ pub mod tests { &mut input_stream, &mut output_stream, signer.as_ref(), - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -7994,15 +7452,8 @@ pub mod tests { // read from new file output_stream.rewind().unwrap(); - let new_store = Store::from_stream( - format, - &mut output_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap(); assert!(!report.has_any_error()); @@ -8037,15 +7488,14 @@ pub mod tests { #[c2pa_test_async] #[cfg(feature = "file_io")] async fn test_boxhash_embeddable_manifest_async() { - let settings = Settings::default(); - let http_resolver = AsyncGenericResolver::new(); + let context = crate::context::Context::new(); // test adding to actual image let ap = fixture_path("boxhash.jpg"); let box_hash_path = fixture_path("boxhash.json"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let mut claim = create_test_claim().unwrap(); @@ -8063,7 +7513,7 @@ pub mod tests { // get the embeddable manifest let em = store - .get_box_hashed_embeddable_manifest_async(&signer, &settings) + .get_box_hashed_embeddable_manifest_async(&signer, &context) .await .unwrap(); @@ -8103,16 +7553,10 @@ pub mod tests { out_stream.rewind().unwrap(); let mut report = StatusTracker::default(); - let _new_store = Store::from_stream_async( - "image/jpeg", - &mut out_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .await - .unwrap(); + let _new_store = + Store::from_stream_async("image/jpeg", &mut out_stream, &mut report, &context) + .await + .unwrap(); assert!(!report.has_any_error()); } @@ -8120,15 +7564,14 @@ pub mod tests { #[test] #[cfg(feature = "file_io")] fn test_boxhash_embeddable_manifest() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); // test adding to actual image let ap = fixture_path("boxhash.jpg"); let box_hash_path = fixture_path("boxhash.json"); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let mut claim = create_test_claim().unwrap(); @@ -8146,7 +7589,7 @@ pub mod tests { // get the embeddable manifest let em = store - .get_box_hashed_embeddable_manifest(signer.as_ref(), &settings) + .get_box_hashed_embeddable_manifest(signer.as_ref(), &context) .unwrap(); // get composed version for embedding to JPEG @@ -8185,15 +7628,8 @@ pub mod tests { out_stream.rewind().unwrap(); let mut report = StatusTracker::default(); - let _new_store = Store::from_stream( - "image/jpeg", - &mut out_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let _new_store = + Store::from_stream("image/jpeg", &mut out_stream, &mut report, &context).unwrap(); assert!(!report.has_any_error()); } @@ -8201,8 +7637,7 @@ pub mod tests { #[c2pa_test_async] #[cfg(feature = "file_io")] async fn test_datahash_embeddable_manifest_async() { - let settings = Settings::default(); - let http_resolver = AsyncGenericResolver::new(); + let context = crate::context::Context::new(); // test adding to actual image use std::io::SeekFrom; @@ -8213,7 +7648,7 @@ pub mod tests { let signer = async_test_signer(SigningAlg::Ps256); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim = create_test_claim().unwrap(); @@ -8256,7 +7691,7 @@ pub mod tests { &signer, "jpeg", Some(&mut output_file), - &settings, + &context, ) .await .unwrap(); @@ -8267,16 +7702,10 @@ pub mod tests { output_file.rewind().unwrap(); let mut report = StatusTracker::default(); - let _new_store = Store::from_stream_async( - "image/jpeg", - &mut output_file, - true, - &mut report, - &http_resolver, - &settings, - ) - .await - .unwrap(); + let _new_store = + Store::from_stream_async("image/jpeg", &mut output_file, &mut report, &context) + .await + .unwrap(); assert!(!report.has_any_error()); } @@ -8284,8 +7713,7 @@ pub mod tests { #[test] #[cfg(feature = "file_io")] fn test_datahash_embeddable_manifest() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); // test adding to actual image @@ -8296,7 +7724,7 @@ pub mod tests { let signer = test_signer(SigningAlg::Ps256); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim = create_test_claim().unwrap(); @@ -8339,7 +7767,7 @@ pub mod tests { signer.as_ref(), "jpeg", Some(&mut output_file), - &settings, + &context, ) .unwrap(); @@ -8349,15 +7777,8 @@ pub mod tests { output_file.rewind().unwrap(); let mut report = StatusTracker::default(); - let _new_store = Store::from_stream( - "image/jpeg", - &mut output_file, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let _new_store = + Store::from_stream("image/jpeg", &mut output_file, &mut report, &context).unwrap(); assert!(!report.has_any_error()); } @@ -8365,8 +7786,7 @@ pub mod tests { #[test] #[cfg(feature = "file_io")] fn test_datahash_embeddable_manifest_user_hashed() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); use std::io::SeekFrom; @@ -8381,7 +7801,7 @@ pub mod tests { let signer = test_signer(SigningAlg::Ps256); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim = create_test_claim().unwrap(); @@ -8420,7 +7840,7 @@ pub mod tests { // get the embeddable manifest, using user hashing let cm = store - .get_data_hashed_embeddable_manifest(&dh, signer.as_ref(), "jpeg", None, &settings) + .get_data_hashed_embeddable_manifest(&dh, signer.as_ref(), "jpeg", None, &context) .unwrap(); // path in new composed manifest @@ -8429,23 +7849,15 @@ pub mod tests { output_file.rewind().unwrap(); let mut report = StatusTracker::default(); - let _new_store = Store::from_stream( - "image/jpeg", - &mut output_file, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let _new_store = + Store::from_stream("image/jpeg", &mut output_file, &mut report, &context).unwrap(); assert!(!report.has_any_error()); } #[test] fn test_dynamic_assertions() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); #[derive(Serialize)] struct TestAssertion { @@ -8538,7 +7950,7 @@ pub mod tests { let mut buf_io = Cursor::new(file_buffer); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -8551,14 +7963,7 @@ pub mod tests { let mut result_stream = Cursor::new(result); store - .save_to_stream( - "jpeg", - &mut buf_io, - &mut result_stream, - &signer, - &http_resolver, - &settings, - ) + .save_to_stream("jpeg", &mut buf_io, &mut result_stream, &signer, &context) .unwrap(); // rewind the result stream to read from it @@ -8566,15 +7971,8 @@ pub mod tests { // make sure we can read from new file let mut report = StatusTracker::default(); - let new_store = Store::from_stream( - "image/jpeg", - &mut result_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .unwrap(); + let new_store = + Store::from_stream("image/jpeg", &mut result_stream, &mut report, &context).unwrap(); println!("new_store: {new_store}"); @@ -8586,8 +7984,7 @@ pub mod tests { async fn test_async_dynamic_assertions() { use async_trait::async_trait; - let settings = Settings::default(); - let http_resolver = AsyncGenericResolver::new(); + let context = crate::context::Context::new(); #[derive(Serialize)] struct TestAssertion { @@ -8684,7 +8081,7 @@ pub mod tests { let mut buf_io = Cursor::new(file_buffer); // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim1 = create_test_claim().unwrap(); @@ -8697,14 +8094,7 @@ pub mod tests { let mut result_stream = Cursor::new(result); store - .save_to_stream_async( - "jpeg", - &mut buf_io, - &mut result_stream, - &signer, - &http_resolver, - &settings, - ) + .save_to_stream_async("jpeg", &mut buf_io, &mut result_stream, &signer, &context) .await .unwrap(); @@ -8712,16 +8102,9 @@ pub mod tests { // make sure we can read from new file let mut report = StatusTracker::default(); - let new_store = Store::from_stream_async( - "jpeg", - &mut result_stream, - true, - &mut report, - &http_resolver, - &settings, - ) - .await - .unwrap(); + let new_store = Store::from_stream_async("jpeg", &mut result_stream, &mut report, &context) + .await + .unwrap(); println!("new_store: {new_store}"); @@ -8731,8 +8114,7 @@ pub mod tests { &new_store, &mut ClaimAssetData::Bytes(&result, "jpg"), &mut report, - &http_resolver, - &settings, + &context, ) .await .unwrap(); @@ -8744,8 +8126,9 @@ pub mod tests { #[test] #[cfg(feature = "file_io")] fn test_fragmented_jumbf_generation() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let mut settings = crate::settings::Settings::default(); + settings.verify.verify_after_reading = false; + let context = crate::context::Context::new().with_settings(settings); // test adding to actual image @@ -8772,7 +8155,7 @@ pub mod tests { } // Create claims store. - let mut store = Store::with_settings(&settings); + let mut store = Store::with_context(&context); // Create a new claim. let claim = create_test_claim().unwrap(); @@ -8792,7 +8175,7 @@ pub mod tests { &fragments, new_output_path.as_path(), signer.as_ref(), - &settings, + &context, ) .unwrap(); @@ -8811,8 +8194,7 @@ pub mod tests { &mut init_stream, &mut fragment_stream, &mut validation_log, - &http_resolver, - &settings, + &context, ) .unwrap(); init_stream.seek(std::io::SeekFrom::Start(0)).unwrap(); @@ -8825,16 +8207,13 @@ pub mod tests { output_fragments.push(new_output_path.join(entry.file_name().unwrap())); } - //let mut reader = Cursor::new(init_stream); let mut validation_log = StatusTracker::default(); let _manifest = Store::load_from_file_and_fragments( "mp4", &mut init_stream, &output_fragments, - false, &mut validation_log, - &http_resolver, - &settings, + &context, ) .unwrap(); @@ -8875,8 +8254,7 @@ pub mod tests { #[test] /// Test that we can we load a store from JUMBF and then convert it back to the identical JUMBF. fn test_from_and_to_jumbf() { - let settings = Settings::default(); - let http_resolver = SyncGenericResolver::new(); + let context = crate::context::Context::new(); // test adding to actual image let ap = fixture_path("C.jpg"); @@ -8885,12 +8263,12 @@ pub mod tests { let format = "image/jpeg"; let (manifest_bytes, _remote_url) = - Store::load_jumbf_from_stream(format, &mut stream, &http_resolver, &settings).unwrap(); + Store::load_jumbf_from_stream(format, &mut stream, &context).unwrap(); - let store = Store::from_jumbf_with_settings( + let store = Store::from_jumbf_with_context( &manifest_bytes, &mut StatusTracker::default(), - &settings, + &context, ) .unwrap(); @@ -8903,6 +8281,7 @@ pub mod tests { #[c2pa_test_async] async fn test_store_load_fragment_from_stream_async() { + let context = crate::context::Context::new(); // Use the dash fixtures that are known to work with fragment loading // These are the same files used in test_bmff_fragments let init_segment = include_bytes!("../tests/fixtures/dashinit.mp4"); @@ -8920,8 +8299,7 @@ pub mod tests { &mut init_stream, &mut fragment_stream, &mut validation_log, - &AsyncGenericResolver::new(), - &Settings::default(), + &context, ) .await; diff --git a/sdk/src/utils/test.rs b/sdk/src/utils/test.rs index 7f11bd743..bc885aed4 100644 --- a/sdk/src/utils/test.rs +++ b/sdk/src/utils/test.rs @@ -32,11 +32,11 @@ use crate::{ }, asset_io::CAIReadWrite, claim::Claim, + context::Context, crypto::{cose::CertificateTrustPolicy, raw_signature::SigningAlg}, hash_utils::Hasher, jumbf_io::get_assetio_handler, resource_store::UriOrResource, - settings::Settings, store::Store, utils::{io_utils::tempdirectory, mime::extension_to_mime}, AsyncSigner, ClaimGeneratorInfo, Result, @@ -327,7 +327,7 @@ pub fn create_test_claim_v1() -> Result { /// Creates a store with an unsigned claim for testing pub fn create_test_store() -> Result { // Create claims store. - let mut store = Store::with_settings(&Settings::default()); + let mut store = Store::with_context(&Context::new()); let claim = create_test_claim()?; store.commit_claim(claim).unwrap(); @@ -337,7 +337,7 @@ pub fn create_test_store() -> Result { /// Creates a store with an unsigned v1 claim for testing pub fn create_test_store_v1() -> Result { // Create claims store. - let mut store = Store::with_settings(&Settings::default()); + let mut store = Store::with_context(&Context::new()); let claim = create_test_claim_v1()?; store.commit_claim(claim).unwrap(); From cc52157ac6743a9c32c466337f6fa4e514f33b5a Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Fri, 14 Nov 2025 22:52:31 -0800 Subject: [PATCH 17/26] Builder can be created with context and will use it internally added Builder::from_context() builder.with_json() --- sdk/src/builder.rs | 157 ++++++++++-------- sdk/src/claim.rs | 7 +- sdk/src/context.rs | 8 + .../identity/identity_assertion/assertion.rs | 10 +- sdk/src/ingredient.rs | 3 +- sdk/src/reader.rs | 4 +- sdk/src/store.rs | 13 +- 7 files changed, 115 insertions(+), 87 deletions(-) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index b18c86988..e22358a96 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -40,7 +40,6 @@ use crate::{ error::{Error, Result}, jumbf_io, resource_store::{ResourceRef, ResourceResolver, ResourceStore}, - settings::{self, Settings}, store::Store, utils::{hash_utils::hash_to_b64, mime::format_to_mime}, AsyncSigner, ClaimGeneratorInfo, HashRange, HashedUri, Ingredient, ManifestAssertionKind, @@ -333,9 +332,9 @@ pub struct Builder { #[serde(skip)] pub(crate) resources: ResourceStore, - // Contains the builder settings + // Contains the builder context #[serde(skip)] - settings: Settings, + context: Context, } impl AsRef for Builder { @@ -350,7 +349,19 @@ impl Builder { /// * A new [`Builder`]. pub fn new() -> Self { Self { - settings: settings::get_settings().unwrap_or_default(), + context: Context::new(), + ..Default::default() + } + } + + /// Creates a new [`Builder`] struct from a [`Context`]. + /// # Arguments + /// * `context` - The [`Context`] to use for this [`Builder`]. + /// # Returns + /// * A new [`Builder`]. + pub fn from_context(context: Context) -> Self { + Self { + context, ..Default::default() } } @@ -371,7 +382,7 @@ impl Builder { /// * A mutable reference to the [`Builder`]. #[allow(deprecated)] pub fn set_intent(&mut self, intent: BuilderIntent) -> &mut Self { - self.settings.builder.intent = Some(intent.clone()); + self.context.settings_mut().builder.intent = Some(intent.clone()); self.intent = Some(intent); self } @@ -382,7 +393,7 @@ impl Builder { pub fn intent(&self) -> Option { let mut intent = self.intent.clone(); if intent.is_none() { - intent = self.settings.builder.intent.clone(); + intent = self.context.settings().builder.intent.clone(); } intent } @@ -398,11 +409,25 @@ impl Builder { pub fn from_json(json: &str) -> Result { Ok(Self { definition: serde_json::from_str(json).map_err(Error::JsonError)?, - settings: settings::get_settings().unwrap_or_default(), + context: Context::new(), ..Default::default() }) } + /// Sets the [`ManifestDefinition`] for this [`Builder`] from a JSON string. + /// # Arguments + /// * `json` - A JSON string representing the [`ManifestDefinition`]. + /// # Returns + /// * A mutable reference to the [`Builder`]. + /// # Errors + /// * Returns an [`Error`] if the JSON is malformed or incorrect. + /// # Notes + /// * This will overwrite any existing definition in the [`Builder`]. + pub fn with_json(&mut self, json: &str) -> Result<&mut Self> { + self.definition = serde_json::from_str(json).map_err(Error::JsonError)?; + Ok(self) + } + /// Returns a [Vec] of mime types that [c2pa-rs] is able to sign. pub fn supported_mime_types() -> Vec { jumbf_io::supported_builder_mime_types() @@ -650,12 +675,11 @@ impl Builder { .ok_or(Error::IngredientNotFound); } - let context = Context::new(); let ingredient = if _sync { - ingredient.with_stream(format, stream, &context)? + ingredient.with_stream(format, stream, &self.context)? } else { ingredient - .with_stream_async(format, stream, &context) + .with_stream_async(format, stream, &self.context) .await? }; @@ -851,9 +875,8 @@ impl Builder { /// # Errors /// * Returns an [`Error`] if the archive cannot be written. pub fn to_archive(&mut self, mut stream: impl Write + Seek) -> Result<()> { - let context = Context::new(); - if let Some(true) = context.settings().builder.generate_c2pa_archive { - let c2pa_data = self.working_store_sign(&context)?; + if let Some(true) = self.context.settings().builder.generate_c2pa_archive { + let c2pa_data = self.working_store_sign()?; stream.write_all(&c2pa_data)?; } else { return self.old_to_archive(stream); @@ -881,9 +904,8 @@ impl Builder { // but we need to disable validation since we are not signing yet // so we will read the store directly here //crate::Reader::from_stream("application/c2pa", stream).and_then(|r| r.into_builder()) - let mut settings = settings::get_settings().unwrap_or_default(); - settings.verify.verify_after_reading = false; - let context = Context::new().with_settings(settings); + let mut context = Context::new(); + context.settings_mut().verify.verify_after_reading = false; let mut validation_log = crate::status_tracker::StatusTracker::default(); stream.rewind()?; // Ensure stream is at the start @@ -900,7 +922,7 @@ impl Builder { } // Convert a Manifest into a Claim - fn to_claim(&self, context: &Context) -> Result { + fn to_claim(&self) -> Result { // utility function to add created or gathered assertions fn add_assertion( claim: &mut Claim, @@ -919,7 +941,8 @@ impl Builder { // add the default claim generator info for this library if claim_generator_info.is_empty() { - let claim_generator_info_settings = &context.settings().builder.claim_generator_info; + let claim_generator_info_settings = + &self.context.settings().builder.claim_generator_info; match claim_generator_info_settings { Some(claim_generator_info_settings) => { claim_generator_info.push(claim_generator_info_settings.clone().try_into()?); @@ -1029,7 +1052,7 @@ impl Builder { &mut claim, definition.redactions.clone(), Some(&self.resources), - context, + &self.context, )?; if !id.is_empty() { ingredient_map.insert(id, (ingredient.relationship(), uri)); @@ -1196,10 +1219,11 @@ impl Builder { actions: &mut Actions, ) -> Result<()> { if actions.all_actions_included.is_none() { - actions.all_actions_included = self.settings.builder.actions.all_actions_included; + actions.all_actions_included = + self.context.settings().builder.actions.all_actions_included; } - let action_templates = &self.settings.builder.actions.templates; + let action_templates = &self.context.settings().builder.actions.templates; if let Some(action_templates) = action_templates { let action_templates = action_templates @@ -1214,7 +1238,7 @@ impl Builder { } } - let additional_actions = &self.settings.builder.actions.actions; + let additional_actions = &self.context.settings().builder.actions.actions; if let Some(additional_actions) = additional_actions { let additional_actions = additional_actions @@ -1244,9 +1268,10 @@ impl Builder { ingredient_map: &HashMap, actions: &mut Actions, ) -> Result<()> { + let settings = self.context.settings(); // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_mandatory_presence_of_at_least_one_actions_assertion - let auto_created = self.settings.builder.actions.auto_created_action.enabled; - let auto_opened = self.settings.builder.actions.auto_opened_action.enabled; + let auto_created = settings.builder.actions.auto_created_action.enabled; + let auto_opened = settings.builder.actions.auto_opened_action.enabled; if (self.intent().is_some() || auto_created || auto_opened) && !actions.actions.iter().any(|action| { @@ -1289,7 +1314,7 @@ impl Builder { let mut action = Action::new(c2pa_action::OPENED) .set_parameter("ingredients", vec![parent_uri])?; if let Some(source_type) = - &self.settings.builder.actions.auto_opened_action.source_type + &settings.builder.actions.auto_opened_action.source_type { action = action.set_source_type(source_type.clone()); } @@ -1299,12 +1324,8 @@ impl Builder { } } else if auto_created { let mut action = Action::new(c2pa_action::CREATED); - if let Some(source_type) = &self - .settings - .builder - .actions - .auto_created_action - .source_type + if let Some(source_type) = + &settings.builder.actions.auto_created_action.source_type { action = action.set_source_type(source_type.clone()); } @@ -1322,7 +1343,7 @@ impl Builder { } // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_relationship - if self.settings.builder.actions.auto_placed_action.enabled { + if settings.builder.actions.auto_placed_action.enabled { // Get a list of ingredient URIs referenced by "c2pa.placed" actions. let mut referenced_uris = HashSet::new(); for action in &actions.actions { @@ -1346,8 +1367,7 @@ impl Builder { let action = action.set_parameter("ingredients", vec![uri])?; - let action = match self.settings.builder.actions.auto_placed_action.source_type - { + let action = match settings.builder.actions.auto_placed_action.source_type { Some(ref source_type) => action.set_source_type(source_type.clone()), _ => action, }; @@ -1359,10 +1379,10 @@ impl Builder { } // Convert a Manifest into a Store - fn to_store(&self, context: &Context) -> Result { - let claim = self.to_claim(context)?; + fn to_store(&self) -> Result { + let claim = self.to_claim()?; - let mut store = Store::with_context(context); + let mut store = Store::with_context(&self.context); // if this can be an update manifest, then set the update_manifest flag if self.intent() == Some(BuilderIntent::Update) { @@ -1375,12 +1395,7 @@ impl Builder { } #[cfg(feature = "add_thumbnails")] - fn maybe_add_thumbnail( - &mut self, - format: &str, - stream: &mut R, - settings: &Settings, - ) -> Result<&mut Self> + fn maybe_add_thumbnail(&mut self, format: &str, stream: &mut R) -> Result<&mut Self> where R: Read + Seek + ?Sized, { @@ -1390,7 +1405,7 @@ impl Builder { } // check settings to see if we should auto generate a thumbnail - let auto_thumbnail = settings.builder.thumbnail.enabled; + let auto_thumbnail = self.context.settings().builder.thumbnail.enabled; if self.definition.thumbnail.is_none() && auto_thumbnail { stream.rewind()?; @@ -1400,7 +1415,7 @@ impl Builder { crate::utils::thumbnail::make_thumbnail_bytes_from_stream( format, &mut stream, - settings, + self.context.settings(), )? { stream.rewind()?; @@ -1472,8 +1487,6 @@ impl Builder { reserve_size: usize, format: &str, ) -> Result> { - let context = crate::context::Context::new(); - let dh: Result = self.find_assertion(DataHash::LABEL); if dh.is_err() { let mut ph = DataHash::new("jumbf manifest", "sha256"); @@ -1484,7 +1497,7 @@ impl Builder { } self.definition.format = format.to_string(); self.definition.instance_id = format!("xmp:iid:{}", Uuid::new_v4()); - let mut store = self.to_store(&context)?; + let mut store = self.to_store()?; let placeholder = store.get_data_hashed_manifest_placeholder(reserve_size, format)?; Ok(placeholder) } @@ -1516,15 +1529,23 @@ impl Builder { data_hash: &DataHash, format: &str, ) -> Result> { - let context = Context::new(); - - let mut store = self.to_store(&context)?; + let mut store = self.to_store()?; if _sync { - store.get_data_hashed_embeddable_manifest(data_hash, signer, format, None, &context) + store.get_data_hashed_embeddable_manifest( + data_hash, + signer, + format, + None, + &self.context, + ) } else { store .get_data_hashed_embeddable_manifest_async( - data_hash, signer, format, None, &context, + data_hash, + signer, + format, + None, + &self.context, ) .await } @@ -1550,16 +1571,14 @@ impl Builder { signer: &dyn Signer, format: &str, ) -> Result> { - let context = Context::new(); - self.definition.instance_id = format!("xmp:iid:{}", Uuid::new_v4()); - let mut store = self.to_store(&context)?; + let mut store = self.to_store()?; let bytes = if _sync { - store.get_box_hashed_embeddable_manifest(signer, &context) + store.get_box_hashed_embeddable_manifest(signer, &self.context) } else { store - .get_box_hashed_embeddable_manifest_async(signer, &context) + .get_box_hashed_embeddable_manifest_async(signer, &self.context) .await }?; // get composed version for embedding to JPEG @@ -1595,9 +1614,6 @@ impl Builder { R: Read + Seek + Send, W: Write + Read + Seek + Send, { - let settings = crate::settings::get_settings().unwrap_or_default(); - let context = Context::new().with_settings(settings); - let format = format_to_mime(format); self.definition.format.clone_from(&format); // todo:: read instance_id from xmp from stream ? @@ -1613,17 +1629,17 @@ impl Builder { // generate thumbnail if we don't already have one #[cfg(feature = "add_thumbnails")] - self.maybe_add_thumbnail(&format, source, context.settings())?; + self.maybe_add_thumbnail(&format, source)?; // convert the manifest to a store - let mut store = self.to_store(&context)?; + let mut store = self.to_store()?; // sign and write our store to to the output image file if _sync { - store.save_to_stream(&format, source, dest, signer, &context) + store.save_to_stream(&format, source, dest, signer, &self.context) } else { store - .save_to_stream_async(&format, source, dest, signer, &context) + .save_to_stream_async(&format, source, dest, signer, &self.context) .await } } @@ -1699,8 +1715,7 @@ impl Builder { } // convert the manifest to a store - let context = Context::new(); - let mut store = self.to_store(&context)?; + let mut store = self.to_store()?; // sign and write our store to DASH content store.save_to_bmff_fragmented( @@ -1708,7 +1723,7 @@ impl Builder { fragment_paths, output_path.as_ref(), signer, - &context, + &self.context, ) } @@ -1800,7 +1815,7 @@ impl Builder { /// The working store is signed with a BoxHash over an empty string /// And is returned as a Vec of the c2pa_manifest bytes /// This works as an archive of the store that can be read back to restore the Builder state - fn working_store_sign(&self, context: &Context) -> Result> { + fn working_store_sign(&self) -> Result> { // first we need to generate a BoxHash over an empty string let mut empty_asset = std::io::Cursor::new(""); let boxes = jumbf_io::get_assetio_handler("application/c2pa") @@ -1811,7 +1826,7 @@ impl Builder { let box_hash = BoxHash { boxes }; // then convert the builder to a claim and add the box hash assertion - let mut claim = self.to_claim(context)?; + let mut claim = self.to_claim()?; claim.add_assertion(&box_hash)?; // now commit and sign it. The signing will allow us to detect tampering. @@ -2234,7 +2249,7 @@ mod tests { #[cfg(target_os = "wasi")] Settings::reset().unwrap(); - settings::Settings::from_toml( + crate::settings::Settings::from_toml( &toml::toml! { [builder.actions.auto_opened_action] enabled = true diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs index 6879f8ea3..d1bc80827 100644 --- a/sdk/src/claim.rs +++ b/sdk/src/claim.rs @@ -1375,8 +1375,11 @@ impl Claim { if labels::HASH_LABELS.contains(&base_label) || add_as_created_assertion { ClaimAssertionType::Created } else if let Some(created_assertions) = { - let settings = crate::settings::get_settings().unwrap_or_default(); - settings.builder.created_assertion_labels + Context::new() + .settings() + .builder + .created_assertion_labels + .clone() } { if created_assertions.iter().any(|label| label == base_label) { ClaimAssertionType::Created diff --git a/sdk/src/context.rs b/sdk/src/context.rs index b74f2d87b..2d91e966f 100644 --- a/sdk/src/context.rs +++ b/sdk/src/context.rs @@ -27,6 +27,14 @@ impl Default for Context { } } +impl std::fmt::Debug for Context { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Context") + .field("settings", &self.settings) + .finish() + } +} + impl Context { pub fn new() -> Self { Self::default() diff --git a/sdk/src/identity/identity_assertion/assertion.rs b/sdk/src/identity/identity_assertion/assertion.rs index 424bfb285..cd059cc54 100644 --- a/sdk/src/identity/identity_assertion/assertion.rs +++ b/sdk/src/identity/identity_assertion/assertion.rs @@ -295,7 +295,7 @@ impl IdentityAssertion { partial_claim: &PartialClaim, status_tracker: &mut StatusTracker, ) -> Result> { - let settings = crate::settings::get_settings().unwrap_or_default(); + let settings = crate::Context::new().settings().clone(); self.check_padding(status_tracker)?; self.signer_payload @@ -310,19 +310,19 @@ impl IdentityAssertion { // are checked during setting generation. let cose_verifier = if settings.cawg_trust.verify_trust_list { - if let Some(ta) = settings.cawg_trust.trust_anchors { + if let Some(ta) = &settings.cawg_trust.trust_anchors { let _ = ctp.add_trust_anchors(ta.as_bytes()); } - if let Some(pa) = settings.cawg_trust.user_anchors { + if let Some(pa) = &settings.cawg_trust.user_anchors { let _ = ctp.add_user_trust_anchors(pa.as_bytes()); } - if let Some(tc) = settings.cawg_trust.trust_config { + if let Some(tc) = &settings.cawg_trust.trust_config { ctp.add_valid_ekus(tc.as_bytes()); } - if let Some(al) = settings.cawg_trust.allowed_list { + if let Some(al) = &settings.cawg_trust.allowed_list { let _ = ctp.add_end_entity_credentials(al.as_bytes()); } diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs index e26a11f50..f075dbafd 100644 --- a/sdk/src/ingredient.rs +++ b/sdk/src/ingredient.rs @@ -731,8 +731,7 @@ impl Ingredient { path: P, options: &dyn IngredientOptions, ) -> Result { - let settings = crate::settings::get_settings().unwrap_or_default(); - let context = Context::new().with_settings(settings); + let context = Context::new(); Self::from_file_impl(path.as_ref(), options, &context) } diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index c8894d624..9e3adf3e9 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -530,13 +530,13 @@ impl Reader { /// Get the [`ValidationState`] of the manifest store. pub fn validation_state(&self) -> ValidationState { - let settings = crate::settings::get_settings().unwrap_or_default(); + let context = Context::new(); if let Some(validation_results) = self.validation_results() { return validation_results.validation_state(); } - let verify_trust = settings.verify.verify_trust; + let verify_trust = context.settings().verify.verify_trust; match self.validation_status() { Some(status) => { // if there are any errors, the state is invalid unless the only error is an untrusted credential diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 0cb429423..3c6e026a8 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -8228,20 +8228,23 @@ pub mod tests { #[cfg(feature = "file_io")] fn test_bogus_cert() { use crate::builder::{Builder, BuilderIntent}; + let png = include_bytes!("../tests/fixtures/libpng-test.png"); // Randomly generated local Ed25519 let ed25519 = include_bytes!("../tests/fixtures/certs/ed25519.pem"); let certs = include_bytes!("../tests/fixtures/certs/es256.pub"); - let mut builder = Builder::new(); + + let mut context = Context::new(); + // bypass auto signature checks + context.settings_mut().verify.verify_after_sign = false; + context.settings_mut().verify.verify_trust = false; + + let mut builder = Builder::from_context(context); builder.set_intent(BuilderIntent::Create(DigitalSourceType::Empty)); let signer = crate::create_signer::from_keys(certs, ed25519, SigningAlg::Ed25519, None).unwrap(); let mut dst = Cursor::new(Vec::new()); - // bypass auto sig check - crate::settings::set_settings_value("verify.verify_after_sign", false).unwrap(); - crate::settings::set_settings_value("verify.verify_trust", false).unwrap(); - builder .sign(&signer, "image/png", &mut Cursor::new(png), &mut dst) .unwrap(); From ebf8a592b8c7aa45e8b82c129296708af6f5c474 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Sat, 15 Nov 2025 15:50:54 -0800 Subject: [PATCH 18/26] Add Signer support in Context Add ContentCredential.open_stream() --- sdk/src/context.rs | 75 +++++++++++++++++---- sdk/src/settings/signer.rs | 129 ++++++++++++++++++++----------------- 2 files changed, 132 insertions(+), 72 deletions(-) diff --git a/sdk/src/context.rs b/sdk/src/context.rs index 2d91e966f..58a6e66e2 100644 --- a/sdk/src/context.rs +++ b/sdk/src/context.rs @@ -4,17 +4,16 @@ use crate::{ content_credential::ContentCredential, http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver}, settings::Settings, + AsyncSigner, Result, Signer, }; -pub enum HttpResolver { - Sync(Box), - Async(Box), -} pub struct Context { settings: Settings, http_resolver: OnceCell>, http_resolver_async: OnceCell>, + signer: OnceCell>, + _signer_async: OnceCell>, } impl Default for Context { @@ -23,6 +22,8 @@ impl Default for Context { settings: crate::settings::get_settings().unwrap_or_default(), http_resolver: OnceCell::new(), http_resolver_async: OnceCell::new(), + signer: OnceCell::new(), + _signer_async: OnceCell::new(), } } } @@ -40,41 +41,91 @@ impl Context { Self::default() } + /// use the provided settings in this context pub fn with_settings(mut self, settings: Settings) -> Self { self.settings = settings; self } + /// Returns a reference to the settings. + pub fn settings(&self) -> &Settings { + &self.settings + } + + /// Returns a mutable reference to the settings. + pub fn settings_mut(&mut self) -> &mut Settings { + &mut self.settings + } + + /// Use the provided sync resolver in this context pub fn with_resolver(self, resolver: T) -> Self { let _ = self.http_resolver.set(Box::new(resolver)); self } + /// Use the provided async resolver in this context pub fn with_resolver_async(self, resolver: T) -> Self { let _ = self.http_resolver_async.set(Box::new(resolver)); self } - pub fn settings(&self) -> &Settings { - &self.settings - } - - pub fn settings_mut(&mut self) -> &mut Settings { - &mut self.settings - } - + /// Returns a reference to the sync resolver. pub fn resolver(&self) -> &dyn SyncHttpResolver { self.http_resolver .get_or_init(|| Box::new(SyncGenericResolver::new())) .as_ref() } + /// Returns a reference to the async resolver. pub fn resolver_async(&self) -> &dyn AsyncHttpResolver { self.http_resolver_async .get_or_init(|| Box::new(AsyncGenericResolver::new())) .as_ref() } + /// Returns a reference to the signer. + #[allow(clippy::unwrap_used)] // switch to get_or_try_init when stable + pub fn signer(&self) -> Result<&dyn Signer> { + match self.signer.get() { + Some(s) => Ok(s), + None => { + if let Some(c2pa_settings) = self.settings().signer.clone() { + let mut signer = c2pa_settings.c2pa_signer()?; + if let Some(cawg_settings) = self.settings().cawg_x509_signer.clone() { + signer = cawg_settings.cawg_signer(signer)?; + } + self.signer.set(signer).ok(); + return Ok(self.signer.get().unwrap()); + } + #[cfg(test)] + { + self.signer + .set(crate::utils::test_signer::test_signer( + crate::SigningAlg::Ps256, + )) + .ok(); + Ok(self.signer.get().unwrap()) + } + #[cfg(not(test))] + { + Err(crate::Error::MissingSignerSettings) + } + } + } + } + + // pub fn signer_async(&self) -> Result<&dyn crate::signer::AsyncSigner> { + // match self.signer_async.get() { + // Some(s) => Ok(s.as_ref()), + // None => { + // self.signer_async + // .set(Settings::signer_async()?) + // .ok(); + // Ok(self.signer_async.get().unwrap().as_ref()) + // } + // } + // } + pub fn content_credential(&self) -> ContentCredential<'_> { ContentCredential::new(self) } diff --git a/sdk/src/settings/signer.rs b/sdk/src/settings/signer.rs index d928e496f..b070e56f7 100644 --- a/sdk/src/settings/signer.rs +++ b/sdk/src/settings/signer.rs @@ -66,76 +66,85 @@ impl SignerSettings { /// /// If the signer settings aren't specified, this function will return [Error::MissingSignerSettings]. pub fn signer() -> Result> { - let c2pa_signer = Self::c2pa_signer()?; + let signer_info = match Settings::get_value::>("signer") { + Ok(Some(signer_info)) => signer_info, + #[cfg(test)] + _ => { + return Ok(crate::utils::test_signer::test_signer(SigningAlg::Ps256)); + } + #[cfg(not(test))] + _ => { + return Err(Error::MissingSignerSettings); + } + }; + + let c2pa_signer = Self::c2pa_signer(signer_info)?; // TO DISCUSS: What if get_value returns an Err(...)? if let Ok(Some(cawg_x509_settings)) = Settings::get_value::>("cawg_x509_signer") { - match cawg_x509_settings { - SignerSettings::Local { - alg: cawg_alg, - sign_cert: cawg_sign_cert, - private_key: cawg_private_key, - tsa_url: cawg_tsa_url, - } => { - let cawg_dual_signer = CawgX509IdentitySigner { - c2pa_signer, - cawg_alg, - cawg_sign_cert, - cawg_private_key, - cawg_tsa_url, - }; - - Ok(Box::new(cawg_dual_signer)) - } - - SignerSettings::Remote { - url: _url, - alg: _alg, - sign_cert: _sign_cert, - tsa_url: _tsa_url, - } => todo!("Remote CAWG X.509 signing not yet supported"), - } + cawg_x509_settings.cawg_signer(c2pa_signer) } else { Ok(c2pa_signer) } } - /// Returns a C2PA-only signer from the [`BuilderSettings::signer`] field. - fn c2pa_signer() -> Result> { - let signer_info = Settings::get_value::>("signer"); - - match signer_info { - Ok(Some(signer_info)) => match signer_info { - SignerSettings::Local { - alg, - sign_cert, - private_key, - tsa_url, - } => create_signer::from_keys( - sign_cert.as_bytes(), - private_key.as_bytes(), - alg, - tsa_url.to_owned(), - ), - SignerSettings::Remote { - url, - alg, - sign_cert, - tsa_url, - } => Ok(Box::new(RemoteSigner { - url, - alg, - reserve_size: 10000 + sign_cert.len(), - certs: vec![sign_cert.into_bytes()], - tsa_url, - })), - }, - #[cfg(test)] - _ => Ok(crate::utils::test_signer::test_signer(SigningAlg::Ps256)), - #[cfg(not(test))] - _ => Err(Error::MissingSignerSettings), + /// Returns a c2pa signer using the provided signer settings. + pub fn c2pa_signer(self) -> Result> { + match self { + SignerSettings::Local { + alg, + sign_cert, + private_key, + tsa_url, + } => create_signer::from_keys( + sign_cert.as_bytes(), + private_key.as_bytes(), + alg, + tsa_url.to_owned(), + ), + SignerSettings::Remote { + url, + alg, + sign_cert, + tsa_url, + } => Ok(Box::new(RemoteSigner { + url, + alg, + reserve_size: 10000 + sign_cert.len(), + certs: vec![sign_cert.into_bytes()], + tsa_url, + })), + } + } + + /// Returns a CAWG X.509 identity signer that wraps the provided c2pa signer. + pub fn cawg_signer(self, c2pa_signer: Box) -> Result> { + match self { + SignerSettings::Local { + alg: cawg_alg, + sign_cert: cawg_sign_cert, + private_key: cawg_private_key, + tsa_url: cawg_tsa_url, + } => { + let cawg_dual_signer = CawgX509IdentitySigner { + c2pa_signer, + cawg_alg, + cawg_sign_cert, + cawg_private_key, + cawg_tsa_url, + }; + + Ok(Box::new(cawg_dual_signer)) + } + + SignerSettings::Remote { + url: _url, + alg: _alg, + sign_cert: _sign_cert, + tsa_url: _tsa_url, + } => todo!("Remote CAWG X.509 signing not yet supported"), } } } From fecfbe8c6f2818585b0ec1eb9f30bb1988b7c5de Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Sat, 15 Nov 2025 15:51:51 -0800 Subject: [PATCH 19/26] add content_credential open_stream --- sdk/src/content_credential.rs | 60 +++++++++++++++++------------------ sdk/src/context.rs | 1 - 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index ea9aae29a..fcd0f268c 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -112,6 +112,17 @@ impl<'a> ContentCredential<'a> { Ok(self) } + // edits the content credential by setting the parent ingredient from the provided stream + pub fn open_stream( + mut self, + format: &str, + mut stream: impl Read + Seek + Send, + ) -> Result { + stream.rewind()?; + self.add_ingredient_from_stream(Relationship::ParentOf, format, &mut stream)?; + Ok(self) + } + /// creates a content credential from an existing stream pub fn from_stream( context: &'a Context, @@ -146,26 +157,13 @@ impl<'a> ContentCredential<'a> { Ok(()) } - // /// This is used to add component ingredients from a stream - // /// - // /// Parent ingredients are created using from_stream. - // pub fn add_ingredient_from_stream( - // &mut self, - // format: &str, - // mut stream: impl Read + Seek + Send, - // ) -> Result<&Self> { - // Ok(self - // .add_ingredient(Relationship::ComponentOf, format, &mut stream)? - // .0) - // } - /// internal implementation to add ingredient from stream fn add_ingredient_from_stream( &mut self, relationship: Relationship, format: &str, mut stream: impl Read + Seek + Send, - ) -> Result<&Self> { + ) -> Result { let (ingredient_assertion, store) = Ingredient::from_stream(relationship.clone(), format, &mut stream, self.context)?; @@ -173,26 +171,27 @@ impl<'a> ContentCredential<'a> { let manifest_bytes = store.to_jumbf_internal(0)?; Store::load_ingredient_to_claim(&mut self.claim, &manifest_bytes, None, self.context)?; + if relationship == Relationship::ParentOf { + // we must replace the store for parent ingredients + self.store = store; + } + // add the ingredient assertion and get it's uri let ingredient_uri = self.add_assertion(&ingredient_assertion)?; - // create an action associated with the ingredient + // This part automatically adds the correct associated action for the ingredient + // I'm not sure if we want to have this behavior here or leave it to the user let action_label = if relationship == Relationship::ParentOf { c2pa_action::OPENED } else { c2pa_action::PLACED }; - let action = Action::new(action_label).add_ingredient(ingredient_uri)?; + let action = Action::new(action_label).add_ingredient(ingredient_uri.clone())?; self.add_action(action)?; - if relationship == Relationship::ParentOf { - // we must replace the store for parent ingredients - self.store = store; - } - - Ok(self) + Ok(ingredient_uri) } /// sets the default claim generator info if not already set @@ -216,12 +215,12 @@ impl<'a> ContentCredential<'a> { R: Read + Seek + Send, W: Write + Read + Seek + Send, { - let signer = Settings::signer()?; + let signer = self.context.signer()?; self.set_default_claim_generator_info()?; self.store.commit_claim(self.claim.clone())?; source.rewind()?; // always reset source to start self.store - .save_to_stream(format, source, dest, &signer, self.context) + .save_to_stream(format, source, dest, signer, self.context) } /// Generates a value similar to the C2PA Reader output @@ -280,7 +279,7 @@ mod tests { use crate::utils::test::*; #[test] - fn test_content_credential_created() -> Result<()> { + fn test_content_credential_create() -> Result<()> { let (format, mut source, mut dest) = create_test_streams(CA_JPEG); let context = Context::new(); @@ -288,23 +287,22 @@ mod tests { cr.save_to_stream(format, &mut source, &mut dest)?; - let cr = ContentCredential::from_stream(&context, format, &mut dest)?; + let cr = ContentCredential::new(&context).open_stream(format, &mut dest)?; println!("{cr}"); Ok(()) } #[test] - fn test_content_credential_from_stream() -> Result<()> { + fn test_content_credential_open_stream() -> Result<()> { let (format, mut source, mut dest) = create_test_streams(CA_JPEG); let context = Context::new(); - let mut cr = context.content_credential(); - cr.add_ingredient_from_stream(Relationship::ParentOf, format, &mut source)?; - println!("{cr}"); + let mut cr = ContentCredential::new(&context).open_stream(format, &mut source)?; + cr.add_action(Action::new(c2pa_action::PUBLISHED))?; cr.save_to_stream(format, &mut source, &mut dest)?; - let cr2 = ContentCredential::from_stream(&context, format, &mut dest)?; + let cr2 = ContentCredential::new(&context).open_stream(format, &mut dest)?; println!("{cr2}"); Ok(()) } diff --git a/sdk/src/context.rs b/sdk/src/context.rs index 58a6e66e2..e769d1e5f 100644 --- a/sdk/src/context.rs +++ b/sdk/src/context.rs @@ -7,7 +7,6 @@ use crate::{ AsyncSigner, Result, Signer, }; - pub struct Context { settings: Settings, http_resolver: OnceCell>, From d5ab2171a597d06601cdfb7cbea2657f6d381f93 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Sun, 16 Nov 2025 19:39:44 -0800 Subject: [PATCH 20/26] add update_from_str to settings, so that you can overlay settings on a settings instance --- sdk/src/settings/mod.rs | 195 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/sdk/src/settings/mod.rs b/sdk/src/settings/mod.rs index effc2ebca..f1ff6aed3 100644 --- a/sdk/src/settings/mod.rs +++ b/sdk/src/settings/mod.rs @@ -428,6 +428,78 @@ impl Settings { Settings::from_string(toml, "toml").map(|_| ()) } + /// Update this `Settings` instance from a string representation. + /// This overlays the provided configuration on top of the current settings + /// without affecting the global thread-local settings. + /// + /// # Arguments + /// * `settings_str` - The configuration string + /// * `format` - The format of the configuration ("json" or "toml") + /// + /// # Example + /// ``` + /// use c2pa::settings::Settings; + /// + /// let mut settings = Settings::default(); + /// + /// // Update with TOML + /// settings + /// .update_from_str( + /// r#" + /// [verify] + /// verify_after_sign = false + /// "#, + /// "toml", + /// ) + /// .unwrap(); + /// + /// assert!(!settings.verify.verify_after_sign); + /// + /// // Update with JSON (can set values to null) + /// settings + /// .update_from_str( + /// r#" + /// { + /// "verify": { + /// "verify_after_sign": true + /// } + /// } + /// "#, + /// "json", + /// ) + /// .unwrap(); + /// + /// assert!(settings.verify.verify_after_sign); + /// ``` + pub fn update_from_str(&mut self, settings_str: &str, format: &str) -> Result<()> { + let file_format = match format.to_lowercase().as_str() { + "json" => FileFormat::Json, + "toml" => FileFormat::Toml, + _ => return Err(Error::UnsupportedType), + }; + + // Convert current settings to Config + let current_config = Config::try_from(&*self) + .map_err(|e| Error::BadParam(format!("could not convert settings: {}", e)))?; + + // Build new config with the source + let merged_config = Config::builder() + .add_source(current_config) + .add_source(config::File::from_str(settings_str, file_format)) + .build() + .map_err(|e| Error::BadParam(format!("could not merge configuration: {}", e)))?; + + // Deserialize and validate + let updated_settings = merged_config + .try_deserialize::() + .map_err(|e| Error::BadParam(e.to_string()))?; + + updated_settings.validate()?; + + *self = updated_settings; + Ok(()) + } + /// Set a [Settings] value by path reference. The path is nested names of of the Settings objects /// separated by "." notation. /// @@ -869,4 +941,127 @@ pub mod tests { let toml = include_bytes!("../../examples/c2pa.toml"); Settings::from_toml(std::str::from_utf8(toml).unwrap()).unwrap(); } + + #[test] + fn test_update_from_str_toml() { + let mut settings = Settings::default(); + + // Check defaults + assert!(settings.verify.verify_after_sign); + assert!(settings.verify.verify_trust); + + // Set both to false + settings + .update_from_str( + r#" + [verify] + verify_after_sign = false + verify_trust = false + "#, + "toml", + ) + .unwrap(); + + assert!(!settings.verify.verify_after_sign); + assert!(!settings.verify.verify_trust); + + // Override: set one to true, keep other false + settings + .update_from_str( + r#" + [verify] + verify_after_sign = true + "#, + "toml", + ) + .unwrap(); + + assert!(settings.verify.verify_after_sign); + assert!(!settings.verify.verify_trust); + } + + #[test] + fn test_update_from_str_json() { + let mut settings = Settings::default(); + + // Check defaults + assert!(settings.verify.verify_after_sign); + assert!(settings.verify.verify_trust); + assert!(settings.builder.created_assertion_labels.is_none()); + + // Set both to false and set created_assertion_labels + settings + .update_from_str( + r#" + { + "verify": { + "verify_after_sign": false, + "verify_trust": false + }, + "builder": { + "created_assertion_labels": ["c2pa.metadata"] + } + } + "#, + "json", + ) + .unwrap(); + + assert!(!settings.verify.verify_after_sign); + assert!(!settings.verify.verify_trust); + assert_eq!( + settings.builder.created_assertion_labels, + Some(vec!["c2pa.metadata".to_string()]) + ); + + // Override: set one to true, keep other false + settings + .update_from_str( + r#" + { + "verify": { + "verify_after_sign": true + } + } + "#, + "json", + ) + .unwrap(); + + assert!(settings.verify.verify_after_sign); + assert!(!settings.verify.verify_trust); + assert_eq!( + settings.builder.created_assertion_labels, + Some(vec!["c2pa.metadata".to_string()]) + ); + + // Set created_assertion_labels back to null + settings + .update_from_str( + r#" + { + "builder": { + "created_assertion_labels": null + } + } + "#, + "json", + ) + .unwrap(); + + assert!(settings.verify.verify_after_sign); + assert!(!settings.verify.verify_trust); + assert!(settings.builder.created_assertion_labels.is_none()); + } + + #[test] + fn test_update_from_str_invalid() { + assert!(Settings::default() + .update_from_str("invalid toml { ]", "toml") + .is_err()); + assert!(Settings::default() + .update_from_str("{ invalid json }", "json") + .is_err()); + assert!(Settings::default().update_from_str("data", "yaml").is_err()); + } } From 0b9e6b4f76b4e3c73752c2ad04da866f23231f46 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Sun, 16 Nov 2025 19:42:55 -0800 Subject: [PATCH 21/26] add try_from for &ClaimGeneratorInfoSettings --- sdk/src/settings/builder.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/sdk/src/settings/builder.rs b/sdk/src/settings/builder.rs index de5dcf994..d762ec906 100644 --- a/sdk/src/settings/builder.rs +++ b/sdk/src/settings/builder.rs @@ -222,6 +222,38 @@ impl TryFrom for ClaimGeneratorInfo { } } +impl TryFrom<&ClaimGeneratorInfoSettings> for ClaimGeneratorInfo { + type Error = Error; + + fn try_from(value: &ClaimGeneratorInfoSettings) -> Result { + Ok(ClaimGeneratorInfo { + name: value.name.clone(), + version: value.version.clone(), + icon: value + .icon + .as_ref() + .map(|icon| UriOrResource::ResourceRef(icon.clone())), + operating_system: { + value.operating_system.as_ref().map(|os| match os { + ClaimGeneratorInfoOperatingSystem::Auto => { + format!("{}-unknown-{}", consts::ARCH, consts::OS) + } + ClaimGeneratorInfoOperatingSystem::Other(name) => name.clone(), + }) + }, + other: value + .other + .iter() + .map(|(key, value)| { + serde_json::to_value(value) + .map(|value| (key.clone(), value)) + .map_err(|err| err.into()) + }) + .collect::>>()?, + }) + } +} + /// Settings for an action template. #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] From d110624d853db5f49217f8641a4f15252e664bc2 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Sun, 16 Nov 2025 19:45:59 -0800 Subject: [PATCH 22/26] context.with_settings() support IntoSettings trait --- sdk/src/content_credential.rs | 42 +++++++++------- sdk/src/context.rs | 94 +++++++++++++++++++++++++++++++++-- sdk/src/lib.rs | 2 +- sdk/src/store.rs | 27 ++++------ 4 files changed, 126 insertions(+), 39 deletions(-) diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index fcd0f268c..a982dbcd2 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -31,7 +31,7 @@ use crate::{ store::Store, utils::hash_utils::hash_to_b64, validation_status::ValidationStatus, - ClaimGeneratorInfo, DigitalSourceType, Error, HashedUri, Result, ValidationResults, + DigitalSourceType, Error, HashedUri, Result, ValidationResults, }; /// This Generates the standard Reader output format for a Store @@ -194,16 +194,6 @@ impl<'a> ContentCredential<'a> { Ok(ingredient_uri) } - /// sets the default claim generator info if not already set - fn set_default_claim_generator_info(&mut self) -> Result<&Self> { - if self.claim.claim_generator_info().is_none() { - // only set if not already set - self.claim - .add_claim_generator_info(ClaimGeneratorInfo::default()); - } - Ok(self) - } - /// signs and saves the content credential to the destination stream pub fn save_to_stream( &mut self, @@ -216,7 +206,11 @@ impl<'a> ContentCredential<'a> { W: Write + Read + Seek + Send, { let signer = self.context.signer()?; - self.set_default_claim_generator_info()?; + if self.claim.claim_generator_info().is_none() { + if let Some(cgi) = &self.context.settings().builder.claim_generator_info { + self.claim.add_claim_generator_info(cgi.try_into()?); + } + } self.store.commit_claim(self.claim.clone())?; source.rewind()?; // always reset source to start self.store @@ -278,10 +272,22 @@ mod tests { use super::*; use crate::utils::test::*; + fn test_settings_json() -> serde_json::Value { + serde_json::json!({ + "builder": { + "claim_generator_info": { + "name": "Content Credential Tests", + "version": env!("CARGO_PKG_VERSION") + } + } + }) + } + #[test] fn test_content_credential_create() -> Result<()> { let (format, mut source, mut dest) = create_test_streams(CA_JPEG); - let context = Context::new(); + + let context = Context::new().with_settings(test_settings_json())?; let mut cr = ContentCredential::new(&context).create(DigitalSourceType::Empty)?; @@ -295,7 +301,7 @@ mod tests { #[test] fn test_content_credential_open_stream() -> Result<()> { let (format, mut source, mut dest) = create_test_streams(CA_JPEG); - let context = Context::new(); + let context = Context::new().with_settings(test_settings_json())?; let mut cr = ContentCredential::new(&context).open_stream(format, &mut source)?; cr.add_action(Action::new(c2pa_action::PUBLISHED))?; @@ -310,15 +316,15 @@ mod tests { #[test] fn test_add_ingredient_from_stream() -> Result<()> { let (format, mut source, mut dest) = create_test_streams(CA_JPEG); - let context = Context::new(); + let context = Context::new().with_settings(test_settings_json())?; let mut cr = ContentCredential::new(&context).create(DigitalSourceType::Empty)?; - cr.add_ingredient_from_stream(Relationship::ParentOf, format, &mut source)?; + cr.add_ingredient_from_stream(Relationship::ComponentOf, format, &mut source)?; cr.save_to_stream(format, &mut source, &mut dest)?; - let cr = ContentCredential::from_stream(&context, format, &mut dest)?; - println!("{cr:?}"); + let cr2 = ContentCredential::new(&context).open_stream(format, &mut dest)?; + println!("{cr2}"); Ok(()) } } diff --git a/sdk/src/context.rs b/sdk/src/context.rs index e769d1e5f..abbba50be 100644 --- a/sdk/src/context.rs +++ b/sdk/src/context.rs @@ -4,9 +4,51 @@ use crate::{ content_credential::ContentCredential, http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver}, settings::Settings, - AsyncSigner, Result, Signer, + AsyncSigner, Error, Result, Signer, }; +/// A trait for types that can be converted into Settings +pub trait IntoSettings { + /// Convert this type into Settings + fn into_settings(self) -> Result; +} + +/// Implement for Settings (passthrough) +impl IntoSettings for Settings { + fn into_settings(self) -> Result { + Ok(self) + } +} + +/// Implement for &str (JSON/TOML string - tries both formats) +impl IntoSettings for &str { + fn into_settings(self) -> Result { + let mut settings = Settings::default(); + // Try JSON first, then TOML + settings + .update_from_str(self, "json") + .or_else(|_| settings.update_from_str(self, "toml"))?; + Ok(settings) + } +} + +/// Implement for String +impl IntoSettings for String { + fn into_settings(self) -> Result { + self.as_str().into_settings() + } +} + +/// Implement for serde_json::Value +impl IntoSettings for serde_json::Value { + fn into_settings(self) -> Result { + let json_str = serde_json::to_string(&self).map_err(Error::JsonError)?; + let mut settings = Settings::default(); + settings.update_from_str(&json_str, "json")?; + Ok(settings) + } +} + pub struct Context { settings: Settings, http_resolver: OnceCell>, @@ -41,9 +83,9 @@ impl Context { } /// use the provided settings in this context - pub fn with_settings(mut self, settings: Settings) -> Self { - self.settings = settings; - self + pub fn with_settings(mut self, settings: S) -> Result { + self.settings = settings.into_settings()?; + Ok(self) } /// Returns a reference to the settings. @@ -129,3 +171,47 @@ impl Context { ContentCredential::new(self) } } + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + use super::*; + + #[test] + fn test_into_settings_from_settings() { + let mut settings = Settings::default(); + settings.verify.verify_after_sign = true; + let context = Context::new().with_settings(settings).unwrap(); + assert!(context.settings().verify.verify_after_sign); + } + + #[test] + fn test_into_settings_from_json_str() { + let json = r#"{"verify": {"verify_after_sign": true}}"#; + let context = Context::new().with_settings(json).unwrap(); + assert!(context.settings().verify.verify_after_sign); + } + #[test] + fn test_into_settings_from_toml_str() { + let toml = r#" + [verify] + verify_after_sign = true + "#; + let context = Context::new().with_settings(toml).unwrap(); + assert!(context.settings().verify.verify_after_sign); + } + + #[test] + fn test_into_settings_from_json_value() { + let value = serde_json::json!({"verify": {"verify_after_sign": true}}); + let context = Context::new().with_settings(value).unwrap(); + assert!(context.settings().verify.verify_after_sign); + } + + #[test] + fn test_into_settings_invalid_json() { + let invalid_json = r#"{"verify": {"verify_after_sign": "#; + let result = Context::new().with_settings(invalid_json); + assert!(result.is_err()); + } +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index a1850ca99..22430b4f1 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -128,7 +128,7 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub mod assertions; pub mod context; -pub use context::Context; +pub use context::{Context, IntoSettings}; pub mod content_credential; pub use content_credential::ContentCredential; /// The cose_sign module contains the definitions for the COSE signing algorithms. diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 3c6e026a8..9e216bb6a 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -4321,7 +4321,6 @@ pub mod tests { assertions::{Action, Actions, Uuid}, claim::AssertionStoreJsonFormat, crypto::raw_signature::SigningAlg, - settings::Settings, status_tracker::{LogItem, StatusTracker}, utils::{ patch::patch_bytes, @@ -4496,9 +4495,8 @@ pub mod tests { #[test] fn test_bad_claim_v2_generation() { - let mut settings = Settings::default(); - settings.verify.verify_after_sign = false; - let context = Context::new().with_settings(settings); + let mut context = Context::new(); + context.settings_mut().verify.verify_after_sign = false; let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg"); @@ -4843,9 +4841,8 @@ pub mod tests { #[test] fn test_png_jumbf_generation() { - let mut settings = Settings::default(); - settings.verify.verify_after_sign = false; - let context = crate::context::Context::new().with_settings(settings); + let mut context = Context::new(); + context.settings_mut().verify.verify_after_sign = false; // test adding to actual image let (format, mut input_stream, mut output_stream) = create_test_streams("libpng-test.png"); @@ -6882,9 +6879,9 @@ pub mod tests { #[test] #[ignore = "this test is not generating the expected errors - the test image cert has expired"] fn test_bmff_legacy() { - let mut settings = Settings::default(); - settings.verify.verify_trust = false; - let context = Context::new().with_settings(settings); + let mut context = Context::new(); + context.settings_mut().verify.verify_trust = false; + let (format, mut input_stream, _output_stream) = create_test_streams("legacy.mp4"); // test 1.0 bmff hash let mut report = StatusTracker::default(); @@ -7127,9 +7124,8 @@ pub mod tests { #[test] fn test_bmff_jumbf_stream_generation() { - let mut settings = Settings::default(); - settings.verify.verify_after_reading = false; - let context = crate::context::Context::new().with_settings(settings); + let mut context = Context::new(); + context.settings_mut().verify.verify_after_reading = false; // test adding to actual image let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4"); @@ -8126,9 +8122,8 @@ pub mod tests { #[test] #[cfg(feature = "file_io")] fn test_fragmented_jumbf_generation() { - let mut settings = crate::settings::Settings::default(); - settings.verify.verify_after_reading = false; - let context = crate::context::Context::new().with_settings(settings); + let mut context = crate::context::Context::new(); + context.settings_mut().verify.verify_after_reading = false; // test adding to actual image From aa9381960ca8e5cceede52d3aa1e3b8a3e569b7f Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Mon, 17 Nov 2025 16:12:59 -0800 Subject: [PATCH 23/26] Add context to Reader Reader::new and reader.with_stream added. --- sdk/src/builder.rs | 7 +- sdk/src/reader.rs | 199 ++++++++++++++++++++++++++------------------- 2 files changed, 119 insertions(+), 87 deletions(-) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index e22358a96..daff83fab 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -325,7 +325,7 @@ pub struct Builder { pub base_path: Option, /// A builder should construct a created, opened or updated manifest. - #[deprecated(note = "Use set_intent() to set or intent() to get the builder intent")] + #[deprecated(note = "Use set_intent() to set or intent()")] pub intent: Option, /// Container for binary assets (like thumbnails). @@ -383,7 +383,7 @@ impl Builder { #[allow(deprecated)] pub fn set_intent(&mut self, intent: BuilderIntent) -> &mut Self { self.context.settings_mut().builder.intent = Some(intent.clone()); - self.intent = Some(intent); + self.intent = Some(intent); // self.intent is deprecated but keep in sync for now self } @@ -916,7 +916,8 @@ impl Builder { &mut validation_log, &context, )?; - let reader = Reader::from_store(store, &mut validation_log, &context)?; + let mut reader = Reader::new(context); + reader.with_store(store, &mut validation_log)?; reader.into_builder() }) } diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index 9e3adf3e9..0b86e3146 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -48,6 +48,18 @@ use crate::{ Ingredient, Manifest, ManifestAssertion, Relationship, }; +/// MaybeSend allows for no Send bound on wasm32 targets +/// todo: move this to a common module +#[cfg(not(target_arch = "wasm32"))] +pub trait MaybeSend: Send {} +#[cfg(target_arch = "wasm32")] +pub trait MaybeSend {} + +#[cfg(not(target_arch = "wasm32"))] +impl MaybeSend for T {} +#[cfg(target_arch = "wasm32")] +impl MaybeSend for T {} + /// A trait for post-validation of manifest assertions. pub trait PostValidator { fn validate( @@ -102,36 +114,56 @@ pub struct Reader { /// Map to hold post-validation assertion values for reports /// the key is an assertion uri and the value is the assertion value assertion_values: HashMap, + + #[serde(skip)] + context: Context, } type ValidationFn = dyn Fn(&str, &crate::ManifestAssertion, &mut StatusTracker) -> Option; impl Reader { - // #[async_generic] - // pub fn from_asset<'a>(context: &'a Context, asset: &'a mut Asset<'a>) -> Result { - - // let mut validation_log = StatusTracker::default(); - // //asset.rewind()?; - // let store = if _sync { - // Store::from_asset(context, - // asset, - // &mut validation_log, - // ) - // } else { - // Store::from_asset_async( - // context, - // asset, - // &mut validation_log, - // ) - // .await - // }?; - // if _sync { - // Self::from_store(store, &mut validation_log, context) - // } else { - // Self::from_store_async(store, &mut validation_log, context).await - // } - // } + /// Create a new Reader with the given Context + /// # Arguments + /// * `context` - The Context to use for the Reader + /// # Returns + /// A new Reader + pub fn new(context: Context) -> Self { + Self { + context, + store: Store::new(), + assertion_values: HashMap::new(), + ..Default::default() + } + } + + /// Add manifest store from a stream to the Reader + /// # Arguments + /// * `format` - The format of the stream. MIME type or extension that maps to a MIME type. + /// * `stream` - The stream to read from. Must implement the Read and Seek traits. + /// # Returns + /// The updated Reader. + #[async_generic] + pub fn with_stream( + mut self, + format: &str, + mut stream: impl Read + Seek + MaybeSend, + ) -> Result { + let mut validation_log = StatusTracker::default(); + stream.rewind()?; // Ensure stream is at the start + let store = if _sync { + Store::from_stream(format, stream, &mut validation_log, &self.context) + } else { + Store::from_stream_async(format, stream, &mut validation_log, &self.context).await + }?; + + if _sync { + self.with_store(store, &mut validation_log) + } else { + self.with_store_async(store, &mut validation_log).await + }?; + Ok(self) + } /// Create a manifest store [`Reader`] from a stream. A Reader is used to validate C2PA data from an asset. /// @@ -161,42 +193,13 @@ impl Reader { /// /// [CAWG identity]: https://cawg.io/identity/ #[async_generic] - #[cfg(not(target_arch = "wasm32"))] - pub fn from_stream(format: &str, mut stream: impl Read + Seek + Send) -> Result { - let context = Context::new(); - - let mut validation_log = StatusTracker::default(); - stream.rewind()?; // Ensure stream is at the start - let store = if _sync { - Store::from_stream(format, stream, &mut validation_log, &context) - } else { - Store::from_stream_async(format, stream, &mut validation_log, &context).await - }?; - - if _sync { - Self::from_store(store, &mut validation_log, &context) - } else { - Self::from_store_async(store, &mut validation_log, &context).await - } - } - - #[async_generic] - #[cfg(target_arch = "wasm32")] - pub fn from_stream(format: &str, mut stream: impl Read + Seek) -> Result { - let context = Context::new(); - - let mut validation_log = StatusTracker::default(); - - let store = if _sync { - Store::from_stream(format, &mut stream, &mut validation_log, &context) - } else { - Store::from_stream_async(format, &mut stream, &mut validation_log, &context).await - }?; - + pub fn from_stream(format: &str, stream: impl Read + Seek + MaybeSend) -> Result { if _sync { - Self::from_store(store, &mut validation_log, &context) + Reader::new(Context::new()).with_stream(format, stream) } else { - Self::from_store_async(store, &mut validation_log, &context).await + Reader::new(Context::new()) + .with_stream_async(format, stream) + .await } } @@ -289,6 +292,7 @@ impl Reader { stream: impl Read + Seek + Send, ) -> Result { let context = Context::new(); + let mut reader = Reader::new(context); let mut validation_log = StatusTracker::default(); @@ -298,7 +302,7 @@ impl Reader { format, stream, &mut validation_log, - &context, + &reader.context, ) } else { Store::from_manifest_data_and_stream_async( @@ -306,12 +310,16 @@ impl Reader { format, stream, &mut validation_log, - &context, + &reader.context, ) .await }?; - - Self::from_store(store, &mut validation_log, &context) + if _sync { + reader.with_store(store, &mut validation_log) + } else { + reader.with_store_async(store, &mut validation_log).await + }?; + Ok(reader) } /// Create a [`Reader`] from an initial segment and a fragment stream. @@ -331,7 +339,7 @@ impl Reader { mut stream: impl Read + Seek + Send, mut fragment: impl Read + Seek + Send, ) -> Result { - let context = Context::new(); + let mut reader = Reader::new(Context::new()); let mut validation_log = StatusTracker::default(); @@ -341,7 +349,7 @@ impl Reader { &mut stream, &mut fragment, &mut validation_log, - &context, + &reader.context, ) } else { Store::load_fragment_from_stream_async( @@ -349,12 +357,17 @@ impl Reader { &mut stream, &mut fragment, &mut validation_log, - &context, + &reader.context, ) .await }?; - Self::from_store(store, &mut validation_log, &context) + if _sync { + reader.with_store(store, &mut validation_log) + } else { + reader.with_store_async(store, &mut validation_log).await + }?; + Ok(reader) } /// Loads a [`Reader`]` from an initial segment and fragments. This @@ -365,7 +378,7 @@ impl Reader { path: P, fragments: &Vec, ) -> Result { - let context = Context::new(); + let mut reader = Reader::new(Context::new()); let mut validation_log = StatusTracker::default(); @@ -379,9 +392,12 @@ impl Reader { &mut init_segment, fragments, &mut validation_log, - &context, + &reader.context, ) { - Ok(store) => Self::from_store(store, &mut validation_log, &context), + Ok(store) => { + reader.with_store(store, &mut validation_log)?; + Ok(reader) + } Err(e) => Err(e), } } @@ -703,11 +719,11 @@ impl Reader { } #[async_generic()] - pub(crate) fn from_store( + pub(crate) fn with_store( + &mut self, store: Store, validation_log: &mut StatusTracker, - context: &Context, - ) -> Result { + ) -> Result<&Self> { let active_manifest = store.provenance_label(); let mut manifests = HashMap::new(); let mut options = StoreOptions::default(); @@ -720,7 +736,7 @@ impl Reader { manifest_label, &mut options, validation_log, - context.settings(), + self.context.settings(), ) } else { Manifest::from_store_async( @@ -728,7 +744,7 @@ impl Reader { manifest_label, &mut options, validation_log, - context.settings(), + self.context.settings(), ) .await }; @@ -764,7 +780,6 @@ impl Reader { missing.retain(|item| !options.redacted_assertions.contains(item)); // Add any remaining redacted assertions to the validation results - // todo: figure out what to do here! for uri in &redacted { log_item!(uri.clone(), "assertion not redacted", "Reader::from_store") .validation_status(ASSERTION_NOT_REDACTED) @@ -778,15 +793,14 @@ impl Reader { } let validation_state = validation_results.validation_state(); - Ok(Self { - active_manifest, - manifests, - validation_status: validation_results.validation_errors(), - validation_results: Some(validation_results), - validation_state: Some(validation_state), - store, - assertion_values: HashMap::new(), - }) + + self.active_manifest = active_manifest; + self.manifests = manifests; + self.validation_status = validation_results.validation_errors(); + self.validation_results = Some(validation_results); + self.validation_state = Some(validation_state); + self.store = store; + Ok(self) } /// Populate manifest_data references for all ingredients in a manifest @@ -1174,6 +1188,23 @@ pub mod tests { Ok(()) } + #[test] + fn test_reader_new_with_stream() -> Result<()> { + const TEST_SETTINGS: &str = include_str!("../tests/fixtures/test_settings.toml"); + let context = Context::new().with_settings(TEST_SETTINGS)?; + + let mut source = Cursor::new(IMAGE_WITH_MANIFEST); + + let reader = Reader::new(context).with_stream("image/jpeg", &mut source)?; + + assert_eq!(reader.remote_url(), None); + assert!(reader.is_embedded()); + assert_eq!(reader.validation_state(), ValidationState::Trusted); + assert!(reader.active_manifest().is_some()); + + Ok(()) + } + #[test] #[cfg(feature = "fetch_remote_manifests")] fn test_reader_remote_url() -> Result<()> { From 441522d6b06cf5f9a52dc86820774f3a6b0fec17 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Thu, 20 Nov 2025 14:05:00 -0800 Subject: [PATCH 24/26] remove cr:from_stream --- sdk/src/asset.rs | 5 ++--- sdk/src/content_credential.rs | 20 +++++++------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/sdk/src/asset.rs b/sdk/src/asset.rs index 4e213e3fd..f36e789fc 100644 --- a/sdk/src/asset.rs +++ b/sdk/src/asset.rs @@ -73,8 +73,7 @@ mod tests { } assert!(asset.manifest.is_some()); if let Some(manifest) = &asset.manifest { - let cc = ContentCredential::from_stream( - &context, + let cc = ContentCredential::new(&context).open_stream( "application/c2pa", std::io::Cursor::new(manifest), ) @@ -84,7 +83,7 @@ mod tests { let mut output = Cursor::new(Vec::new()); Asset::write_stream(&mut asset, &mut output, "image/jpeg").unwrap(); - let cc = ContentCredential::from_stream(&context, "image/jpeg", &mut output).unwrap(); + let cc = ContentCredential::new(&context).open_stream("image/jpeg", &mut output).unwrap(); println!("manifest: {:?}", cc); } } diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index a982dbcd2..f1b070666 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -18,6 +18,7 @@ use std::{ use serde::{Deserialize, Serialize}; use serde_json::Value; +use serde_with::skip_serializing_none; use crate::{ assertion::AssertionBase, @@ -35,9 +36,12 @@ use crate::{ }; /// This Generates the standard Reader output format for a Store +#[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Default)] pub struct StandardStoreReport { - #[serde(skip_serializing_if = "Option::is_none")] + title: Option, + format: Option, + instance_id: Option, /// A label for the active (most recent) manifest in the store active_manifest: Option, @@ -80,6 +84,7 @@ impl StandardStoreReport { active_manifest, manifests, validation_results, + ..Default::default() }) } } @@ -123,17 +128,6 @@ impl<'a> ContentCredential<'a> { Ok(self) } - /// creates a content credential from an existing stream - pub fn from_stream( - context: &'a Context, - format: &str, - mut stream: impl Read + Seek + Send, - ) -> Result { - let mut cr = Self::new(context); - stream.rewind()?; - cr.add_ingredient_from_stream(Relationship::ParentOf, format, &mut stream)?; - Ok(cr) - } fn parent_ingredient(&self) -> Option { for i in self.claim.ingredient_assertions() { @@ -258,7 +252,7 @@ impl std::fmt::Display for ContentCredential<'_> { impl std::fmt::Debug for ContentCredential<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let value = self.detailed_value().map_err(|_| std::fmt::Error)?; + let value = self.reader_value().map_err(|_| std::fmt::Error)?; f.write_str( serde_json::to_string_pretty(&value) .map_err(|_| std::fmt::Error)? From e3de30369844d19d0ea5051c654b48295ef513da Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Fri, 21 Nov 2025 11:06:22 -0800 Subject: [PATCH 25/26] formatting --- sdk/src/asset.rs | 12 ++++++------ sdk/src/content_credential.rs | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/sdk/src/asset.rs b/sdk/src/asset.rs index f36e789fc..7676a41db 100644 --- a/sdk/src/asset.rs +++ b/sdk/src/asset.rs @@ -73,17 +73,17 @@ mod tests { } assert!(asset.manifest.is_some()); if let Some(manifest) = &asset.manifest { - let cc = ContentCredential::new(&context).open_stream( - "application/c2pa", - std::io::Cursor::new(manifest), - ) - .unwrap(); + let cc = ContentCredential::new(&context) + .open_stream("application/c2pa", std::io::Cursor::new(manifest)) + .unwrap(); println!("manifest: {:?}", cc); } let mut output = Cursor::new(Vec::new()); Asset::write_stream(&mut asset, &mut output, "image/jpeg").unwrap(); - let cc = ContentCredential::new(&context).open_stream("image/jpeg", &mut output).unwrap(); + let cc = ContentCredential::new(&context) + .open_stream("image/jpeg", &mut output) + .unwrap(); println!("manifest: {:?}", cc); } } diff --git a/sdk/src/content_credential.rs b/sdk/src/content_credential.rs index f1b070666..9d1346bd6 100644 --- a/sdk/src/content_credential.rs +++ b/sdk/src/content_credential.rs @@ -128,7 +128,6 @@ impl<'a> ContentCredential<'a> { Ok(self) } - fn parent_ingredient(&self) -> Option { for i in self.claim.ingredient_assertions() { if let Ok(ingredient) = Ingredient::from_assertion(i.assertion()) { From 007e8f59c92da18dfeb57e911a8e2e2ff9b25f84 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Mon, 24 Nov 2025 19:43:29 -0800 Subject: [PATCH 26/26] fmt --- sdk/src/settings/signer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/src/settings/signer.rs b/sdk/src/settings/signer.rs index e9a2ee100..b3bc71ff5 100644 --- a/sdk/src/settings/signer.rs +++ b/sdk/src/settings/signer.rs @@ -141,8 +141,7 @@ impl SignerSettings { cawg_sign_cert, cawg_private_key, cawg_tsa_url, - cawg_referenced_assertions: cawg_referenced_assertions - .unwrap_or_default(), + cawg_referenced_assertions: cawg_referenced_assertions.unwrap_or_default(), }; Ok(Box::new(cawg_dual_signer))