commit 2646d95c62f82a86bb3d90efb3d0dd88eed8e70e
parent aa0b276b32d17a2aa50acfadbdc13d7a67f53a90
Author: Gavin Peacock <gpeacock@adobe.com>
Date: Tue, 16 Aug 2022 10:02:10 -0700
Updates Manifest API to support remote and external manifests (#107)
* Return c2pa data from embed
* Add more detailed error reporting to add_manifest_uri_to_file
Diffstat:
4 files changed, 108 insertions(+), 27 deletions(-)
diff --git a/Makefile b/Makefile
@@ -26,7 +26,7 @@ test-local:
cargo test --all-features
test-no-defaults:
- cd sdk && cargo test --features="file_io" --no-default-features
+ cd sdk && cargo test --features="file_io, xmp_write, bmff" --no-default-features
test-wasm:
cd sdk && wasm-pack test --node
diff --git a/sdk/src/embedded_xmp.rs b/sdk/src/embedded_xmp.rs
@@ -14,7 +14,7 @@
use std::path::Path;
use log::error;
-use xmp_toolkit::{OpenFileOptions, XmpError, XmpFile, XmpMeta};
+use xmp_toolkit::{OpenFileOptions, XmpError, XmpErrorType, XmpFile, XmpMeta};
use crate::{Error, Result};
@@ -49,5 +49,11 @@ pub(crate) fn add_manifest_uri_to_file<P: AsRef<Path>>(path: P, manifest_uri: &s
fn xmp_write_err(err: XmpError) -> crate::Error {
error!("Unable to add manifest URI to file: {:?}", err);
- Error::XmpWriteError
+ match err.error_type {
+ // convert to OS permission error code so we can detect it correctly upstream
+ XmpErrorType::FilePermission => Error::IoError(std::io::Error::from_raw_os_error(13)),
+ XmpErrorType::NoFile => Error::NotFound,
+ XmpErrorType::NoFileHandler => Error::UnsupportedType,
+ _ => Error::XmpWriteError,
+ }
}
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -48,7 +48,7 @@ pub struct Manifest {
/// Optional prefix added to the generated Manifest Label
/// This is typically Internet domain name for the vendor (i.e. `adobe`)
#[serde(skip_serializing_if = "Option::is_none")]
- pub vendor: Option<String>,
+ vendor: Option<String>,
/// 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
@@ -87,6 +87,9 @@ pub struct Manifest {
#[serde(skip_serializing_if = "Option::is_none")]
signature_info: Option<SignatureInfo>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ label: Option<String>,
+
#[serde(skip_deserializing, skip_serializing)]
remote_manifest: Option<RemoteManifest>,
}
@@ -108,14 +111,21 @@ impl Manifest {
redactions: None,
credentials: None,
signature_info: None,
+ label: None,
remote_manifest: None,
}
}
+ /// Returns a User Agent formatted string identifying the software/hardware/system produced this claim
pub fn claim_generator(&self) -> &str {
self.claim_generator.as_str()
}
+ /// returns the manifest label for this Manifest, as referenced in a ManifestStore
+ pub fn label(&self) -> Option<&str> {
+ self.label.as_deref()
+ }
+
/// Returns a MIME content_type for the asset associated with this manifest.
pub fn format(&self) -> &str {
&self.format
@@ -162,6 +172,14 @@ impl Manifest {
self
}
+ /// Sets the label for this manifest
+ /// A label will be generated if this is not called
+ /// This is needed if embedding a URL that references the manifest label
+ pub fn set_label<S: Into<String>>(&mut self, label: S) -> &mut Self {
+ self.label = Some(label.into());
+ self
+ }
+
/// Sets a human readable name for the product that created this manifest
pub fn set_claim_generator<S: Into<String>>(&mut self, generator: S) -> &mut Self {
self.claim_generator = generator.into();
@@ -191,16 +209,23 @@ impl Manifest {
self.thumbnail = Some((format.into(), thumbnail));
self
}
+
+ /// If set, the embed calls will create a sidecar .c2pa manifest file next to the output file
+ /// No change will be made to the output file
pub fn set_sidecar_manifest(&mut self) -> &mut Self {
self.remote_manifest = Some(RemoteManifest::SideCar);
self
}
+ /// If set, the embed calls will put the remote url into the output file xmp provenance
+ /// and create a c2pa manifest file next to the output file
pub fn set_remote_manifest<S: Into<String>>(&mut self, remote_url: S) -> &mut Self {
self.remote_manifest = Some(RemoteManifest::Remote(remote_url.into()));
self
}
+ /// If set, the embed calls will put the remote url into the output file xmp provenance
+ /// and will embed the manifest into the output file
pub fn set_embedded_manifest_with_remote_ref<S: Into<String>>(
&mut self,
remote_url: S,
@@ -402,6 +427,7 @@ impl Manifest {
let claim_generator = claim.claim_generator().to_owned();
let mut manifest = Manifest::new(claim_generator);
+ manifest.set_label(claim.label());
manifest.claim_generator_hints = claim.get_claim_generator_hint_map().cloned();
// get credentials converting from AssertionData to Value
@@ -516,8 +542,8 @@ impl Manifest {
}
}
- // Convert a Manifest into a Store
- pub(crate) fn to_store(&self) -> Result<Store> {
+ // Convert a Manifest into a Claim
+ pub(crate) fn to_claim(&self) -> Result<Claim> {
// add library identifier to claim_generator
let generator = format!(
"{} {}/{}",
@@ -525,7 +551,11 @@ impl Manifest {
crate::NAME,
crate::VERSION
);
- let mut claim = Claim::new(&generator, self.vendor.as_deref());
+
+ let mut claim = match self.label() {
+ Some(label) => Claim::new_with_user_guid(&generator, &label.to_string()),
+ None => Claim::new(&generator, self.vendor.as_deref()),
+ };
if let Some(remote_op) = &self.remote_manifest {
match remote_op {
@@ -622,10 +652,15 @@ impl Manifest {
}?;
}
+ Ok(claim)
+ }
+
+ // Convert a Manifest into a Store
+ pub(crate) fn to_store(&self) -> Result<Store> {
+ let claim = self.to_claim()?;
// commit the claim
let mut store = Store::new();
let _provenance = store.commit_claim(claim)?;
-
Ok(store)
}
@@ -689,7 +724,7 @@ impl Manifest {
source_path: P,
dest_path: P,
signer: &dyn Signer,
- ) -> Result<Store> {
+ ) -> Result<Vec<u8>> {
// Add manifest info for this target file
let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?;
@@ -697,10 +732,7 @@ impl Manifest {
let mut store = self.to_store()?;
// sign and write our store to to the output image file
- store.save_to_asset(source_path.as_ref(), signer, dest_path.as_ref())?;
-
- // todo: update xmp
- Ok(store)
+ store.save_to_asset(source_path.as_ref(), signer, dest_path.as_ref())
}
/// Embed a signed manifest into the target file using a supplied [`AsyncSigner`].
@@ -711,7 +743,7 @@ impl Manifest {
source_path: P,
dest_path: P,
signer: &dyn AsyncSigner,
- ) -> Result<Store> {
+ ) -> Result<Vec<u8>> {
// Add manifest info for this target file
let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?;
// convert the manifest to a store
@@ -719,10 +751,7 @@ impl Manifest {
// sign and write our store to to the output image file
store
.save_to_asset_async(source_path.as_ref(), signer, dest_path.as_ref())
- .await?;
-
- // todo: update xmp
- Ok(store)
+ .await
}
/// Embed a signed manifest into the target file using a supplied [`RemoteSigner`].
@@ -732,7 +761,7 @@ impl Manifest {
source_path: P,
dest_path: P,
signer: &dyn RemoteSigner,
- ) -> Result<Store> {
+ ) -> Result<Vec<u8>> {
// Add manifest info for this target file
let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?;
// convert the manifest to a store
@@ -740,10 +769,7 @@ impl Manifest {
// sign and write our store to to the output image file
store
.save_to_asset_remote_signed(source_path.as_ref(), signer, dest_path.as_ref())
- .await?;
-
- // todo: update xmp
- Ok(store)
+ .await
}
}
@@ -961,7 +987,11 @@ pub(crate) mod tests {
let signer = temp_signer();
- let store1 = manifest.embed(&output, &output, &signer).expect("embed");
+ let c2pa_data = manifest.embed(&output, &output, &signer).expect("embed");
+ let mut validation_log = DetailedStatusTracker::new();
+
+ let store1 = Store::load_from_memory("c2pa", &c2pa_data, true, &mut validation_log)
+ .expect("load from memory");
let claim1_label = store1.provenance_label().unwrap();
let claim = store1.provenance_claim().unwrap();
assert!(claim.get_claim_assertion(ASSERTION_LABEL, 0).is_some()); // verify the assertion is there
@@ -1083,4 +1113,49 @@ pub(crate) mod tests {
TEST_SMALL_JPEG
);
}
+
+ #[cfg(all(feature = "file_io", feature = "xmp_write"))]
+ #[actix::test]
+ async fn test_embed_user_label() {
+ let temp_dir = tempdir().expect("temp dir");
+ let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
+
+ let signer = temp_signer();
+
+ let mut manifest = test_manifest();
+ manifest.set_label("MyLabel");
+ manifest.embed(&output, &output, &signer).expect("embed");
+ let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
+ assert_eq!(manifest_store.active_label(), Some("MyLabel"));
+ assert_eq!(
+ manifest_store.get_active().unwrap().title().unwrap(),
+ TEST_SMALL_JPEG
+ );
+ }
+
+ #[cfg(all(feature = "file_io", feature = "xmp_write"))]
+ #[actix::test]
+ async fn test_embed_sidecar_user_label() {
+ let temp_dir = tempdir().expect("temp dir");
+ let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
+ let sidecar = output.with_extension("c2pa");
+ let fp = format!("file:/{}", sidecar.to_str().unwrap());
+ let url = url::Url::parse(&fp).unwrap();
+
+ let signer = temp_signer();
+
+ let mut manifest = test_manifest();
+ manifest.set_label("MyLabel");
+ manifest.set_remote_manifest(url);
+ let c2pa_data = manifest.embed(&output, &output, &signer).expect("embed");
+
+ //let manifest_store = crate::ManifestStore::from_file(&sidecar).expect("from_file");
+ let manifest_store =
+ crate::ManifestStore::from_bytes("c2pa", c2pa_data, true).expect("from_bytes");
+ assert_eq!(manifest_store.active_label(), Some("MyLabel"));
+ assert_eq!(
+ manifest_store.get_active().unwrap().title().unwrap(),
+ TEST_SMALL_JPEG
+ );
+ }
}
diff --git a/sdk/src/store.rs b/sdk/src/store.rs
@@ -2866,7 +2866,7 @@ pub mod tests {
}
#[test]
- #[cfg(feature = "file_io")]
+ #[cfg(all(feature = "file_io", feature = "bmff"))]
fn test_bmff_jumbf_generation() {
// test adding to actual image
let ap = fixture_path("video1.mp4");
@@ -2930,7 +2930,7 @@ pub mod tests {
}
#[test]
- fn test_external_manifest_embedded_url() {
+ fn test_external_manifest_embedded() {
// test adding to actual image
let ap = fixture_path("libpng-test.png");
let temp_dir = tempdir().expect("temp dir");
@@ -2983,7 +2983,7 @@ pub mod tests {
}
#[test]
- fn test_external_manifest_embedded_manifest_embedded_url() {
+ fn test_user_guid_external_manifest_embedded() {
// test adding to actual image
let ap = fixture_path("libpng-test.png");
let temp_dir = tempdir().expect("temp dir");