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

assertion.rs (23640B)


      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::fmt;
     15 
     16 use serde::{de::DeserializeOwned, Deserialize, Serialize};
     17 use serde_bytes::ByteBuf;
     18 use serde_json::Value;
     19 use thiserror::Error;
     20 
     21 use crate::{
     22     assertions::labels,
     23     error::{Error, Result},
     24 };
     25 
     26 /// Check to see if this a label whose string can vary, if so return the root of the label and version if available
     27 fn get_mutable_label(var_label: &str) -> (String, Option<usize>) {
     28     if var_label.starts_with(labels::SCHEMA_ORG) {
     29         (var_label.to_string(), None)
     30     } else {
     31         // is it a type of thumbnail
     32         let tn = get_thumbnail_type(var_label);
     33 
     34         if tn == "none" {
     35             let components: Vec<&str> = var_label.split('.').collect();
     36             match components.last() {
     37                 Some(last) => {
     38                     // check for a valid version number
     39                     if last.len() > 1 {
     40                         let (ver, ver_inst_str) = last.split_at(1);
     41                         if ver == "v" {
     42                             if let Ok(ver_inst) = ver_inst_str.parse::<usize>() {
     43                                 let ver_trim = format!(".{last}");
     44                                 let root_label = var_label.trim_end_matches(&ver_trim);
     45                                 return (root_label.to_string(), Some(ver_inst));
     46                             }
     47                         }
     48                     }
     49                     (var_label.to_string(), None)
     50                 }
     51                 None => (var_label.to_string(), None),
     52             }
     53         } else {
     54             (tn, None)
     55         }
     56     }
     57 }
     58 
     59 pub fn get_thumbnail_type(thumbnail_label: &str) -> String {
     60     if thumbnail_label.starts_with(labels::CLAIM_THUMBNAIL) {
     61         return labels::CLAIM_THUMBNAIL.to_string();
     62     }
     63     if thumbnail_label.starts_with(labels::INGREDIENT_THUMBNAIL) {
     64         return labels::INGREDIENT_THUMBNAIL.to_string();
     65     }
     66     "none".to_string()
     67 }
     68 
     69 pub fn get_thumbnail_image_type(thumbnail_label: &str) -> String {
     70     let components: Vec<&str> = thumbnail_label.split('.').collect();
     71 
     72     if thumbnail_label.contains("thumbnail") && components.len() >= 4 {
     73         let image_type: Vec<&str> = components[3].split('_').collect(); // strip and other label adornments
     74         image_type[0].to_ascii_lowercase()
     75     } else {
     76         "none".to_string()
     77     }
     78 }
     79 
     80 pub fn get_thumbnail_instance(label: &str) -> Option<usize> {
     81     let label_type = get_thumbnail_type(label);
     82     // only ingredients thumbs store ids in the label, so use placeholder ids for the others
     83     match label_type.as_ref() {
     84         labels::INGREDIENT_THUMBNAIL => {
     85             // extract id from underscore separated part of the full label
     86             let components: Vec<&str> = label.split("__").collect();
     87             if components.len() == 2 {
     88                 let subparts: Vec<&str> = components[1].split('.').collect();
     89                 match subparts[0].parse::<usize>() {
     90                     Ok(i) => Some(i),
     91                     Err(_e) => None,
     92                 }
     93             } else {
     94                 Some(0)
     95             }
     96         }
     97         _ => None,
     98     }
     99 }
    100 
    101 /// The core required trait for all assertions.
    102 ///
    103 /// This defines the label and version for the assertion
    104 /// and supplies the to/from converters for C2PA assertion format.
    105 pub trait AssertionBase
    106 where
    107     Self: Sized,
    108 {
    109     const LABEL: &'static str = "unknown";
    110 
    111     const VERSION: Option<usize> = None;
    112 
    113     /// Returns a label for this assertion.
    114     fn label(&self) -> &str {
    115         Self::LABEL
    116     }
    117 
    118     /// Returns a version for this assertion.
    119     fn version(&self) -> Option<usize> {
    120         Self::VERSION
    121     }
    122 
    123     /// Returns an Assertion upon success or Error otherwise.
    124     fn to_assertion(&self) -> Result<Assertion>;
    125 
    126     /// Returns Self or AssertionDecode Result from an assertion
    127     fn from_assertion(assertion: &Assertion) -> Result<Self>;
    128 }
    129 
    130 /// Trait to handle default Cbor encoding/decoding of Assertions
    131 pub trait AssertionCbor: Serialize + DeserializeOwned + AssertionBase {
    132     fn to_cbor_assertion(&self) -> Result<Assertion> {
    133         let data =
    134             AssertionData::Cbor(serde_cbor::to_vec(self).map_err(|_err| Error::AssertionEncoding)?);
    135         Ok(Assertion::new(self.label(), self.version(), data))
    136     }
    137 
    138     fn from_cbor_assertion(assertion: &Assertion) -> Result<Self> {
    139         assertion.check_max_version(Self::VERSION)?;
    140 
    141         match assertion.decode_data() {
    142             AssertionData::Cbor(data) => Ok(serde_cbor::from_slice(data).map_err(|e| {
    143                 Error::AssertionDecoding(AssertionDecodeError::from_assertion_and_cbor_err(
    144                     assertion, e,
    145                 ))
    146             })?),
    147 
    148             data => Err(AssertionDecodeError::from_assertion_unexpected_data_type(
    149                 assertion, data, "cbor",
    150             )
    151             .into()),
    152         }
    153     }
    154 }
    155 
    156 /// Trait to handle default Json encoding/decoding of Assertions
    157 pub trait AssertionJson: Serialize + DeserializeOwned + AssertionBase {
    158     fn to_json_assertion(&self) -> Result<Assertion> {
    159         let data = AssertionData::Json(
    160             serde_json::to_string(self).map_err(|_err| Error::AssertionEncoding)?,
    161         );
    162         Ok(Assertion::new(self.label(), self.version(), data).set_content_type("application/json"))
    163     }
    164 
    165     fn from_json_assertion(assertion: &Assertion) -> Result<Self> {
    166         assertion.check_max_version(Self::VERSION)?;
    167 
    168         match assertion.decode_data() {
    169             AssertionData::Json(data) => Ok(serde_json::from_str(data)
    170                 .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(assertion, e))?),
    171             data => Err(Error::AssertionDecoding(
    172                 AssertionDecodeError::from_assertion_unexpected_data_type(assertion, data, "json"),
    173             )),
    174         }
    175     }
    176 }
    177 
    178 /// Assertion data as binary CBOR or JSON depending upon
    179 /// the Assertion type (see spec).
    180 /// For JSON assertions the data is a JSON string and a Vec of u8 values for
    181 /// binary data and JSON data to be CBOR encoded.
    182 #[derive(Deserialize, Serialize, PartialEq, Eq, Clone)]
    183 pub enum AssertionData {
    184     Json(String),          // json encoded data
    185     Binary(Vec<u8>),       // binary data
    186     Cbor(Vec<u8>),         // binary cbor encoded data
    187     Uuid(String, Vec<u8>), // user defined content (uuid, data)
    188 }
    189 
    190 impl fmt::Debug for AssertionData {
    191     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    192         match self {
    193             Self::Json(s) => write!(f, "{s:?}"), // json encoded data
    194             Self::Binary(_) => write!(f, "<omitted>"),
    195             Self::Uuid(uuid, _) => {
    196                 write!(f, "uuid: {uuid}, <omitted>")
    197             }
    198             Self::Cbor(s) => {
    199                 let buf: Vec<u8> = Vec::new();
    200                 let mut from = serde_cbor::Deserializer::from_slice(s);
    201                 let mut to = serde_json::Serializer::pretty(buf);
    202 
    203                 serde_transcode::transcode(&mut from, &mut to).map_err(|_err| fmt::Error)?;
    204                 let buf2 = to.into_inner();
    205 
    206                 let decoded: Value = serde_json::from_slice(&buf2).map_err(|_err| fmt::Error)?;
    207 
    208                 write!(f, "{:?}", decoded.to_string())
    209             }
    210         }
    211     }
    212 }
    213 
    214 /// Internal Assertion structure
    215 ///
    216 /// Each assertion type will
    217 /// contain its AssertionData.  For the User Assertion type we
    218 /// allow a String to set the label. The AssertionData contains
    219 /// the data payload for the assertion and the version number for its schema (if supported).
    220 #[derive(Clone, Debug, PartialEq, Eq)]
    221 pub struct Assertion {
    222     label: String,
    223     version: Option<usize>,
    224     data: AssertionData,
    225     content_type: String,
    226 }
    227 
    228 impl Assertion {
    229     pub(crate) fn new(label: &str, version: Option<usize>, data: AssertionData) -> Self {
    230         Self {
    231             label: label.to_owned(),
    232             version,
    233             content_type: "application/cbor".to_owned(),
    234             data,
    235         }
    236     }
    237 
    238     pub(crate) fn set_content_type(mut self, content_type: &str) -> Self {
    239         content_type.clone_into(&mut self.content_type);
    240         self
    241     }
    242 
    243     /// return content_type for the the data enclosed in the Assertion
    244     pub(crate) fn content_type(&self) -> String {
    245         self.content_type.clone()
    246     }
    247 
    248     // pub(crate) fn set_data(mut self, data: &AssertionData) -> Self {
    249     //     self.data = data.to_owned();
    250     //     self
    251     // }
    252 
    253     // Return version string of known assertion if available
    254     pub(crate) const fn get_ver(&self) -> Option<usize> {
    255         self.version
    256     }
    257 
    258     // pub fn check_version(&self, max_version: usize) -> AssertionDecodeResult<()> {
    259     //     match self.version {
    260     //         Some(version) if version > max_version => Err(AssertionDecodeError {
    261     //             label: self.label.clone(),
    262     //             version: self.version,
    263     //             content_type: self.content_type.clone(),
    264     //             source: AssertionDecodeErrorCause::AssertionTooNew {
    265     //                 max: max_version,
    266     //                 found: version,
    267     //             },
    268     //         }),
    269     //         _ => Ok(()),
    270     //     }
    271     // }
    272 
    273     /// Return a reference to the AssertionData bound to this Assertion
    274     pub(crate) const fn decode_data(&self) -> &AssertionData {
    275         &self.data
    276     }
    277 
    278     /// return mimetype for the data enclosed in the Assertion
    279     pub(crate) fn mime_type(&self) -> String {
    280         self.content_type.clone()
    281     }
    282 
    283     /// Test to see if the Assertions are of the same variant
    284     pub(crate) fn assertions_eq(a: &Assertion, b: &Assertion) -> bool {
    285         a.label_root() == b.label_root()
    286     }
    287 
    288     /// Return the CAI label for this Assertion (no version)
    289     pub(crate) fn label_root(&self) -> String {
    290         let label = get_mutable_label(&self.label).0;
    291         // thumbnails need the image_type added
    292         match get_thumbnail_image_type(&self.label).as_str() {
    293             "none" => label,
    294             image_type => format!("{label}.{image_type}"),
    295         }
    296     }
    297 
    298     /// Return the CAI label for this Assertion with version string if available
    299     pub(crate) fn label(&self) -> String {
    300         let base_label = self.label_root();
    301         match self.get_ver() {
    302             Some(v) => {
    303                 if v > 1 {
    304                     // c2pa does not include v1 labels
    305                     format!("{base_label}.v{v}")
    306                 } else {
    307                     base_label
    308                 }
    309             }
    310             None => base_label,
    311         }
    312     }
    313 
    314     /// Return a reference to the data as a byte array
    315     pub(crate) fn data(&self) -> &[u8] {
    316         // return bytes of the assertion data
    317         match self.decode_data() {
    318             AssertionData::Json(x) => x.as_bytes(), // json encoded data
    319             AssertionData::Binary(x) | AssertionData::Uuid(_, x) => x, // binary data
    320             AssertionData::Cbor(x) => x,
    321         }
    322     }
    323 
    324     /// Return assertion as serde_json Object
    325     /// this may have loss of cbor structure if unsupported in conversion to json
    326     /// It should always do the correct thing when using the correct tagged CBOR types
    327     pub(crate) fn as_json_object(&self) -> AssertionDecodeResult<Value> {
    328         match self.decode_data() {
    329             AssertionData::Json(x) => serde_json::from_str(x)
    330                 .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e)),
    331 
    332             AssertionData::Cbor(x) => {
    333                 let buf: Vec<u8> = Vec::new();
    334                 let mut from = serde_cbor::Deserializer::from_slice(x);
    335                 let mut to = serde_json::Serializer::new(buf);
    336 
    337                 serde_transcode::transcode(&mut from, &mut to)
    338                     .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))?;
    339 
    340                 let buf2 = to.into_inner();
    341                 serde_json::from_slice(&buf2)
    342                     .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))
    343             }
    344 
    345             AssertionData::Binary(x) => {
    346                 let binary_bytes = ByteBuf::from(x.clone());
    347                 let binary_str = serde_json::to_string(&binary_bytes)
    348                     .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))?;
    349 
    350                 serde_json::from_str(&binary_str)
    351                     .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))
    352             }
    353             AssertionData::Uuid(uuid, x) => {
    354                 #[derive(Serialize)]
    355                 struct TmpObj<'a> {
    356                     uuid: &'a str,
    357                     data: ByteBuf,
    358                 }
    359 
    360                 let v = TmpObj {
    361                     uuid,
    362                     data: ByteBuf::from(x.clone()),
    363                 };
    364 
    365                 let binary_str = serde_json::to_string(&v)
    366                     .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))?;
    367 
    368                 serde_json::from_str(&binary_str)
    369                     .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))
    370             }
    371         }
    372     }
    373 
    374     fn from_assertion_data(label: &str, content_type: &str, data: AssertionData) -> Assertion {
    375         use crate::claim::Claim;
    376         let version = labels::version(label);
    377         let (label, instance) = Claim::assertion_label_from_link(label);
    378         let label = Claim::label_with_instance(&label, instance);
    379 
    380         Self {
    381             label,
    382             version,
    383             data,
    384             content_type: content_type.to_owned(),
    385         }
    386     }
    387 
    388     /// create an assertion from binary data
    389     pub(crate) fn from_data_binary(label: &str, mime_type: &str, binary_data: &[u8]) -> Assertion {
    390         Self::from_assertion_data(
    391             label,
    392             mime_type,
    393             AssertionData::Binary(binary_data.to_vec()),
    394         )
    395     }
    396 
    397     /// create an assertion from user binary data
    398     pub(crate) fn from_data_uuid(label: &str, uuid_str: &str, binary_data: &[u8]) -> Assertion {
    399         Self::from_assertion_data(
    400             label,
    401             "application/octet-stream",
    402             AssertionData::Uuid(uuid_str.to_owned(), binary_data.to_vec()),
    403         )
    404     }
    405 
    406     pub(crate) fn from_data_cbor(label: &str, binary_data: &[u8]) -> Assertion {
    407         Self::from_assertion_data(
    408             label,
    409             "application/cbor",
    410             AssertionData::Cbor(binary_data.to_vec()),
    411         )
    412     }
    413 
    414     pub(crate) fn from_data_json(
    415         label: &str,
    416         binary_data: &[u8],
    417     ) -> AssertionDecodeResult<Assertion> {
    418         let json = String::from_utf8(binary_data.to_vec()).map_err(|_| AssertionDecodeError {
    419             label: label.to_string(),
    420             version: None, // TODO: Can we get this info?
    421             content_type: "json".to_string(),
    422             source: AssertionDecodeErrorCause::BinaryDataNotUtf8,
    423         })?;
    424 
    425         Ok(Self::from_assertion_data(
    426             label,
    427             "application/json",
    428             AssertionData::Json(json),
    429         ))
    430     }
    431 
    432     // Check assertion label against a target label.
    433     pub(crate) fn check_version_from_label(
    434         &self,
    435         desired_version: usize,
    436     ) -> AssertionDecodeResult<()> {
    437         if let Some(base_version) = labels::version(&self.label) {
    438             if desired_version > base_version {
    439                 return Err(AssertionDecodeError {
    440                     label: self.label.clone(),
    441                     version: self.version,
    442                     content_type: self.content_type.clone(),
    443                     source: AssertionDecodeErrorCause::AssertionTooNew {
    444                         max: desired_version,
    445                         found: base_version,
    446                     },
    447                 });
    448             }
    449         }
    450 
    451         Ok(())
    452     }
    453 
    454     fn check_max_version(&self, max_version: Option<usize>) -> AssertionDecodeResult<()> {
    455         if let Some(data_version) = self.version {
    456             if let Some(max_version) = max_version {
    457                 if data_version > max_version {
    458                     return Err(AssertionDecodeError {
    459                         label: self.label.clone(),
    460                         version: self.version,
    461                         content_type: self.content_type.clone(),
    462                         source: AssertionDecodeErrorCause::AssertionTooNew {
    463                             max: max_version,
    464                             found: data_version,
    465                         },
    466                     });
    467                 }
    468             }
    469         }
    470         Ok(())
    471     }
    472 }
    473 
    474 #[derive(Serialize, Deserialize, Debug)]
    475 pub(crate) struct JsonAssertionData {
    476     label: String,
    477     data: Value,
    478     is_cbor: bool,
    479 }
    480 
    481 /// This error type is returned when an assertion can not be decoded.
    482 #[non_exhaustive]
    483 pub struct AssertionDecodeError {
    484     pub label: String,
    485     pub version: Option<usize>,
    486     pub content_type: String,
    487     pub source: AssertionDecodeErrorCause,
    488 }
    489 
    490 impl AssertionDecodeError {
    491     fn fmt_internal(&self, f: &mut fmt::Formatter) -> fmt::Result {
    492         write!(
    493             f,
    494             "could not decode assertion {} (version {}, content type {}): {}",
    495             self.label,
    496             self.version
    497                 .map_or("(no version)".to_string(), |v| v.to_string()),
    498             self.content_type,
    499             self.source
    500         )
    501     }
    502 
    503     pub(crate) fn from_assertion_and_cbor_err(
    504         assertion: &Assertion,
    505         source: serde_cbor::error::Error,
    506     ) -> Self {
    507         Self {
    508             label: assertion.label.clone(),
    509             version: assertion.version,
    510             content_type: assertion.content_type.clone(),
    511             source: source.into(),
    512         }
    513     }
    514 
    515     pub(crate) fn from_assertion_and_json_err(
    516         assertion: &Assertion,
    517         source: serde_json::error::Error,
    518     ) -> Self {
    519         Self {
    520             label: assertion.label.clone(),
    521             version: assertion.version,
    522             content_type: assertion.content_type.clone(),
    523             source: source.into(),
    524         }
    525     }
    526 
    527     pub(crate) fn from_assertion_unexpected_data_type(
    528         assertion: &Assertion,
    529         assertion_data: &AssertionData,
    530         expected: &str,
    531     ) -> Self {
    532         Self {
    533             label: assertion.label.clone(),
    534             version: assertion.version,
    535             content_type: assertion.content_type.clone(),
    536             source: AssertionDecodeErrorCause::UnexpectedDataType {
    537                 expected: expected.to_string(),
    538                 found: Self::data_type_from_assertion_data(assertion_data),
    539             },
    540         }
    541     }
    542 
    543     fn data_type_from_assertion_data(assertion_data: &AssertionData) -> String {
    544         match assertion_data {
    545             AssertionData::Json(_) => "json".to_string(),
    546             AssertionData::Binary(_) => "binary".to_string(),
    547             AssertionData::Cbor(_) => "cbor".to_string(),
    548             AssertionData::Uuid(_, _) => "uuid".to_string(),
    549         }
    550     }
    551 
    552     pub(crate) fn from_json_err(
    553         label: String,
    554         version: Option<usize>,
    555         content_type: String,
    556         source: serde_json::error::Error,
    557     ) -> Self {
    558         Self {
    559             label,
    560             version,
    561             content_type,
    562             source: source.into(),
    563         }
    564     }
    565 
    566     #[cfg(feature = "unstable_api")]
    567     pub(crate) fn from_err<S: Into<AssertionDecodeErrorCause>>(
    568         label: String,
    569         version: Option<usize>,
    570         content_type: String,
    571         source: S,
    572     ) -> Self {
    573         Self {
    574             label,
    575             version,
    576             content_type,
    577             source: source.into(),
    578         }
    579     }
    580 }
    581 
    582 impl std::fmt::Debug for AssertionDecodeError {
    583     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    584         self.fmt_internal(f)
    585     }
    586 }
    587 
    588 impl std::fmt::Display for AssertionDecodeError {
    589     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    590         self.fmt_internal(f)
    591     }
    592 }
    593 
    594 impl std::error::Error for AssertionDecodeError {
    595     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
    596         Some(&self.source)
    597     }
    598 }
    599 
    600 /// This error type is used inside `AssertionDecodeError` to describe the
    601 /// root cause for the decoding error.
    602 #[derive(Debug, Error)]
    603 #[non_exhaustive]
    604 pub enum AssertionDecodeErrorCause {
    605     /// The assertion had an unexpected data type.
    606     #[error("the assertion had an unexpected data type: expected {expected}, found {found}")]
    607     UnexpectedDataType { expected: String, found: String },
    608 
    609     /// The assertion has a version that is newer that this toolkit can understand.
    610     #[error("the assertion version is too new: expected no later than {max}, found {found}")]
    611     AssertionTooNew { max: usize, found: usize },
    612 
    613     /// Binary data could not be interpreted as UTF-8.
    614     #[error("binary data could not be interpreted as UTF-8")]
    615     BinaryDataNotUtf8,
    616 
    617     /// Assertion data did not match hash link.
    618     #[error("the assertion data did not match the hash embedded in the link")]
    619     AssertionDataIncorrect,
    620 
    621     #[error(transparent)]
    622     JsonError(#[from] serde_json::Error),
    623 
    624     #[error(transparent)]
    625     CborError(#[from] serde_cbor::Error),
    626 }
    627 
    628 pub(crate) type AssertionDecodeResult<T> = std::result::Result<T, AssertionDecodeError>;
    629 
    630 #[cfg(test)]
    631 pub mod tests {
    632     #![allow(clippy::unwrap_used)]
    633 
    634     use super::*;
    635     use crate::assertions::{Action, Actions};
    636 
    637     #[test]
    638     fn test_version_label() {
    639         let test_json = r#"{
    640             "left": 0,
    641             "right": 2000,
    642             "top": 1000,
    643             "bottom": 4000
    644         }"#;
    645         let json = AssertionData::Json(test_json.to_string());
    646         let json2 = AssertionData::Json(test_json.to_string());
    647 
    648         let a = Assertion::new(Actions::LABEL, Some(2), json);
    649         let a_no_ver = Assertion::new(Actions::LABEL, None, json2);
    650 
    651         assert_eq!(a.get_ver().unwrap(), 2);
    652         assert_eq!(a_no_ver.get_ver(), None);
    653         assert_eq!(a.label(), format!("{}.{}", Actions::LABEL, "v2"));
    654         assert_eq!(a.label_root(), Actions::LABEL);
    655         assert_eq!(a_no_ver.label(), Actions::LABEL);
    656     }
    657 
    658     #[test]
    659     fn test_cbor_conversion() {
    660         let action = Actions::new()
    661             .add_action(
    662                 Action::new("c2pa.cropped")
    663                     .set_parameter(
    664                         "coordinate".to_owned(),
    665                         serde_json::json!({"left": 0,"right": 2000,"top": 1000,"bottom": 4000}),
    666                     )
    667                     .unwrap(),
    668             )
    669             .add_action(
    670                 Action::new("c2pa.filtered")
    671                     .set_parameter("name".to_owned(), "gaussian blur")
    672                     .unwrap()
    673                     .set_software_agent("Photoshop")
    674                     .set_when("2015-06-26T16:43:23+0200"),
    675             )
    676             .to_assertion()
    677             .unwrap();
    678 
    679         let action_cbor = action.data();
    680 
    681         let action_restored = Assertion::from_data_cbor(&action.label(), action_cbor);
    682 
    683         assert!(Assertion::assertions_eq(&action, &action_restored));
    684 
    685         let action_obj = action.as_json_object().unwrap();
    686         let action_restored_obj = action_restored.as_json_object().unwrap();
    687 
    688         assert_eq!(action_obj, action_restored_obj);
    689     }
    690 }