diff --git a/Cargo.lock b/Cargo.lock index bf0075ee5..94440b905 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -850,13 +850,14 @@ version = "0.5.0" [[package]] name = "c2patool" -version = "0.13.1" +version = "0.13.2" dependencies = [ "anyhow", "assert_cmd", "atree", "c2pa", "c2pa-crypto", + "cawg-identity", "clap", "env_logger", "glob", @@ -871,6 +872,7 @@ dependencies = [ "serde_derive", "serde_json", "tempfile", + "tokio", "treeline", "url", ] diff --git a/Makefile b/Makefile index e3f8b0a81..9b54d8294 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,8 @@ test-wasm: test-wasm-web: cd sdk && wasm-pack test --chrome --headless -- --features="serialize_thumbnails" +test-no-wasm: check-format check-docs clippy test-local + # Full local validation, build and test all features including wasm # Run this before pushing a PR to pre-validate test: check-format check-docs clippy test-local test-wasm-web diff --git a/cawg_identity/src/claim_aggregation/ica_signature_verifier.rs b/cawg_identity/src/claim_aggregation/ica_signature_verifier.rs index 7b4fc2ff0..6da3df3d8 100644 --- a/cawg_identity/src/claim_aggregation/ica_signature_verifier.rs +++ b/cawg_identity/src/claim_aggregation/ica_signature_verifier.rs @@ -153,8 +153,6 @@ impl SignatureVerifier for IcaSignatureVerifier { )); }; - dbg!(&jwk_prop); - // OMG SO HACKY! let Ok(jwk_json) = serde_json::to_string_pretty(jwk_prop) else { return Err(ValidationError::SignatureError( diff --git a/cawg_identity/src/claim_aggregation/w3c_vc/did_web.rs b/cawg_identity/src/claim_aggregation/w3c_vc/did_web.rs index 4dbe6ac71..6f0e57375 100644 --- a/cawg_identity/src/claim_aggregation/w3c_vc/did_web.rs +++ b/cawg_identity/src/claim_aggregation/w3c_vc/did_web.rs @@ -68,8 +68,6 @@ pub(crate) async fn resolve(did: &Did<'_>) -> Result { let method_specific_id = did.method_specific_id(); - dbg!(method_specific_id); - let url = to_url(method_specific_id)?; // TODO: https://w3c-ccg.github.io/did-method-web/#in-transit-security diff --git a/cawg_identity/src/claim_aggregation/w3c_vc/serialization.rs b/cawg_identity/src/claim_aggregation/w3c_vc/serialization.rs index 2ea80288d..21fe2a5dd 100644 --- a/cawg_identity/src/claim_aggregation/w3c_vc/serialization.rs +++ b/cawg_identity/src/claim_aggregation/w3c_vc/serialization.rs @@ -57,8 +57,6 @@ pub(crate) mod one_or_many { where M: de::MapAccess<'de>, { - eprintln!("Yo!"); - let one = Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?; Ok(nev!(one)) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index da47633d4..f25834193 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -22,6 +22,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(test)'] } [dependencies] anyhow = "1.0" atree = "0.5.2" +cawg-identity = { path = "../cawg_identity", version = "0.6.0" } c2pa = { path = "../sdk", version = "0.45.2", features = [ "fetch_remote_manifests", "file_io", @@ -37,6 +38,7 @@ serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0" serde_json = "1.0" tempfile = "3.3" +tokio = { version = "1.42", features = ["full"] } treeline = "0.1.0" pem = "3.0.3" openssl = { version = "0.10.61", features = ["vendored"] } diff --git a/cli/src/decorators/cawg_decorator.rs b/cli/src/decorators/cawg_decorator.rs new file mode 100644 index 000000000..f6ed6ebca --- /dev/null +++ b/cli/src/decorators/cawg_decorator.rs @@ -0,0 +1,228 @@ +// 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 c2pa::Reader; +use cawg_identity::{claim_aggregation::IcaSignatureVerifier, IdentityAssertion}; +use serde_json::{Map, Value}; +use tokio::runtime::Runtime; + +/// Update/decorate the displayed JSON assertions for a more human-readable JSON output. +pub(crate) fn decorate_cawg_assertion_from_detailed_report( + reader: &Reader, + json_content: &mut Value, + tokio_runtime: &Runtime, +) -> Result<(), anyhow::Error> { + if let Value::Object(map) = json_content { + // Iterate over the key-value pairs + for (key, value) in &mut *map { + // Get additional CAWG details + + // Get the assertions as array from the JSON + let assertions = match value.get_mut("assertion_store") { + Some(assertions) => assertions, + None => { + return Err(anyhow::Error::msg( + "Could not parse JSON assertions as object", + )); + } + }; + + let cawg_assertion = match assertions.get_mut("cawg.identity") { + Some(cawg_assertion) => cawg_assertion, + None => { + return Err(anyhow::Error::msg( + "Could not parse CAWG identity details from assertion store", + )); + } + }; + + let holding_manifest = match reader.get_manifest(key) { + Some(holding_manifest) => holding_manifest, + None => { + return Err(anyhow::Error::msg( + "Could not recover manifest holding CAWG data", + )); + } + }; + + let parsed_cawg_json_string = + match get_cawg_details_for_manifest(holding_manifest, tokio_runtime) { + Some(parsed_cawg_json_string) => parsed_cawg_json_string, + None => { + // Not a show-stopper: + // Could not parse CAWG details for manifest, + // so leaving original raw data unformatted. + return Ok(()); + } + }; + + cawg_assertion["signature"] = match serde_json::from_str(&parsed_cawg_json_string) { + Ok(decoded_cawg_assertion) => decoded_cawg_assertion, + Err(err) => { + return Err(anyhow::Error::msg(err.to_string())); + } + }; + + let cawg_assertion = match cawg_assertion.as_object_mut() { + Some(cawg_assertion) => cawg_assertion, + None => { + return Err(anyhow::Error::msg( + "Could not parse CAWG assertion data as object to decorate for display", + )); + } + }; + cawg_assertion.remove("pad1"); + cawg_assertion.remove("pad2"); + } + } + + Ok(()) +} + +/// Update/decorate the displayed CAWG assertion for a more human-readable JSON output. +pub(crate) fn decorate_json_cawg_assertions( + holding_manifest: &c2pa::Manifest, + assertion: &mut Value, + tokio_runtime: &Runtime, +) -> Result<(), anyhow::Error> { + let parsed_cawg_json_string = + match get_cawg_details_for_manifest(holding_manifest, tokio_runtime) { + Some(parsed_cawg_json_string) => parsed_cawg_json_string, + None => { + // Could not parse CAWG details for manifest (leaving original raw data unformatted). + // Not a fatal failure, so leaving raw data unformatted. + return Ok(()); + } + }; + + // Let's look at the assertion data + let assertion_data = match assertion.get_mut("data") { + Some(assertion_data) => assertion_data, + None => { + return Err(anyhow::Error::msg("Could not parse CAWG assertion data")); + } + }; + + // Update signature with parsed content + let parsed_signature = match serde_json::from_str(&parsed_cawg_json_string) { + Ok(parsed_signature) => parsed_signature, + Err(err) => { + return Err(anyhow::Error::msg(err.to_string())); + } + }; + assertion_data["signature"] = parsed_signature; + + // We don't need to show the padding fields either + let assertion_data_map = match assertion_data.as_object_mut() { + Some(assertion_data_map) => assertion_data_map, + None => { + return Err(anyhow::Error::msg( + "Could not parse CAWG assertion data as object", + )); + } + }; + assertion_data_map.remove("pad1"); + assertion_data_map.remove("pad2"); + + Ok(()) +} + +/// Parse additional CAWG details from the manifest store to update displayed results. +/// As CAWG mostly async, this will block on network requests for checks using a tokio runtime. +fn get_cawg_details_for_manifest( + manifest: &c2pa::Manifest, + tokio_runtime: &Runtime, +) -> Option { + let ia_iter = IdentityAssertion::from_manifest(manifest); + + // TODO: Determine what should happen when multiple identities are reported (currently only 1 is supported) + let mut parsed_cawg_json = String::new(); + + ia_iter.for_each(|ia| { + let identity_assertion = match ia { + Ok(ia) => ia, + Err(err) => { + eprintln!("Could not parse CAWG identity assertion: {:?}", err); + return; + } + }; + + let isv = IcaSignatureVerifier {}; + let ica_validated = tokio_runtime.block_on(identity_assertion.validate(manifest, &isv)); + let ica = match ica_validated { + Ok(ica) => ica, + Err(err) => { + eprintln!("Could not validate CAWG identity assertion: {:?}", err); + return; + } + }; + + parsed_cawg_json = match serde_json::to_string(&ica) { + Ok(parsed_cawg_json) => parsed_cawg_json, + Err(err) => { + eprintln!( + "Could not parse CAWG identity claims aggregation details for manifest: {:?}", + err + ); + return; + } + }; + }); + + if parsed_cawg_json.is_empty() { + return None; + } + + // Get the JSON as mutable, so we can further parse and format CAWG data + let maybe_map = serde_json::from_str(parsed_cawg_json.as_str()); + let mut map: Map = match maybe_map { + Ok(map) => map, + Err(err) => { + eprintln!( + "Could not parse convert CAWG identity claims details to JSON string map: {:?}", + err + ); + return None; + } + }; + + // Get the credentials subject information... + let credentials_subject_maybe = map.get_mut("credentialSubject"); + let credentials_subject = match credentials_subject_maybe { + Some(credentials_subject) => credentials_subject, + None => { + eprintln!("Could not find credential subject in CAWG details for manifest"); + return None; + } + }; + let credentials_subject_as_obj = credentials_subject.as_object_mut(); + let credential_subject_details = match credentials_subject_as_obj { + Some(credentials_subject) => credentials_subject, + None => { + eprintln!("Could not parse credential subject as object in CAWG details for manifest"); + return None; + } + }; + // As per design CAWG has some repetition between assertion an signature (c2paAsset field) + // so we remove the c2paAsset field from the credential subject details too + credential_subject_details.remove("c2paAsset"); + + // return the for-display json-formatted string + let serialized_content = serde_json::to_string(&map); + match serialized_content { + Ok(serialized_content) => Some(serialized_content), + Err(err) => { + eprintln!("Could not parse CAWG details for manifest: {:?}", err); + None + } + } +} diff --git a/cli/src/decorators/mod.rs b/cli/src/decorators/mod.rs new file mode 100644 index 000000000..4e1d1963f --- /dev/null +++ b/cli/src/decorators/mod.rs @@ -0,0 +1,13 @@ +// 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. + +pub mod cawg_decorator; diff --git a/cli/src/display_decorator.rs b/cli/src/display_decorator.rs new file mode 100644 index 000000000..01898f70b --- /dev/null +++ b/cli/src/display_decorator.rs @@ -0,0 +1,177 @@ +// 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::convert::TryInto; + +use c2pa::Reader; +use serde_json::{Map, Value}; +use tokio::runtime::Runtime; + +// the decorators we know about an plan to use +use crate::decorators::cawg_decorator::{ + decorate_cawg_assertion_from_detailed_report, decorate_json_cawg_assertions, +}; + +// Display decorators for JSON output/display +// We do not mutate any Reader here, only make JSON output "nicer", +// by doing some additional parsing or clean-ups. + +/// Update/decorate the displayed JSON assertions for a more human-readable JSON output. +fn decorate_json_assertions( + reader: &Reader, + json_content: &mut Value, + tokio_runtime: &Runtime, +) -> Result<(), anyhow::Error> { + if let Value::Object(map) = json_content { + for (key, value) in &mut *map { + // extract manifest we're looking at + let current_manifest = reader.get_manifest(key); + let current_manifest = match current_manifest { + Some(current_manifest) => current_manifest, + None => { + return Err(anyhow::Error::msg( + "Could not recover manifest for assertion decoration", + )); + } + }; + + // Get the assertions as array from the JSON + let assertions = match value.get_mut("assertions") { + Some(assertions) => assertions, + None => { + return Err(anyhow::Error::msg( + "Could not parse JSON assertions as object", + )); + } + }; + let assertions_array = match assertions.as_array_mut() { + Some(assertions_array) => assertions_array, + None => { + return Err(anyhow::Error::msg( + "Could not parse JSON assertions as array", + )); + } + }; + + // Loop over the assertions to process those of interest + for assertion in assertions_array { + let label = match assertion.get("label") { + Some(label) => label.to_string(), + None => { + return Err(anyhow::Error::msg("Could not parse assertion label")); + } + }; + + // here we decorate something: for CAWG assertions, further parse the signature + if label.contains("cawg.identity") { + decorate_json_cawg_assertions(current_manifest, assertion, tokio_runtime)?; + } + } + } + } + + Ok(()) +} + +/// Update/decorate the detailed displayed JSON string for a more human-readable JSON output. +pub(crate) fn decorate_json_detailed_display( + reader: &Reader, + tokio_runtime: &Runtime, +) -> Result { + let extracted_report = format!("{:#?}", reader); + + let mut report_json_map: Map = match serde_json::from_str(&extracted_report) { + Ok(report_json_map) => report_json_map, + Err(err) => { + let message = format!("Could not parse extracted JSON detailed report: {:?}", err); + return Err(anyhow::Error::msg(message)); + } + }; + + let manifests = match report_json_map.get_mut("manifests") { + Some(manifests) => manifests, + None => { + return Err(anyhow::Error::msg( + "No parsable JSON in manifest store (key: manifests)", + )); + } + }; + + // Here we decorate something: Update assertion with more details (eg. for CAWG) + match decorate_cawg_assertion_from_detailed_report(reader, manifests, tokio_runtime) { + Ok(_) => (), + Err(err) => { + let message = format!("Could not decorate detailed JSON for display: {:?}", err); + return Err(anyhow::Error::msg(message)); + } + }; + + // return decorated detailed JSON to display + match serde_json::to_string_pretty(&report_json_map) { + Ok(decorated_result) => Ok(decorated_result), + Err(err) => { + let message = format!( + "Could not decorate displayed detailed JSON with additional details: {:?}", + err + ); + Err(anyhow::Error::msg(message)) + } + } +} + +/// Update/decorate the displayed JSON string for a more human-readable JSON output. +pub(crate) fn decorate_json_display( + reader: &Reader, + tokio_runtime: &Runtime, +) -> Result { + let mut reader_content: serde_json::Map = match reader.try_into() { + Ok(mapped_json) => mapped_json, + Err(_) => { + return Err(anyhow::Error::msg( + "Could not parse manifest store JSON content", + )); + } + }; + + let manifests_json_content = match reader_content.get_mut("manifests") { + Some(json) => json, + None => { + return Err(anyhow::Error::msg( + "No JSON to parse in manifest store (key: manifests)", + )); + } + }; + + // Update assertion with more details (eg. for CAWG) + match decorate_json_assertions(reader, manifests_json_content, tokio_runtime) { + Ok(_) => (), + Err(err) => { + let message = format!( + "Could not decorate displayed JSON with additional details: {:?}", + err + ); + return Err(anyhow::Error::msg(message)); + } + }; + + // return decorated JSON to display + match serde_json::to_string_pretty(&reader_content) { + Ok(decorated_result) => Ok(decorated_result), + Err(err) => { + let message = format!( + "Could not decorate displayed JSON with additional details: {:?}", + err + ); + Err(anyhow::Error::msg(message)) + } + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 1c3d56203..ac194915a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -31,10 +31,12 @@ use clap::{Parser, Subcommand}; use log::debug; use serde::Deserialize; use signer::SignConfig; +use tokio::runtime::Runtime; use url::Url; use crate::{ callback_signer::{CallbackSigner, CallbackSignerConfig, ExternalProcessRunner}, + display_decorator::{decorate_json_detailed_display, decorate_json_display}, info::info, }; @@ -42,6 +44,8 @@ mod info; mod tree; mod callback_signer; +mod decorators; +mod display_decorator; mod signer; /// Tool for displaying and creating C2PA manifests. @@ -464,6 +468,9 @@ fn main() -> Result<()> { // configure the SDK configure_sdk(&args).context("Could not configure c2pa-rs")?; + // configure tokio runtime for blocking operations + let tokio_runtime: Runtime = Runtime::new()?; + // Remove manifest needs to also remove XMP provenance // if args.remove_manifest { // match args.output { @@ -674,10 +681,15 @@ fn main() -> Result<()> { Ingredient::from_file(&args.path).map_err(special_errs)? ) } else if args.detailed { - println!( - "{:#?}", - Reader::from_file(&args.path).map_err(special_errs)? - ) + let reader = Reader::from_file(&args.path).map_err(special_errs)?; + let decorated_details_manifest = decorate_json_detailed_display(&reader, &tokio_runtime); + match decorated_details_manifest { + Ok(decorated_details_manifest) => println!("{}", decorated_details_manifest), + Err(_) => { + // non-fatal: fall back to raw debug display of data (eg. unparsed CAWG) + println!("{:#?}", reader) + } + }; } else if let Some(Commands::Fragment { fragments_glob: Some(fg), }) = &args.command @@ -689,7 +701,14 @@ fn main() -> Result<()> { println!("{} Init manifests validated", stores.len()); } } else { - println!("{}", Reader::from_file(&args.path).map_err(special_errs)?) + let reader: Reader = Reader::from_file(&args.path).map_err(special_errs)?; + match decorate_json_display(&reader, &tokio_runtime) { + Ok(stringified_decorated_json) => println!("{}", stringified_decorated_json), + Err(_) => { + // non-fatal: fall back to raw display of data (eg. unparsed CAWG) + println!("{:?}", reader) + } + }; } Ok(()) diff --git a/cli/tests/fixtures/C_with_CAWG_data.jpg b/cli/tests/fixtures/C_with_CAWG_data.jpg new file mode 100644 index 000000000..4088d9900 Binary files /dev/null and b/cli/tests/fixtures/C_with_CAWG_data.jpg differ diff --git a/cli/tests/integration.rs b/cli/tests/integration.rs index 04fd18f52..4eabe8aa7 100644 --- a/cli/tests/integration.rs +++ b/cli/tests/integration.rs @@ -36,6 +36,7 @@ fn temp_path(name: &str) -> PathBuf { create_dir_all(&path).ok(); path.join(name) } + #[test] fn tool_not_found() -> Result<(), Box> { let mut cmd = Command::cargo_bin("c2patool")?; @@ -43,6 +44,7 @@ fn tool_not_found() -> Result<(), Box> { cmd.assert().failure().stderr(str::contains("os error")); Ok(()) } + #[test] fn tool_not_found_info() -> Result<(), Box> { let mut cmd = Command::cargo_bin("c2patool")?; @@ -52,6 +54,7 @@ fn tool_not_found_info() -> Result<(), Box> { .stderr(str::contains("file not found")); Ok(()) } + #[test] fn tool_jpeg_no_report() -> Result<(), Box> { let mut cmd = Command::cargo_bin("c2patool")?; @@ -93,6 +96,7 @@ fn tool_embed_jpeg_report() -> Result<(), Box> { .stdout(str::contains("My Title")); Ok(()) } + #[test] fn tool_fs_output_report() -> Result<(), Box> { let path = temp_path("output_dir"); @@ -120,6 +124,7 @@ fn tool_fs_output_report() -> Result<(), Box> { ); Ok(()) } + #[test] fn tool_fs_output_report_supports_detailed_flag() -> Result<(), Box> { let path = temp_path("./output_detailed"); @@ -144,6 +149,7 @@ fn tool_fs_output_report_supports_detailed_flag() -> Result<(), Box> .is_some()); Ok(()) } + #[test] fn tool_fs_output_fails_when_output_exists() -> Result<(), Box> { let path = temp_path("./output_conflict"); @@ -160,6 +166,7 @@ fn tool_fs_output_fails_when_output_exists() -> Result<(), Box> { )); Ok(()) } + #[test] // c2patool tests/fixtures/C.jpg -fo target/tmp/manifest_test fn tool_test_manifest_folder() -> Result<(), Box> { @@ -180,6 +187,7 @@ fn tool_test_manifest_folder() -> Result<(), Box> { assert!(json.contains("make_test_images")); Ok(()) } + #[test] // c2patool tests/fixtures/C.jpg -ifo target/tmp/ingredient_test fn tool_test_ingredient_folder() -> Result<(), Box> { @@ -199,6 +207,7 @@ fn tool_test_ingredient_folder() -> Result<(), Box> { assert!(json.contains("manifest_data")); Ok(()) } + #[test] // c2patool tests/fixtures/C.jpg -ifo target/tmp/ingredient_json // c2patool tests/fixtures/earth_apollo17.jpg -m sample/test.json -p target/tmp/ingredient_json/ingredient.json -fo target/tmp/out_2.jpg @@ -230,6 +239,7 @@ fn tool_test_manifest_ingredient_json() -> Result<(), Box .stdout(str::contains("My Title")); Ok(()) } + #[test] // c2patool tests/fixtures/earth_apollo17.jpg -m tests/fixtures/ingredient_test.json -fo target/tmp/ingredients.jpg fn tool_embed_jpeg_with_ingredients_report() -> Result<(), Box> { @@ -248,6 +258,7 @@ fn tool_embed_jpeg_with_ingredients_report() -> Result<(), Box> { .stdout(str::contains("earth_apollo17.jpg")); Ok(()) } + #[test] fn tool_extensions_do_not_match() -> Result<(), Box> { let path = temp_path("./foo.png"); @@ -262,6 +273,7 @@ fn tool_extensions_do_not_match() -> Result<(), Box> { .stderr(str::contains("Output type must match source type")); Ok(()) } + #[test] fn tool_similar_extensions_match() -> Result<(), Box> { let path = temp_path("./similar.JpEg"); @@ -277,6 +289,7 @@ fn tool_similar_extensions_match() -> Result<(), Box> { .stdout(str::contains("similar.")); Ok(()) } + #[test] fn tool_fail_if_thumbnail_missing() -> Result<(), Box> { Command::cargo_bin("c2patool")? @@ -534,3 +547,39 @@ fn tool_tree() -> Result<(), Box> { .stdout(str::contains("Assertion:c2pa.actions")); Ok(()) } + +#[test] +// c2patool C_with_CAWG_data.jpg +fn tool_read_image_with_cawg_data() -> Result<(), Box> { + Command::cargo_bin("c2patool")? + .arg(fixture_path("C_with_CAWG_data.jpg")) + .assert() + .success() + .stdout(str::contains("cawg.identity")) + .stdout(str::contains("credentialSubject")) + .stdout(str::contains("verifiedIdentities")) + .stdout(str::contains("credentialSchema")) + .stdout(str::contains("cawg.social_media")) + .stdout(str::contains("VerifiableCredential")) + .stdout(str::contains("IdentityClaimsAggregationCredential")); + Ok(()) +} + +#[test] +// c2patool --detailed C_with_CAWG_data.jpg +fn tool_read_image_with_details_with_cawg_data() -> Result<(), Box> { + Command::cargo_bin("c2patool")? + .arg(fixture_path("C_with_CAWG_data.jpg")) + .arg("--detailed") + .assert() + .success() + .stdout(str::contains("assertion_store")) + .stdout(str::contains("cawg.identity")) + .stdout(str::contains("credentialSubject")) + .stdout(str::contains("verifiedIdentities")) + .stdout(str::contains("credentialSchema")) + .stdout(str::contains("cawg.social_media")) + .stdout(str::contains("VerifiableCredential")) + .stdout(str::contains("IdentityClaimsAggregationCredential")); + Ok(()) +} diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 749e564a1..b9cb17479 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -115,7 +115,7 @@ serde_bytes = "0.11.5" serde_cbor = "0.11.1" serde_derive = "1.0.197" serde_json = { version = "1.0.117", features = ["preserve_order"] } -serde_with = "3.11.0" +serde_with = "3.12.0" serde-transcode = "1.1.1" sha1 = "0.10.6" sha2 = "0.10.6" diff --git a/sdk/src/hashed_uri.rs b/sdk/src/hashed_uri.rs index ce9e6de21..350103acc 100644 --- a/sdk/src/hashed_uri.rs +++ b/sdk/src/hashed_uri.rs @@ -112,7 +112,7 @@ mod tests { fn impl_clone() { let h = HashedUri::new( "self#jumbf=c2pa/urn:uuid:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.assertions/c2pa.hash.data".to_owned(), - Some("sha256".to_owned()), + Some("sha256".to_owned()), &hex!("53d1b2cf4e6d9a97ed9281183fa5d836c32751b9d2fca724b40836befee7d67f"), ); @@ -124,7 +124,7 @@ mod tests { fn impl_debug() { let h = HashedUri::new( "self#jumbf=c2pa/urn:uuid:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.assertions/c2pa.hash.data".to_owned(), - Some("sha256".to_owned()), + Some("sha256".to_owned()), &hex!("53d1b2cf4e6d9a97ed9281183fa5d836c32751b9d2fca724b40836befee7d67f"), ); @@ -135,7 +135,7 @@ mod tests { fn impl_display() { let h = HashedUri::new( "self#jumbf=c2pa/urn:uuid:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.assertions/c2pa.hash.data".to_owned(), - Some("sha256".to_owned()), + Some("sha256".to_owned()), &hex!("53d1b2cf4e6d9a97ed9281183fa5d836c32751b9d2fca724b40836befee7d67f"), ); diff --git a/sdk/src/manifest_store.rs b/sdk/src/manifest_store.rs index 15d2882d3..a0119145e 100644 --- a/sdk/src/manifest_store.rs +++ b/sdk/src/manifest_store.rs @@ -589,6 +589,10 @@ impl std::fmt::Display for ManifestStore { } json = b64_tag(json, "hash"); + + // list of tags to omit (padding tags) + // Reason of padding, see note at: + // https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_going_back_and_filling_in json = omit_tag(json, "pad"); f.write_str(&json) diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index fa650a884..d91ee3413 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -241,7 +241,7 @@ impl Reader { }) } - /// Get the manifest store as a JSON string + /// Get the manifest store as a JSON string. pub fn json(&self) -> String { self.manifest_store.to_string() } @@ -430,6 +430,24 @@ impl Default for Reader { } } +impl TryInto> for Reader { + type Error = crate::Error; + + /// Get the manifest store as a serde serialized JSON value map. + fn try_into(self) -> Result> { + serde_json::from_str(self.json().as_str()).map_err(crate::Error::JsonError) + } +} + +impl TryInto> for &Reader { + type Error = crate::Error; + + /// Get the manifest store as a serde serialized JSON value map. + fn try_into(self) -> Result> { + serde_json::from_str(self.json().as_str()).map_err(crate::Error::JsonError) + } +} + /// Prints the JSON of the manifest data. impl std::fmt::Display for Reader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {