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

actions.rs (27720B)


      1 // Copyright 2022 Adobe. All rights reserved.
      2 // This file is licensed to you under the Apache License,
      3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
      4 // or the MIT license (http://opensource.org/licenses/MIT),
      5 // at your option.
      6 
      7 // Unless required by applicable law or agreed to in writing,
      8 // this software is distributed on an "AS IS" BASIS, WITHOUT
      9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
     10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the
     11 // specific language governing permissions and limitations under
     12 // each license.
     13 
     14 use std::collections::HashMap;
     15 
     16 use serde::{Deserialize, Serialize};
     17 use serde_cbor::Value;
     18 
     19 use crate::{
     20     assertion::{Assertion, AssertionBase, AssertionCbor},
     21     assertions::{labels, Actor, Metadata},
     22     error::Result,
     23     resource_store::UriOrResource,
     24     utils::cbor_types::DateT,
     25     ClaimGeneratorInfo,
     26 };
     27 
     28 const ASSERTION_CREATION_VERSION: usize = 2;
     29 
     30 /// Specification defined C2PA actions
     31 pub mod c2pa_action {
     32     /// Changes to tone, saturation, etc.
     33     pub const COLOR_ADJUSTMENTS: &str = "c2pa.color_adjustments";
     34     /// The format of the asset was changed.
     35     pub const CONVERTED: &str = "c2pa.converted";
     36     /// The asset was first created, usually the asset's origin.
     37     pub const CREATED: &str = "c2pa.created";
     38     /// Areas of the asset's "editorial" content were cropped out.
     39     pub const CROPPED: &str = "c2pa.cropped";
     40     /// Changes using drawing tools including brushes or eraser.
     41     pub const DRAWING: &str = "c2pa.drawing";
     42     /// Generalized actions that affect the "editorial" meaning of the content.
     43     pub const EDITED: &str = "c2pa.edited";
     44     /// Changes to appearance with applied filters, styles, etc.
     45     pub const FILTERED: &str = "c2pa.filtered";
     46     /// An existing asset was opened and is being set as the `parentOf` ingredient.
     47     pub const OPENED: &str = "c2pa.opened";
     48     /// Changes to the direction and position of content.
     49     pub const ORIENTATION: &str = "c2pa.orientation";
     50     /// Added/Placed a `componentOf` ingredient into the asset.
     51     pub const PLACED: &str = "c2pa.placed";
     52     /// Asset is released to a wider audience.
     53     pub const PUBLISHED: &str = "c2pa.published";
     54     /// A conversion of one packaging or container format to another. Content may be repackaged without transcoding.
     55     /// Does not include any adjustments that would affect the "editorial" meaning of the content.
     56     pub const REPACKAGED: &str = "c2pa.repackaged";
     57     /// Changes to content dimensions and/or file size
     58     pub const RESIZED: &str = "c2pa.resized";
     59     /// A direct conversion of one encoding to another, including resolution scaling, bitrate adjustment and encoding format change.
     60     /// Does not include any adjustments that would affect the "editorial" meaning of the content.
     61     pub const TRANSCODED: &str = "c2pa.transcoded";
     62     /// Something happened, but the claim_generator cannot specify what.
     63     pub const UNKNOWN: &str = "c2pa.unknown";
     64 }
     65 
     66 /// We use this to allow SourceAgent to be either a string or a ClaimGeneratorInfo
     67 #[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
     68 #[serde(untagged)]
     69 pub enum SoftwareAgent {
     70     String(String),
     71     ClaimGeneratorInfo(ClaimGeneratorInfo),
     72 }
     73 
     74 impl From<&str> for SoftwareAgent {
     75     fn from(s: &str) -> Self {
     76         Self::String(s.to_owned())
     77     }
     78 }
     79 
     80 impl From<ClaimGeneratorInfo> for SoftwareAgent {
     81     fn from(c: ClaimGeneratorInfo) -> Self {
     82         Self::ClaimGeneratorInfo(c)
     83     }
     84 }
     85 
     86 /// Defines a single action taken on an asset.
     87 ///
     88 /// An [`Action`] describes what took place on the asset, when it took place,
     89 /// along with possible other information such as what software performed
     90 /// the action.
     91 ///
     92 /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>.
     93 #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq, Eq)]
     94 pub struct Action {
     95     /// The label associated with this action. See ([`c2pa_action`]).
     96     action: String,
     97 
     98     /// Timestamp of when the action occurred.
     99     #[serde(skip_serializing_if = "Option::is_none")]
    100     when: Option<DateT>,
    101 
    102     /// The software agent that performed the action.
    103     #[serde(rename = "softwareAgent", skip_serializing_if = "Option::is_none")]
    104     software_agent: Option<SoftwareAgent>,
    105 
    106     /// A semicolon-delimited list of the parts of the resource that were changed since the previous event history.
    107     #[serde(skip_serializing_if = "Option::is_none")]
    108     changed: Option<String>,
    109 
    110     /// A list of the regions of interest of the resource that were changed.
    111     ///
    112     /// If not present, presumed to be undefined.
    113     /// When tracking changes and the scope of the changed components is unknown,
    114     /// it should be assumed that anything might have changed.
    115     #[serde(skip_serializing_if = "Option::is_none")]
    116     changes: Option<Vec<serde_json::Value>>,
    117 
    118     /// The value of the `xmpMM:InstanceID` property for the modified (output) resource.
    119     #[serde(rename = "instanceId", skip_serializing_if = "Option::is_none")]
    120     instance_id: Option<String>,
    121 
    122     /// Additional parameters of the action. These vary by the type of action.
    123     #[serde(skip_serializing_if = "Option::is_none")]
    124     parameters: Option<HashMap<String, Value>>,
    125 
    126     /// An array of the creators that undertook this action.
    127     #[serde(skip_serializing_if = "Option::is_none")]
    128     actors: Option<Vec<Actor>>,
    129 
    130     /// One of the defined URI values at `<https://cv.iptc.org/newscodes/digitalsourcetype/>`
    131     #[serde(rename = "digitalSourceType", skip_serializing_if = "Option::is_none")]
    132     source_type: Option<String>,
    133 
    134     /// List of related actions.
    135     #[serde(skip_serializing_if = "Option::is_none")]
    136     related: Option<Vec<Action>>,
    137 
    138     // The reason why this action was performed, required when the action is `c2pa.redacted`
    139     #[serde(skip_serializing_if = "Option::is_none")]
    140     reason: Option<String>,
    141 }
    142 
    143 impl Action {
    144     /// Create a new action with a specific action label.
    145     ///
    146     /// This label is often one of the labels defined in [`c2pa_action`],
    147     /// but can also be a custom string in reverse-domain format.
    148     pub fn new(label: &str) -> Self {
    149         Self {
    150             action: label.to_owned(),
    151             ..Default::default()
    152         }
    153     }
    154 
    155     const fn is_v2(&self) -> bool {
    156         matches!(
    157             self.software_agent,
    158             Some(SoftwareAgent::ClaimGeneratorInfo(_))
    159         ) || self.changes.is_some() // only defined for v2
    160     }
    161 
    162     /// Returns the label for this action.
    163     ///
    164     /// This label is often one of the labels defined in [`c2pa_action`],
    165     /// but can also be a custom string in reverse-domain format.
    166     pub fn action(&self) -> &str {
    167         &self.action
    168     }
    169 
    170     /// Returns the timestamp of when the action occurred.
    171     ///
    172     /// This string, if present, will be in ISO-8601 date format.
    173     pub fn when(&self) -> Option<&str> {
    174         self.when.as_deref()
    175     }
    176 
    177     /// Returns the software agent that performed the action.
    178     pub const fn software_agent(&self) -> Option<&SoftwareAgent> {
    179         self.software_agent.as_ref()
    180     }
    181 
    182     /// Returns a mutable software agent that performed the action.
    183     pub fn software_agent_mut(&mut self) -> Option<&mut SoftwareAgent> {
    184         self.software_agent.as_mut()
    185     }
    186 
    187     /// Returns the value of the `xmpMM:InstanceID` property for the modified
    188     /// (output) resource.
    189     pub fn instance_id(&self) -> Option<&str> {
    190         self.instance_id.as_deref()
    191     }
    192 
    193     /// Returns the additional parameters for this action.
    194     ///
    195     /// These vary by the type of action.
    196     pub const fn parameters(&self) -> Option<&HashMap<String, Value>> {
    197         self.parameters.as_ref()
    198     }
    199 
    200     /// Returns an individual action parameter if it exists.
    201     pub fn get_parameter(&self, key: &str) -> Option<&Value> {
    202         match self.parameters.as_ref() {
    203             Some(parameters) => parameters.get(key),
    204             None => None,
    205         }
    206     }
    207 
    208     /// An array of the [`Actor`]s that undertook this action.
    209     pub fn actors(&self) -> Option<&[Actor]> {
    210         self.actors.as_deref()
    211     }
    212 
    213     /// Returns a digitalSourceType as defined at <https://cv.iptc.org/newscodes/digitalsourcetype/>.
    214     pub fn source_type(&self) -> Option<&str> {
    215         self.source_type.as_deref()
    216     }
    217 
    218     /// Returns the list of related actions.
    219     ///
    220     /// This is only present in C2PA v2.
    221     /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_related>.
    222     pub fn related(&self) -> Option<&[Action]> {
    223         self.related.as_deref()
    224     }
    225 
    226     /// Returns the reason why this action was performed.
    227     ///
    228     /// This is only present in C2PA v2.
    229     /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_reason>.
    230     pub fn reason(&self) -> Option<&str> {
    231         self.reason.as_deref()
    232     }
    233 
    234     /// Sets the timestamp for when the action occurred.
    235     ///
    236     /// This timestamp must be in ISO-8601 date.
    237     pub fn set_when<S: Into<String>>(mut self, when: S) -> Self {
    238         self.when = Some(DateT(when.into()));
    239         self
    240     }
    241 
    242     /// Sets the software agent that performed the action.
    243     pub fn set_software_agent<S: Into<SoftwareAgent>>(mut self, software_agent: S) -> Self {
    244         self.software_agent = Some(software_agent.into());
    245         self
    246     }
    247 
    248     /// Sets the list of the parts of the resource that were changed
    249     /// since the previous event history.
    250     pub fn set_changed(mut self, changed: Option<&Vec<&str>>) -> Self {
    251         self.changed = changed.map(|v| v.join(";"));
    252         self
    253     }
    254 
    255     /// Sets the value of the `xmpMM:InstanceID` property for the
    256     /// modified (output) resource.
    257     pub fn set_instance_id<S: Into<String>>(mut self, id: S) -> Self {
    258         self.instance_id = Some(id.into());
    259         self
    260     }
    261 
    262     /// Sets the additional parameters for this action.
    263     ///
    264     /// These vary by the type of action.
    265     pub fn set_parameter<S: Into<String>, T: Serialize>(
    266         mut self,
    267         key: S,
    268         value: T,
    269     ) -> Result<Self> {
    270         let value_bytes = serde_cbor::ser::to_vec(&value)?;
    271         let value = serde_cbor::from_slice(&value_bytes)?;
    272 
    273         self.parameters = Some(match self.parameters {
    274             Some(mut parameters) => {
    275                 parameters.insert(key.into(), value);
    276                 parameters
    277             }
    278             None => {
    279                 let mut p = HashMap::new();
    280                 p.insert(key.into(), value);
    281                 p
    282             }
    283         });
    284         Ok(self)
    285     }
    286 
    287     /// Sets the array of [`Actor`]s that undertook this action.
    288     pub fn set_actors(mut self, actors: Option<&Vec<Actor>>) -> Self {
    289         self.actors = actors.cloned();
    290         self
    291     }
    292 
    293     /// Set a digitalSourceType URI as defined at <https://cv.iptc.org/newscodes/digitalsourcetype/>.
    294     pub fn set_source_type<S: Into<String>>(mut self, uri: S) -> Self {
    295         self.source_type = Some(uri.into());
    296         self
    297     }
    298 
    299     /// Sets the list of related actions.
    300     ///
    301     /// This is only present in C2PA v2.
    302     /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_related>.
    303     pub fn set_related(mut self, related: Option<&Vec<Action>>) -> Self {
    304         self.related = related.cloned();
    305         self
    306     }
    307 
    308     /// Sets the reason why this action was performed.
    309     ///
    310     /// This is only present in C2PA v2.
    311     /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_reason>.
    312     pub fn set_reason<S: Into<String>>(mut self, reason: S) -> Self {
    313         self.reason = Some(reason.into());
    314         self
    315     }
    316 }
    317 
    318 #[derive(Deserialize, Serialize, Debug, Default, PartialEq, Eq)]
    319 #[non_exhaustive]
    320 pub struct ActionTemplate {
    321     /// The label associated with this action. See ([`c2pa_action`]).
    322     pub action: String,
    323 
    324     /// The software agent that performed the action.
    325     #[serde(rename = "softwareAgent", skip_serializing_if = "Option::is_none")]
    326     pub software_agent: Option<SoftwareAgent>,
    327 
    328     /// One of the defined URI values at `<https://cv.iptc.org/newscodes/digitalsourcetype/>`
    329     #[serde(rename = "digitalSourceType", skip_serializing_if = "Option::is_none")]
    330     pub source_type: Option<String>,
    331 
    332     #[serde(skip_serializing_if = "Option::is_none")]
    333     pub icon: Option<UriOrResource>,
    334 
    335     #[serde(skip_serializing_if = "Option::is_none")]
    336     pub description: Option<String>,
    337 
    338     #[serde(skip_serializing_if = "Option::is_none")]
    339     pub parameters: Option<HashMap<String, Value>>,
    340 }
    341 
    342 impl ActionTemplate {
    343     /// Creates a new ActionTemplate.
    344     pub fn new<S: Into<String>>(action: S) -> Self {
    345         Self {
    346             action: action.into(),
    347             ..Default::default()
    348         }
    349     }
    350 }
    351 
    352 /// An `Actions` assertion provides information on edits and other
    353 /// actions taken that affect the asset’s content.
    354 ///
    355 /// This assertion contains a list of [`Action`], each one declaring
    356 /// what took place on the asset, when it took place, along with possible
    357 /// other information such as what software performed the action.
    358 ///
    359 /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>.
    360 #[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
    361 #[non_exhaustive]
    362 pub struct Actions {
    363     /// A list of [`Action`]s.
    364     pub actions: Vec<Action>,
    365 
    366     /// list of templates for the [`Action`]s
    367     #[serde(skip_serializing_if = "Option::is_none")]
    368     pub templates: Option<Vec<ActionTemplate>>,
    369 
    370     /// Additional information about the assertion.
    371     #[serde(skip_serializing_if = "Option::is_none")]
    372     pub metadata: Option<Metadata>,
    373 }
    374 
    375 impl Actions {
    376     /// Label prefix for an [`Actions`] assertion.
    377     ///
    378     /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>.
    379     pub const LABEL: &'static str = labels::ACTIONS;
    380 
    381     /// Creates a new [`Actions`] assertion struct.
    382     pub const fn new() -> Self {
    383         Self {
    384             actions: Vec::new(),
    385             templates: None,
    386             metadata: None,
    387         }
    388     }
    389 
    390     /// determines if actions is V2
    391     fn is_v2(&self) -> bool {
    392         if self.templates.is_some() {
    393             return true;
    394         };
    395         self.actions.iter().any(|a| a.is_v2())
    396     }
    397 
    398     /// Returns the list of [`Action`]s.
    399     pub fn actions(&self) -> &[Action] {
    400         &self.actions
    401     }
    402 
    403     /// Returns mutable list of [`Action`]s.
    404     pub fn actions_mut(&mut self) -> &mut [Action] {
    405         &mut self.actions
    406     }
    407 
    408     /// Returns the assertion's [`Metadata`], if it exists.
    409     pub const fn metadata(&self) -> Option<&Metadata> {
    410         self.metadata.as_ref()
    411     }
    412 
    413     /// Internal method to update actions to meet spec requirements
    414     pub(crate) fn update_action(mut self, index: usize, action: Action) -> Self {
    415         self.actions[index] = action;
    416         self
    417     }
    418 
    419     /// Adds an [`Action`] to this assertion's list of actions.
    420     pub fn add_action(mut self, action: Action) -> Self {
    421         self.actions.push(action);
    422         self
    423     }
    424 
    425     /// Sets [`Metadata`] for the action.
    426     pub fn add_metadata(mut self, metadata: Metadata) -> Self {
    427         self.metadata = Some(metadata);
    428         self
    429     }
    430 
    431     /// Creates a CBOR [`Actions`] assertion from a compatible JSON value.
    432     pub fn from_json_value(json: &serde_json::Value) -> Result<Self> {
    433         let buf: Vec<u8> = Vec::new();
    434         let json_str = json.to_string();
    435         let mut from = serde_json::Deserializer::from_str(&json_str);
    436         let mut to = serde_cbor::Serializer::new(buf);
    437 
    438         serde_transcode::transcode(&mut from, &mut to)?;
    439         let buf2 = to.into_inner();
    440 
    441         let actions: Actions = serde_cbor::from_slice(&buf2)?;
    442         Ok(actions)
    443     }
    444 }
    445 
    446 impl AssertionCbor for Actions {}
    447 
    448 impl AssertionBase for Actions {
    449     const LABEL: &'static str = labels::ACTIONS;
    450     const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
    451 
    452     /// if we require v2 fields then use V2
    453     fn version(&self) -> Option<usize> {
    454         if self.is_v2() {
    455             Some(2)
    456         } else {
    457             Some(1)
    458         }
    459     }
    460 
    461     /// if we require v2 fields then use V2
    462     fn label(&self) -> &str {
    463         if self.is_v2() {
    464             "c2pa.actions.v2"
    465         } else {
    466             labels::ACTIONS
    467         }
    468     }
    469 
    470     fn to_assertion(&self) -> Result<Assertion> {
    471         Self::to_cbor_assertion(self)
    472     }
    473 
    474     fn from_assertion(assertion: &Assertion) -> Result<Self> {
    475         Self::from_cbor_assertion(assertion)
    476     }
    477 }
    478 
    479 impl Default for Actions {
    480     fn default() -> Self {
    481         Self::new()
    482     }
    483 }
    484 
    485 #[cfg(test)]
    486 pub mod tests {
    487     #![allow(clippy::expect_used)]
    488     #![allow(clippy::panic)]
    489     #![allow(clippy::unwrap_used)]
    490 
    491     use super::*;
    492     use crate::{
    493         assertion::AssertionData,
    494         assertions::metadata::{c2pa_source::GENERATOR_REE, DataSource, ReviewRating},
    495         hashed_uri::HashedUri,
    496     };
    497 
    498     fn make_hashed_uri1() -> HashedUri {
    499         HashedUri::new(
    500             "self#jumbf=verified_credentials/1234".to_string(),
    501             None,
    502             b"hashed",
    503         )
    504     }
    505 
    506     fn make_action1() -> Action {
    507         Action::new(c2pa_action::CROPPED)
    508             .set_software_agent("test")
    509             .set_when("2015-06-26T16:43:23+0200")
    510             .set_parameter(
    511                 "foo".to_owned(),
    512                 r#"{
    513                 "left": 0,
    514                 "right": 2000,
    515                 "top": 1000,
    516                 "bottom": 4000
    517             }"#
    518                 .to_owned(),
    519             )
    520             .unwrap()
    521             .set_parameter("ingredient".to_owned(), make_hashed_uri1())
    522             .unwrap()
    523             .set_changed(Some(&["this", "that"].to_vec()))
    524             .set_instance_id("xmp.iid:cb9f5498-bb58-4572-8043-8c369e6bfb9b")
    525             .set_actors(Some(
    526                 &[Actor::new(
    527                     Some("Somebody"),
    528                     Some(&[make_hashed_uri1()].to_vec()),
    529                 )]
    530                 .to_vec(),
    531             ))
    532     }
    533 
    534     #[test]
    535     fn assertion_actions() {
    536         let original = Actions::new()
    537             .add_action(make_action1())
    538             .add_action(
    539                 Action::new("c2pa.filtered")
    540                     .set_parameter("name".to_owned(), "gaussian blur")
    541                     .unwrap()
    542                     .set_when("2015-06-26T16:43:23+0200")
    543                     .set_source_type("digsrctype:algorithmicMedia"),
    544             )
    545             .add_metadata(
    546                 Metadata::new()
    547                     .add_review(ReviewRating::new("foo", Some("bar".to_owned()), 3))
    548                     .set_reference(make_hashed_uri1())
    549                     .set_data_source(DataSource::new(GENERATOR_REE)),
    550             );
    551 
    552         assert_eq!(original.actions.len(), 2);
    553         let assertion = original.to_assertion().expect("build_assertion");
    554         assert_eq!(assertion.mime_type(), "application/cbor");
    555         assert_eq!(assertion.label(), Actions::LABEL);
    556 
    557         let result = Actions::from_assertion(&assertion).expect("extract_assertion");
    558         assert_eq!(result.actions.len(), 2);
    559         assert_eq!(result.actions[0].action(), original.actions[0].action());
    560         assert_eq!(
    561             result.actions[0].parameters().unwrap().get("name"),
    562             original.actions[0].parameters().unwrap().get("name")
    563         );
    564         assert_eq!(result.actions[1].action(), original.actions[1].action());
    565         assert_eq!(
    566             result.actions[1].parameters.as_ref().unwrap().get("name"),
    567             original.actions[1].parameters.as_ref().unwrap().get("name")
    568         );
    569         assert_eq!(result.actions[1].when(), original.actions[1].when());
    570         assert_eq!(
    571             result.actions[1].source_type().unwrap(),
    572             "digsrctype:algorithmicMedia"
    573         );
    574         assert_eq!(
    575             result.metadata.unwrap().date_time(),
    576             original.metadata.unwrap().date_time()
    577         );
    578     }
    579 
    580     #[test]
    581     fn test_build_assertion() {
    582         let assertion = Actions::new()
    583             .add_action(
    584                 Action::new("c2pa.cropped")
    585                     .set_parameter(
    586                         "coordinate".to_owned(),
    587                         r#"{
    588                         "left": 0,
    589                         "right": 2000,
    590                         "top": 1000,
    591                         "bottom": 4000
    592                     }"#,
    593                     )
    594                     .unwrap(),
    595             )
    596             .add_action(
    597                 Action::new("c2pa.filtered")
    598                     .set_parameter("name".to_owned(), "gaussian blur")
    599                     .unwrap()
    600                     .set_when("2015-06-26T16:43:23+0200"),
    601             )
    602             .to_assertion()
    603             .unwrap();
    604 
    605         println!("assertion label: {}", assertion.label());
    606 
    607         let j = assertion.data();
    608         //println!("assertion as json {:#?}", j);
    609 
    610         let from_j = Assertion::from_data_cbor(&assertion.label(), j);
    611         let ad_ref = from_j.decode_data();
    612 
    613         if let AssertionData::Cbor(ref ad_cbor) = ad_ref {
    614             // compare results
    615             let orig_d = assertion.decode_data();
    616             if let AssertionData::Cbor(ref orig_cbor) = orig_d {
    617                 assert_eq!(orig_cbor, ad_cbor);
    618             } else {
    619                 panic!("Couldn't decode orig_d");
    620             }
    621         } else {
    622             panic!("Couldn't decode ad_ref");
    623         }
    624     }
    625 
    626     #[test]
    627     fn test_binary_round_trip() {
    628         let assertion = Actions::new()
    629             .add_action(
    630                 Action::new("c2pa.cropped")
    631                     .set_parameter(
    632                         "name".to_owned(),
    633                         r#"{
    634                         "left": 0,
    635                         "right": 2000,
    636                         "top": 1000,
    637                         "bottom": 4000
    638                     }"#,
    639                     )
    640                     .unwrap(),
    641             )
    642             .add_action(
    643                 Action::new("c2pa.filtered")
    644                     .set_parameter("name".to_owned(), "gaussian blur")
    645                     .unwrap()
    646                     .set_when("2015-06-26T16:43:23+0200"),
    647             )
    648             .to_assertion()
    649             .unwrap();
    650 
    651         let orig_bytes = assertion.data();
    652 
    653         let assertion_from_binary = Assertion::from_data_cbor(&assertion.label(), orig_bytes);
    654 
    655         println!(
    656             "Label Match Test {} = {}",
    657             assertion.label(),
    658             assertion_from_binary.label()
    659         );
    660 
    661         assert_eq!(assertion.label(), assertion_from_binary.label());
    662 
    663         // compare the data as bytes
    664         assert_eq!(orig_bytes, assertion_from_binary.data());
    665         println!("Decoded binary matches")
    666     }
    667 
    668     #[test]
    669     fn test_json_round_trip() {
    670         let json = serde_json::json!({
    671             "actions": [
    672                   {
    673                     "action": "c2pa.edited",
    674                     "parameters": {
    675                       "description": "gradient",
    676                       "name": "any value"
    677                     },
    678                     "softwareAgent": "TestApp"
    679                   },
    680                   {
    681                     "action": "c2pa.opened",
    682                     "instanceId": "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
    683                     "parameters": {
    684                       "description": "import"
    685                     },
    686                     "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia",
    687                     "softwareAgent": "TestApp 1.0",
    688                   },
    689                 ],
    690             "metadata": {
    691                 "mytag": "myvalue"
    692             }
    693         });
    694         let original = Actions::from_json_value(&json).expect("from json");
    695         let assertion = original.to_assertion().expect("build_assertion");
    696         let result = Actions::from_assertion(&assertion).expect("extract_assertion");
    697         assert_eq!(result.label(), labels::ACTIONS);
    698         println!("{}", serde_json::to_string_pretty(&result).unwrap());
    699         assert_eq!(original.actions, result.actions);
    700         assert_eq!(
    701             result.actions[0].software_agent().unwrap(),
    702             &SoftwareAgent::String("TestApp".to_string())
    703         );
    704     }
    705 
    706     #[test]
    707     fn test_json_v2_round_trip() {
    708         let json = serde_json::json!({
    709             "actions": [
    710                 {
    711                     "action": "c2pa.edited",
    712                     "parameters": {
    713                         "description": "gradient",
    714                         "name": "any value"
    715                     },
    716                     "softwareAgent": "TestApp"
    717                 },
    718                 {
    719                     "action": "c2pa.opened",
    720                     "instanceId": "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
    721                     "parameters": {
    722                         "description": "import"
    723                     },
    724                     "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia",
    725                     "softwareAgent": {
    726                         "name": "TestApp",
    727                         "version": "1.0",
    728                         "something": "else"
    729                     },
    730                 },
    731                 {
    732                     "action": "com.joesphoto.filter",
    733                 },
    734                 {
    735                     "action": "c2pa.dubbed",
    736                     "changes": [
    737                         {
    738                             "description": "translated to klingon",
    739                             "region": [
    740                                 {
    741                                     "type": "temporal",
    742                                     "time": {}
    743                                 },
    744                                 {
    745                                     "type": "identified",
    746                                     "item": {
    747                                         "identifier": "https://bioportal.bioontology.org/ontologies/FMA",
    748                                         "value": "lips"
    749                                     }
    750                                 }
    751                             ]
    752                         }
    753                     ]
    754                 }
    755 
    756             ],
    757             "templates": [
    758                 {
    759                     "action": "com.joesphoto.filter",
    760                     "description": "Magic Filter",
    761                     "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic",
    762                     "softwareAgent" : {
    763                         "name": "Joe's Photo Editor",
    764                         "version": "2.0",
    765                         "schema.org.SoftwareApplication.operatingSystem": "Windows 10"
    766                     }
    767                 }
    768             ],
    769             "metadata": {
    770                 "mytag": "myvalue"
    771             }
    772         });
    773         let original = Actions::from_json_value(&json).expect("from json");
    774         let assertion = original.to_assertion().expect("build_assertion");
    775         let result = Actions::from_assertion(&assertion).expect("extract_assertion");
    776         println!("{}", serde_json::to_string_pretty(&result).unwrap());
    777         assert_eq!(result.label(), "c2pa.actions.v2");
    778         assert_eq!(original.actions, result.actions);
    779         assert_eq!(original.templates, result.templates);
    780         assert_eq!(
    781             result.actions[0].software_agent().unwrap(),
    782             &SoftwareAgent::String("TestApp".to_string())
    783         );
    784         assert_eq!(
    785             result.actions[3].changes.as_deref().unwrap()[0]
    786                 .get("description")
    787                 .unwrap(),
    788             "translated to klingon"
    789         );
    790     }
    791 }