commit 9da7a0785e5b0e0bd0a66f592ddf69ab25c55f30
parent aeb4a32d2928d32bf63332346e2a7f713e839f42
Author: Gavin Peacock <gpeacock@adobe.com>
Date: Tue, 7 Jun 2022 20:09:47 -0700
(MINOR) Update ManifestAssertion supporting instances (#34)
* Added support for ManifestAssertion instances and kinds
ManifestAssertion is a new file
Added ManifestAsssertionKind
ManifestAssertion getters & setters and make private
Changed Actions assertion getters to mut self pattern
* Add value and binary getters for manifest_assertion
* Integrations with new signer from main
* respond to review and add docs
Diffstat:
14 files changed, 433 insertions(+), 160 deletions(-)
diff --git a/c2patool/Cargo.toml b/c2patool/Cargo.toml
@@ -8,7 +8,7 @@ rust-version = "1.58.0"
[dependencies]
anyhow = "1.0"
-c2pa = { version = "0.2", features = ["file_io"] }
+c2pa = { path = "../sdk", features = ["file_io"] }
env_logger = "0.9"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
diff --git a/c2patool/src/main.rs b/c2patool/src/main.rs
@@ -33,7 +33,7 @@ use tempfile::tempdir;
pub mod config;
use config::Config;
mod signer;
-use signer::get_signer;
+use signer::get_c2pa_signer;
// define the command line options
#[derive(Debug, StructOpt)]
@@ -85,12 +85,14 @@ fn handle_config(
) -> Result<()> {
let config: Config = serde_json::from_str(json)?;
+ // if the config has a base path, use it for relative paths in the config
+ // otherwise set the base path to the location of the config file
let base_path = match &config.base_path {
Some(path) => PathBuf::from(path),
None => PathBuf::from(base_dir),
};
- let signer = get_signer(&config, &base_path)?;
+ let signer = get_c2pa_signer(&config, &base_path)?;
let claim_generator = match config.claim_generator {
Some(claim_generator) => claim_generator,
@@ -143,7 +145,7 @@ fn handle_config(
// add any assertions
for assertion in config.assertions {
- manifest.add_labeled_assertion(&assertion.label, &assertion.data)?;
+ manifest.add_labeled_assertion(assertion.label(), &assertion.value()?)?;
}
// if we have an output option, then we must have a source image to add a claim to
diff --git a/c2patool/src/signer.rs b/c2patool/src/signer.rs
@@ -11,15 +11,13 @@
// specific language governing permissions and limitations under
// each license.
-use crate::{config::Config, fix_relative_path};
-use anyhow::{Context, Result};
/// Provides a method to read configured certs and generate a singer
///
-use c2pa::{
- openssl::{EcSigner, EdSigner, RsaSigner},
- signer::ConfigurableSigner,
- signer::Signer,
-};
+use crate::{config::Config, fix_relative_path};
+
+use anyhow::{Context, Result};
+use c2pa::{get_signer, Signer};
+
use std::{env, path::Path, process::exit};
pub fn get_ta_url() -> Option<String> {
@@ -30,7 +28,7 @@ pub fn get_ta_url() -> Option<String> {
/// keys can be directly in environment variables
/// or in a folder referenced by CAI_KEY_PATH
/// also supports default dev environment keys
-pub fn get_signer(config: &Config, base_path: &Path) -> Result<Box<dyn Signer>> {
+pub fn get_c2pa_signer(config: &Config, base_path: &Path) -> Result<Box<dyn Signer>> {
let alg = config.alg.as_deref().unwrap_or("ps256").to_lowercase();
let tsa_url = config.ta_url.clone().or_else(get_ta_url);
@@ -60,31 +58,7 @@ pub fn get_signer(config: &Config, base_path: &Path) -> Result<Box<dyn Signer>>
if let Some(private_key) = private_key {
if let Some(sign_cert) = sign_cert {
- let signer: Box<dyn Signer> = match alg.as_str() {
- "ps256" | "ps384" | "ps512" => Box::new(RsaSigner::from_signcert_and_pkey(
- &sign_cert,
- &private_key,
- alg,
- tsa_url,
- )?),
- "es256" | "es384" | "es512" => Box::new(EcSigner::from_signcert_and_pkey(
- &sign_cert,
- &private_key,
- alg,
- tsa_url,
- )?),
- "ed25519" => Box::new(EdSigner::from_signcert_and_pkey(
- &sign_cert,
- &private_key,
- alg.to_owned(),
- tsa_url,
- )?),
- _ => {
- eprintln!("Unsupported signing algorithm, must be one of [ ps256 | ps384 | ps512 | es256 | es384 | es512 | ed25519 ]");
- exit(2);
- }
- };
-
+ let signer = get_signer(&sign_cert, &private_key, &alg, tsa_url)?;
return Ok(signer);
}
}
diff --git a/c2patool/tests/integration.rs b/c2patool/tests/integration.rs
@@ -117,7 +117,7 @@ mod integration {
if !(priv_key_path.exists() && sign_cert_path.exists()) {
// Creating the signer (which we don't use) has the side effect of
// creating temporary private key and signing certificate.
- c2pa::openssl::temp_signer::get_signer(&x509_path);
+ c2pa::get_temp_signer(&x509_path);
}
}
}
diff --git a/make_test_images/src/make_test_images.rs b/make_test_images/src/make_test_images.rs
@@ -177,7 +177,7 @@ impl MakeTestImages {
let src_path = &self.make_path(src);
let parent = Ingredient::from_file_with_options(src_path, &options)?;
- actions.add_action(
+ actions = actions.add_action(
Action::new(c2pa_action::OPENED)
.set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?,
);
@@ -189,7 +189,7 @@ impl MakeTestImages {
// adjust brightness to show we made an edit
img = img.brighten(30);
- actions.add_action(
+ actions = actions.add_action(
Action::new(c2pa_action::COLOR_ADJUSTMENTS)
.set_parameter("name".to_owned(), "brightnesscontrast")?,
);
@@ -206,7 +206,7 @@ impl MakeTestImages {
*pixel = image::Rgb([r, 100, b]);
}
}
- actions
+ actions = actions
.add_action(Action::new(c2pa_action::CREATED))
.add_action(
Action::new(c2pa_action::DRAWING)
@@ -238,18 +238,17 @@ impl MakeTestImages {
// create and add the ingredient
let ingredient = Ingredient::from_file_with_options(ing_path, &options)?;
- actions.add_action(
- Action::new(c2pa_action::PLACED).set_parameter(
+ actions =
+ actions.add_action(Action::new(c2pa_action::PLACED).set_parameter(
"identifier".to_owned(),
ingredient.instance_id().to_owned(),
- )?,
- );
+ )?);
manifest.add_ingredient(ingredient);
x += width;
}
// record what we did as an action (only need to record this once)
- actions.add_action(Action::new(c2pa_action::RESIZED));
+ actions = actions.add_action(Action::new(c2pa_action::RESIZED));
}
// save the changes to the image as our target file
diff --git a/sdk/examples/client/client.rs b/sdk/examples/client/client.rs
@@ -44,7 +44,8 @@ fn show_manifest(manifest_store: &ManifestStore, manifest_label: &str, level: us
}
for assertion in manifest.assertions().iter() {
- match assertion.label.as_str() {
+ println!("{}", assertion.label_with_instance());
+ match assertion.label() {
labels::ACTIONS => {
let actions: Actions = assertion.to_assertion()?;
for action in actions.actions {
@@ -95,8 +96,7 @@ pub fn main() -> Result<()> {
let source = PathBuf::from(&args[1]);
// create an action assertion stating that we imported this file
- let mut actions = Actions::new();
- actions.add_action(
+ let actions = Actions::new().add_action(
Action::new(c2pa_action::PLACED)
.set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?,
);
diff --git a/sdk/src/assertions/actions.rs b/sdk/src/assertions/actions.rs
@@ -259,13 +259,13 @@ impl Actions {
}
/// Adds an [`Action`] to this assertion's list of actions.
- pub fn add_action(&mut self, action: Action) -> &mut Self {
+ pub fn add_action(mut self, action: Action) -> Self {
self.actions.push(action);
self
}
/// Sets [`Metadata`] for the action.
- pub fn add_metadata(&mut self, metadata: Metadata) -> &mut Self {
+ pub fn add_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = Some(metadata);
self
}
@@ -347,8 +347,7 @@ pub mod tests {
#[test]
fn assertion_actions() {
- let mut original = Actions::new();
- original
+ let original = Actions::new()
.add_action(make_action1())
.add_action(
Action::new("c2pa.filtered")
diff --git a/sdk/src/assertions/creative_work.rs b/sdk/src/assertions/creative_work.rs
@@ -47,8 +47,8 @@ impl CreativeWork {
}
/// insert key / value pair
- pub fn insert<T: Serialize>(self, key: String, value: T) -> Result<Self> {
- self.0.insert(key, value).map(Self)
+ pub fn insert<S: Into<String>, T: Serialize>(self, key: S, value: T) -> Result<Self> {
+ self.0.insert(key.into(), value).map(Self)
}
/// get creative work from json string
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
@@ -78,7 +78,9 @@ mod ingredient;
pub use ingredient::{Ingredient, IngredientOptions};
pub mod jumbf_io;
mod manifest;
-pub use manifest::{Manifest, ManifestAssertion};
+pub use manifest::Manifest;
+mod manifest_assertion;
+pub use manifest_assertion::{ManifestAssertion, ManifestAssertionKind};
mod manifest_store;
pub use manifest_store::ManifestStore;
@@ -108,15 +110,10 @@ pub(crate) mod assertion;
pub(crate) mod asset_handlers;
pub(crate) mod asset_io;
pub(crate) mod claim;
-pub mod validation_status;
-// TODO: Make this a private module again once we no longer need
-// access to this from claims signer.
#[cfg(feature = "file_io")]
pub(crate) mod cose_sign;
-
#[cfg(feature = "file_io")]
pub(crate) mod embedded_xmp;
-
pub(crate) mod hashed_uri;
#[allow(dead_code)]
pub(crate) mod jumbf;
@@ -125,6 +122,7 @@ pub(crate) mod status_tracker;
pub(crate) mod store;
pub(crate) mod time_stamp;
pub(crate) mod utils;
+pub mod validation_status;
pub(crate) use utils::cbor_types;
pub(crate) use utils::hash_utils;
pub(crate) use utils::xmp_inmemory_utils;
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -14,13 +14,14 @@
#[cfg(feature = "file_io")]
use crate::utils::thumbnail::make_thumbnail;
use crate::{
- assertion::{AssertionBase, AssertionData, AssertionDecodeError},
- assertions::{labels, Actions, CreativeWork, SchemaDotOrg, Thumbnail, UserCbor},
+ assertion::{AssertionBase, AssertionData},
+ assertions::{labels, Actions, CreativeWork, Thumbnail, User, UserCbor},
claim::Claim,
error::{Error, Result},
jumbf,
+ salt::DefaultSalt,
store::Store,
- Ingredient,
+ Ingredient, ManifestAssertion, ManifestAssertionKind,
};
#[cfg(feature = "file_io")]
@@ -75,10 +76,10 @@ pub struct Manifest {
impl Manifest {
/// Create a new Manifest
/// requires a claim_generator string (User Agent))
- pub fn new(claim_generator: String) -> Self {
+ pub fn new<S: Into<String>>(claim_generator: S) -> Self {
Self {
vendor: None,
- claim_generator,
+ claim_generator: claim_generator.into(),
claim_generator_hints: None,
asset: None,
ingredients: Vec::new(),
@@ -117,14 +118,14 @@ impl Manifest {
/// Sets the vendor prefix to be used when generating manifest labels
/// Optional prefix added to the generated Manifest Label
/// This is typically a lower case Internet domain name for the vendor (i.e. `adobe`)
- pub fn set_vendor(&mut self, vendor: String) -> &mut Self {
- self.vendor = Some(vendor);
+ pub fn set_vendor<S: Into<String>>(&mut self, vendor: S) -> &mut Self {
+ self.vendor = Some(vendor.into());
self
}
/// Sets a human readable name for the product that created this manifest
- pub fn set_claim_generator(&mut self, generator: String) -> &mut Self {
- self.claim_generator = generator;
+ pub fn set_claim_generator<S: Into<String>>(&mut self, generator: S) -> &mut Self {
+ self.claim_generator = generator.into();
self
}
@@ -180,10 +181,24 @@ impl Manifest {
self
}
- /// Adds assertion using given label - the data for predefined assertions must be in correct format
- pub fn add_labeled_assertion<T: Serialize>(
+ /// Adds assertion using given label and any serde serializable
+ /// The data for predefined assertions must be in correct format
+ ///
+ /// # Example: Creating a custom assertion from a serde_json object.
+ ///```
+ /// # use c2pa::Result;
+ /// use c2pa::Manifest;
+ /// use serde_json::json;
+ /// # fn main() -> Result<()> {
+ /// let mut manifest = Manifest::new("my_app");
+ /// let value = json!({"my_tag": "Anything I want"});
+ /// manifest.add_labeled_assertion("org.contentauth.foo", &value)?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn add_labeled_assertion<S: Into<String>, T: Serialize>(
&mut self,
- label: &str,
+ label: S,
data: &T,
) -> Result<&mut Self> {
self.assertions
@@ -191,7 +206,23 @@ impl Manifest {
Ok(self)
}
- /// Adds assertions, data for predefined assertions must be in correct format
+ /// Adds ManifestAssertions from existing assertions
+ /// The data for standard assertions must be in correct format
+ ///
+ /// # Example: Creating a from an Actions object.
+ ///```
+ /// # use c2pa::Result;
+ /// use c2pa::{
+ /// assertions::{Actions, Action, c2pa_action},
+ /// Manifest
+ /// };
+ /// # fn main() -> Result<()> {
+ /// let mut manifest = Manifest::new("my_app");
+ /// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED));
+ /// manifest.add_assertion(&actions)?;
+ /// # Ok(())
+ /// # }
+ /// ```
pub fn add_assertion<T: Serialize + AssertionBase>(&mut self, data: &T) -> Result<&mut Self> {
self.assertions
.push(ManifestAssertion::from_assertion(data)?);
@@ -199,8 +230,46 @@ impl Manifest {
}
/// Retrieves an assertion by label if it exists or Error::NotFound
+ ///
+ /// Example: Find an Actions Assertion
+ /// ```
+ /// # use c2pa::Result;
+ /// use c2pa::{
+ /// assertions::{Actions, Action, c2pa_action},
+ /// Manifest
+ /// };
+ /// # fn main() -> Result<()> {
+ /// let mut manifest = Manifest::new("my_app");
+ /// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED));
+ /// manifest.add_assertion(&actions)?;
+ ///
+ /// let actions: Actions = manifest.find_assertion(Actions::LABEL)?;
+ /// for action in actions.actions {
+ /// println!("{}", action.action());
+ /// }
+ /// # Ok(())
+ /// # }
+ /// ```
pub fn find_assertion<T: DeserializeOwned>(&self, label: &str) -> Result<T> {
- if let Some(manifest_assertion) = self.assertions.iter().find(|a| a.label == label) {
+ if let Some(manifest_assertion) = self.assertions.iter().find(|a| a.label() == label) {
+ manifest_assertion.to_assertion()
+ } else {
+ Err(Error::NotFound)
+ }
+ }
+
+ /// Retrieves an assertion by label and instance if it exists or Error::NotFound
+ ///
+ pub fn find_assertion_with_instance<T: DeserializeOwned>(
+ &self,
+ label: &str,
+ instance: usize,
+ ) -> Result<T> {
+ if let Some(manifest_assertion) = self
+ .assertions
+ .iter()
+ .find(|a| a.label() == label && a.instance() == instance)
+ {
manifest_assertion.to_assertion()
} else {
Err(Error::NotFound)
@@ -209,11 +278,11 @@ impl Manifest {
// keep this private until we support it externally
#[allow(dead_code)]
- pub(crate) fn add_redaction(&mut self, label: &str) -> Result<&mut Self> {
+ pub(crate) fn add_redaction<S: Into<String>>(&mut self, label: S) -> Result<&mut Self> {
// todo: any way to verify if this assertion exists in the parent claim here?
match self.redactions.as_mut() {
- Some(redactions) => redactions.push(label.to_string()),
- None => self.redactions = Some([label.to_string()].to_vec()),
+ Some(redactions) => redactions.push(label.into()),
+ None => self.redactions = Some([label.into()].to_vec()),
}
Ok(self)
}
@@ -229,7 +298,7 @@ impl Manifest {
}
/// Sets the signature information for the report
- pub fn set_signature(&mut self, issuer: Option<&String>, time: Option<&String>) -> &mut Self {
+ fn set_signature(&mut self, issuer: Option<&String>, time: Option<&String>) -> &mut Self {
self.signature_info = Some(SignatureInfo {
issuer: issuer.cloned(),
time: time.cloned(),
@@ -295,29 +364,29 @@ impl Manifest {
let ingredient = Ingredient::from_ingredient_uri(store, &assertion_uri)?;
manifest.add_ingredient(ingredient);
}
- Actions::LABEL => {
- let actions = Actions::from_assertion(assertion)?;
- manifest.add_assertion(&actions)?; // assertion.as_json_object()?)?;
- }
label if label.starts_with(labels::CLAIM_THUMBNAIL) => {
let thumbnail = Thumbnail::from_assertion(assertion)?;
asset.set_thumbnail(thumbnail.content_type, thumbnail.data);
}
_ => {
- // inject assertions for all json data
+ // inject assertions for all other assertions
match assertion.decode_data() {
AssertionData::Json(_x) => {
let value = assertion.as_json_object()?;
- manifest.add_labeled_assertion(&label, &value)?;
+ let ma = ManifestAssertion::new(label, value)
+ .set_instance(claim_assertion.instance())
+ .set_kind(ManifestAssertionKind::Json);
+ manifest.assertions.push(ma);
}
AssertionData::Cbor(_x) => {
let value = assertion.as_json_object()?; //todo: should this be cbor?
- manifest.add_labeled_assertion(&label, &value)?;
- }
- AssertionData::Binary(_x) => {
- //let _value = Value::String("<omitted>".to_owned());
- // claim_report.add_assertion(&label, &value)?;
+ let ma = ManifestAssertion::new(label, value)
+ .set_instance(claim_assertion.instance());
+
+ manifest.assertions.push(ma);
}
+ // todo: support binary forms
+ AssertionData::Binary(_x) => {}
AssertionData::Uuid(_, _) => {}
}
}
@@ -407,17 +476,19 @@ impl Manifest {
let lib_hint = format!("\"{}\";v=\"{}\"", crate::NAME, crate::VERSION);
claim.add_claim_generator_hint(GH_UA, Value::from(lib_hint));
+ let salt = DefaultSalt::default();
+
// add any additional assertions
- for assertion in &self.assertions {
- match assertion.label.as_str() {
+ for manifest_assertion in &self.assertions {
+ match manifest_assertion.label() {
Actions::LABEL => {
+ let actions: Actions = manifest_assertion.to_assertion()?;
// todo: fixup parameters field from instance_id to ingredient uri for
// c2pa.transcoded, c2pa.repackaged, and c2pa.placed action
- claim.add_assertion(&Actions::from_json_value(&assertion.data)?)
+ claim.add_assertion(&actions)
}
CreativeWork::LABEL => {
- let mut cw = CreativeWork::from_json_str(&assertion.data.to_string())?;
-
+ let mut cw: CreativeWork = manifest_assertion.to_assertion()?;
// insert a credentials field if we have a vc that matches the identifier
// todo: this should apply to any person, not just author
if let Some(cw_authors) = cw.author() {
@@ -435,20 +506,32 @@ impl Manifest {
}
cw = cw.set_author(&authors)?;
}
- claim.add_assertion(&cw)
- }
- labels::CLAIM_REVIEW => {
- claim.add_assertion(&SchemaDotOrg::from_json_str(&assertion.data.to_string())?)
- }
- _ => {
- // default to creating UserCbor assertions
- claim.add_assertion(&UserCbor::new(
- &assertion.label,
- serde_cbor::to_vec(&assertion.data)?,
- ))
- // todo: add option to use json
- //claim.add_assertion(&User::new(&assertion.label, &assertion.data.to_string()), &NoSalt{})?;
+ claim.add_assertion_with_salt(&cw, &salt)
}
+ _ => match manifest_assertion.kind() {
+ ManifestAssertionKind::Cbor => claim.add_assertion_with_salt(
+ &UserCbor::new(
+ manifest_assertion.label(),
+ serde_cbor::to_vec(&manifest_assertion.value()?)?,
+ ),
+ &salt,
+ ),
+ ManifestAssertionKind::Json => claim.add_assertion_with_salt(
+ &User::new(
+ manifest_assertion.label(),
+ &serde_json::to_string(&manifest_assertion.value()?)?,
+ ),
+ &salt,
+ ),
+ ManifestAssertionKind::Binary => {
+ // todo: Support binary kinds
+ return Err(Error::AssertionEncoding);
+ }
+ ManifestAssertionKind::Uri => {
+ // todo: Support binary kinds
+ return Err(Error::AssertionEncoding);
+ }
+ },
}?;
}
@@ -514,42 +597,6 @@ impl std::fmt::Display for Manifest {
f.write_str(&json)
}
}
-#[derive(Debug, Deserialize, Serialize, Clone)]
-/// A labeled container for an Assertion value in a Manifest
-pub struct ManifestAssertion {
- /// An assertion label in reverse domain format
- pub label: String,
- /// The data of the assertion as Value
- pub data: Value,
-}
-
-impl ManifestAssertion {
- pub fn from_labeled_assertion<T: Serialize>(label: &str, data: &T) -> Result<Self> {
- Ok(Self {
- label: label.to_owned(),
- data: serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?,
- })
- }
-
- pub fn from_assertion<T: Serialize + AssertionBase>(data: &T) -> Result<Self> {
- Ok(Self {
- label: data.label().to_owned(),
- data: serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?,
- })
- }
-
- pub fn to_assertion<T: DeserializeOwned>(&self) -> Result<T> {
- serde_json::from_value(self.data.clone()).map_err(|e| {
- Error::AssertionDecoding(AssertionDecodeError::from_json_err(
- self.label.to_owned(),
- None,
- "application/json".to_owned(),
- e,
- ))
- })
- }
-}
-
#[derive(Clone, Debug, Deserialize, Serialize)]
/// Holds information about a signature
pub struct SignatureInfo {
@@ -561,20 +608,26 @@ pub struct SignatureInfo {
time: Option<String>,
}
#[cfg(test)]
-#[cfg(feature = "file_io")]
pub(crate) mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
- use super::{Ingredient, Manifest, Store};
-
use crate::{
assertions::{c2pa_action, Action, Actions},
+ utils::test::TEST_VC,
+ Manifest, Result,
+ };
+
+ #[cfg(feature = "file_io")]
+ use crate::{
openssl::temp_signer::get_temp_signer,
status_tracker::{DetailedStatusTracker, StatusTracker},
- utils::test::{fixture_path, temp_dir_path, temp_fixture_path, TEST_SMALL_JPEG, TEST_VC},
+ store::Store,
+ utils::test::{fixture_path, temp_dir_path, temp_fixture_path, TEST_SMALL_JPEG},
+ Ingredient,
};
+ #[cfg(feature = "file_io")]
use tempfile::tempdir;
// example of random data structure as an assertion
@@ -613,9 +666,7 @@ pub(crate) mod tests {
)
.expect("add_assertion");
- let mut actions = Actions::new();
-
- actions.add_action(
+ let actions = Actions::new().add_action(
Action::new(c2pa_action::EDITED)
.set_parameter("name".to_owned(), "gaussian_blur")
.unwrap(),
@@ -683,7 +734,6 @@ pub(crate) mod tests {
}
#[test]
- #[cfg(feature = "file_io")]
fn test_verifiable_credential() {
let mut manifest = test_manifest();
let vc: serde_json::Value = serde_json::from_str(TEST_VC).unwrap();
@@ -696,7 +746,6 @@ pub(crate) mod tests {
}
#[test]
- #[cfg(feature = "file_io")]
fn test_assertion_user_cbor() {
use crate::assertions::UserCbor;
use crate::Manifest;
@@ -792,4 +841,25 @@ pub(crate) mod tests {
let claim1 = store3.get_claim(&claim1_label).unwrap();
assert!(claim1.get_claim_assertion(redacted_uri, 0).is_none());
}
+
+ #[test]
+ fn manifest_assertion_instances() {
+ let mut manifest = Manifest::new("test".to_owned());
+ let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED));
+ // add three assertions with the same label
+ manifest.add_assertion(&actions).expect("add_assertion");
+ manifest.add_assertion(&actions).expect("add_assertion");
+ manifest.add_assertion(&actions).expect("add_assertion");
+
+ // convert to a store and read back again
+ 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");
+ 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());
+ assert_eq!(action2.unwrap().actions()[0].action(), c2pa_action::EDITED);
+ }
}
diff --git a/sdk/src/manifest_assertion.rs b/sdk/src/manifest_assertion.rs
@@ -0,0 +1,233 @@
+use crate::{
+ assertion::{AssertionBase, AssertionDecodeError},
+ error::{Error, Result},
+};
+
+use serde::{de::DeserializeOwned, Deserialize, Serialize}; //, Deserializer, Serializer};
+use serde_json::Value;
+
+/// Assertions in C2PA can be stored in several formats
+#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
+pub enum ManifestAssertionKind {
+ Cbor,
+ Json,
+ Binary,
+ Uri,
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+#[serde(untagged)]
+enum ManifestData {
+ Json(Value), // { label: String, instance: usize, data: Value },
+ Binary(Vec<u8>), // ) { label: String, instance: usize, data: Value },
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+/// A labeled container for an Assertion value in a Manifest
+pub struct ManifestAssertion {
+ /// An assertion label in reverse domain format
+ label: String,
+ /// The data of the assertion as Value
+ data: ManifestData,
+ /// There can be more than one assertion for any label
+ #[serde(skip_serializing_if = "Option::is_none")]
+ instance: Option<usize>,
+ /// The [ManifestAssertionKind] for this assertion (as stored in c2pa content)
+ #[serde(skip_serializing_if = "Option::is_none")]
+ kind: Option<ManifestAssertionKind>,
+}
+
+impl ManifestAssertion {
+ /// Create with label and value
+ pub fn new(label: String, data: Value) -> Self {
+ Self {
+ label,
+ data: ManifestData::Json(data),
+ instance: None,
+ kind: None,
+ }
+ }
+
+ /// An assertion label in reverse domain format
+ pub fn label(&self) -> &str {
+ &self.label
+ }
+
+ /// An assertion label in reverse domain format with appended instance number
+ /// The instance number follows two underscores and is only added when the instance is > 1
+ /// This is a c2pa spec internal standard format
+ pub fn label_with_instance(&self) -> String {
+ match self.instance {
+ Some(i) if i > 1 => format!("{}__{}", self.label, i),
+ _ => self.label.to_owned(),
+ }
+ }
+
+ /// The data of the assertion as a serde_Json::Value
+ /// This will return UnsupportedType if the assertion data is binary
+ pub fn value(&self) -> Result<&Value> {
+ match &self.data {
+ ManifestData::Json(d) => Ok(d),
+ ManifestData::Binary(_) => Err(Error::UnsupportedType),
+ }
+ }
+
+ /// The data of the assertion as u8 binary vector
+ /// This will return UnsupportedType if the assertion data is Json/String
+ pub fn binary(&self) -> Result<&[u8]> {
+ match &self.data {
+ ManifestData::Json(_) => Err(Error::UnsupportedType),
+ ManifestData::Binary(b) => Ok(b),
+ }
+ }
+
+ /// The instance number of this assertion
+ /// If the same label is used for multiple assertions, incremental instances are added
+ /// The first instance is always 1 and increased by 1 per duplicated label
+ pub fn instance(&self) -> usize {
+ self.instance.unwrap_or(1)
+ }
+
+ /// The ManifestAssertionKind for this assertion
+ /// This refers to how the format of the assertion inside a C2PA manifest
+ /// The default is ManifestAssertionKind::Cbor
+ pub fn kind(&self) -> &ManifestAssertionKind {
+ match self.kind.as_ref() {
+ Some(kind) => kind,
+ None => &ManifestAssertionKind::Cbor,
+ }
+ }
+
+ /// This can be used to set an instance number, but generally should not be used
+ /// Instance numbers will be assigned automatically when the assertions are embedded
+ pub(crate) fn set_instance(mut self, instance: usize) -> Self {
+ self.instance = if instance > 1 { Some(instance) } else { None };
+ self
+ }
+
+ /// Allows overriding the default [ManifestAssertionKind] to Json
+ /// For assertions like Schema.org that require being stored in Json format
+ pub fn set_kind(mut self, kind: ManifestAssertionKind) -> Self {
+ self.kind = Some(kind);
+ self
+ }
+
+ /// Creates a ManifestAssertion with the given label and any serde serializable object
+ ///
+ /// # Example: Creating a custom assertion from a serde_json object.
+ ///
+ ///```
+ /// # use c2pa::Result;
+ /// use c2pa::ManifestAssertion;
+ /// use serde_json::json;
+ /// # fn main() -> Result<()> {
+ /// let value = json!({"my_tag": "Anything I want"});
+ /// let _ma = ManifestAssertion::from_labeled_assertion("org.contentauth.foo", &value)?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn from_labeled_assertion<S: Into<String>, T: Serialize>(
+ label: S,
+ data: &T,
+ ) -> Result<Self> {
+ Ok(Self::new(
+ label.into(),
+ serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?,
+ ))
+ }
+
+ /// Creates a ManifestAssertion from an AssertionBase object
+ ///
+ /// # Example: Creating a custom assertion an Action assertion
+ ///
+ ///```
+ /// # use c2pa::Result;
+ /// use c2pa::{
+ /// assertions::{Actions, Action, c2pa_action},
+ /// ManifestAssertion
+ /// };
+ /// # fn main() -> Result<()> {
+ /// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED));
+ /// let _ma = ManifestAssertion::from_assertion(&actions)?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn from_assertion<T: Serialize + AssertionBase>(data: &T) -> Result<Self> {
+ Ok(Self::new(
+ data.label().to_owned(),
+ serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?,
+ ))
+ }
+
+ /// Creates an Assertion object from a ManifestAssertion
+ ///
+ /// # Example: extracting an Actions Assertion
+ /// ```
+ /// # use c2pa::Result;
+ /// use c2pa::{
+ /// assertions::{Actions, Action, c2pa_action},
+ /// ManifestAssertion
+ /// };
+ /// # fn main() -> Result<()> {
+ /// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED));
+ /// let manifest_assertion = ManifestAssertion::from_assertion(&actions)?;
+ ///
+ /// let actions: Actions = manifest_assertion.to_assertion()?;
+ /// for action in actions.actions {
+ /// println!("{}", action.action());
+ /// }
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn to_assertion<T: DeserializeOwned>(&self) -> Result<T> {
+ serde_json::from_value(self.value()?.to_owned()).map_err(|e| {
+ Error::AssertionDecoding(AssertionDecodeError::from_json_err(
+ self.label.to_owned(),
+ None,
+ "application/json".to_owned(),
+ e,
+ ))
+ })
+ }
+}
+
+#[cfg(test)]
+pub(crate) mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+ use crate::assertions::{c2pa_action, Action, Actions};
+
+ #[test]
+ fn test_from_labeled() {
+ let data = serde_json::json!({"mytag": "mydata"});
+ let ma = ManifestAssertion::from_labeled_assertion("org.contentauth.foo", &data)
+ .expect("from_labeled_assertion");
+ assert_eq!(ma.label(), "org.contentauth.foo");
+ assert!(ma.value().is_ok());
+ }
+
+ #[test]
+ fn test_manifest_assertion() {
+ let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED));
+ let value = serde_json::to_value(actions).unwrap();
+ let mut ma = ManifestAssertion::new(Actions::LABEL.to_owned(), value);
+ assert_eq!(ma.label(), Actions::LABEL);
+
+ ma = ma.set_instance(1);
+ assert_eq!(ma.instance, None);
+ ma = ma.set_instance(2);
+ assert_eq!(ma.instance(), 2);
+ assert_eq!(ma.kind(), &ManifestAssertionKind::Cbor);
+ ma = ma.set_kind(ManifestAssertionKind::Json);
+ assert_eq!(ma.kind(), &ManifestAssertionKind::Json);
+
+ let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED));
+ let ma2 = ManifestAssertion::from_assertion(&actions).expect("from_assertion");
+ let actions2: Actions = ma2.to_assertion().expect("to_assertion");
+ let actions3 = ManifestAssertion::from_labeled_assertion("foo".to_owned(), &actions2)
+ .expect("from_labeled_assertion");
+ assert_eq!(actions3.label(), "foo");
+ }
+}
diff --git a/sdk/src/store.rs b/sdk/src/store.rs
@@ -1715,8 +1715,7 @@ pub mod tests {
}
fn create_capture_claim(claim: &mut Claim) -> Result<&mut Claim> {
- let mut actions = Actions::new();
- actions.add_action(Action::new("c2pa.created"));
+ let actions = Actions::new().add_action(Action::new("c2pa.created"));
claim.add_assertion(&actions)?;
diff --git a/sdk/src/utils/test.rs b/sdk/src/utils/test.rs
@@ -58,8 +58,7 @@ pub fn create_test_claim() -> Result<Claim> {
let _hu = claim.add_verifiable_credential(TEST_VC)?;
// Add assertions.
- let mut actions = Actions::new();
- actions
+ let actions = Actions::new()
.add_action(
Action::new("c2pa.cropped")
.set_parameter(
diff --git a/sdk/tests/integration.rs b/sdk/tests/integration.rs
@@ -45,7 +45,7 @@ mod integration_1 {
// add a parent ingredient
let parent = Ingredient::from_file(&parent_path)?;
// add an action assertion stating that we imported this file
- actions.add_action(
+ actions = actions.add_action(
Action::new(c2pa_action::EDITED)
.set_parameter("name".to_owned(), "import")?
.set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?,
@@ -58,7 +58,7 @@ mod integration_1 {
let mut img = image::open(&parent_path)?;
img = img.brighten(50); // brighten the image
- actions.add_action(
+ actions = actions.add_action(
Action::new("c2pa.edit").set_parameter("name".to_owned(), "brightnesscontrast")?,
);
@@ -71,7 +71,7 @@ mod integration_1 {
image::imageops::overlay(&mut img, &img_small, 0, 0);
// add an action assertion stating that we imported this file
- actions.add_action(
+ actions = actions.add_action(
Action::new(c2pa_action::EDITED)
.set_parameter("name".to_owned(), "import")?
.set_parameter("identifier".to_owned(), ingredient.instance_id().to_owned())?,