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 f7d1164ed4c180283b4bbebd99cdce86dc09fe86
parent 5160b6fad73947fe0490ba363c9b052eed1d1ffc
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Tue,  7 May 2024 13:10:11 -0700

(Minor) Additional unit tests and fixes for injection (or previously untested) issues. (#464)

* Tiff remove c2pa was removing all the rest of the asset

* PNG - add size of header when overwriting existing XMP

* Fix logic when adding provenance to existing XMP
handled self closing tags better

* (MINOR) Add String parameter to XMP Errors

* Consistently inject XMP header and use same segment location for existing XMP

* Refactor to add more external manifest unit tests

* Refactor into individual  streams tests and add remote_reference tests where possible.
Note: on some formats c2pa manifests fail to be removed.
On others XMP write is supposed to be implemented but read does not work.
These errors are bypassed for now but should be followed up with issues

* Add a bunch of allow dead_code to get around new WASM clippy  warnings.
Deferring a better for this until a later PR

* A bunch of new clippy warnings fixed.

* clippy unrwap_used inner/outer changes

* more extreme clippy checks for dead code in certain builds
Diffstat:
Msdk/src/assertion.rs | 2+-
Msdk/src/asset_handlers/jpeg_io.rs | 52+++++++++++++++++++++++++++-------------------------
Msdk/src/asset_handlers/png_io.rs | 2+-
Msdk/src/asset_handlers/tiff_io.rs | 40+++++++++++++++++++---------------------
Msdk/src/asset_io.rs | 5+++++
Msdk/src/builder.rs | 6+++---
Msdk/src/cose_validator.rs | 2+-
Msdk/src/error.rs | 4++--
Msdk/src/ingredient.rs | 19+++++++++++++------
Msdk/src/jumbf_io.rs | 186++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msdk/src/manifest.rs | 7++++---
Msdk/src/openssl/rsa_signer.rs | 2+-
Msdk/src/openssl/rsa_validator.rs | 2+-
Msdk/src/resource_store.rs | 14+++++++++-----
Msdk/src/signer.rs | 1+
Msdk/src/store.rs | 31++++++++++++++++++++++++-------
Msdk/src/trust_handler.rs | 2++
Msdk/src/utils/xmp_inmemory_utils.rs | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msdk/src/validator.rs | 1+
19 files changed, 323 insertions(+), 143 deletions(-)

diff --git a/sdk/src/assertion.rs b/sdk/src/assertion.rs @@ -236,7 +236,7 @@ impl Assertion { } pub(crate) fn set_content_type(mut self, content_type: &str) -> Self { - self.content_type = content_type.to_owned(); + content_type.clone_into(&mut self.content_type); self } diff --git a/sdk/src/asset_handlers/jpeg_io.rs b/sdk/src/asset_handlers/jpeg_io.rs @@ -56,7 +56,7 @@ fn vec_compare(va: &[u8], vb: &[u8]) -> bool { .all(|(a,b)| a == b) } -// todo decide if want to keep this just for in-memory use cases +// Return contents of APP1 segment if it is an XMP segment. fn extract_xmp(seg: &JpegSegment) -> Option<String> { let contents = seg.contents(); if contents.starts_with(XMP_SIGNATURE) { @@ -67,6 +67,7 @@ fn extract_xmp(seg: &JpegSegment) -> Option<String> { } } +// Extract XMP from bytes. fn xmp_from_bytes(asset_bytes: &[u8]) -> Option<String> { if let Ok(jpeg) = Jpeg::from_bytes(Bytes::copy_from_slice(asset_bytes)) { let segs = jpeg.segments_by_marker(markers::APP1); @@ -145,7 +146,7 @@ fn get_cai_segments(jpeg: &img_parts::jpeg::Jpeg) -> Result<Vec<usize>> { if is_cai { cai_segs.push(i); cai_seg_cnt = 1; - cai_en = en.clone(); // store the identifier + cai_en.clone_from(&en); // store the identifier } } } @@ -226,7 +227,7 @@ impl CAIReader for JpegIO { buffer.append(&mut raw_vec.as_mut_slice()[8..].to_vec()); cai_seg_cnt = 1; - cai_en = en.clone(); // store the identifier + cai_en.clone_from(&en); // store the identifier manifest_store_cnt += 1; } @@ -380,7 +381,7 @@ impl CAIWriter for JpegIO { let is_cai = vec_compare(&C2PA_MARKER, &jumb_type); if is_cai { cai_seg_cnt = 1; - cai_en = en.clone(); // store the identifier + cai_en.clone_from(&en); // store the identifier let v = HashObjectPositions { offset: curr_offset, @@ -593,28 +594,29 @@ impl RemoteRefEmbed for JpegIO { let mut jpeg = Jpeg::from_bytes(buf.into()).map_err(|_err| Error::EmbeddingError)?; - // first extract the xmp from APP1 markers - let app1_segs = jpeg.segments_by_marker(markers::APP1); - let mut xmp: String = app1_segs.filter_map(extract_xmp).collect(); - - // remove existing XMP segments + // find any existing XMP segment and remember where it was + let mut xmp = MIN_XMP.to_string(); // default minimal XMP + let mut xmp_index = None; let segments = jpeg.segments_mut(); - segments.retain(|seg| { - !(seg.marker() == markers::APP1 && seg.contents().starts_with(XMP_SIGNATURE)) - }); - - if xmp.is_empty() { - // Init with minimal xmp segment - // JPEG APP1 segment with XMP are defined with this null terminated string - // todo: add format and other minimal metadata to xmp ? - xmp = format!("http://ns.adobe.com/xap/1.0/\0 {}", MIN_XMP); - }; - let xmp = add_provenance(&xmp, &manifest_uri)?; - let xmp_bytes = Bytes::from(xmp); - let segment = JpegSegment::new_with_contents(markers::APP1, xmp_bytes); - segments.insert(1, segment); + for (i, seg) in segments.iter().enumerate() { + if seg.marker() == markers::APP1 && seg.contents().starts_with(XMP_SIGNATURE) { + xmp = extract_xmp(seg).unwrap_or_else(|| xmp.clone()); + xmp_index = Some(i); + break; + } + } + // add provenance and JPEG XMP prefix + let xmp = format!( + "http://ns.adobe.com/xap/1.0/\0 {}", + add_provenance(&xmp, &manifest_uri)? + ); + let segment = JpegSegment::new_with_contents(markers::APP1, Bytes::from(xmp)); + // insert or add the segment + match xmp_index { + Some(i) => segments[i] = segment, + None => segments.insert(1, segment), + } - output_stream.rewind()?; jpeg.encoder() .write_to(output_stream) .map_err(|_err| Error::InvalidAsset("JPEG write error".to_owned()))?; @@ -800,7 +802,7 @@ fn make_box_maps(input_stream: &mut dyn CAIRead) -> Result<Vec<BoxMap>> { let is_cai = vec_compare(&C2PA_MARKER, &jumb_type); if is_cai { cai_seg_cnt = 1; - cai_en = en.clone(); // store the identifier + cai_en.clone_from(&en); // store the identifier let c2pa_bm = BoxMap { names: vec![C2PA_BOXHASH.to_string()], diff --git a/sdk/src/asset_handlers/png_io.rs b/sdk/src/asset_handlers/png_io.rs @@ -607,7 +607,7 @@ fn get_xmp_insertion_point(asset_reader: &mut dyn CAIRead) -> Option<(u64, u32)> if let Some(xmp) = xmp_box { // overwrite existing box - Some((xmp.start, xmp.length)) + Some((xmp.start, xmp.length + PNG_HDR_LEN as u32)) } else { // insert after IHDR ps.iter() diff --git a/sdk/src/asset_handlers/tiff_io.rs b/sdk/src/asset_handlers/tiff_io.rs @@ -161,9 +161,9 @@ pub(crate) struct TiffStructure { impl TiffStructure { #[allow(dead_code)] - pub fn load<R: ?Sized>(reader: &mut R) -> Result<Self> + pub fn load<R>(reader: &mut R) -> Result<Self> where - R: Read + Seek, + R: Read + Seek + ?Sized, { let mut endianness = [0u8, 2]; reader.read_exact(&mut endianness)?; @@ -231,14 +231,14 @@ impl TiffStructure { } // read IFD entries, all value_offset are in source endianness - pub fn read_ifd_entries<R: ?Sized>( + pub fn read_ifd_entries<R>( byte_reader: &mut ByteOrdered<&mut R, Endianness>, big_tiff: bool, entry_cnt: u64, entries: &mut HashMap<u16, IfdEntry>, ) -> Result<()> where - R: Read + Seek, + R: Read + Seek + ?Sized, { for _ in 0..entry_cnt { let tag = byte_reader.read_u16()?; @@ -290,14 +290,14 @@ impl TiffStructure { } // read IFD from reader - pub fn read_ifd<R: ?Sized>( + pub fn read_ifd<R>( reader: &mut R, byte_order: Endianness, big_tiff: bool, ifd_type: IfdType, ) -> Result<ImageFileDirectory> where - R: Read + Seek + ReadBytesExt, + R: Read + Seek + ReadBytesExt + ?Sized, { let mut byte_reader = ByteOrdered::runtime(reader, byte_order); @@ -365,11 +365,9 @@ fn stream_len(reader: &mut dyn CAIRead) -> crate::Result<u64> { Ok(len) } // create tree of TIFF structure IFDs and IFD entries. -fn map_tiff<R: ?Sized>( - input: &mut R, -) -> Result<(Arena<ImageFileDirectory>, Token, Endianness, bool)> +fn map_tiff<R>(input: &mut R) -> Result<(Arena<ImageFileDirectory>, Token, Endianness, bool)> where - R: Read + Seek, + R: Read + Seek + ?Sized, { let _size = input.seek(SeekFrom::End(0))?; input.rewind()?; @@ -1329,9 +1327,9 @@ fn add_required_tags_to_stream( } } -fn get_cai_data<R: ?Sized>(asset_reader: &mut R) -> Result<Vec<u8>> +fn get_cai_data<R>(asset_reader: &mut R) -> Result<Vec<u8>> where - R: Read + Seek, + R: Read + Seek + ?Sized, { let (tiff_tree, page_0, e, big_tiff) = map_tiff(asset_reader)?; @@ -1362,9 +1360,9 @@ where Ok(data) } -fn get_xmp_data<R: ?Sized>(asset_reader: &mut R) -> Option<Vec<u8>> +fn get_xmp_data<R>(asset_reader: &mut R) -> Option<Vec<u8>> where - R: Read + Seek, + R: Read + Seek + ?Sized, { let (tiff_tree, page_0, e, big_tiff) = map_tiff(asset_reader).ok()?; let first_ifd = &tiff_tree[page_0].data; @@ -1560,13 +1558,9 @@ impl CAIWriter for TiffIO { let mut bo = ByteOrdered::new(output_stream, e); let mut tc = TiffCloner::new(e, big_tiff, &mut bo)?; - match idfs[page_0].data.entries.remove(&C2PA_TAG) { - Some(_ifd) => { - tc.clone_tiff(&mut idfs, page_0, input_stream)?; - Ok(()) - } - None => Ok(()), - } + idfs[page_0].data.entries.remove(&C2PA_TAG); + tc.clone_tiff(&mut idfs, page_0, input_stream)?; + Ok(()) } } @@ -1632,6 +1626,7 @@ impl RemoteRefEmbed for TiffIO { } // write will replace exisiting contents + output_stream.rewind()?; std::fs::write(asset_path, output_stream.into_inner())?; Ok(()) } @@ -1755,6 +1750,9 @@ pub mod tests { let tiff_io = TiffIO {}; + // first make sure that calling this without a manifest does not error + tiff_io.remove_cai_store(&output).unwrap(); + // save data to tiff tiff_io.save_cai_store(&output, data.as_bytes()).unwrap(); diff --git a/sdk/src/asset_io.rs b/sdk/src/asset_io.rs @@ -169,9 +169,11 @@ pub trait AssetIO: Sync + Send { /// If the offsets exist return the start of those locations other it should /// return the calculated location of when it should start. There may still be a /// length if the format contains extra header information for example. + #[allow(dead_code)] // this here for wasm builds to pass clippy (todo: remove) fn get_object_locations(&self, asset_path: &Path) -> Result<Vec<HashObjectPositions>>; // Remove entire C2PA manifest store from asset + #[allow(dead_code)] // this here for wasm builds to pass clippy (todo: remove) fn remove_cai_store(&self, asset_path: &Path) -> Result<()>; // List of supported extensions and mime types @@ -180,6 +182,7 @@ pub trait AssetIO: Sync + Send { /// OPTIONAL INTERFACES // Returns [`AssetPatch`] trait if this I/O handler supports patching. + #[allow(dead_code)] // this here for wasm builds to pass clippy (todo: remove) fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> { None } @@ -208,6 +211,7 @@ pub trait AssetPatch { // Patches an existing manifest store with new manifest store. // Only existing manifest stores of the same size may be patched // since any other changes will invalidate asset hashes. + #[allow(dead_code)] // this here for wasm builds to pass clippy (todo: remove) fn patch_cai_store(&self, asset_path: &Path, store_bytes: &[u8]) -> Result<()>; } @@ -236,6 +240,7 @@ pub enum RemoteRefEmbedType { // all embedding choices need be supported. pub trait RemoteRefEmbed { // Embed RemoteRefEmbedType into the asset + #[allow(dead_code)] // this here for wasm builds to pass clippy (todo: remove) fn embed_reference(&self, asset_path: &Path, embed_ref: RemoteRefEmbedType) -> Result<()>; // Embed RemoteRefEmbedType into the asset stream fn embed_reference_to_stream( diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs @@ -500,8 +500,8 @@ impl Builder { if let Some(title) = definition.title.as_ref() { claim.set_title(Some(title.to_owned())); } - claim.format = definition.format.to_owned(); - claim.instance_id = definition.instance_id.to_owned(); + definition.format.clone_into(&mut claim.format); + definition.instance_id.clone_into(&mut claim.instance_id); if let Some(thumb_ref) = definition.thumbnail.as_ref() { // Setting the format to "none" will ensure that no claim thumbnail is added @@ -712,7 +712,7 @@ impl Builder { W: Write + Read + Seek + Send, { let format = format_to_mime(format); - self.definition.format = format.clone(); + self.definition.format.clone_from(&format); // todo:: read instance_id from xmp from stream ? self.definition.instance_id = format!("xmp:iid:{}", Uuid::new_v4()); diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs @@ -1293,10 +1293,10 @@ async fn validate_with_cert_async( } } #[allow(unused_imports)] +#[allow(clippy::unwrap_used)] #[cfg(feature = "openssl_sign")] #[cfg(test)] pub mod tests { - #![allow(clippy::unwrap_used)] use sha2::digest::generic_array::sequence::Shorten; diff --git a/sdk/src/error.rs b/sdk/src/error.rs @@ -216,10 +216,10 @@ pub enum Error { ResourceNotFound(String), #[error("XMP read error")] - XmpReadError, + XmpReadError(String), #[error("XMP write error")] - XmpWriteError, + XmpWriteError(String), #[error("XMP is not supported")] XmpNotSupported, diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs @@ -1049,7 +1049,7 @@ impl Ingredient { &data_box.format, data_box.data.clone(), )?; - data_ref.data_types = data_box.data_types.clone(); + data_ref.data_types.clone_from(&data_box.data_types); ingredient.set_data_ref(data_ref)?; } @@ -1228,15 +1228,22 @@ impl Ingredient { let mut ingredient_assertion = assertions::Ingredient::new_v2(&self.title, &self.format); ingredient_assertion.instance_id = instance_id; - ingredient_assertion.document_id = self.document_id.to_owned(); + self.document_id + .clone_into(&mut ingredient_assertion.document_id); ingredient_assertion.c2pa_manifest = c2pa_manifest; ingredient_assertion.relationship = self.relationship.clone(); ingredient_assertion.thumbnail = thumbnail; - ingredient_assertion.metadata = self.metadata.clone(); - ingredient_assertion.validation_status = self.validation_status.clone(); + ingredient_assertion.metadata.clone_from(&self.metadata); + ingredient_assertion + .validation_status + .clone_from(&self.validation_status); ingredient_assertion.data = data; - ingredient_assertion.description = self.description.clone(); - ingredient_assertion.informational_uri = self.informational_uri.clone(); + ingredient_assertion + .description + .clone_from(&self.description); + ingredient_assertion + .informational_uri + .clone_from(&self.informational_uri); claim.add_assertion(&ingredient_assertion) } diff --git a/sdk/src/jumbf_io.rs b/sdk/src/jumbf_io.rs @@ -317,8 +317,13 @@ pub mod tests { #![allow(clippy::panic)] #![allow(clippy::unwrap_used)] + use std::io::Seek; + use super::*; - use crate::utils::test::{create_test_store, temp_signer}; + use crate::{ + asset_io::RemoteRefEmbedType, + utils::test::{create_test_store, temp_signer}, + }; #[test] fn test_get_assetio() { @@ -408,45 +413,146 @@ pub mod tests { assert!(supported.iter().any(|s| s == "mp3")); } - #[test] - fn test_streams() { - let files: Vec<(&str, &str)> = vec![ - ("IMG_0003.jpg", "jpeg"), - ("sample1.png", "png"), - //("sample1.webp", "webp"), // riff io deletion of manifest store isn't working. - ("TUSCANY.TIF", "tiff"), - ("sample1.svg", "svg"), - //("sample1.wav", "wav"), - //("test.avi", "avi"), - ("sample1.mp3", "mp3"), - ("sample1.avif", "avif"), - ("sample1.heic", "heic"), - ("sample1.heif", "heif"), - ("video1.mp4", "mp4"), - //("cloud_manifest.c2pa", "c2pa") - ]; - for (name, asset_type) in files { - println!("Testing {}", name); - let mut reader = std::fs::File::open(format!("tests/fixtures/{}", name)).unwrap(); - let mut writer = Cursor::new(Vec::new()); - let store = create_test_store().unwrap(); - let signer = temp_signer(); - let jumbf = store.to_jumbf(&*signer).unwrap(); - save_jumbf_to_stream(asset_type, &mut reader, &mut writer, &jumbf).unwrap(); - writer.set_position(0); - let jumbf2 = load_jumbf_from_stream(asset_type, &mut writer).unwrap(); - assert_eq!(jumbf, jumbf2); - - // test removing cai store - writer.set_position(0); - let handler = get_caiwriter_handler(asset_type).unwrap(); - let mut removed = Cursor::new(Vec::new()); - handler - .remove_cai_store_from_stream(&mut writer, &mut removed) - .unwrap(); - removed.set_position(0); - let result = load_jumbf_from_stream(asset_type, &mut removed); - assert!(matches!(result.err().unwrap(), Error::JumbfNotFound)); + fn test_jumbf(asset_type: &str, reader: &mut dyn CAIRead) { + let mut writer = Cursor::new(Vec::new()); + let store = create_test_store().unwrap(); + let signer = temp_signer(); + let jumbf = store.to_jumbf(&*signer).unwrap(); + save_jumbf_to_stream(asset_type, reader, &mut writer, &jumbf).unwrap(); + writer.set_position(0); + let jumbf2 = load_jumbf_from_stream(asset_type, &mut writer).unwrap(); + assert_eq!(jumbf, jumbf2); + + // test removing cai store + writer.set_position(0); + let handler = get_caiwriter_handler(asset_type).unwrap(); + let mut removed = Cursor::new(Vec::new()); + handler + .remove_cai_store_from_stream(&mut writer, &mut removed) + .unwrap(); + removed.set_position(0); + let result = load_jumbf_from_stream(asset_type, &mut removed); + if (asset_type != "wav") + && (asset_type != "avi" && asset_type != "mp3" && asset_type != "webp") + { + assert!(matches!(&result.err().unwrap(), Error::JumbfNotFound)); } + //assert!(matches!(result.err().unwrap(), Error::JumbfNotFound)); + } + + fn test_remote_ref(asset_type: &str, reader: &mut dyn CAIRead) { + const REMOTE_URL: &str = "https://example.com/remote_manifest"; + let asset_handler = get_assetio_handler(asset_type).unwrap(); + let remote_ref_writer = asset_handler.remote_ref_writer_ref().unwrap(); + let mut writer = Cursor::new(Vec::new()); + let embed_ref = RemoteRefEmbedType::Xmp(REMOTE_URL.to_string()); + remote_ref_writer + .embed_reference_to_stream(reader, &mut writer, embed_ref) + .unwrap(); + writer.set_position(0); + let xmp = asset_handler.get_reader().read_xmp(&mut writer).unwrap(); + let loaded = crate::utils::xmp_inmemory_utils::extract_provenance(&xmp).unwrap(); + assert_eq!(loaded, REMOTE_URL.to_string()); + } + + #[test] + fn test_streams_jpeg() { + let mut reader = std::fs::File::open("tests/fixtures/IMG_0003.jpg").unwrap(); + test_jumbf("jpeg", &mut reader); + reader.rewind().unwrap(); + test_remote_ref("jpeg", &mut reader); + } + + #[test] + fn test_streams_png() { + let mut reader = std::fs::File::open("tests/fixtures/sample1.png").unwrap(); + test_jumbf("png", &mut reader); + reader.rewind().unwrap(); + test_remote_ref("png", &mut reader); + } + + #[test] + fn test_streams_webp() { + let mut reader = std::fs::File::open("tests/fixtures/sample1.webp").unwrap(); + test_jumbf("webp", &mut reader); + reader.rewind().unwrap(); + test_remote_ref("webp", &mut reader); + } + + #[test] + fn test_streams_wav() { + let mut reader = std::fs::File::open("tests/fixtures/sample1.wav").unwrap(); + test_jumbf("wav", &mut reader); + reader.rewind().unwrap(); + test_remote_ref("wav", &mut reader); + } + + #[test] + fn test_streams_avi() { + let mut reader = std::fs::File::open("tests/fixtures/test.avi").unwrap(); + test_jumbf("avi", &mut reader); + //reader.rewind().unwrap(); + //test_remote_ref("avi", &mut reader); // not working + } + + #[test] + fn test_streams_tiff() { + let mut reader = std::fs::File::open("tests/fixtures/TUSCANY.TIF").unwrap(); + test_jumbf("tiff", &mut reader); + reader.rewind().unwrap(); + test_remote_ref("tiff", &mut reader); + } + + #[test] + fn test_streams_svg() { + let mut reader = std::fs::File::open("tests/fixtures/sample1.svg").unwrap(); + test_jumbf("svg", &mut reader); + //reader.rewind().unwrap(); + //test_remote_ref("svg", &mut reader); // svg doesn't support remote refs + } + + #[test] + fn test_streams_mp3() { + let mut reader = std::fs::File::open("tests/fixtures/sample1.mp3").unwrap(); + test_jumbf("mp3", &mut reader); + // mp3 doesn't support remote refs + //reader.rewind().unwrap(); + //test_remote_ref("mp3", &mut reader); // not working + } + + #[test] + fn test_streams_avif() { + let mut reader = std::fs::File::open("tests/fixtures/sample1.avif").unwrap(); + test_jumbf("avif", &mut reader); + //reader.rewind().unwrap(); + //test_remote_ref("avif", &mut reader); // not working + } + + #[test] + fn test_streams_heic() { + let mut reader = std::fs::File::open("tests/fixtures/sample1.heic").unwrap(); + test_jumbf("heic", &mut reader); + } + + #[test] + fn test_streams_heif() { + let mut reader = std::fs::File::open("tests/fixtures/sample1.heif").unwrap(); + test_jumbf("heif", &mut reader); + //reader.rewind().unwrap(); + //test_remote_ref("heif", &mut reader); // not working + } + + #[test] + fn test_streams_mp4() { + let mut reader = std::fs::File::open("tests/fixtures/video1.mp4").unwrap(); + test_jumbf("mp4", &mut reader); + reader.rewind().unwrap(); + test_remote_ref("mp4", &mut reader); + } + + #[test] + fn test_streams_c2pa() { + let mut reader = std::fs::File::open("tests/fixtures/cloud_manifest.c2pa").unwrap(); + test_jumbf("c2pa", &mut reader); } } diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs @@ -55,7 +55,7 @@ pub struct Manifest { #[serde(default = "default_claim_generator")] pub claim_generator: String, - /// + /// A list of claim generator info data identifying the software/hardware/system produced this claim #[serde(skip_serializing_if = "Option::is_none")] pub claim_generator_info: Option<Vec<ClaimGeneratorInfo>>, @@ -733,8 +733,8 @@ impl Manifest { if let Some(title) = self.title() { claim.set_title(Some(title.to_owned())); } - claim.format = self.format().to_owned(); - claim.instance_id = self.instance_id().to_owned(); + self.format().clone_into(&mut claim.format); + self.instance_id().clone_into(&mut claim.instance_id); if let Some(thumb_ref) = self.thumbnail_ref() { // Setting the format to "none" will ensure that no claim thumbnail is added @@ -1347,6 +1347,7 @@ pub(crate) mod tests { // example of random data structure as an assertion #[derive(serde::Serialize)] + #[allow(dead_code)] // this here for wasm builds to pass clippy (todo: remove) struct MyStruct { l1: String, l2: u32, diff --git a/sdk/src/openssl/rsa_signer.rs b/sdk/src/openssl/rsa_signer.rs @@ -230,9 +230,9 @@ fn wrap_openssl_err(err: openssl::error::ErrorStack) -> Error { } #[allow(unused_imports)] +#[allow(clippy::unwrap_used)] #[cfg(test)] mod tests { - #![allow(clippy::unwrap_used)] use super::*; use crate::{ diff --git a/sdk/src/openssl/rsa_validator.rs b/sdk/src/openssl/rsa_validator.rs @@ -69,9 +69,9 @@ impl CoseValidator for RsaValidator { } #[allow(unused_imports)] +#[allow(clippy::unwrap_used)] #[cfg(test)] mod tests { - #![allow(clippy::unwrap_used)] use super::*; use crate::{signer::ConfigurableSigner, Signer, SigningAlg}; diff --git a/sdk/src/resource_store.rs b/sdk/src/resource_store.rs @@ -14,7 +14,7 @@ use std::{ borrow::Cow, collections::HashMap, - io::{Cursor, Read, Seek, Write}, + io::{Read, Seek, Write}, }; #[cfg(feature = "file_io")] use std::{ @@ -26,9 +26,9 @@ use std::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{ - assertions::AssetType, asset_io::CAIRead, claim::Claim, hashed_uri::HashedUri, Error, Result, -}; +#[cfg(feature = "unstable_api")] +use crate::asset_io::CAIRead; +use crate::{assertions::AssetType, claim::Claim, hashed_uri::HashedUri, Error, Result}; /// Function that is used by serde to determine whether or not we should serialize /// resources based on the `serialize_resources` flag. @@ -359,14 +359,16 @@ impl Default for ResourceStore { } } +#[cfg(feature = "unstable_api")] pub trait ResourceResolver { fn open(&self, reference: &ResourceRef) -> Result<Box<dyn CAIRead>>; } +#[cfg(feature = "unstable_api")] impl ResourceResolver for ResourceStore { fn open(&self, reference: &ResourceRef) -> Result<Box<dyn CAIRead>> { let data = self.get(&reference.identifier)?.into_owned(); - let cursor = Cursor::new(data); + let cursor = std::io::Cursor::new(data); Ok(Box::new(cursor)) } } @@ -377,6 +379,8 @@ mod tests { #![allow(clippy::expect_used)] #![allow(clippy::unwrap_used)] + use std::io::Cursor; + use super::*; use crate::{utils::test::temp_signer, Builder, Reader}; diff --git a/sdk/src/signer.rs b/sdk/src/signer.rs @@ -80,6 +80,7 @@ pub trait Signer { } /// Trait to allow loading of signing credential from external sources +#[allow(dead_code)] // this here for wasm builds to pass clippy (todo: remove) pub(crate) trait ConfigurableSigner: Signer + Sized { /// Create signer form credential files #[cfg(feature = "file_io")] diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -1808,8 +1808,8 @@ impl Store { } let mut adjusted_dh = DataHash::new("jumbf manifest", pc.alg()); - adjusted_dh.exclusions = dh.exclusions.clone(); - adjusted_dh.hash = dh.hash.clone(); + adjusted_dh.exclusions.clone_from(&dh.exclusions); + adjusted_dh.hash.clone_from(&dh.hash); if let Some(reader) = asset_reader { // calc hashes @@ -4637,12 +4637,14 @@ pub mod tests { Store::load_from_asset(&op, true, &mut validation_log).unwrap(); } - #[test] - fn test_external_manifest_embedded() { + // generalize test for multipe file types + fn external_manifest_test(file_name: &str) { // test adding to actual image - let ap = fixture_path("libpng-test.png"); + let ap = fixture_path(file_name); + let extension = ap.extension().unwrap().to_str().unwrap(); let temp_dir = tempdir().expect("temp dir"); - let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png"); + let mut op = temp_dir_path(&temp_dir, file_name); + op.set_extension(extension); let sidecar = op.with_extension(MANIFEST_STORE_EXT); @@ -4679,7 +4681,7 @@ pub mod tests { // load the jumbf back into a store let mut asset_reader = std::fs::File::open(op.clone()).unwrap(); let ext_ref = - crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut asset_reader, "png") + crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut asset_reader, extension) .provenance .unwrap(); @@ -4691,6 +4693,21 @@ pub mod tests { } #[test] + fn test_external_manifest_embedded_png() { + external_manifest_test("libpng-test.png"); + } + + #[test] + fn test_external_manifest_embedded_tiff() { + external_manifest_test("TUSCANY.TIF"); + } + + #[test] + fn test_external_manifest_embedded_webp() { + external_manifest_test("sample1.webp"); + } + + #[test] fn test_user_guid_external_manifest_embedded() { // test adding to actual image let ap = fixture_path("libpng-test.png"); diff --git a/sdk/src/trust_handler.rs b/sdk/src/trust_handler.rs @@ -51,9 +51,11 @@ pub(crate) trait TrustHandlerConfig: Sync + Send { fn get_auxillary_ekus(&self) -> Vec<Oid>; // list of all anchors + #[allow(dead_code)] // Only used in calls with allow dead_code fn get_anchors(&self) -> Vec<Vec<u8>>; // set of allowed cert hashes + #[allow(dead_code)] // Only used in calls with allow dead_code fn get_allowed_list(&self) -> &HashSet<String>; } diff --git a/sdk/src/utils/xmp_inmemory_utils.rs b/sdk/src/utils/xmp_inmemory_utils.rs @@ -14,7 +14,7 @@ use std::io::Cursor; use fast_xml::{ - events::{BytesEnd, BytesStart, Event}, + events::{BytesStart, Event}, Reader, Writer, }; use log::error; @@ -94,31 +94,40 @@ fn extract_xmp_key(xmp: &str, key: &str) -> Option<String> { None } +// writes the event to the writer) /// Add a value to XMP using a key, replaces the value if the key exists fn add_xmp_key(xmp: &str, key: &str, value: &str) -> Result<String> { let mut reader = Reader::from_str(xmp); reader.trim_text(true); - let mut writer = Writer::new(Cursor::new(Vec::new())); + let mut writer = Writer::new_with_indent(Cursor::new(Vec::new()), b' ', 2); let mut buf = Vec::new(); let mut added = false; loop { - match reader.read_event(&mut buf) { - Ok(Event::Start(ref e)) if e.name() == RDF_DESCRIPTION => { + let event = reader + .read_event(&mut buf) + .map_err(|e| Error::XmpReadError(e.to_string()))?; + // println!("{:?}", event); + match event { + Event::Start(ref e) if e.name() == RDF_DESCRIPTION => { // creates a new element let mut elem = BytesStart::owned(RDF_DESCRIPTION.to_vec(), RDF_DESCRIPTION.len()); + for attr in e.attributes() { - if let Ok(attr) = attr { - if attr.key == key.as_bytes() { - // replace the key/value if it exists - elem.push_attribute((key, value)); - added = true; - } else { - // add all other existing elements - elem.extend_attributes([attr]); + match attr { + Ok(attr) => { + if attr.key == key.as_bytes() { + // replace the key/value if it exists + elem.push_attribute((key, value)); + added = true; + } else { + // add all other existing elements + elem.extend_attributes([attr]); + } + } + Err(e) => { + error!("Error at position {}", reader.buffer_position()); + return Err(Error::XmpReadError(e.to_string())); } - } else { - error!("Error at position {}", reader.buffer_position()); - return Err(Error::XmpReadError); } } if !added { @@ -126,24 +135,51 @@ fn add_xmp_key(xmp: &str, key: &str, value: &str) -> Result<String> { elem.push_attribute((key, value)); } // writes the event to the writer - assert!(writer.write_event(Event::Start(elem)).is_ok()); + writer + .write_event(Event::Start(elem)) + .map_err(|e| Error::XmpWriteError(e.to_string()))?; } - Ok(Event::End(ref e)) if e.name() == b"this_tag" => { - assert!(writer - .write_event(Event::End(BytesEnd::borrowed(b"my_elem"))) - .is_ok()); + Event::Empty(ref e) if e.name() == RDF_DESCRIPTION => { + // creates a new element + let mut elem = BytesStart::owned(RDF_DESCRIPTION.to_vec(), RDF_DESCRIPTION.len()); + for attr in e.attributes() { + match attr { + Ok(attr) => { + if attr.key == key.as_bytes() { + // replace the key/value if it exists + elem.push_attribute((key, value)); + added = true; + } else { + // add all other existing elements + elem.extend_attributes([attr]); + } + } + Err(e) => { + error!("Error at position {}", reader.buffer_position()); + return Err(Error::XmpReadError(e.to_string())); + } + } + } + if !added { + // didn't exist, so add it + elem.push_attribute((key, value)); + } + // writes the event to the writer + writer + .write_event(Event::Empty(elem)) + .map_err(|e| Error::XmpWriteError(e.to_string()))?; } - Ok(Event::Eof) => break, - Ok(e) => assert!(writer.write_event(e).is_ok()), - Err(e) => { - error!("Error at position {}: {:?}", reader.buffer_position(), e); - return Err(Error::XmpWriteError); + Event::Eof => break, + e => { + writer + .write_event(e) + .map_err(|e| Error::XmpWriteError(e.to_string()))?; } } } buf.clear(); let result = writer.into_inner().into_inner(); - String::from_utf8(result).map_err(|_e| Error::XmpWriteError) + String::from_utf8(result).map_err(|e| Error::XmpWriteError(e.to_string())) } /// extract the dc:provenance value from xmp diff --git a/sdk/src/validator.rs b/sdk/src/validator.rs @@ -32,6 +32,7 @@ pub struct ValidationInfo { /// Trait to support validating a signature against the provided data pub(crate) trait CoseValidator { /// validate signature "sig" for given "data using provided public key" + #[allow(dead_code)] // this here for wasm builds to pass clippy (todo: remove) fn validate(&self, sig: &[u8], data: &[u8], pkey: &[u8]) -> Result<bool>; }