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 86e928e0739a5f7d4f2402fdb65b46c12a33239b
parent 892438dfded16d76b4a861f040303ff24d975e95
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Tue, 17 Oct 2023 09:04:41 -0700

(MINOR) Use JUMBF URIs for ManifestStore identifiers (#323)

* Use jumbf uris for ManifestStore identifiers
New generated file paths with manifest label folders written to base_path
Added ResourceStore set_label()
Diffstat:
Msdk/src/ingredient.rs | 73+++++++++++++++++++++++++++++++++++++++++--------------------------------
Msdk/src/manifest.rs | 9++++++++-
Msdk/src/resource_store.rs | 61++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
3 files changed, 103 insertions(+), 40 deletions(-)

diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs @@ -29,7 +29,12 @@ use crate::{ claim::{Claim, ClaimAssetData}, error::{Error, Result}, hashed_uri::HashedUri, - jumbf, + jumbf::{ + self, + labels::{ + assertion_label_from_uri, manifest_label_from_uri, to_assertion_uri, to_relative_uri, + }, + }, jumbf_io::load_jumbf_from_stream, resource_store::{skip_serializing_resources, ResourceRef, ResourceStore}, status_tracker::{log_item, DetailedStatusTracker, StatusTracker}, @@ -379,6 +384,7 @@ impl Ingredient { /// This is only used for internally generated thumbnails - when /// reading thumbnails from files, we don't want to write these to file /// So this ensures they stay in memory unless written out. + #[deprecated(note = "Please use set_thumbnail instead", since = "0.28.0")] pub fn set_memory_thumbnail<S: Into<String>, B: Into<Vec<u8>>>( &mut self, format: S, @@ -913,26 +919,7 @@ impl Ingredient { let active_manifest = ingredient_assertion .c2pa_manifest - .and_then(|hash_url| jumbf::labels::manifest_label_from_uri(&hash_url.url())); - - let thumbnail = ingredient_assertion.thumbnail.and_then(|hashed_uri| { - // This could be a relative or absolute thumbnail reference to another manifest - let target_label = match jumbf::labels::manifest_label_from_uri(&hashed_uri.url()) { - Some(label) => label, // use the manifest from the thumbnail uri - None => claim_label.to_owned(), // relative so use the whole url from the thumbnail assertion - }; - match store.get_assertion_from_uri_and_claim(&hashed_uri.url(), &target_label) { - Some(assertion) => Some(Self::thumbnail_from_assertion(assertion)), - None => { - error!("failed to get {} from {}", hashed_uri.url(), ingredient_uri); - validation_status.push( - ValidationStatus::new(validation_status::ASSERTION_MISSING.to_string()) - .set_url(hashed_uri.url()), - ); - None - } - } - }); + .and_then(|hash_url| manifest_label_from_uri(&hash_url.url())); debug!( "Adding Ingredient {} {:?}", @@ -948,15 +935,40 @@ impl Ingredient { .unwrap_or_else(default_instance_id), ); ingredient.document_id = ingredient_assertion.document_id; + ingredient.resources.set_label(claim_label); // set the label for relative paths #[cfg(feature = "file_io")] if let Some(base_path) = resource_path { ingredient.resources_mut().set_base_path(base_path) } - if let Some((format, image)) = thumbnail { - ingredient.set_thumbnail(format, image)?; - } + if let Some(hashed_uri) = ingredient_assertion.thumbnail.as_ref() { + // This could be a relative or absolute thumbnail reference to another manifest + let target_claim_label = match manifest_label_from_uri(&hashed_uri.url()) { + Some(label) => label, // use the manifest from the thumbnail uri + None => claim_label.to_owned(), /* relative so use the whole url from the thumbnail assertion */ + }; + match store.get_assertion_from_uri_and_claim(&hashed_uri.url(), &target_claim_label) { + Some(assertion) => { + let (format, image) = Self::thumbnail_from_assertion(assertion); + let assertion_label = assertion_label_from_uri(&hashed_uri.url()) + .unwrap_or_else(|| "thumbnail".to_owned()); + // construct an id from the manifest and assertion labels + let mut id = to_assertion_uri(&target_claim_label, &assertion_label); + if target_claim_label == claim_label { + id = to_relative_uri(&id); + } + ingredient.thumbnail = Some(ingredient.resources.add_uri(&id, &format, image)?); + } + None => { + error!("failed to get {} from {}", hashed_uri.url(), ingredient_uri); + validation_status.push( + ValidationStatus::new(validation_status::ASSERTION_MISSING.to_string()) + .set_url(hashed_uri.url()), + ); + } + } + }; if let Some(data_uri) = ingredient_assertion.data.as_ref() { let data_box = store @@ -968,9 +980,8 @@ impl Ingredient { } })?; - let id_base = ingredient.instance_id().to_owned(); - let mut data_ref = ingredient.resources_mut().add_with( - &id_base, + let mut data_ref = ingredient.resources_mut().add_uri( + &data_uri.url(), &data_box.format, data_box.data.clone(), )?; @@ -1011,7 +1022,7 @@ impl Ingredient { true => redactions.as_ref().map(|redactions| { redactions .iter() - .map(|r| jumbf::labels::to_assertion_uri(&manifest_label, r)) + .map(|r| to_assertion_uri(&manifest_label, r)) .collect() }), false => None, @@ -1056,10 +1067,8 @@ impl Ingredient { let assertion_label = jumbf::labels::assertion_label_from_uri(&t.url()) .unwrap_or_default(); - let url = jumbf::labels::to_assertion_uri( - &manifest_label, - &assertion_label, - ); + let url = + to_assertion_uri(&manifest_label, &assertion_label); HashedUri::new(url, t.alg(), &t.hash()) }); } diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs @@ -525,6 +525,7 @@ impl Manifest { } manifest.set_label(claim.label()); + manifest.resources.set_label(claim.label()); // default manifest for relative urls manifest.claim_generator_hints = claim.get_claim_generator_hint_map().cloned(); // get credentials converting from AssertionData to Value @@ -629,7 +630,13 @@ impl Manifest { } label if label.starts_with(labels::CLAIM_THUMBNAIL) => { let thumbnail = Thumbnail::from_assertion(assertion)?; - manifest.set_thumbnail(thumbnail.content_type, thumbnail.data)?; + let id = jumbf::labels::to_assertion_uri(claim.label(), label); + let id = jumbf::labels::to_relative_uri(&id); + manifest.thumbnail = Some(manifest.resources.add_uri( + &id, + &thumbnail.content_type, + thumbnail.data, + )?); } _ => { // inject assertions for all other assertions diff --git a/sdk/src/resource_store.rs b/sdk/src/resource_store.rs @@ -11,9 +11,12 @@ // specific language governing permissions and limitations under // each license. -#[cfg(feature = "file_io")] -use std::path::{Path, PathBuf}; use std::{borrow::Cow, collections::HashMap}; +#[cfg(feature = "file_io")] +use std::{ + fs::{create_dir_all, read, write}, + path::{Path, PathBuf}, +}; #[cfg(feature = "json_schema")] use schemars::JsonSchema; @@ -110,6 +113,8 @@ pub struct ResourceStore { #[cfg(feature = "file_io")] #[serde(skip_serializing_if = "Option::is_none")] base_path: Option<PathBuf>, + #[serde(skip_serializing_if = "Option::is_none")] + label: Option<String>, } impl ResourceStore { @@ -118,9 +123,15 @@ impl ResourceStore { resources: HashMap::new(), #[cfg(feature = "file_io")] base_path: None, + label: None, } } + pub fn set_label<S: Into<String>>(&mut self, label: S) -> &Self { + self.label = Some(label.into()); + self + } + #[cfg(feature = "file_io")] pub fn base_path(&self) -> Option<&Path> { self.base_path.as_deref() @@ -142,7 +153,7 @@ impl ResourceStore { "jpg" | "jpeg" | "image/jpeg" => ".jpg", "png" | "image/png" => ".png", //make "svg" | "image/svg+xml" => ".svg", - "c2pa" | "application/x-c2pa-manifest-store" => ".cp2a", + "c2pa" | "application/x-c2pa-manifest-store" => ".c2pa", _ => "", }; // clean string for possible filesystem use @@ -170,6 +181,43 @@ impl ResourceStore { Ok(ResourceRef::new(format, id)) } + /// Adds a resource from a URI, generating a [`ResourceRef`]. + /// + /// The generated identifier may be different from the key. + pub(crate) fn add_uri<R>( + &mut self, + uri: &str, + format: &str, + value: R, + ) -> crate::Result<ResourceRef> + where + R: Into<Vec<u8>>, + { + #[cfg(feature = "file_io")] + let mut id = uri.to_string(); + #[cfg(not(feature = "file_io"))] + let id = uri.to_string(); + + // if it isn't jumbf, assume it's an external uri and use it as is + if id.starts_with("self#jumbf=") { + #[cfg(feature = "file_io")] + if self.base_path.is_some() { + // convert to a file path always including the manifest label + id = id.replace("self#jumbf=", ""); + if id.starts_with("/c2pa/") { + id = id.replacen("/c2pa/", "", 1); + } else if let Some(label) = self.label.as_ref() { + id = format!("{}/{id}", label); + } + id = id.replace([':'], "_"); + } + if !self.exists(&id) { + self.add(&id, value)?; + } + } + Ok(ResourceRef::new(format, id)) + } + /// Adds a resource, using a given id value. pub fn add<S, R>(&mut self, id: S, value: R) -> crate::Result<&mut Self> where @@ -179,9 +227,8 @@ impl ResourceStore { #[cfg(feature = "file_io")] if let Some(base) = self.base_path.as_ref() { let path = base.join(id.into()); - std::fs::create_dir_all(path.parent().unwrap_or(Path::new("")))?; - #[allow(clippy::expect_used)] - std::fs::write(path, value.into())?; + create_dir_all(path.parent().unwrap_or(Path::new("")))?; + write(path, value.into())?; return Ok(self); } self.resources.insert(id.into(), value.into()); @@ -202,7 +249,7 @@ impl ResourceStore { Some(base) => { // read the file, save in Map and then return a reference let path = base.join(id); - let value = std::fs::read(path).map_err(|_| { + let value = read(path).map_err(|_| { let path = base.join(id).to_string_lossy().into_owned(); Error::ResourceNotFound(path) })?;