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 }