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:
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)
})?;