c2pa-rs

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

commit e2627a9bfccbe7f825206e1c6d6b19f275defe7d
parent 8f555302c2be26f258b3b9019cdd01f88056e447
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Thu, 18 May 2023 12:49:47 -0700

(MINOR) Improved Remote Manifest handling (#250)

* return manifest.inaccessible validation_status
for ingredients with url instead of Error::RemoteManifestUrl(url)

* Add Ingredient::from_manifest_and_asset_bytes_async
Ingredient::from_manifest_and_asset_stream_async

* call async validatator in from_manifest_and_asset_stream_async
Diffstat:
Msdk/src/ingredient.rs | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msdk/src/store.rs | 33++++++++++++++++++++++++++++-----
2 files changed, 174 insertions(+), 10 deletions(-)

diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs @@ -504,6 +504,20 @@ impl Ingredient { | Err(Error::ProvenanceMissing) | Err(Error::UnsupportedType) => Ok(()), // no claims but valid file Err(Error::BadParam(desc)) if desc == *"unrecognized file type" => Ok(()), + Err(Error::RemoteManifestUrl(url)) => { + let status = ValidationStatus::new(validation_status::MANIFEST_INACCESSIBLE) + .set_url(url) + .set_explanation("Remote manifest not fetched".to_string()); + self.validation_status = Some(vec![status]); + Ok(()) + } + Err(Error::RemoteManifestFetch(url)) => { + let status = ValidationStatus::new(validation_status::MANIFEST_INACCESSIBLE) + .set_url(url) + .set_explanation("Unable to fetch remote manifest".to_string()); + self.validation_status = Some(vec![status]); + Ok(()) + } Err(e) => { // we can ignore the error here because it should have a log entry corresponding to it debug!("ingredient {:?}", e); @@ -719,7 +733,7 @@ impl Ingredient { let mut validation_log = DetailedStatusTracker::new(); // retrieve the manifest bytes from embedded, sidecar or remote and convert to store if found - let (result, manifest_bytes) = match load_jumbf_from_stream(format, stream) { + let (result, manifest_bytes) = match Store::load_jumbf_from_stream(format, stream) { Ok(manifest_bytes) => { ( // generate a store from the buffer and then validate from the asset path @@ -967,6 +981,88 @@ impl Ingredient { self.resources.set_base_path(base_path.as_ref()); Ok(self) } + + /// Asynchronously create an Ingredient from a binary manifest (.c2pa) and asset bytes + /// + /// # Example: Create an Ingredient from a binary manifest (.c2pa) and asset bytes + /// ``` + /// use c2pa::{Result, Ingredient}; + /// + /// # fn main() -> Result<()> { + /// # async { + /// let asset_bytes = include_bytes!("../tests/fixtures/cloud.jpg"); + /// let manifest_bytes = include_bytes!("../tests/fixtures/cloud_manifest.c2pa"); + /// + /// let ingredient = Ingredient::from_manifest_and_asset_bytes_async(manifest_bytes.to_vec(), "image/jpeg", asset_bytes) + /// .await + /// .unwrap(); + /// + /// println!("{}", ingredient); + /// # }; + /// # + /// # Ok(()) + /// } + /// ``` + pub async fn from_manifest_and_asset_bytes_async<M: Into<Vec<u8>>>( + manifest_bytes: M, + format: &str, + asset_bytes: &[u8], + ) -> Result<Self> { + let mut stream = Cursor::new(asset_bytes); + Self::from_manifest_and_asset_stream_async(manifest_bytes, format, &mut stream).await + } + + /// Asynchronously create an Ingredient from a binary manifest (.c2pa) and asset + pub async fn from_manifest_and_asset_stream_async<M: Into<Vec<u8>>>( + manifest_bytes: M, + format: &str, + stream: &mut dyn CAIRead, + ) -> Result<Self> { + let mut ingredient = Self::from_stream_info(stream, format, "untitled"); + stream.rewind()?; + + let mut validation_log = DetailedStatusTracker::new(); + + let manifest_bytes: Vec<u8> = manifest_bytes.into(); + // generate a store from the buffer and then validate from the asset path + let result = match Store::from_jumbf(&manifest_bytes, &mut validation_log) { + Ok(store) => { + // verify the store + //todo, change this when we have a stream version of verify + let mut buf: Vec<u8> = Vec::new(); + stream.rewind()?; + stream.read_to_end(&mut buf).map_err(Error::IoError)?; + Store::verify_store_async(&store, &buf, &mut validation_log) + .await + .map(|_| store) + } + Err(e) => { + // add a log entry for the error so we act like verify + validation_log.log_silent( + log_item!("asset", "error loading file", "Ingredient::from_file").set_error(&e), + ); + Err(e) + } + }; + + // set validation status from result and log + ingredient.update_validation_status(result, Some(manifest_bytes), &mut validation_log)?; + + // create a thumbnail if we don't already have a manifest with a thumb we can use + #[cfg(feature = "add_thumbnails")] + if ingredient.thumbnail.is_none() { + stream.rewind()?; + match crate::utils::thumbnail::make_thumbnail_from_stream(format, stream) { + Ok((format, image)) => { + ingredient.set_thumbnail(format, image)?; + } + Err(err) => { + log::warn!("Could not create thumbnail. {err}"); + } + } + } + Ok(ingredient) + } } impl std::fmt::Display for Ingredient { @@ -1148,10 +1244,57 @@ mod tests { assert_eq!(ingredient.format(), format); #[cfg(feature = "add_thumbnails")] assert!(ingredient.thumbnail().is_some()); - //assert!(ingredient.provenance().is_some()); assert!(ingredient.manifest_data().is_some()); assert!(ingredient.metadata().is_none()); - //assert!(ingredient.validation_status().is_none()); + assert!(ingredient.validation_status().is_some()); + assert_eq!( + ingredient.validation_status().unwrap()[0].code(), + validation_status::ASSERTION_DATAHASH_MISMATCH + ); + } + + #[cfg_attr(not(target_arch = "wasm32"), actix::test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + async fn test_jpg_cloud_from_memory() { + let image_bytes = include_bytes!("../tests/fixtures/cloud.jpg"); + let format = "image/jpeg"; + let ingredient = Ingredient::from_memory_async(format, image_bytes) + .await + .expect("from_memory_async"); + //println!("ingredient = {ingredient}"); + assert!(ingredient.validation_status().is_some()); + assert_eq!( + ingredient.validation_status().unwrap()[0].code(), + validation_status::MANIFEST_INACCESSIBLE + ); + assert!(ingredient.validation_status().unwrap()[0] + .url() + .unwrap() + .starts_with("http")); + assert!(ingredient.manifest_data().is_none()); + } + + #[cfg_attr(not(target_arch = "wasm32"), actix::test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + async fn test_jpg_cloud_from_memory_and_manifest() { + let asset_bytes = include_bytes!("../tests/fixtures/cloud.jpg"); + let manifest_bytes = include_bytes!("../tests/fixtures/cloud_manifest.c2pa"); + let format = "image/jpeg"; + let ingredient = Ingredient::from_manifest_and_asset_bytes_async( + manifest_bytes.to_vec(), + format, + asset_bytes, + ) + .await + .unwrap(); + #[cfg(target_arch = "wasm32")] + web_sys::console::debug_2( + &"ingredient_from_memory_async:".into(), + &ingredient.to_string().into(), + ); + assert!(ingredient.validation_status().is_none()); + assert!(ingredient.manifest_data().is_some()); + assert!(ingredient.provenance().is_some()); } } @@ -1350,7 +1493,6 @@ mod tests_file_io { assert!(ingredient.manifest_data().is_some()); } - /* this test cannot succeed because memory loading path does not support validation status at the moment #[test] #[cfg(feature = "fetch_remote_manifests")] fn test_jpg_cloud_failure() { @@ -1363,7 +1505,6 @@ mod tests_file_io { validation_status::MANIFEST_INACCESSIBLE ); } - */ #[test] #[cfg(feature = "file_io")] diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -42,8 +42,8 @@ use crate::{ labels::{ASSERTIONS, CREDENTIALS, SIGNATURE}, }, jumbf_io::{ - get_assetio_handler, load_jumbf_from_memory, object_locations_from_stream, - save_jumbf_to_memory, save_jumbf_to_stream, + get_assetio_handler, load_jumbf_from_memory, load_jumbf_from_stream, + object_locations_from_stream, save_jumbf_to_memory, save_jumbf_to_stream, }, status_tracker::{log_item, OneShotStatusTracker, StatusTracker}, utils::{ @@ -2210,9 +2210,7 @@ impl Store { code, resp.status_text() ))), - Err(uError::Transport(_)) => Err(Error::RemoteManifestFetch(format!( - "fetch failed: url: {url}" - ))), + Err(uError::Transport(_)) => Err(Error::RemoteManifestFetch(url.to_string())), } } @@ -2245,6 +2243,31 @@ impl Store { } } + /// load jumbf given a stream + /// + /// This handles, embedded and remote manifests + /// + /// asset_type - mime type of the stream + /// stream - a readable stream of an asset + pub fn load_jumbf_from_stream(asset_type: &str, stream: &mut dyn CAIRead) -> Result<Vec<u8>> { + match load_jumbf_from_stream(asset_type, stream) { + Ok(manifest_bytes) => Ok(manifest_bytes), + Err(Error::JumbfNotFound) => { + stream.rewind()?; + if let Some(ext_ref) = + crate::utils::xmp_inmemory_utils::XmpInfo::from_source(stream, asset_type) + .provenance + { + // return an error with the url that should be read + Err(Error::RemoteManifestUrl(ext_ref)) + } else { + Err(Error::JumbfNotFound) + } + } + Err(e) => Err(e), + } + } + /// load jumbf given a file path /// /// This handles, embedded, sidecar and remote manifests