c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

commit 3aa77b991763b4bcc5d0271497f60a39c7149743
parent a181bde72fe37010544681bcca13055a1adbbf8c
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Thu,  2 Jun 2022 16:02:19 -0700

Add documentation for the `Actions` and `Metadata` assertions (#30)

* Actions and metadata fields private

* Refine documentation for Actions and Metadata

* Remove HashedUri reference from doc

Co-authored-by: Eric Scouten <scouten@adobe.com>
Diffstat:
Msdk/examples/client/client.rs | 7+------
Msdk/src/assertions/actions.rs | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msdk/src/assertions/data_hash.rs | 1+
Msdk/src/assertions/ingredient.rs | 22+++++++---------------
Msdk/src/assertions/metadata.rs | 125+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msdk/src/lib.rs | 6+++---
Msdk/src/utils/hash_utils.rs | 3+--
7 files changed, 225 insertions(+), 103 deletions(-)

diff --git a/sdk/examples/client/client.rs b/sdk/examples/client/client.rs @@ -49,12 +49,7 @@ fn show_manifest(manifest_store: &ManifestStore, manifest_label: &str, level: us labels::ACTIONS => { let actions: Actions = assertion.to_assertion()?; for action in actions.actions { - println!( - "{}{:?}, {:?}", - indent, - action.label, - action.parameters.unwrap_or_default() - ); + println!("{}{}", indent, action.action()); } } labels::CREATIVE_WORK => { diff --git a/sdk/src/assertions/actions.rs b/sdk/src/assertions/actions.rs @@ -58,29 +58,55 @@ pub mod c2pa_action { pub const UNKNOWN: &str = "c2pa.unknown"; } -/// Defines an action taken on an image +/// Defines a single action taken on an asset. +/// +/// An [`Action`] describes what took place on the asset, when it took place, +/// along with possible other information such as what software performed +/// the action. +/// +/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>. #[derive(Deserialize, Serialize, Debug, PartialEq)] pub struct Action { - #[serde(rename = "action")] - pub label: String, + /// The label associated with this action. See ([`c2pa_action`]). + action: String, + + /// Timestamp of when the action occurred. #[serde(skip_serializing_if = "Option::is_none")] - pub when: Option<String>, + when: Option<String>, + + /// The software agent that performed the action. #[serde(rename = "softwareAgent", skip_serializing_if = "Option::is_none")] - pub software_agent: Option<String>, + software_agent: Option<String>, + + /// A semicolon-delimited list of the parts of the resource that were changed since the previous event history. + /// + /// If not present, presumed to be undefined. + /// When tracking changes and the scope of the changed components is unknown, + /// it should be assumed that anything might have changed. #[serde(skip_serializing_if = "Option::is_none")] - pub changed: Option<String>, + changed: Option<String>, + + /// The value of the `xmpMM:InstanceID` property for the modified (output) resource. #[serde(rename = "InstanceId", skip_serializing_if = "Option::is_none")] - pub instance_id: Option<String>, + instance_id: Option<String>, + + /// Additional parameters of the action. These vary by the type of action. #[serde(skip_serializing_if = "Option::is_none")] - pub parameters: Option<HashMap<String, Value>>, + parameters: Option<HashMap<String, Value>>, + + /// An array of the creators that undertook this action. #[serde(skip_serializing_if = "Option::is_none")] - pub actors: Option<Vec<Actor>>, + actors: Option<Vec<Actor>>, } impl Action { + /// Create a new action with a specific action label. + /// + /// This label is often one of the labels defined in [`c2pa_action`], + /// but can also be a custom string in reverse-domain format. pub fn new(label: &str) -> Self { Self { - label: label.to_owned(), + action: label.to_owned(), when: None, software_agent: None, changed: None, @@ -90,31 +116,83 @@ impl Action { } } - /// set Timestamp of when the action occurred + /// Returns the label for this action. + /// + /// This label is often one of the labels defined in [`c2pa_action`], + /// but can also be a custom string in reverse-domain format. + pub fn action(&self) -> &str { + &self.action + } + + /// Returns the timestamp of when the action occurred. + /// + /// This string, if present, will be in ISO-8601 date format. + pub fn when(&self) -> Option<&str> { + self.when.as_deref() + } + + /// Returns the software agent that performed the action. + pub fn software_agent(&self) -> Option<&str> { + self.software_agent.as_deref() + } + + /// Returns the value of the `xmpMM:InstanceID` property for the modified + /// (output) resource. + pub fn instance_id(&self) -> Option<&str> { + self.instance_id.as_deref() + } + + /// Returns the additional parameters for this action. + /// + /// These vary by the type of action. + pub fn parameters(&self) -> Option<&HashMap<String, Value>> { + self.parameters.as_ref() + } + + /// Returns an individual action parameter if it exists. + pub fn get_parameter(&self, key: &str) -> Option<&Value> { + match self.parameters.as_ref() { + Some(parameters) => parameters.get(key), + None => None, + } + } + + /// An array of the [`Actor`]s that undertook this action. + pub fn actors(&self) -> Option<&[Actor]> { + self.actors.as_deref() + } + + /// Sets the timestamp for when the action occurred. + /// + /// This timestamp must be in ISO-8601 date. pub fn set_when(mut self, when: &str) -> Self { self.when = Some(when.to_owned()); self } - /// Set the software agent that performed the action. + /// Sets the software agent that performed the action. pub fn set_software_agent(mut self, software_agent: &str) -> Self { self.software_agent = Some(software_agent.to_owned()); self } - /// Set a list of the parts of the resource that were changed since the previous event history. + /// Sets the list of the parts of the resource that were changed + /// since the previous event history. pub fn set_changed(mut self, changed: Option<&Vec<&str>>) -> Self { self.changed = changed.map(|v| v.join(";")); self } - /// The value of the xmpMM:InstanceID property for the modified (output) resource + /// Sets the value of the `xmpMM:InstanceID` property for the + /// modified (output) resource. pub fn set_instance_id(mut self, id: &str) -> Self { self.instance_id = Some(id.to_owned()); self } - /// Set additional parameters of the action. These will often vary by the type of action + /// Sets the additional parameters for this action. + /// + /// These vary by the type of action. pub fn set_parameter<T: Serialize>(mut self, key: String, value: T) -> Result<Self> { let value = serde_json::to_value(value).map_err(|_| Error::AssertionEncoding)?; self.parameters = Some(match self.parameters { @@ -131,28 +209,38 @@ impl Action { Ok(self) } - /// An array of the creators that undertook this action + /// Sets the array of [`Actor`]s that undertook this action. pub fn set_actors(mut self, actors: Option<&Vec<Actor>>) -> Self { self.actors = actors.cloned(); self } } -/// A list of actions as an assertion +/// An `Actions` assertion provides information on edits and other +/// actions taken that affect the asset’s content. +/// +/// This assertion contains a list of [`Action`]s, each one declaring +/// what took place on the asset, when it took place, along with possible +/// other information such as what software performed the action. +/// +/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>. #[derive(Deserialize, Serialize, Debug, PartialEq)] pub struct Actions { + /// A list of [`Action`]s. pub actions: Vec<Action>, + + /// Additional information about the assertion. #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option<Metadata>, } impl Actions { - /// Label prefix for an actions assertion. + /// Label prefix for an [`Actions`] assertion. /// /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>. pub const LABEL: &'static str = labels::ACTIONS; - /// creates a new Actions object + /// Creates a new [`Actions`] assertion struct. pub fn new() -> Self { Self { actions: Vec::new(), @@ -160,19 +248,29 @@ impl Actions { } } - /// Adds an action + /// Returns the list of [`Action`]s. + pub fn actions(&self) -> &[Action] { + &self.actions + } + + /// Returns the assertion's [`Metadata`], if it exists. + pub fn metadata(&self) -> Option<&Metadata> { + self.metadata.as_ref() + } + + /// Adds an [`Action`] to this assertion's list of actions. pub fn add_action(&mut self, action: Action) -> &mut Self { self.actions.push(action); self } - /// Adds a metadata structure to the action - pub fn add_metadata(&mut self, metadata: Metadata) -> &Self { + /// Sets [`Metadata`] for the action. + pub fn add_metadata(&mut self, metadata: Metadata) -> &mut Self { self.metadata = Some(metadata); self } - /// creates an actions assertion from a compatible JSON Value + /// Creates an [`Actions`] assertion from a compatible JSON value. pub fn from_json_value(json: &serde_json::Value) -> Result<Self> { let actions: Actions = serde_json::from_value(json.clone())?; Ok(actions) @@ -208,7 +306,7 @@ pub mod tests { use super::*; use crate::assertion::{Assertion, AssertionData}; - use crate::assertions::metadata::{DataSource, ReviewRating, C2PA_SOURCE_GENERATOR_REE}; + use crate::assertions::metadata::{c2pa_source::GENERATOR_REE, DataSource, ReviewRating}; use crate::hashed_uri::HashedUri; fn make_hashed_uri1() -> HashedUri { @@ -261,8 +359,8 @@ pub mod tests { .add_metadata( Metadata::new() .add_review(ReviewRating::new("foo", Some("bar".to_owned()), 3)) - .set_reference(Some(make_hashed_uri1())) - .set_data_source(Some(DataSource::new(C2PA_SOURCE_GENERATOR_REE))), + .set_reference(make_hashed_uri1()) + .set_data_source(DataSource::new(GENERATOR_REE)), ); dbg!(&original); @@ -273,20 +371,20 @@ pub mod tests { let result = Actions::from_assertion(&assertion).expect("extract_assertion"); assert_eq!(result.actions.len(), 2); - assert_eq!(result.actions[0].label, original.actions[0].label); + assert_eq!(result.actions[0].action(), original.actions[0].action()); assert_eq!( - result.actions[0].parameters.as_ref().unwrap().get("name"), - original.actions[0].parameters.as_ref().unwrap().get("name") + result.actions[0].parameters().unwrap().get("name"), + original.actions[0].parameters().unwrap().get("name") ); - assert_eq!(result.actions[1].label, original.actions[1].label); + assert_eq!(result.actions[1].action(), original.actions[1].action()); assert_eq!( result.actions[1].parameters.as_ref().unwrap().get("name"), original.actions[1].parameters.as_ref().unwrap().get("name") ); - assert_eq!(result.actions[1].when, original.actions[1].when); + assert_eq!(result.actions[1].when(), original.actions[1].when()); assert_eq!( - result.metadata.unwrap().date_time, - original.metadata.unwrap().date_time + result.metadata.unwrap().date_time(), + original.metadata.unwrap().date_time() ); } diff --git a/sdk/src/assertions/data_hash.rs b/sdk/src/assertions/data_hash.rs @@ -193,6 +193,7 @@ impl DataHash { } /// Used to verify a DataHash against an asset. + #[allow(dead_code)] // used in tests pub fn verify_hash(&self, asset_path: &Path) -> Result<()> { let buf = fs::read(asset_path).map_err(wrap_io_err)?; self.verify_in_memory_hash(&buf, self.alg.clone()) diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs @@ -102,22 +102,14 @@ impl Ingredient { } pub fn add_review(mut self, review: ReviewRating) -> Self { - if self.metadata.is_none() { - self.metadata = Some(Metadata::new()) - } - if let Some(metadata) = &mut self.metadata { - match &mut metadata.reviews { - None => metadata.reviews = Some(vec![review]), - Some(reviews) => reviews.push(review), - } - } + let metadata = self.metadata.unwrap_or_else(Metadata::new); + self.metadata = Some(metadata.add_review(review)); self } pub fn add_reviews(mut self, reviews: Option<Vec<ReviewRating>>) -> Self { if let Some(reviews) = reviews { - let mut metadata = Metadata::new(); - metadata.reviews = Some(reviews); + let metadata = Metadata::new().set_reviews(reviews); self.metadata = Some(metadata); }; self @@ -263,13 +255,13 @@ pub mod tests { assert!(restored.metadata.is_some()); let metadata = restored.metadata.unwrap(); - let date_time = metadata.date_time.unwrap(); - let date_time_parsed = chrono::DateTime::parse_from_rfc3339(&date_time); + let date_time = metadata.date_time().unwrap(); + let date_time_parsed = chrono::DateTime::parse_from_rfc3339(date_time); - assert!(metadata.reviews.is_some()); + assert!(metadata.reviews().is_some()); assert!(date_time_parsed.is_ok()); - let reviews = metadata.reviews.unwrap(); + let reviews = metadata.reviews().unwrap(); assert_eq!(reviews.len(), 1); assert_eq!( diff --git a/sdk/src/assertions/metadata.rs b/sdk/src/assertions/metadata.rs @@ -29,13 +29,13 @@ const ASSERTION_CREATION_VERSION: usize = 1; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct Metadata { #[serde(rename = "reviewRatings", skip_serializing_if = "Option::is_none")] - pub reviews: Option<Vec<ReviewRating>>, + reviews: Option<Vec<ReviewRating>>, #[serde(rename = "dateTime", skip_serializing_if = "Option::is_none")] - pub date_time: Option<String>, + date_time: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] - pub reference: Option<HashedUri>, + reference: Option<HashedUri>, #[serde(skip_serializing_if = "Option::is_none")] - pub data_source: Option<DataSource>, + data_source: Option<DataSource>, #[serde(flatten)] other: HashMap<String, Value>, } @@ -56,7 +56,22 @@ impl Metadata { } } - /// add a review rating associated with the assertion + /// Returns the list of [`ReviewRating`] for this assertion if it exists. + pub fn reviews(&self) -> Option<&[ReviewRating]> { + self.reviews.as_deref() + } + + /// Returns the ISO 8601 date-time string when the assertion was created/generated. + pub fn date_time(&self) -> Option<&str> { + self.date_time.as_deref() + } + + /// Returns the [`DataSource`] for this assertion if it exists. + pub fn data_source(&self) -> Option<&DataSource> { + self.data_source.as_ref() + } + + /// Adds a [`ReviewRating`] associated with the assertion. pub fn add_review(mut self, review: ReviewRating) -> Self { match &mut self.reviews { None => self.reviews = Some(vec![review]), @@ -65,33 +80,42 @@ impl Metadata { self } - /// Set review ratings associated with the assertion - pub fn set_reviews(mut self, reviews: Option<Vec<ReviewRating>>) -> Self { - self.reviews = reviews; + /// Sets the list of [`ReviewRating`]s associated with the assertion. + /// + /// This replaces any previous list. + pub fn set_reviews(mut self, reviews: Vec<ReviewRating>) -> Self { + self.reviews = Some(reviews); self } - /// Set hashed_uri reference to another assertion to which this metadata applies - pub fn set_reference(mut self, reference: Option<HashedUri>) -> Self { - self.reference = reference; + /// Sets the ISO 8601 date-time string when the assertion was created/generated. + pub fn set_date_time(&mut self, date_time: String) -> &mut Self { + self.date_time = Some(date_time); self } - /// set a description of the source of the assertion data, selected from a predefined list - pub fn set_data_source(mut self, data_source: Option<DataSource>) -> Self { - self.data_source = data_source; + /// Sets a [`HashedUri`] reference to another assertion to which this metadata applies. + #[cfg(test)] // only referenced from test code + pub(crate) fn set_reference(mut self, reference: HashedUri) -> Self { + self.reference = Some(reference); self } - /// add additional key / value pair - pub fn insert(&mut self, key: &str, value: &Value) -> &mut Self { - self.other.insert(key.to_string(), value.clone()); + /// Sets a description of the source of the assertion data, selected from a predefined list. + pub fn set_data_source(mut self, data_source: DataSource) -> Self { + self.data_source = Some(data_source); self } - /// get additional values by key - pub fn get(self, key: &str) -> Option<Value> { - self.other.get(key).cloned() + /// Adds an additional key / value pair. + pub fn insert(&mut self, key: &str, value: Value) -> &mut Self { + self.other.insert(key.to_string(), value); + self + } + + /// Gets additional values by key. + pub fn get(&self, key: &str) -> Option<&Value> { + self.other.get(key) } } @@ -117,25 +141,32 @@ impl AssertionBase for Metadata { } /// DATA_SOURCE Type values -pub const C2PA_SOURCE_SIGNER: &str = "signer"; -pub const C2PA_SOURCE_GENERATOR_REE: &str = "claimGenerator.REE"; -pub const C2PA_SOURCE_GENERATOR_TEE: &str = "claimGenerator.TEE"; -pub const C2PA_SOURCE_LOCAL_REE: &str = "localProvider.REE"; -pub const C2PA_SOURCE_LOCAL_TEE: &str = "localProvider.TEE"; -pub const C2PA_SOURCE_REMOTE_REE: &str = "remoteProvider.1stParty"; -pub const C2PA_SOURCE_REMOTE_TEE: &str = "remoteProvider.3rdParty"; -pub const C2PA_SOURCE_HUMAN_ANONYMOUS: &str = "humanEntry.anonymous"; -pub const C2PA_SOURCE_HUMAN_IDENTIFIED: &str = "humanEntry.identified"; +pub mod c2pa_source { + pub const SIGNER: &str = "signer"; + pub const GENERATOR_REE: &str = "claimGenerator.REE"; + pub const GENERATOR_TEE: &str = "claimGenerator.TEE"; + pub const LOCAL_REE: &str = "localProvider.REE"; + pub const LOCAL_TEE: &str = "localProvider.TEE"; + pub const REMOTE_REE: &str = "remoteProvider.1stParty"; + pub const REMOTE_TEE: &str = "remoteProvider.3rdParty"; + pub const HUMAN_ANONYMOUS: &str = "humanEntry.anonymous"; + pub const HUMAN_IDENTIFIED: &str = "humanEntry.identified"; +} /// A description of the source for assertion data #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct DataSource { + /// A value from among the enumerated list indicating the source of the assertion. #[serde(rename = "type")] - pub source_type: String, // A value from among the enumerated list indicating the source of the assertion + pub source_type: String, + + /// A human-readable string giving details about the source of the assertion data. #[serde(skip_serializing_if = "Option::is_none")] - pub details: Option<String>, // A human readable string giving details about the source of the assertion data + pub details: Option<String>, + + /// A list of [`Actor`]s associated with this source. #[serde(skip_serializing_if = "Option::is_none")] - pub actors: Option<Vec<Actor>>, // array of hashed_uri references to W3C Verifiable Credentials + pub actors: Option<Vec<Actor>>, } impl DataSource { @@ -147,25 +178,29 @@ impl DataSource { } } - /// Set a human readable string giving details about the source of the assertion data - pub fn set_details(mut self, details: Option<&str>) -> Self { - self.details = details.map(|s| s.to_owned()); + /// Sets a human-readable string giving details about the source of the assertion data. + pub fn set_details(mut self, details: String) -> Self { + self.details = Some(details); self } - /// Set list of actors associated with this source - pub fn set_actors(mut self, actors: Option<&Vec<Actor>>) -> Self { - self.actors = actors.cloned(); + /// Sets a list of [`Actor`]s associated with this source. + pub fn set_actors(mut self, actors: Option<Vec<Actor>>) -> Self { + self.actors = actors; self } } -/// identifies a person responsible for an action + +/// Identifies a person responsible for an action. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Actor { + /// An identifier for a human actor, used when the "type" is `humanEntry.identified`. #[serde(skip_serializing_if = "Option::is_none")] - pub identifier: Option<String>, // An identifier for a human actor, used when the "type" is humanEntry.identified + pub identifier: Option<String>, + + /// List of references to W3C Verifiable Credentials. #[serde(skip_serializing_if = "Option::is_none")] - pub credentials: Option<Vec<HashedUri>>, // array of hashed_uri references to W3C Verifiable Credentials + pub credentials: Option<Vec<HashedUri>>, } impl Actor { @@ -202,7 +237,9 @@ pub enum ReviewCode { Other(String), } -/// A rating on an assertion +/// A rating on an [`Assertion`]. +/// +/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_claim_review>. #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct ReviewRating { pub explanation: String, @@ -233,7 +270,7 @@ pub mod tests { let review = ReviewRating::new("foo", Some("bar".to_owned()), 3); let test_value = Value::from("test"); let mut original = Metadata::new().add_review(review); - original.insert("foo", &test_value); + original.insert("foo", test_value); println!("{:?}", &original); let assertion = original.to_assertion().expect("build_assertion"); assert_eq!(assertion.mime_type(), "application/cbor"); @@ -242,7 +279,7 @@ pub mod tests { println!("{:?}", serde_json::to_string(&result)); assert_eq!(original.date_time, result.date_time); assert_eq!(original.reviews, result.reviews); - assert_eq!(original.get("foo").unwrap(), "test".to_string()); + assert_eq!(original.get("foo").unwrap(), "test"); //assert_eq!(original.reviews.unwrap().len(), 1); } } diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs @@ -19,7 +19,7 @@ //! This library supports reading, creating and embedding C2PA data //! with JPEG and PNG images. //! -//! # Example: Reading a `ManifestStore` +//! # Example: Reading a ManifestStore //! //! ``` //! # use c2pa::Result; @@ -32,14 +32,14 @@ //! if let Some(manifest) = manifest_store.get_active() { //! let actions: Actions = manifest.find_assertion(Actions::LABEL)?; //! for action in actions.actions { -//! println!("{}\n", action.label); +//! println!("{}\n", action.action()); //! } //! } //! # Ok(()) //! # } //! ``` //! -//! # Example: Adding a `Manifest` to a file +//! # Example: Adding a Manifest to a file //! //! ``` //! # use c2pa::Result; diff --git a/sdk/src/utils/hash_utils.rs b/sdk/src/utils/hash_utils.rs @@ -37,6 +37,7 @@ impl Exclusion { } /// update the start value + #[allow(dead_code)] pub fn set_start(&mut self, start: usize) { self.start = start; } @@ -199,7 +200,6 @@ pub fn hash256(data: &[u8]) -> String { /// Verify muiltihash against input data. True if match, /// false if no match or unsupported. The hash value should be /// be multibase encoded string. -#[allow(dead_code)] pub fn verify_hash(hash: &str, data: &[u8]) -> bool { match decode(hash) { Ok((_code, mh)) => { @@ -223,7 +223,6 @@ pub fn verify_hash(hash: &str, data: &[u8]) -> bool { } /// Return the hash of data in the same hash format in_hash -#[allow(dead_code)] pub fn hash_as_source(in_hash: &str, data: &[u8]) -> Option<String> { match decode(in_hash) { Ok((code, mh)) => {