commit 84c55283facf0cd6c26c059fa0846c9b633ad2e7
parent aadbb3227d388e526bc1e1161481fc0c9eb1481a
Author: Gavin Peacock <gpeacock@adobe.com>
Date: Tue, 31 Jan 2023 15:25:18 -0800
(MINOR) Create a ResourceStore for binary assets (#180)
* ResourceStore for binary file or in memory
Can read and write files thumbs and manifests
* integrate into Manifest and Ingredient
* Manifest
thumbnail field is now a ResourceRef
thumbnail() returns Option<(&str, Cow<Vec<u8>>)>
added resources field, resources() and resources_mut()
added thumbnail_ref() and set_thumbnail_ref()
added ingredients_mut()
new defaults for claim_generator, instance_id and format
added from_json() // create manifest from json string
added with_base_path() // sets manifest to be file based
added fmt for to_string() - as pretty json
* Ingredient
* move thumbnails into ResourceStore
* add resources to ingredient
* add folder support
* from_json manifest and ingredient paths working
with_files and without
* Assign resources to embedded ingredients
* Add some defaults for Json deserializing
* reading and writing thumbnail images is now done via ResourceStore
* Reduce level of API change
manifest_data now uses resource_ref
Added exists to ResourceStore
* make filenames from instanceId/manifest labels
add default claim_generator
add parent function on manifest
* updates for rust v1.67
Diffstat:
7 files changed, 858 insertions(+), 121 deletions(-)
diff --git a/sdk/src/assertion.rs b/sdk/src/assertion.rs
@@ -641,7 +641,7 @@ pub mod tests {
Action::new("c2pa.cropped")
.set_parameter(
"coordinate".to_owned(),
- r#"{"left": 0,"right": 2000,"top": 1000,"botton": 4000}"#,
+ serde_json::json!({"left": 0,"right": 2000,"top": 1000,"bottom": 4000}),
)
.unwrap(),
)
diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs
@@ -12,11 +12,13 @@
// each license.
#![deny(missing_docs)]
-
-use std::ops::Deref;
+use std::borrow::Cow;
+#[cfg(feature = "file_io")]
+use std::path::{Path, PathBuf};
use log::{debug, error};
use serde::{Deserialize, Serialize};
+use uuid::Uuid;
use crate::{
assertion::{get_thumbnail_image_type, Assertion, AssertionBase},
@@ -25,27 +27,20 @@ use crate::{
error::{Error, Result},
hashed_uri::HashedUri,
jumbf,
+ resource_store::{skip_serializing_resources, ResourceRef, ResourceStore},
store::Store,
validation_status::{self, ValidationStatus},
};
#[cfg(feature = "file_io")]
use crate::{error::wrap_io_err, validation_status::status_for_store, xmp_inmemory_utils::XmpInfo};
-
-/// Function that is used by serde to determine whether or not we should serialize
-/// thumbnail data based on the "serialize_thumbnails" flag (serialization is disabled by default)
-fn skip_serializing_thumbnails(value: &Option<(String, Vec<u8>)>) -> bool {
- !cfg!(feature = "serialize_thumbnails") || value.is_none()
-}
-
-#[cfg(feature = "file_io")]
-use std::path::Path;
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Default, Deserialize, Serialize)]
/// An `Ingredient` is any external asset that has been used in the creation of an image.
pub struct Ingredient {
/// A human-readable title, generally source filename.
title: String,
/// The format of the source file as a MIME type.
+ #[serde(default = "default_format")]
format: String,
/// Document ID from `xmpMM:DocumentID` in XMP metadata.
@@ -53,6 +48,7 @@ pub struct Ingredient {
document_id: Option<String>,
/// Instance ID from `xmpMM:InstanceID` in XMP metadata.
+ #[serde(default = "default_instance_id")]
instance_id: String,
/// URI from `dcterms:provenance` in XMP metadata.
@@ -62,8 +58,8 @@ pub struct Ingredient {
/// A thumbnail image capturing the visual state at the time of import.
///
/// A tuple of thumbnail MIME format (i.e. `image/jpeg`) and binary bits of the image.
- #[serde(skip_serializing_if = "skip_serializing_thumbnails")]
- thumbnail: Option<(String, Vec<u8>)>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ thumbnail: Option<ResourceRef>,
/// An optional hash of the asset to prevent duplicates.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -98,8 +94,20 @@ pub struct Ingredient {
/// A [`ManifestStore`] from the source asset extracted as a binary C2PA blob.
///
/// [`ManifestStore`]: crate::ManifestStore
- #[serde(skip_serializing)]
- manifest_data: Option<Vec<u8>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ manifest_data: Option<ResourceRef>,
+
+ #[serde(skip_deserializing)]
+ #[serde(skip_serializing_if = "skip_serializing_resources")]
+ resources: ResourceStore,
+}
+
+fn default_instance_id() -> String {
+ format!("xmp:iid:{}", Uuid::new_v4())
+}
+
+fn default_format() -> String {
+ "application/octet-stream".to_owned()
}
impl Ingredient {
@@ -124,16 +132,8 @@ impl Ingredient {
Self {
title: title.into(),
format: format.into(),
- document_id: None,
instance_id: instance_id.into(),
- provenance: None,
- thumbnail: None,
- hash: None,
- is_parent: None,
- validation_status: None,
- metadata: None,
- active_manifest: None,
- manifest_data: None,
+ ..Default::default()
}
}
@@ -162,11 +162,26 @@ impl Ingredient {
self.provenance.as_deref()
}
- /// Returns a tuple with thumbnail format and image bytes or `None`.
- pub fn thumbnail(&self) -> Option<(&str, &[u8])> {
+ /// Returns a ResourceRef or `None`.
+ pub fn thumbnail_ref(&self) -> Option<&ResourceRef> {
+ self.thumbnail.as_ref()
+ }
+
+ /// Returns thumbnail tuple Some((content_type, bytes)) or None
+ ///
+ pub fn thumbnail(&self) -> Option<(&str, Cow<Vec<u8>>)> {
self.thumbnail
.as_ref()
- .map(|(format, image)| (format.as_str(), image.deref()))
+ .and_then(|t| Some(t.content_type.as_str()).zip(self.resources.get(&t.identifier).ok()))
+ }
+
+ /// Returns a Cow of thumbnail bytes or Err(Error::NotFound)`.
+ ///
+ pub fn thumbnail_bytes(&self) -> Result<Cow<Vec<u8>>> {
+ match self.thumbnail.as_ref() {
+ Some(thumbnail) => self.resources.get(&thumbnail.identifier),
+ None => Err(Error::NotFound),
+ }
}
/// Returns an optional hash to uniquely identify this asset
@@ -201,9 +216,18 @@ impl Ingredient {
/// Returns a reference to C2PA manifest data if it exists.
///
- /// This is the binary form of a manifest store in .c2pa format.
- pub fn manifest_data(&self) -> Option<&[u8]> {
- self.manifest_data.as_deref()
+ /// manifest_data is the binary form of a manifest store in .c2pa format.
+ pub fn manifest_data_ref(&self) -> Option<&ResourceRef> {
+ self.manifest_data.as_ref()
+ }
+
+ /// Returns a copy on write ref to the manifest data bytes or None`.
+ ///
+ /// manifest_data is the binary form of a manifest store in .c2pa format.
+ pub fn manifest_data(&self) -> Option<Cow<Vec<u8>>> {
+ self.manifest_data
+ .as_ref()
+ .and_then(|r| self.resources.get(&r.identifier).ok())
}
/// Sets a human-readable title for this ingredient.
@@ -241,10 +265,28 @@ impl Ingredient {
self
}
- /// Sets the thumbnail format and image data.
- pub fn set_thumbnail<S: Into<String>>(&mut self, format: S, thumbnail: Vec<u8>) -> &mut Self {
- self.thumbnail = Some((format.into(), thumbnail));
- self
+ /// Sets the thumbnail from a ResourceRef.
+ pub fn set_thumbnail_ref(&mut self, thumbnail: ResourceRef) -> Result<&mut Self> {
+ // verify the resource referenced exists
+ if !self.resources.exists(&thumbnail.identifier) {
+ return Err(Error::NotFound);
+ };
+ self.thumbnail = Some(thumbnail);
+ Ok(self)
+ }
+
+ /// Sets the thumbnail content_type and image data.
+ pub fn set_thumbnail<S: Into<String>, B: Into<Vec<u8>>>(
+ &mut self,
+ content_type: S,
+ bytes: B,
+ ) -> Result<&mut Self> {
+ let base_id = self.instance_id().to_string();
+ self.thumbnail = Some(
+ self.resources
+ .add_with(&base_id, &content_type.into(), bytes)?,
+ );
+ Ok(self)
}
/// Sets the hash value generated from the entire asset.
@@ -274,10 +316,31 @@ impl Ingredient {
self
}
- /// Sets the Manifest C2PA data for this ingredient.
- pub fn set_manifest_data(&mut self, data: Vec<u8>) -> &mut Self {
- self.manifest_data = Some(data);
- self
+ /// Sets a reference to Manifest C2PA data - does not verify the resource exists
+ pub fn set_manifest_data_ref(&mut self, data_ref: ResourceRef) -> Result<&mut Self> {
+ // verify the resource referenced exists
+ if !self.resources.exists(&data_ref.identifier) {
+ return Err(Error::NotFound);
+ };
+ self.manifest_data = Some(data_ref);
+ Ok(self)
+ }
+
+ /// Sets the Manifest C2PA data for this ingredient with bytes
+ pub fn set_manifest_data(&mut self, data: Vec<u8>) -> Result<&mut Self> {
+ let base_id = self.instance_id().to_string();
+ self.manifest_data = Some(self.resources.add_with(&base_id, "c2pa", data)?);
+ Ok(self)
+ }
+
+ /// Return an immutable reference to the ingredient resources
+ pub fn resources(&self) -> &ResourceStore {
+ &self.resources
+ }
+
+ /// Return an mutable reference to the ingredient resources
+ pub fn resources_mut(&mut self) -> &mut ResourceStore {
+ &mut self.resources
}
/// Gathers filename, extension, and format from a file path.
@@ -334,7 +397,6 @@ impl Ingredient {
#[cfg(feature = "file_io")]
pub fn from_file_info<P: AsRef<Path>>(path: P) -> Self {
fn make_id(id_type: &str) -> String {
- use uuid::Uuid;
let uuid = Uuid::new_v4();
//warn!("Generating fake id {}", uuid);
format!("xmp:{id_type}id:{uuid}")
@@ -362,7 +424,18 @@ impl Ingredient {
#[cfg(feature = "file_io")]
/// Creates an `Ingredient` from a file path.
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
- Self::from_file_with_options(path.as_ref(), &DefaultOptions {})
+ Self::from_file_with_options(path.as_ref(), &DefaultOptions { base: None })
+ }
+
+ #[cfg(feature = "file_io")]
+ /// Creates an `Ingredient` from a file path.
+ pub fn from_file_with_folder<P: AsRef<Path>>(path: P, folder: P) -> Result<Self> {
+ Self::from_file_with_options(
+ path.as_ref(),
+ &DefaultOptions {
+ base: Some(PathBuf::from(folder.as_ref())),
+ },
+ )
}
fn thumbnail_from_assertion(assertion: &Assertion) -> (String, Vec<u8>) {
@@ -402,6 +475,11 @@ impl Ingredient {
return Err(Error::FileNotFound(ingredient.title));
}
+ // configure for writing to folders if that option is set
+ if let Some(folder) = options.base_path().as_ref() {
+ ingredient.with_base_path(folder)?;
+ }
+
// if options includes a title, use it
if let Some(opt_title) = options.title(path) {
ingredient.title = opt_title;
@@ -455,12 +533,15 @@ impl Ingredient {
{
let (format, image) =
Self::thumbnail_from_assertion(claim_assertion.assertion());
- ingredient.set_thumbnail(format, image);
+ ingredient.set_thumbnail(format, image)?;
}
}
ingredient.active_manifest = Some(claim.label().to_string());
}
- ingredient.manifest_data = manifest_bytes;
+ if let Some(bytes) = manifest_bytes {
+ ingredient.set_manifest_data(bytes)?;
+ }
+
ingredient.validation_status = if statuses.is_empty() {
None
} else {
@@ -492,7 +573,7 @@ impl Ingredient {
// create a thumbnail if we don't already have a manifest with a thumb we can use
if ingredient.thumbnail.is_none() {
if let Some((format, image)) = options.thumbnail(path) {
- ingredient.set_thumbnail(format, image);
+ ingredient.set_thumbnail(format, image)?;
}
}
@@ -500,7 +581,11 @@ impl Ingredient {
}
/// Creates an Ingredient from a store and a URI to an ingredient assertion.
- pub(crate) fn from_ingredient_uri(store: &Store, ingredient_uri: &str) -> Result<Self> {
+ pub(crate) fn from_ingredient_uri(
+ store: &Store,
+ ingredient_uri: &str,
+ #[cfg(feature = "file_io")] resource_path: Option<&Path>,
+ ) -> Result<Self> {
let assertion =
store
.get_assertion_from_uri(ingredient_uri)
@@ -554,8 +639,14 @@ impl Ingredient {
&ingredient_assertion.instance_id,
);
ingredient.document_id = ingredient_assertion.document_id;
+
+ #[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);
+ ingredient.set_thumbnail(format, image)?;
}
ingredient.is_parent = is_parent;
@@ -596,7 +687,7 @@ impl Ingredient {
};
// have Store check and load ingredients and add them to a claim
- Store::load_ingredient_to_claim(claim, &manifest_label, buffer, redactions)?;
+ Store::load_ingredient_to_claim(claim, &manifest_label, &buffer, redactions)?;
// get the ingredient map loaded in previous
match claim.claim_ingredient(&manifest_label) {
@@ -656,10 +747,10 @@ impl Ingredient {
// add ingredient thumbnail assertion if one is given and we don't already have one from the parent claim
if thumbnail.is_none() {
- if let Some((format, image)) = &self.thumbnail() {
+ if let Some((format, data)) = self.thumbnail() {
let hash_url = claim.add_assertion(&Thumbnail::new(
&labels::add_thumbnail_format(labels::INGREDIENT_THUMBNAIL, format),
- image.to_vec(),
+ data.to_vec(),
))?;
thumbnail = Some(hash_url);
@@ -680,6 +771,16 @@ impl Ingredient {
ingredient_assertion.validation_status = self.validation_status.clone();
claim.add_assertion(&ingredient_assertion)
}
+
+ /// Setting a base path will make the ingredient use resource files instead of memory buffers
+ ///
+ /// The files will be relative to the given base path
+ #[cfg(feature = "file_io")]
+ pub fn with_base_path<P: AsRef<Path>>(&mut self, base_path: P) -> Result<&Self> {
+ std::fs::create_dir_all(&base_path)?;
+ self.resources.set_base_path(base_path.as_ref());
+ Ok(self)
+ }
}
impl std::fmt::Display for Ingredient {
@@ -718,15 +819,32 @@ pub trait IngredientOptions {
#[cfg(not(feature = "add_thumbnails"))]
None
}
+
+ /// Returns an optional folder path
+ ///
+ /// If Some, binary data will be stored in files in the given folder
+ fn base_path(&self) -> Option<&Path> {
+ None
+ }
}
-#[cfg(feature = "file_io")]
/// DefaultOptions returns None for Title and Hash and generates thumbnail for supported thumbnails
///
/// This can be use with Ingredient::from_file_with_options
-pub struct DefaultOptions {}
#[cfg(feature = "file_io")]
-impl IngredientOptions for DefaultOptions {}
+pub struct DefaultOptions {
+ /// If Some, the ingredient will read/write binary assets using this folder.
+ ///
+ /// If None, the assets will be kept in memory.
+ pub base: Option<std::path::PathBuf>,
+}
+
+#[cfg(feature = "file_io")]
+impl IngredientOptions for DefaultOptions {
+ fn base_path(&self) -> Option<&Path> {
+ self.base.as_deref()
+ }
+}
#[cfg(test)]
mod tests {
@@ -747,8 +865,10 @@ mod tests {
.set_is_parent()
.set_metadata(Metadata::new())
.set_thumbnail("format", "thumbnail".as_bytes().to_vec())
+ .unwrap()
.set_active_manifest("active_manifest")
.set_manifest_data("data".as_bytes().to_vec())
+ .expect("set_manifest")
.add_validation_status(ValidationStatus::new("status_code"));
assert_eq!(ingredient.title(), "title2");
assert_eq!(ingredient.format(), "format");
@@ -758,12 +878,17 @@ mod tests {
assert_eq!(ingredient.hash(), Some("hash"));
assert!(ingredient.is_parent());
assert!(ingredient.metadata().is_some());
+ assert_eq!(ingredient.thumbnail().unwrap().0, "format");
+ assert_eq!(
+ *ingredient.thumbnail().unwrap().1,
+ "thumbnail".as_bytes().to_vec()
+ );
assert_eq!(
- ingredient.thumbnail(),
- Some(("format", "thumbnail".as_bytes()))
+ *ingredient.thumbnail_bytes().unwrap(),
+ "thumbnail".as_bytes().to_vec()
);
assert_eq!(ingredient.active_manifest(), Some("active_manifest"));
- assert_eq!(ingredient.manifest_data(), Some("data".as_bytes()));
+
assert_eq!(
ingredient.validation_status().unwrap()[0].code(),
"status_code"
@@ -786,8 +911,8 @@ mod tests_file_io {
const PRERELEASE_JPEG: &str = "prerelease.jpg";
fn stats(ingredient: &Ingredient) -> usize {
- let thumb_size = ingredient.thumbnail().map_or(0, |(_, image)| image.len());
- let manifest_data_size = ingredient.manifest_data().map_or(0, |v| v.len());
+ let thumb_size = ingredient.thumbnail_bytes().map_or(0, |i| i.len());
+ let manifest_data_size = ingredient.manifest_data().map_or(0, |r| r.len());
println!(
" {} instance_id: {}, thumb size: {}, manifest_data size: {}",
@@ -950,4 +1075,57 @@ mod tests_file_io {
println!("ingredient = {ingredient}");
assert_eq!(ingredient.validation_status(), None);
}
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_jpg_with_path() {
+ let ap = fixture_path("CIE-sig-CA.jpg");
+ let mut folder = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ folder.push("../target/tmp/ingredient");
+ let mut ingredient = Ingredient::from_file_with_folder(ap, folder).expect("from_file");
+ println!("ingredient = {ingredient}");
+ assert_eq!(ingredient.validation_status(), None);
+
+ // verify we can't set a references that don't exist
+ assert!(ingredient
+ .set_thumbnail_ref(ResourceRef::new("Foo", "bar"))
+ .is_err());
+ assert!(ingredient
+ .set_manifest_data_ref(ResourceRef::new("Foo", "bar"))
+ .is_err());
+ // verify we can set a references that do exist
+ assert!(ingredient
+ .set_thumbnail_ref(ResourceRef::new("Foo", "bar"))
+ .is_err());
+ assert!(ingredient
+ .set_manifest_data_ref(ResourceRef::new("Foo", "bar"))
+ .is_err());
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_crate_file_based_ingredient() {
+ let mut folder = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ folder.push("tests/fixtures");
+ let mut ingredient = Ingredient::new("title", "format", "instance_id");
+ ingredient.resources.set_base_path(folder);
+ // verify we can't set a references that don't exist
+ assert!(ingredient
+ .set_thumbnail_ref(ResourceRef::new("image/jpg", "foo"))
+ .is_err());
+ assert!(ingredient.thumbnail_ref().is_none());
+ assert!(ingredient
+ .set_manifest_data_ref(ResourceRef::new("image/jpg", "foo"))
+ .is_err());
+ assert!(ingredient.manifest_data_ref().is_none());
+ // verify we can set a references that do exist
+ assert!(ingredient
+ .set_thumbnail_ref(ResourceRef::new("image/jpg", "C.jpg"))
+ .is_ok());
+ assert!(ingredient.thumbnail_ref().is_some());
+ assert!(ingredient
+ .set_manifest_data_ref(ResourceRef::new("c2pa", "cloud_manifest.c2pa"))
+ .is_ok());
+ assert!(ingredient.manifest_data_ref().is_some());
+ }
}
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
@@ -106,6 +106,9 @@ pub use manifest_store::ManifestStore;
mod manifest_store_report;
pub use manifest_store_report::ManifestStoreReport;
+mod resource_store;
+pub use resource_store::{ResourceRef, ResourceStore};
+
mod signing_alg;
#[cfg(feature = "file_io")]
pub use ingredient::{DefaultOptions, IngredientOptions};
@@ -121,12 +124,12 @@ mod signer;
pub use signer::Signer;
#[cfg(feature = "async_signer")]
pub use signer::{AsyncSigner, RemoteSigner};
-/// crate private declarations
#[allow(dead_code, clippy::enum_variant_names)]
pub(crate) mod asn1;
pub(crate) mod assertion;
pub(crate) mod asset_handlers;
pub(crate) mod asset_io;
+/// crate private declarations
pub(crate) mod claim;
#[cfg(feature = "sign")]
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -11,9 +11,9 @@
// specific language governing permissions and limitations under
// each license.
-use std::collections::HashMap;
#[cfg(feature = "file_io")]
use std::path::Path;
+use std::{borrow::Cow, collections::HashMap};
use log::{debug, error, warn};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -26,6 +26,7 @@ use crate::{
claim::{Claim, RemoteManifest},
error::{Error, Result},
jumbf,
+ resource_store::{skip_serializing_resources, ResourceRef, ResourceStore},
salt::DefaultSalt,
store::Store,
Ingredient, ManifestAssertion, ManifestAssertionKind,
@@ -35,15 +36,8 @@ use crate::{asset_io::CAIReadWrite, Signer};
#[cfg(all(feature = "async_signer", feature = "file_io"))]
use crate::{AsyncSigner, RemoteSigner};
-/// Function that is used by serde to determine whether or not we should serialize
-/// thumbnail data based on the `serialize_thumbnails` flag.
-/// (Serialization is disabled by default.)
-fn skip_serializing_thumbnails(value: &Option<(String, Vec<u8>)>) -> bool {
- !cfg!(feature = "serialize_thumbnails") || value.is_none()
-}
-
/// A Manifest represents all the information in a c2pa manifest
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Manifest {
/// Optional prefix added to the generated Manifest Label
/// This is typically Internet domain name for the vendor (i.e. `adobe`)
@@ -52,24 +46,28 @@ pub struct Manifest {
/// A User Agent formatted string identifying the software/hardware/system produced this claim
/// Spaces are not allowed in names, versions can be specified with product/1.0 syntax
+ #[serde(default = "default_claim_generator")]
pub claim_generator: String,
/// A human-readable title, generally source filename.
title: Option<String>,
/// The format of the source file as a MIME type.
+ #[serde(default = "default_format")]
format: String,
/// Instance ID from `xmpMM:InstanceID` in XMP metadata.
+ #[serde(default = "default_instance_id")]
instance_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
claim_generator_hints: Option<HashMap<String, Value>>,
- #[serde(skip_serializing_if = "skip_serializing_thumbnails")]
- thumbnail: Option<(String, Vec<u8>)>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ thumbnail: Option<ResourceRef>,
/// A List of ingredients
+ #[serde(default = "default_vec")]
ingredients: Vec<Ingredient>,
/// A List of verified credentials
@@ -77,6 +75,7 @@ pub struct Manifest {
credentials: Option<Vec<Value>>,
/// A list of assertions
+ #[serde(default = "default_vec")]
assertions: Vec<ManifestAssertion>,
/// A list of redactions - URIs to a redacted assertions
@@ -90,8 +89,30 @@ pub struct Manifest {
#[serde(skip_serializing_if = "Option::is_none")]
label: Option<String>,
- #[serde(skip_deserializing, skip_serializing)]
+ /// Indicates where a generated manifest goes
+ #[serde(skip)]
remote_manifest: Option<RemoteManifest>,
+
+ /// container for binary assets (like thumbnails)
+ #[serde(skip_deserializing)]
+ #[serde(skip_serializing_if = "skip_serializing_resources")]
+ resources: ResourceStore,
+}
+
+fn default_claim_generator() -> String {
+ format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
+}
+
+fn default_instance_id() -> String {
+ format!("xmp:iid:{}", Uuid::new_v4())
+}
+
+fn default_format() -> String {
+ "application/octet-stream".to_owned()
+}
+
+fn default_vec<T>() -> Vec<T> {
+ Vec::new()
}
impl Manifest {
@@ -99,20 +120,10 @@ impl Manifest {
/// requires a claim_generator string (User Agent))
pub fn new<S: Into<String>>(claim_generator: S) -> Self {
Self {
- vendor: None,
- title: None,
- format: "application/octet-stream".to_owned(),
- instance_id: format!("xmp:iid:{}", Uuid::new_v4()),
claim_generator: claim_generator.into(),
- claim_generator_hints: None,
- thumbnail: None,
- ingredients: Vec::new(),
- assertions: Vec::new(),
- redactions: None,
- credentials: None,
- signature_info: None,
- label: None,
- remote_manifest: None,
+ format: default_format(),
+ instance_id: default_instance_id(),
+ ..Default::default()
}
}
/// Returns a User Agent formatted string identifying the software/hardware/system produced this claim
@@ -140,19 +151,31 @@ impl Manifest {
self.title.as_deref()
}
- /// Returns a tuple with thumbnail format and image bytes or `None`.
- pub fn thumbnail(&self) -> Option<(&str, &[u8])> {
+ /// Returns thumbnail tuple with Some((content_type, bytes)) or None
+ ///
+ pub fn thumbnail(&self) -> Option<(&str, Cow<Vec<u8>>)> {
self.thumbnail
.as_ref()
- .map(|(format, image)| (format.as_str(), image.as_ref()))
+ .and_then(|t| Some(t.content_type.as_str()).zip(self.resources.get(&t.identifier).ok()))
}
- /// Returns the [Ingredient]s used by this Manifest
+ /// Returns a thumbnail ResourceRef or `None`.
+ pub fn thumbnail_ref(&self) -> Option<&ResourceRef> {
+ self.thumbnail.as_ref()
+ }
+
+ /// Returns immutable [Ingredient]s used by this Manifest
/// This can include a parent as well as any placed assets
pub fn ingredients(&self) -> &[Ingredient] {
&self.ingredients
}
+ /// Returns mutable [Ingredient]s used by this Manifest
+ /// This can include a parent as well as any placed assets
+ pub fn ingredients_mut(&mut self) -> &mut [Ingredient] {
+ &mut self.ingredients
+ }
+
/// Returns Assertions for this Manifest
pub fn assertions(&self) -> &[ManifestAssertion] {
&self.assertions
@@ -203,10 +226,31 @@ impl Manifest {
self
}
+ /// Sets the thumbnail from a ResourceRef.
+ pub fn set_thumbnail_ref(&mut self, thumbnail: ResourceRef) -> Result<&mut Self> {
+ // verify the resource referenced exists
+ if !self.resources.exists(&thumbnail.identifier) {
+ return Err(Error::NotFound);
+ };
+ self.thumbnail = Some(thumbnail);
+ Ok(self)
+ }
+
/// Sets the thumbnail format and image data.
- pub fn set_thumbnail<S: Into<String>>(&mut self, format: S, thumbnail: Vec<u8>) -> &mut Self {
- self.thumbnail = Some((format.into(), thumbnail));
- self
+ pub fn set_thumbnail<S: Into<String>, B: Into<Vec<u8>>>(
+ &mut self,
+ format: S,
+ thumbnail: B,
+ ) -> Result<&mut Self> {
+ let base_id = self
+ .label()
+ .unwrap_or_else(|| self.instance_id())
+ .to_string();
+ self.thumbnail = Some(
+ self.resources
+ .add_with(&base_id, &format.into(), thumbnail)?,
+ );
+ Ok(self)
}
/// If set, the embed calls will create a sidecar .c2pa manifest file next to the output file
@@ -237,10 +281,15 @@ impl Manifest {
self.signature_info.as_ref()
}
+ /// Returns the parent ingredient if it exists
+ pub fn parent(&self) -> Option<&Ingredient> {
+ self.ingredients.iter().find(|i| i.is_parent())
+ }
+
/// Sets the parent ingredient, assuring it is first and setting the is_parent flag
pub fn set_parent(&mut self, mut ingredient: Ingredient) -> Result<&mut Self> {
// there should only be one parent so return an error if we already have one
- if self.ingredients.iter().any(|i| i.is_parent()) {
+ if self.parent().is_some() {
error!("parent already added");
return Err(Error::BadParam("Parent parent already added".to_owned()));
}
@@ -413,8 +462,42 @@ impl Manifest {
self.signature_info.to_owned().and_then(|sig| sig.time)
}
+ /// Return an immutable reference to the manifest resources
+ pub fn resources(&self) -> &ResourceStore {
+ &self.resources
+ }
+
+ /// Return a mutable reference to the manifest resources
+ pub fn resources_mut(&mut self) -> &mut ResourceStore {
+ &mut self.resources
+ }
+
+ /// Creates a Manifest from a JSON string formatted as a Manifest
+ pub fn from_json(json: &str) -> Result<Self> {
+ serde_json::from_slice(json.as_bytes()).map_err(Error::JsonError)
+ }
+
+ /// Setting a base path will make the manifest use resource files instead of memory buffers
+ ///
+ /// The files will be relative to the given base path
+ /// Ingredients resources will also be relative to this path
+ #[cfg(feature = "file_io")]
+ pub fn with_base_path<P: AsRef<Path>>(&mut self, base_path: P) -> Result<&Self> {
+ std::fs::create_dir_all(&base_path)?;
+ self.resources.set_base_path(base_path.as_ref());
+ for i in 0..self.ingredients.len() {
+ // todo: create different subpath for each ingredient?
+ self.ingredients[i].with_base_path(base_path.as_ref())?;
+ }
+ Ok(self)
+ }
+
// Generates a Manifest given a store and a manifest label
- pub(crate) fn from_store(store: &Store, manifest_label: &str) -> Result<Self> {
+ pub(crate) fn from_store(
+ store: &Store,
+ manifest_label: &str,
+ #[cfg(feature = "file_io")] resource_path: Option<&Path>,
+ ) -> Result<Self> {
let claim = store
.get_claim(manifest_label)
.ok_or_else(|| Error::ClaimMissing {
@@ -425,6 +508,11 @@ impl Manifest {
let claim_generator = claim.claim_generator().to_owned();
let mut manifest = Manifest::new(claim_generator);
+ #[cfg(feature = "file_io")]
+ if let Some(base_path) = resource_path {
+ manifest.with_base_path(base_path)?;
+ }
+
manifest.set_label(claim.label());
manifest.claim_generator_hints = claim.get_claim_generator_hint_map().cloned();
@@ -467,12 +555,17 @@ impl Manifest {
match base_label.as_ref() {
labels::INGREDIENT => {
let assertion_uri = jumbf::labels::to_assertion_uri(claim.label(), &label);
- let ingredient = Ingredient::from_ingredient_uri(store, &assertion_uri)?;
+ let ingredient = Ingredient::from_ingredient_uri(
+ store,
+ &assertion_uri,
+ #[cfg(feature = "file_io")]
+ resource_path,
+ )?;
manifest.add_ingredient(ingredient);
}
label if label.starts_with(labels::CLAIM_THUMBNAIL) => {
let thumbnail = Thumbnail::from_assertion(assertion)?;
- manifest.set_thumbnail(thumbnail.content_type, thumbnail.data);
+ manifest.set_thumbnail(thumbnail.content_type, thumbnail.data)?;
}
_ => {
// inject assertions for all other assertions
@@ -499,8 +592,6 @@ impl Manifest {
}
}
- // manifest.set_asset(asset);
-
let issuer = claim.signing_issuer();
let signing_time = claim
.signing_time()
@@ -522,7 +613,7 @@ impl Manifest {
/// this method can be used to ensure that data is correct
/// it will extract filename,format and xmp info and generate a thumbnail
#[cfg(feature = "file_io")]
- pub fn set_asset_from_path<P: AsRef<Path>>(&mut self, path: P) {
+ pub fn set_asset_from_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
// Gather the information we need from the target path
let ingredient = Ingredient::from_file_info(path.as_ref());
@@ -538,9 +629,10 @@ impl Manifest {
if self.thumbnail().is_none() {
#[cfg(feature = "add_thumbnails")]
if let Ok((format, image)) = crate::utils::thumbnail::make_thumbnail(path.as_ref()) {
- self.set_thumbnail(format, image);
+ self.set_thumbnail(format, image)?;
}
}
+ Ok(())
}
// Convert a Manifest into a Claim
@@ -572,10 +664,10 @@ impl Manifest {
}
claim.format = self.format().to_owned();
claim.instance_id = self.instance_id().to_owned();
- if let Some((format, image)) = self.thumbnail() {
+ if let Some((content_type, data)) = self.thumbnail() {
claim.add_assertion(&Thumbnail::new(
- &labels::add_thumbnail_format(labels::CLAIM_THUMBNAIL, format),
- image.to_vec(),
+ &labels::add_thumbnail_format(labels::CLAIM_THUMBNAIL, content_type),
+ data.to_vec(),
))?;
}
@@ -592,10 +684,8 @@ impl Manifest {
let mut ingredient_map = HashMap::new();
// add all ingredients to the claim
for ingredient in &self.ingredients {
- ingredient_map.insert(
- ingredient.instance_id(),
- ingredient.add_to_claim(&mut claim, self.redactions.clone())?,
- );
+ let uri = ingredient.add_to_claim(&mut claim, self.redactions.clone())?;
+ ingredient_map.insert(ingredient.instance_id(), uri);
}
let salt = DefaultSalt::default();
@@ -713,7 +803,7 @@ impl Manifest {
copied = true;
}
// first add the information about the target file
- self.set_asset_from_path(dest_path.as_ref());
+ self.set_asset_from_path(dest_path.as_ref())?;
if copied {
Ok(dest_path)
@@ -804,7 +894,7 @@ impl Manifest {
if let Ok((format, image)) =
crate::utils::thumbnail::make_thumbnail_from_stream(format, stream)
{
- self.set_thumbnail(format, image);
+ self.set_thumbnail(format, image)?;
}
}
@@ -888,6 +978,7 @@ pub(crate) mod tests {
use crate::{
assertions::labels::ACTIONS,
error::Error,
+ resource_store::ResourceRef,
status_tracker::{DetailedStatusTracker, StatusTracker},
store::Store,
utils::test::{fixture_path, temp_dir_path, temp_fixture_path, TEST_SMALL_JPEG},
@@ -1035,8 +1126,13 @@ pub(crate) mod tests {
manifest.add_assertion(&cbor).expect("add_assertion");
let store = manifest.to_store().expect("to_store");
- let _manifest2 =
- Manifest::from_store(&store, &store.provenance_label().unwrap()).expect("from_store");
+ let _manifest2 = Manifest::from_store(
+ &store,
+ &store.provenance_label().unwrap(),
+ #[cfg(feature = "file_io")]
+ None,
+ )
+ .expect("from_store");
println!("{store}");
println!("{_manifest2:?}");
let cbor2: UserCbor = manifest.find_assertion(LABEL).expect("get_assertion");
@@ -1167,8 +1263,16 @@ pub(crate) mod tests {
let store = manifest.to_store().expect("to_store");
println!("{store}");
let active_label = store.provenance_label().unwrap();
- let manifest2 = Manifest::from_store(&store, &active_label).expect("from_store");
+
+ let manifest2 = Manifest::from_store(
+ &store,
+ &active_label,
+ #[cfg(feature = "file_io")]
+ None,
+ )
+ .expect("from_store");
println!("{manifest2}");
+
// now check to see if we have three separate assertions with different instances
let action2: Result<Actions> = manifest2.find_assertion_with_instance(Actions::LABEL, 2);
assert!(action2.is_ok());
@@ -1316,8 +1420,7 @@ pub(crate) mod tests {
);
#[cfg(feature = "add_thumbnails")]
assert!(manifest_store.get_active().unwrap().thumbnail().is_some());
-
- println!("{manifest_store}");
+ //println!("{manifest_store}");main
}
#[cfg(feature = "file_io")]
@@ -1386,12 +1489,139 @@ pub(crate) mod tests {
let signer = temp_signer();
let mut manifest = test_manifest();
- manifest.set_thumbnail("image/jpeg", vec![1, 2, 3]);
+ let thumb_data = vec![1, 2, 3];
+ manifest
+ .set_thumbnail("image/jpeg", thumb_data.clone())
+ .expect("set_thumbnail");
manifest.embed(&output, &output, &signer).expect("embed");
let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
let active_manifest = manifest_store.get_active().unwrap();
- let (format, thumb) = active_manifest.thumbnail().unwrap();
- assert_eq!(format, "image/jpeg");
- assert_eq!(thumb, vec![1, 2, 3]);
+ let (content_type, image) = active_manifest.thumbnail().unwrap();
+ assert_eq!(content_type, "image/jpeg");
+ assert_eq!(image.into_owned(), thumb_data);
+ }
+
+ #[cfg(feature = "file_io")]
+ const MANIFEST_JSON: &str = r#"{
+ "claim_generator": "test",
+ "format" : "image/jpeg",
+ "thumbnail": {
+ "content_type": "image/jpeg",
+ "identifier": "IMG_0003.jpg"
+ },
+ "ingredients": [{
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "is_parent": true,
+ "thumbnail": {
+ "content_type": "image/png",
+ "identifier": "exp-test1.png"
+ }
+ }]
+ }"#;
+
+ #[test]
+ #[cfg(feature = "sign")]
+ /// tests and illustrates how to add assets to a non-file based manifest
+ fn from_json_with_memory() {
+ let mut manifest = Manifest::from_json(MANIFEST_JSON).unwrap();
+ // add binary resources to manifest and ingredients giving matching the identifiers given in JSON
+ manifest
+ .resources_mut()
+ .add("IMG_0003.jpg", *b"my value")
+ .expect("add resource");
+ manifest.ingredients_mut()[0]
+ .resources_mut()
+ .add("exp-test1.png", *b"my value")
+ .expect("add_resource");
+
+ 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.
+ manifest
+ .embed_stream("jpeg", &mut stream, &signer)
+ .expect("embed_stream");
+
+ // get the updated image
+ let image = stream.into_inner();
+
+ let manifest_store =
+ crate::ManifestStore::from_bytes("jpeg", &image, true).expect("from_bytes");
+ let m = manifest_store.get_active().unwrap();
+
+ assert!(m.thumbnail().is_some());
+ let (content_type, image) = m.thumbnail().unwrap();
+ assert_eq!(content_type, "image/jpeg");
+ assert_eq!(image.to_vec(), b"my value");
+ // println!("{manifest_store}");
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn from_json_with_files() {
+ let mut manifest = Manifest::from_json(MANIFEST_JSON).unwrap();
+ let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ path.push("tests/fixtures"); // the path we want to read files from
+ manifest.with_base_path(path).expect("with_files");
+ println!("{manifest}");
+ // convert the manifest to a store
+ let store = manifest.to_store().expect("to store");
+ let mut resource_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ resource_path.push("../target/tmp/manifest");
+ let m2 = Manifest::from_store(
+ &store,
+ &store.provenance_label().unwrap(),
+ Some(&resource_path),
+ )
+ .expect("from store");
+ assert!(m2.thumbnail().is_some());
+ assert!(m2.ingredients()[0].thumbnail().is_some());
+ }
+
+ #[cfg(feature = "file_io")]
+ #[test]
+ fn test_embed_from_json() {
+ let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ fixtures.push("tests/fixtures"); // the path we want to read files from
+
+ let temp_dir = tempdir().expect("temp dir");
+ let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
+
+ let signer = temp_signer();
+
+ let mut manifest = Manifest::from_json(MANIFEST_JSON).expect("from_json");
+ manifest.with_base_path(fixtures).expect("with_base");
+ manifest.embed(&output, &output, &signer).expect("embed");
+
+ let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
+ println!("{manifest_store}");
+ let active_manifest = manifest_store.get_active().unwrap();
+ let (content_type, _) = active_manifest.thumbnail().unwrap();
+ assert_eq!(content_type, "image/jpeg");
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_crate_file_based_ingredient() {
+ let mut folder = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ folder.push("tests/fixtures");
+ let mut manifest = Manifest::new("claim_generator");
+ manifest.resources.set_base_path(folder);
+ // verify we can't set a references that don't exist
+ assert!(manifest
+ .set_thumbnail_ref(ResourceRef::new("image/jpg", "foo"))
+ .is_err());
+ assert!(manifest.thumbnail_ref().is_none());
+ // verify we can set a references that do exist
+ assert!(manifest
+ .set_thumbnail_ref(ResourceRef::new("image/jpg", "C.jpg"))
+ .is_ok());
+ assert!(manifest.thumbnail_ref().is_some());
}
}
diff --git a/sdk/src/manifest_store.rs b/sdk/src/manifest_store.rs
@@ -77,11 +77,35 @@ impl ManifestStore {
self.validation_status.as_deref()
}
- /// creates a ManifestStore from a Store
+ /// creates a ManifestStore from a Store with validation
pub(crate) fn from_store(
store: &Store,
validation_log: &mut impl StatusTracker,
) -> ManifestStore {
+ Self::from_store_impl(
+ store,
+ validation_log,
+ #[cfg(feature = "file_io")]
+ None,
+ )
+ }
+
+ /// creates a ManifestStore from a Store writing resources to resource_path
+ #[cfg(feature = "file_io")]
+ pub fn from_store_with_resources(
+ store: &Store,
+ validation_log: &mut impl StatusTracker,
+ resource_path: &Path,
+ ) -> ManifestStore {
+ Self::from_store_impl(store, validation_log, Some(resource_path))
+ }
+
+ // internal implementation of from_store
+ fn from_store_impl(
+ store: &Store,
+ validation_log: &mut impl StatusTracker,
+ #[cfg(feature = "file_io")] resource_path: Option<&Path>,
+ ) -> ManifestStore {
let mut statuses = status_for_store(store, validation_log);
let mut manifest_store = ManifestStore::new();
@@ -89,7 +113,11 @@ impl ManifestStore {
for claim in store.claims() {
let manifest_label = claim.label();
- match Manifest::from_store(store, manifest_label) {
+ #[cfg(feature = "file_io")]
+ let result = Manifest::from_store(store, manifest_label, resource_path);
+ #[cfg(not(feature = "file_io"))]
+ let result = Manifest::from_store(store, manifest_label);
+ match result {
Ok(manifest) => {
manifest_store
.manifests
@@ -112,7 +140,12 @@ impl ManifestStore {
pub fn from_manifest(manifest: &Manifest) -> Result<Self> {
use crate::status_tracker::OneShotStatusTracker;
let store = manifest.to_store()?;
- Ok(Self::from_store(&store, &mut OneShotStatusTracker::new()))
+ Ok(Self::from_store_impl(
+ &store,
+ &mut OneShotStatusTracker::new(),
+ #[cfg(feature = "file_io")]
+ manifest.resources().base_path(),
+ ))
}
/// generate a Store from a format string and bytes
@@ -143,6 +176,33 @@ impl ManifestStore {
Ok(Self::from_store(&store, &mut validation_log))
}
+ #[cfg(feature = "file_io")]
+ /// Loads a ManifestStore from a file adding resources to a folder
+ /// Example:
+ ///
+ /// ```
+ /// # use c2pa::Result;
+ /// use c2pa::ManifestStore;
+ /// # fn main() -> Result<()> {
+ /// let manifest_store = ManifestStore::from_file_with_resources("tests/fixtures/C.jpg","../target/tmp/manifest_store")?;
+ /// println!("{}", manifest_store);
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn from_file_with_resources<P: AsRef<Path>>(
+ path: P,
+ resource_path: P,
+ ) -> Result<ManifestStore> {
+ let mut validation_log = DetailedStatusTracker::new();
+
+ let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?;
+ Ok(Self::from_store_with_resources(
+ &store,
+ &mut validation_log,
+ resource_path.as_ref(),
+ ))
+ }
+
/// Loads a ManifestStore from a file
pub async fn from_bytes_async(
format: &str,
@@ -346,4 +406,24 @@ mod tests {
assert!(manifest_store.validation_status().is_none());
println!("{manifest_store}");
}
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn manifest_report_from_file_with_resources() {
+ let manifest_store = ManifestStore::from_file_with_resources(
+ "tests/fixtures/CIE-sig-CA.jpg",
+ "../target/tmp/ms",
+ )
+ .expect("from_store_with_resources");
+ println!("{manifest_store}");
+
+ assert!(manifest_store.active_label().is_some());
+ assert!(manifest_store.get_active().is_some());
+ assert!(!manifest_store.manifests().is_empty());
+ assert!(manifest_store.validation_status().is_none());
+ let manifest = manifest_store.get_active().unwrap();
+ assert!(!manifest.ingredients().is_empty());
+ assert_eq!(manifest.issuer().unwrap(), "C2PA Test Signing Cert");
+ assert!(manifest.time().is_some());
+ }
}
diff --git a/sdk/src/manifest_store_report.rs b/sdk/src/manifest_store_report.rs
@@ -292,7 +292,11 @@ impl ManifestReport {
Ok(Self {
claim: serde_json::to_value(claim)?, // todo: this will lose tagging info
assertion_store,
- credential_store: (!credential_store.is_empty()).then(|| credential_store),
+ credential_store: if !credential_store.is_empty() {
+ Some(credential_store)
+ } else {
+ None
+ },
signature,
})
}
diff --git a/sdk/src/resource_store.rs b/sdk/src/resource_store.rs
@@ -0,0 +1,242 @@
+// Copyright 2023 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+#[cfg(feature = "file_io")]
+use std::path::{Path, PathBuf};
+use std::{borrow::Cow, collections::HashMap};
+
+use serde::{Deserialize, Serialize};
+
+use crate::{Error, Result};
+
+/// Function that is used by serde to determine whether or not we should serialize
+/// resources based on the `serialize_resources` flag.
+/// (Serialization is disabled by default.)
+pub(crate) fn skip_serializing_resources(_: &ResourceStore) -> bool {
+ !cfg!(feature = "serialize_thumbnails") || cfg!(test)
+}
+
+/// A reference to a resource to be used in JSON serialization
+#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
+pub struct ResourceRef {
+ pub content_type: String,
+ pub identifier: String,
+}
+
+impl ResourceRef {
+ pub fn new<S: Into<String>, I: Into<String>>(content_type: S, identifier: I) -> Self {
+ Self {
+ content_type: content_type.into(),
+ identifier: identifier.into(),
+ }
+ }
+}
+
+/// Resource store to contain binary objects referenced from JSON serializable structures
+#[derive(Debug, Serialize)]
+pub struct ResourceStore {
+ resources: HashMap<String, Vec<u8>>,
+ #[cfg(feature = "file_io")]
+ base_path: Option<PathBuf>,
+}
+
+impl ResourceStore {
+ pub fn new() -> Self {
+ ResourceStore {
+ resources: HashMap::new(),
+ #[cfg(feature = "file_io")]
+ base_path: None,
+ }
+ }
+
+ #[cfg(feature = "file_io")]
+ pub fn base_path(&self) -> Option<&Path> {
+ self.base_path.as_deref()
+ }
+
+ #[cfg(feature = "file_io")]
+ pub fn set_base_path<P: Into<PathBuf>>(&mut self, base_path: P) {
+ self.base_path = Some(base_path.into());
+ }
+
+ /// generate a unique id for a given content type (adds a file extension)
+ pub fn id_from(&self, key: &str, format: &str) -> String {
+ let ext = match format {
+ "jpg" | "jpeg" | "image/jpeg" => ".jpg",
+ "png" | "image/png" => ".png",
+ "c2pa" | "application/x-c2pa-manifest-store" => ".cp2a",
+ _ => "",
+ };
+ // clean string for possible filesystem use
+ let mut id = key.replace(['/', ':'], "-") + ext;
+
+ // ensure it is unique in this store
+ let count = 1;
+ while self.exists(&id) {
+ id = format!("{id}-{count}{ext}");
+ }
+ id
+ }
+
+ /// Adds a resource, generating a resource ref from a key and format.
+ ///
+ /// The generated identifier may be different from the key
+ pub fn add_with<R>(&mut self, key: &str, format: &str, value: R) -> crate::Result<ResourceRef>
+ where
+ R: Into<Vec<u8>>,
+ {
+ let id = self.id_from(key, format);
+ self.add(id.clone(), 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<()>
+ where
+ S: Into<String>,
+ R: Into<Vec<u8>>,
+ {
+ #[cfg(feature = "file_io")]
+ if let Some(base) = self.base_path.as_ref() {
+ let path = base.join(id.into());
+ std::fs::write(path, value.into())?;
+ return Ok(());
+ }
+ self.resources.insert(id.into(), value.into());
+ Ok(())
+ }
+
+ /// Returns a copy on write reference to the resource if found.
+ ///
+ /// returns Error::NotFound if it cannot find a resource matching that id
+ pub fn get(&self, id: &str) -> Result<Cow<Vec<u8>>> {
+ #[cfg(feature = "file_io")]
+ if !self.resources.contains_key(id) {
+ match self.base_path.as_ref() {
+ Some(base) => {
+ // read the file, save in Map and then return a reference
+ let path = base.join(id);
+ let value = std::fs::read(path)?;
+ return Ok(Cow::Owned(value));
+ }
+ None => return Err(Error::NotFound),
+ }
+ }
+ self.resources
+ .get(id)
+ .map_or_else(|| Err(Error::NotFound), |v| Ok(Cow::Borrowed(v)))
+ }
+
+ /// Returns true if the resource has been added or exists as file.
+ pub fn exists(&self, id: &str) -> bool {
+ if !self.resources.contains_key(id) {
+ #[cfg(feature = "file_io")]
+ match self.base_path.as_ref() {
+ Some(base) => {
+ let path = base.join(id);
+ path.exists()
+ }
+ None => false,
+ }
+ #[cfg(not(feature = "file_io"))]
+ false
+ } else {
+ true
+ }
+ }
+
+ #[cfg(feature = "file_io")]
+ // return the full path for an id
+ pub fn path_for_id(&self, id: &str) -> Option<PathBuf> {
+ self.base_path.as_ref().map(|base| base.join(id))
+ }
+}
+
+impl Default for ResourceStore {
+ fn default() -> Self {
+ ResourceStore::new()
+ }
+}
+
+#[cfg(test)]
+#[cfg(feature = "sign")]
+mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+ use super::*;
+ use crate::{utils::test::temp_signer, Manifest};
+
+ #[test]
+ #[cfg(feature = "sign")]
+ fn resource_store() {
+ let mut c = ResourceStore::new();
+ let value = b"my value";
+ c.add("abc123.jpg", value.to_vec()).expect("add");
+ let v = c.get("abc123.jpg").unwrap();
+ assert_eq!(v.to_vec(), b"my value");
+ c.add("cba321.jpg", value.to_vec()).expect("add");
+ assert!(c.exists("cba321.jpg"));
+ assert!(!c.exists("foo"));
+
+ let json = r#"{
+ "claim_generator": "test",
+ "format" : "image/jpeg",
+ "instance_id": "12345",
+ "assertions": [],
+ "thumbnail": {
+ "content_type": "image/jpeg",
+ "identifier": "abc123"
+ },
+ "ingredients": [{
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "is_parent": true,
+ "thumbnail": {
+ "content_type": "image/jpeg",
+ "identifier": "cba321"
+ }
+ }]
+ }"#;
+
+ let mut manifest = Manifest::from_json(json).expect("from json");
+ manifest
+ .resources_mut()
+ .add("abc123", *value)
+ .expect("add_resource");
+ let ingredient = &mut manifest.ingredients_mut()[0];
+ ingredient
+ .resources_mut()
+ .add("cba321", *value)
+ .expect("add_resource");
+ 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.
+ manifest
+ .embed_stream("jpeg", &mut stream, &signer)
+ .expect("embed_stream");
+
+ // get the updated image
+ let image = stream.into_inner();
+
+ let _manifest_store =
+ crate::ManifestStore::from_bytes("jpeg", &image, true).expect("from_bytes");
+ // println!("{manifest_store}");
+ }
+}