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