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 2d440a21313a35b5a8665e2556e33f52ba89edf7
parent 58e6094426c16565404837115e738ba792862ce9
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Thu, 12 Oct 2023 17:39:13 -0700

Adds embed_to_stream (#313)

* Adds embed_to_stream with output stream and returning c2pa_data
Fixes a bug in embed_stream where missing thumbnails were not reported

* Deprecate embed_stream

* A whole lot of clippy update fixes
Update to use nightly clippy in make test
Diffstat:
MMakefile | 2+-
Msdk/src/asn1/rfc3281.rs | 2+-
Msdk/src/asn1/rfc5652.rs | 6+++---
Msdk/src/assertions/ingredient.rs | 2+-
Msdk/src/asset_handlers/bmff_io.rs | 2+-
Msdk/src/asset_handlers/tiff_io.rs | 2+-
Msdk/src/ingredient.rs | 10+++++-----
Msdk/src/manifest.rs | 88++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msdk/src/manifest_store.rs | 27++++++++++++---------------
Msdk/src/manifest_store_report.rs | 6+++---
Msdk/src/resource_store.rs | 4+---
Msdk/src/store.rs | 2+-
Msdk/src/time_stamp.rs | 2+-
Msdk/src/utils/hash_utils.rs | 4++--
Msdk/src/validation_status.rs | 2+-
15 files changed, 101 insertions(+), 60 deletions(-)

diff --git a/Makefile b/Makefile @@ -20,7 +20,7 @@ check-docs: cargo doc --no-deps --workspace --all-features clippy: - cargo clippy --all-features --all-targets -- -D warnings + cargo +nightly clippy --all-features --all-targets -- -D warnings test-local: cargo test --all-features diff --git a/sdk/src/asn1/rfc3281.rs b/sdk/src/asn1/rfc3281.rs @@ -69,7 +69,7 @@ pub struct AttributeCertificateInfo { } impl AttributeCertificateInfo { - pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, DecodeError<S::Error>> { + pub fn take_from<S: Source>(cons: &Constructed<S>) -> Result<Self, DecodeError<S::Error>> { Err(cons.content_err("AttributeCertificateInfo parsing not implemented")) } } diff --git a/sdk/src/asn1/rfc5652.rs b/sdk/src/asn1/rfc5652.rs @@ -159,7 +159,7 @@ pub struct SignedData { impl SignedData { /// Attempt to decode BER encoded bytes to a parsed data structure. pub fn decode_ber(data: &[u8]) -> Result<Self, DecodeError<std::convert::Infallible>> { - Constructed::decode(data, bcder::Mode::Ber, |cons| Self::decode(cons)) + Constructed::decode(data, bcder::Mode::Ber, Self::decode) } pub fn decode<S: Source>(cons: &mut Constructed<S>) -> Result<Self, DecodeError<S::Error>> { @@ -1118,7 +1118,7 @@ pub type KeyDerivationAlgorithmIdentifier = AlgorithmIdentifier; pub struct RevocationInfoChoices(Vec<RevocationInfoChoice>); impl RevocationInfoChoices { - pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, DecodeError<S::Error>> { + pub fn take_from<S: Source>(cons: &Constructed<S>) -> Result<Self, DecodeError<S::Error>> { Err(cons.content_err("RevocationInfoChoices parsing not implemented")) } } @@ -1232,7 +1232,7 @@ pub struct OtherCertificateFormat { } impl OtherCertificateFormat { - pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, DecodeError<S::Error>> { + pub fn take_from<S: Source>(cons: &Constructed<S>) -> Result<Self, DecodeError<S::Error>> { Err(cons.content_err("OtherCertificateFormat parsing not implemented")) } } diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs @@ -124,7 +124,7 @@ impl Ingredient { } pub fn add_review(mut self, review: ReviewRating) -> Self { - let metadata = self.metadata.unwrap_or_else(Metadata::new); + let metadata = self.metadata.unwrap_or_default(); self.metadata = Some(metadata.add_review(review)); self } diff --git a/sdk/src/asset_handlers/bmff_io.rs b/sdk/src/asset_handlers/bmff_io.rs @@ -354,7 +354,7 @@ fn add_token_to_cache(bmff_path_map: &mut HashMap<String, Vec<Token>>, path: Str } } -fn path_from_token(bmff_tree: &mut Arena<BoxInfo>, current_node_token: &Token) -> Result<String> { +fn path_from_token(bmff_tree: &Arena<BoxInfo>, current_node_token: &Token) -> Result<String> { let ancestors = current_node_token.ancestors(bmff_tree); let mut path = bmff_tree[*current_node_token].data.path.clone(); diff --git a/sdk/src/asset_handlers/tiff_io.rs b/sdk/src/asset_handlers/tiff_io.rs @@ -863,7 +863,7 @@ impl<T: Read + Write + Seek> TiffCloner<T> { fn clone_sub_files<R: Read + Seek>( &mut self, - tiff_tree: &mut Arena<ImageFileDirectory>, + tiff_tree: &Arena<ImageFileDirectory>, page: Token, asset_reader: &mut R, ) -> Result<HashMap<u16, Vec<u64>>> { diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs @@ -569,7 +569,7 @@ impl Ingredient { &mut self, result: Result<Store>, manifest_bytes: Option<Vec<u8>>, - validation_log: &mut impl StatusTracker, + validation_log: &impl StatusTracker, ) -> Result<()> { match result { Ok(store) => { @@ -736,7 +736,7 @@ impl Ingredient { }; // set validation status from result and log - ingredient.update_validation_status(result, manifest_bytes, &mut validation_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 if ingredient.thumbnail.is_none() { @@ -798,7 +798,7 @@ impl Ingredient { }; // set validation status from result and log - ingredient.update_validation_status(result, manifest_bytes, &mut validation_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")] @@ -871,7 +871,7 @@ impl Ingredient { }; // set validation status from result and log - ingredient.update_validation_status(result, manifest_bytes, &mut validation_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")] @@ -1206,7 +1206,7 @@ impl Ingredient { }; // set validation status from result and log - ingredient.update_validation_status(result, Some(manifest_bytes), &mut validation_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")] diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs @@ -29,7 +29,7 @@ use crate::{ assertions::{ labels, Actions, CreativeWork, DataHash, Exif, SoftwareAgent, Thumbnail, User, UserCbor, }, - asset_io::CAIRead, + asset_io::{CAIRead, CAIReadWrite}, claim::{Claim, RemoteManifest}, error::{Error, Result}, jumbf, @@ -1017,18 +1017,38 @@ impl Manifest { // todo:: see if we can pass a trait with to_vec support like we to for Strings let asset = asset.to_vec(); let mut stream = std::io::Cursor::new(asset); - - self.embed_stream(format, &mut stream, signer) + let mut output_stream = Cursor::new(Vec::new()); + self.embed_to_stream(format, &mut stream, &mut output_stream, signer)?; + Ok(output_stream.into_inner()) } /// Embed a signed manifest into a stream using a supplied signer. /// returns the bytes of the new asset + #[deprecated(since = "0.27.2", note = "use embed_to_stream instead")] pub fn embed_stream( &mut self, format: &str, stream: &mut dyn CAIRead, signer: &dyn Signer, ) -> Result<Vec<u8>> { + // sign and write our store to to the output image file + let output_vec: Vec<u8> = Vec::new(); + let mut output_stream = Cursor::new(output_vec); + + self.embed_to_stream(format, stream, &mut output_stream, signer)?; + + Ok(output_stream.into_inner()) + } + + /// Embed a signed manifest into a stream using a supplied signer. + /// returns the bytes of c2pa_manifest that was embedded + pub fn embed_to_stream( + &mut self, + format: &str, + source: &mut dyn CAIRead, + dest: &mut dyn CAIReadWrite, + signer: &dyn Signer, + ) -> Result<Vec<u8>> { self.set_format(format); // todo:: read instance_id from xmp from stream self.set_instance_id(format!("xmp:iid:{}", Uuid::new_v4())); @@ -1036,9 +1056,9 @@ impl Manifest { // generate thumbnail if we don't already have one #[cfg(feature = "add_thumbnails")] { - if self.thumbnail().is_none() { + if self.thumbnail_ref().is_none() { if let Ok((format, image)) = - crate::utils::thumbnail::make_thumbnail_from_stream(format, stream) + crate::utils::thumbnail::make_thumbnail_from_stream(format, source) { self.set_thumbnail(format, image)?; } @@ -1049,12 +1069,7 @@ impl Manifest { let mut store = self.to_store()?; // sign and write our store to to the output image file - let output_vec: Vec<u8> = Vec::new(); - let mut output_stream = Cursor::new(output_vec); - - store.save_to_stream(format, stream, &mut output_stream, signer)?; - - Ok(output_stream.into_inner()) + store.save_to_stream(format, source, dest, signer) } /// Embed a signed manifest into a stream using a supplied signer. @@ -1075,7 +1090,7 @@ impl Manifest { let mut stream = std::io::Cursor::new(asset); #[cfg(feature = "add_thumbnails")] { - if self.thumbnail().is_none() { + if self.thumbnail_ref().is_none() { if let Ok((format, image)) = crate::utils::thumbnail::make_thumbnail_from_stream(format, &mut stream) { @@ -1229,6 +1244,8 @@ pub(crate) mod tests { #![allow(clippy::expect_used)] #![allow(clippy::unwrap_used)] + use std::io::Cursor; + #[cfg(feature = "file_io")] use tempfile::tempdir; #[cfg(target_arch = "wasm32")] @@ -1777,13 +1794,14 @@ pub(crate) mod tests { .unwrap(); let signer = temp_signer(); + let mut output = Cursor::new(Vec::new()); // Embed a manifest using the signer. - let output_image = manifest - .embed_stream("jpeg", &mut stream, signer.as_ref()) + manifest + .embed_to_stream("jpeg", &mut stream, &mut output, signer.as_ref()) .expect("embed_stream"); - let manifest_store = - crate::ManifestStore::from_bytes("jpeg", &output_image, true).expect("from_bytes"); + let manifest_store = crate::ManifestStore::from_bytes("jpeg", &output.into_inner(), true) + .expect("from_bytes"); assert_eq!( manifest_store.get_active().unwrap().title().unwrap(), "EmbedStream" @@ -1992,16 +2010,17 @@ pub(crate) mod tests { let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg"); // convert buffer to cursor with Read/Write/Seek capability - let mut stream = std::io::Cursor::new(image.to_vec()); + let mut input = std::io::Cursor::new(image.to_vec()); let signer = temp_signer(); // Embed a manifest using the signer. - let output_image = manifest - .embed_stream("jpeg", &mut stream, signer.as_ref()) + let mut output = Cursor::new(Vec::new()); + manifest + .embed_to_stream("jpeg", &mut input, &mut output, signer.as_ref()) .expect("embed_stream"); - let manifest_store = - crate::ManifestStore::from_bytes("jpeg", &output_image, true).expect("from_bytes"); + let manifest_store = crate::ManifestStore::from_bytes("jpeg", &output.into_inner(), true) + .expect("from_bytes"); println!("manifest_store = {manifest_store}"); let m = manifest_store.get_active().unwrap(); @@ -2210,6 +2229,33 @@ pub(crate) mod tests { } #[test] + fn test_missing_thumbnail() { + const MANIFEST_JSON: &str = r#" + { + "claim_generator": "test", + "format" : "image/jpeg", + "thumbnail": { + "format": "image/jpeg", + "identifier": "does_not_exist.jpg" + } + } + "#; + + let mut manifest = Manifest::from_json(MANIFEST_JSON).expect("from_json"); + + let mut source = std::io::Cursor::new(vec![1, 2, 3]); + let mut dest = std::io::Cursor::new(Vec::new()); + let signer = temp_signer(); + let result = + manifest.embed_to_stream("image/jpeg", &mut source, &mut dest, signer.as_ref()); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("resource not found: does_not_exist.jpg")); + } + + #[test] #[cfg(feature = "file_io")] fn test_data_hash_embeddable_manifest() { let ap = fixture_path("cloud.jpg"); diff --git a/sdk/src/manifest_store.rs b/sdk/src/manifest_store.rs @@ -82,10 +82,7 @@ impl ManifestStore { } /// creates a ManifestStore from a Store with validation - pub(crate) fn from_store( - store: &Store, - validation_log: &mut impl StatusTracker, - ) -> ManifestStore { + pub(crate) fn from_store(store: &Store, validation_log: &impl StatusTracker) -> ManifestStore { Self::from_store_impl( store, validation_log, @@ -98,7 +95,7 @@ impl ManifestStore { #[cfg(feature = "file_io")] pub fn from_store_with_resources( store: &Store, - validation_log: &mut impl StatusTracker, + validation_log: &impl StatusTracker, resource_path: &Path, ) -> ManifestStore { Self::from_store_impl(store, validation_log, Some(resource_path)) @@ -107,7 +104,7 @@ impl ManifestStore { // internal implementation of from_store fn from_store_impl( store: &Store, - validation_log: &mut impl StatusTracker, + validation_log: &impl StatusTracker, #[cfg(feature = "file_io")] resource_path: Option<&Path>, ) -> ManifestStore { let mut statuses = status_for_store(store, validation_log); @@ -146,7 +143,7 @@ impl ManifestStore { let store = manifest.to_store()?; Ok(Self::from_store_impl( &store, - &mut OneShotStatusTracker::new(), + &OneShotStatusTracker::new(), #[cfg(feature = "file_io")] manifest.resources().base_path(), )) @@ -157,7 +154,7 @@ impl ManifestStore { let mut validation_log = DetailedStatusTracker::new(); Store::load_from_memory(format, image_bytes, verify, &mut validation_log) - .map(|store| Self::from_store(&store, &mut validation_log)) + .map(|store| Self::from_store(&store, &validation_log)) } #[cfg(feature = "file_io")] @@ -177,7 +174,7 @@ impl ManifestStore { let mut validation_log = DetailedStatusTracker::new(); let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; - Ok(Self::from_store(&store, &mut validation_log)) + Ok(Self::from_store(&store, &validation_log)) } #[cfg(feature = "file_io")] @@ -205,7 +202,7 @@ impl ManifestStore { let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; Ok(Self::from_store_with_resources( &store, - &mut validation_log, + &validation_log, resource_path.as_ref(), )) } @@ -220,7 +217,7 @@ impl ManifestStore { Store::load_from_memory_async(format, image_bytes, verify, &mut validation_log) .await - .map(|store| Self::from_store(&store, &mut validation_log)) + .map(|store| Self::from_store(&store, &validation_log)) } /// Loads a ManifestStore from an init segment and fragment. This @@ -242,7 +239,7 @@ impl ManifestStore { &mut validation_log, ) .await - .map(|store| Self::from_store(&store, &mut validation_log)) + .map(|store| Self::from_store(&store, &validation_log)) } /// Asynchronously loads a manifest from a buffer holding a binary manifest (.c2pa) and validates against an asset buffer @@ -281,7 +278,7 @@ impl ManifestStore { ) .await?; - Ok(Self::from_store(&store, &mut validation_log)) + Ok(Self::from_store(&store, &validation_log)) } /// Synchronously loads a manifest from a buffer holding a binary manifest (.c2pa) and validates against an asset buffer @@ -318,7 +315,7 @@ impl ManifestStore { &mut validation_log, )?; - Ok(Self::from_store(&store, &mut validation_log)) + Ok(Self::from_store(&store, &validation_log)) } } @@ -397,7 +394,7 @@ mod tests { fn manifest_report() { let store = create_test_store().expect("creating test store"); - let manifest_store = ManifestStore::from_store(&store, &mut OneShotStatusTracker::new()); + let manifest_store = ManifestStore::from_store(&store, &OneShotStatusTracker::new()); assert!(manifest_store.active_manifest.is_some()); assert!(!manifest_store.manifests.is_empty()); let manifest = manifest_store.get_active().unwrap(); diff --git a/sdk/src/manifest_store_report.rs b/sdk/src/manifest_store_report.rs @@ -124,7 +124,7 @@ impl ManifestStoreReport { /// Creates a ManifestStoreReport from an existing Store and a validation log pub(crate) fn from_store_with_log( store: &Store, - validation_log: &mut impl StatusTracker, + validation_log: &impl StatusTracker, ) -> Result<Self> { let mut report = Self::from_store(store)?; @@ -149,7 +149,7 @@ impl ManifestStoreReport { pub fn from_bytes(format: &str, image_bytes: &[u8]) -> Result<Self> { let mut validation_log = DetailedStatusTracker::new(); let store = Store::load_from_memory(format, image_bytes, true, &mut validation_log)?; - Self::from_store_with_log(&store, &mut validation_log) + Self::from_store_with_log(&store, &validation_log) } /// Creates a ManifestStoreReport from a file @@ -157,7 +157,7 @@ impl ManifestStoreReport { pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> { let mut validation_log = DetailedStatusTracker::new(); let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; - Self::from_store_with_log(&store, &mut validation_log) + Self::from_store_with_log(&store, &validation_log) } /// create a json string representation of this structure, omitting binaries diff --git a/sdk/src/resource_store.rs b/sdk/src/resource_store.rs @@ -303,13 +303,11 @@ mod tests { println!("{manifest}"); let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg"); - // convert buffer to cursor with Read/Write/Seek capability - let mut stream = std::io::Cursor::new(image.to_vec()); let signer = temp_signer(); // Embed a manifest using the signer. let output_image = manifest - .embed_stream("jpeg", &mut stream, signer.as_ref()) + .embed_from_memory("jpeg", image, signer.as_ref()) .expect("embed_stream"); let _manifest_store = diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -156,7 +156,7 @@ impl Store { if let Some(bh) = self.manifest_box_hash_cache.get(claim.label()) { bh.clone() } else { - Store::calc_manifest_box_hash(claim, None, claim.alg()).unwrap_or(Vec::new()) + Store::calc_manifest_box_hash(claim, None, claim.alg()).unwrap_or_default() } } diff --git a/sdk/src/time_stamp.rs b/sdk/src/time_stamp.rs @@ -236,7 +236,7 @@ impl TimeStampResponse { token .content .clone() - .decode(|cons| SignedData::take_from(cons)) + .decode(SignedData::take_from) .map_err(|_err| Error::CoseTimeStampGeneration)?, )) } else { diff --git a/sdk/src/utils/hash_utils.rs b/sdk/src/utils/hash_utils.rs @@ -139,14 +139,14 @@ impl Hasher { pub fn hash_by_alg(alg: &str, data: &[u8], exclusions: Option<Vec<HashRange>>) -> Vec<u8> { let mut reader = Cursor::new(data); - hash_stream_by_alg(alg, &mut reader, exclusions, true).unwrap_or(Vec::new()) + hash_stream_by_alg(alg, &mut reader, exclusions, true).unwrap_or_default() } // Return hash inclusive bytes for desired hashing algorithm. pub fn hash_by_alg_with_inclusions(alg: &str, data: &[u8], inclusions: Vec<HashRange>) -> Vec<u8> { let mut reader = Cursor::new(data); - hash_stream_by_alg(alg, &mut reader, Some(inclusions), false).unwrap_or(Vec::new()) + hash_stream_by_alg(alg, &mut reader, Some(inclusions), false).unwrap_or_default() } // Return hash bytes for asset using desired hashing algorithm. diff --git a/sdk/src/validation_status.rs b/sdk/src/validation_status.rs @@ -161,7 +161,7 @@ impl PartialEq for ValidationStatus { /// be reported as a validation error for any ingredient. pub fn status_for_store( store: &Store, - validation_log: &mut impl StatusTracker, + validation_log: &impl StatusTracker, ) -> Vec<ValidationStatus> { let statuses: Vec<ValidationStatus> = validation_log .get_log()