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

ingredient.rs (10385B)


      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 #[cfg(feature = "json_schema")]
     15 use schemars::JsonSchema;
     16 use serde::{Deserialize, Serialize};
     17 
     18 use crate::{
     19     assertion::{Assertion, AssertionBase, AssertionCbor},
     20     assertions::{labels, Metadata, ReviewRating},
     21     error::Result,
     22     hashed_uri::HashedUri,
     23     validation_status::ValidationStatus,
     24 };
     25 
     26 const ASSERTION_CREATION_VERSION: usize = 2;
     27 
     28 // Used to differentiate a parent from a component
     29 #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
     30 #[cfg_attr(feature = "json_schema", derive(JsonSchema))]
     31 pub enum Relationship {
     32     #[serde(rename = "parentOf")]
     33     ParentOf,
     34     #[serde(rename = "componentOf")]
     35     #[default]
     36     ComponentOf,
     37     #[serde(rename = "inputTo")]
     38     InputTo,
     39 }
     40 
     41 /// An ingredient assertion
     42 #[derive(Serialize, Deserialize, Debug, Default)]
     43 pub struct Ingredient {
     44     #[serde(rename = "dc:title")]
     45     pub title: String,
     46     #[serde(rename = "dc:format")]
     47     pub format: String,
     48     #[serde(rename = "documentID", skip_serializing_if = "Option::is_none")]
     49     pub document_id: Option<String>,
     50     #[serde(rename = "instanceID", skip_serializing_if = "Option::is_none")]
     51     pub instance_id: Option<String>,
     52     #[serde(skip_serializing_if = "Option::is_none")]
     53     pub c2pa_manifest: Option<HashedUri>,
     54     #[serde(rename = "validationStatus", skip_serializing_if = "Option::is_none")]
     55     pub validation_status: Option<Vec<ValidationStatus>>,
     56     pub relationship: Relationship,
     57     #[serde(skip_serializing_if = "Option::is_none")]
     58     pub thumbnail: Option<HashedUri>,
     59     #[serde(skip_serializing_if = "Option::is_none")]
     60     pub metadata: Option<Metadata>,
     61     #[serde(skip_serializing_if = "Option::is_none")]
     62     pub data: Option<HashedUri>,
     63     #[serde(skip_serializing_if = "Option::is_none")]
     64     pub description: Option<String>,
     65     #[serde(rename = "informational_URI", skip_serializing_if = "Option::is_none")]
     66     pub informational_uri: Option<String>,
     67 }
     68 
     69 impl Ingredient {
     70     /// Label prefix for an ingredient assertion.
     71     ///
     72     /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_ingredient>.
     73     pub const LABEL: &'static str = labels::INGREDIENT;
     74 
     75     pub fn new(title: &str, format: &str, instance_id: &str, document_id: Option<&str>) -> Self {
     76         Self {
     77             title: title.to_owned(),
     78             format: format.to_owned(),
     79             document_id: document_id.map(|id| id.to_owned()),
     80             instance_id: Some(instance_id.to_owned()),
     81             ..Default::default()
     82         }
     83     }
     84 
     85     pub fn new_v2<S1, S2>(title: S1, format: S2) -> Self
     86     where
     87         S1: Into<String>,
     88         S2: Into<String>,
     89     {
     90         Self {
     91             title: title.into(),
     92             format: format.into(),
     93             ..Default::default()
     94         }
     95     }
     96 
     97     /// determines if an ingredient is a v2 ingredient
     98     const fn is_v2(&self) -> bool {
     99         self.instance_id.is_none()
    100             || self.data.is_some()
    101             || self.description.is_some()
    102             || self.informational_uri.is_some()
    103     }
    104 
    105     pub const fn set_parent(mut self) -> Self {
    106         self.relationship = Relationship::ParentOf;
    107         self
    108     }
    109 
    110     pub fn set_c2pa_manifest_from_hashed_uri(mut self, provenance: Option<HashedUri>) -> Self {
    111         self.c2pa_manifest = provenance;
    112         self
    113     }
    114 
    115     pub fn set_thumbnail_hash_link(mut self, thumbnail: Option<&str>) -> Self {
    116         self.thumbnail =
    117             thumbnail.map(|thumb| HashedUri::new(thumb.to_owned(), None, "Hash".as_bytes()));
    118         self
    119     }
    120 
    121     pub fn set_thumbnail(mut self, hashed_uri: Option<&HashedUri>) -> Self {
    122         self.thumbnail = hashed_uri.map(|h| h.to_owned());
    123         self
    124     }
    125 
    126     pub fn add_review(mut self, review: ReviewRating) -> Self {
    127         let metadata = self.metadata.unwrap_or_default();
    128         self.metadata = Some(metadata.add_review(review));
    129         self
    130     }
    131 
    132     pub fn add_reviews(mut self, reviews: Option<Vec<ReviewRating>>) -> Self {
    133         if let Some(reviews) = reviews {
    134             let metadata = Metadata::new().set_reviews(reviews);
    135             self.metadata = Some(metadata);
    136         };
    137         self
    138     }
    139 
    140     pub fn add_validation_status(mut self, status: ValidationStatus) {
    141         match &mut self.validation_status {
    142             None => self.validation_status = Some(vec![status]),
    143             Some(validation_status) => validation_status.push(status),
    144         }
    145     }
    146 }
    147 
    148 impl AssertionCbor for Ingredient {}
    149 
    150 impl AssertionBase for Ingredient {
    151     const LABEL: &'static str = Self::LABEL;
    152     const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
    153 
    154     /// if we require v2 fields then use V2
    155     fn version(&self) -> Option<usize> {
    156         if self.is_v2() {
    157             Some(2)
    158         } else {
    159             Some(1)
    160         }
    161     }
    162 
    163     fn to_assertion(&self) -> Result<Assertion> {
    164         Self::to_cbor_assertion(self)
    165     }
    166 
    167     fn from_assertion(assertion: &Assertion) -> Result<Self> {
    168         Self::from_cbor_assertion(assertion)
    169     }
    170 }
    171 #[cfg(test)]
    172 pub mod tests {
    173     #![allow(clippy::expect_used)]
    174     #![allow(clippy::panic)]
    175     #![allow(clippy::unwrap_used)]
    176 
    177     use super::*;
    178     use crate::assertion::AssertionData;
    179 
    180     #[test]
    181     fn assertion_ingredient() {
    182         let original = Ingredient::new(
    183             "image 1.jpg",
    184             "image/jpeg",
    185             "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
    186             Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c347"),
    187         )
    188         .set_thumbnail_hash_link(Some("#c2pa.ingredient.thumbnail.jpeg"));
    189         let assertion = original.to_assertion().expect("build_assertion");
    190         assert_eq!(assertion.mime_type(), "application/cbor");
    191         assert_eq!(assertion.label(), Ingredient::LABEL);
    192         let result = Ingredient::from_cbor_assertion(&assertion).expect("from_assertion");
    193         assert_eq!(original.title, result.title);
    194         assert_eq!(original.format, result.format);
    195         assert_eq!(original.document_id, result.document_id);
    196         assert_eq!(original.instance_id, result.instance_id);
    197         assert_eq!(original.thumbnail, result.thumbnail);
    198     }
    199 
    200     #[test]
    201     fn test_build_assertion() {
    202         let assertion = Ingredient::new(
    203             "image 1.jpg",
    204             "image/jpeg",
    205             "xmp.did:87d51599-286e-43b2-9478-88c79f49c347",
    206             Some("xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d"),
    207         )
    208         .set_thumbnail_hash_link(Some("#c2pa.ingredient.thumbnail.jpeg"))
    209         .to_assertion()
    210         .unwrap();
    211 
    212         println!("assertion label: {}", assertion.label());
    213 
    214         let j = assertion.data();
    215 
    216         let from_j = Assertion::from_data_cbor(&assertion.label(), j);
    217         let ad_ref = from_j.decode_data();
    218 
    219         if let AssertionData::Cbor(ref ad_cbor) = ad_ref {
    220             // compare results
    221             let orig_d = assertion.decode_data();
    222             if let AssertionData::Cbor(ref orig_cbor) = orig_d {
    223                 assert_eq!(orig_cbor, ad_cbor);
    224             } else {
    225                 panic!("Couldn't decode orig_d");
    226             }
    227         } else {
    228             panic!("Couldn't decode ad_ref");
    229         }
    230     }
    231 
    232     #[test]
    233     fn test_binary_round_trip() {
    234         let assertion = Ingredient::new(
    235             "image 1.jpg",
    236             "image/jpeg",
    237             "xmp.did:87d51599-286e-43b2-9478-88c79f49c347",
    238             Some("xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d"),
    239         )
    240         //.set_provenance("")
    241         .set_thumbnail_hash_link(Some("#c2pa.ingredient.thumbnail.jpeg"))
    242         .to_assertion()
    243         .unwrap();
    244 
    245         let orig_bytes = assertion.data();
    246 
    247         let assertion_from_binary = Assertion::from_data_cbor(&assertion.label(), orig_bytes);
    248 
    249         println!(
    250             "Label Match Test {} = {}",
    251             assertion.label(),
    252             assertion_from_binary.label()
    253         );
    254         assert_eq!(assertion.label(), assertion_from_binary.label());
    255 
    256         // compare the data as bytes
    257         assert_eq!(orig_bytes, assertion_from_binary.data());
    258         println!("Decoded binary matches")
    259     }
    260 
    261     #[test]
    262     fn test_assertion_with_reviews() {
    263         let review = ReviewRating::new(
    264             "a 3rd party plugin was used",
    265             Some("actions.unknownActionsPerformed".to_string()),
    266             1,
    267         );
    268         let original = Ingredient::new(
    269             "image 1.jpg",
    270             "image/jpeg",
    271             "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
    272             Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c347"),
    273         )
    274         .add_review(review);
    275 
    276         let assertion = original.to_assertion().expect("build_assertion");
    277         assert_eq!(assertion.mime_type(), "application/cbor");
    278         assert_eq!(assertion.label(), Ingredient::LABEL);
    279         let restored = Ingredient::from_cbor_assertion(&assertion).expect("from_assertion");
    280         assert_eq!(original.title, restored.title);
    281         assert_eq!(original.format, restored.format);
    282         assert_eq!(original.document_id, restored.document_id);
    283         assert_eq!(original.instance_id, restored.instance_id);
    284         assert_eq!(original.thumbnail, restored.thumbnail);
    285 
    286         assert!(restored.metadata.is_some());
    287         let metadata = restored.metadata.unwrap();
    288         let date_time = metadata.date_time().unwrap();
    289         let date_time_parsed = chrono::DateTime::parse_from_rfc3339(date_time);
    290 
    291         assert!(metadata.reviews().is_some());
    292         assert!(date_time_parsed.is_ok());
    293 
    294         let reviews = metadata.reviews().unwrap();
    295 
    296         assert_eq!(reviews.len(), 1);
    297         assert_eq!(
    298             reviews[0].code.as_ref().unwrap(),
    299             "actions.unknownActionsPerformed"
    300         );
    301         assert_eq!(reviews[0].explanation, "a 3rd party plugin was used");
    302         assert_eq!(reviews[0].value, 1);
    303     }
    304 }