manifest_store.rs (21465B)
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 = "file_io")] 15 use std::path::Path; 16 use std::{ 17 collections::HashMap, 18 io::{Read, Seek, Write}, 19 }; 20 21 use async_generic::async_generic; 22 #[cfg(feature = "json_schema")] 23 use schemars::JsonSchema; 24 use serde::{Deserialize, Serialize}; 25 26 use crate::{ 27 claim::ClaimAssetData, 28 jumbf::labels::manifest_label_from_uri, 29 status_tracker::{DetailedStatusTracker, StatusTracker}, 30 store::Store, 31 utils::base64, 32 validation_status::{status_for_store, ValidationStatus}, 33 Error, Manifest, Result, 34 }; 35 36 #[derive(Debug, Serialize, Deserialize)] 37 #[cfg_attr(feature = "json_schema", derive(JsonSchema))] 38 /// A Container for a set of Manifests and a ValidationStatus list 39 pub struct ManifestStore { 40 #[serde(skip_serializing_if = "Option::is_none")] 41 /// A label for the active (most recent) manifest in the store 42 active_manifest: Option<String>, 43 /// A HashMap of Manifests 44 manifests: HashMap<String, Manifest>, 45 #[serde(skip_serializing_if = "Option::is_none")] 46 /// ValidationStatus generated when loading the ManifestStore from an asset 47 validation_status: Option<Vec<ValidationStatus>>, 48 #[serde(skip)] 49 /// The internal store representing the manifest store 50 store: Store, 51 } 52 53 impl ManifestStore { 54 /// allocates a new empty ManifestStore 55 pub fn new() -> Self { 56 ManifestStore { 57 active_manifest: None, 58 manifests: HashMap::<String, Manifest>::new(), 59 validation_status: None, 60 store: Store::new(), 61 } 62 } 63 64 /// Returns a reference to the active manifest label or None 65 pub fn active_label(&self) -> Option<&str> { 66 self.active_manifest.as_deref() 67 } 68 69 /// Returns a reference to the active manifest or None 70 pub fn get_active(&self) -> Option<&Manifest> { 71 if let Some(label) = self.active_manifest.as_ref() { 72 self.get(label) 73 } else { 74 None 75 } 76 } 77 78 /// Returns a reference to manifest HashMap 79 #[cfg(feature = "v1_api")] 80 pub const fn manifests(&self) -> &HashMap<String, Manifest> { 81 &self.manifests 82 } 83 84 /// Returns a reference to the requested manifest or None 85 pub fn get(&self, label: &str) -> Option<&Manifest> { 86 self.manifests.get(label) 87 } 88 89 // writes a resource identified uri to the given stream 90 pub fn get_resource(&self, uri: &str, stream: impl Write + Read + Seek + Send) -> Result<u64> { 91 // get the manifest referenced by the uri, or the active one if None 92 let manifest = match manifest_label_from_uri(uri) { 93 Some(label) => self.get(&label), 94 None => self.get_active(), 95 }; 96 if let Some(manifest) = manifest { 97 let mut resources = manifest.resources(); 98 if !resources.exists(uri) { 99 // also search ingredients to support Reader model 100 for ingredient in manifest.ingredients() { 101 if ingredient.resources().exists(uri) { 102 resources = ingredient.resources(); 103 break; 104 } 105 } 106 } 107 resources.write_stream(uri, stream) 108 } else { 109 Err(Error::ResourceNotFound(uri.to_owned())) 110 } 111 } 112 113 /// Returns a reference the [ValidationStatus] Vec or None 114 pub fn validation_status(&self) -> Option<&[ValidationStatus]> { 115 self.validation_status.as_deref() 116 } 117 118 /// creates a ManifestStore from a Store with validation 119 pub(crate) fn from_store(store: Store, validation_log: &impl StatusTracker) -> ManifestStore { 120 Self::from_store_impl( 121 store, 122 validation_log, 123 #[cfg(feature = "file_io")] 124 None, 125 ) 126 } 127 128 /// creates a ManifestStore from a Store writing resources to resource_path 129 #[cfg(feature = "file_io")] 130 pub(crate) fn from_store_with_resources( 131 store: Store, 132 validation_log: &impl StatusTracker, 133 resource_path: &Path, 134 ) -> ManifestStore { 135 Self::from_store_impl(store, validation_log, Some(resource_path)) 136 } 137 138 // internal implementation of from_store 139 fn from_store_impl( 140 store: Store, 141 validation_log: &impl StatusTracker, 142 #[cfg(feature = "file_io")] resource_path: Option<&Path>, 143 ) -> ManifestStore { 144 let mut statuses = status_for_store(&store, validation_log); 145 146 let mut manifest_store = ManifestStore::new(); 147 manifest_store.active_manifest = store.provenance_label(); 148 manifest_store.store = store; 149 150 let store = &manifest_store.store; 151 for claim in store.claims() { 152 let manifest_label = claim.label(); 153 #[cfg(feature = "file_io")] 154 let result = Manifest::from_store(store, manifest_label, resource_path); 155 #[cfg(not(feature = "file_io"))] 156 let result = Manifest::from_store(store, manifest_label); 157 match result { 158 Ok(manifest) => { 159 manifest_store 160 .manifests 161 .insert(manifest_label.to_string(), manifest); 162 } 163 Err(e) => { 164 statuses.push(ValidationStatus::from_error(&e)); 165 } 166 }; 167 } 168 169 if !statuses.is_empty() { 170 manifest_store.validation_status = Some(statuses); 171 } 172 173 manifest_store 174 } 175 176 pub(crate) const fn store(&self) -> &Store { 177 &self.store 178 } 179 180 /// Creates a new Manifest Store from a Manifest 181 #[allow(dead_code)] 182 pub fn from_manifest(manifest: &Manifest) -> Result<Self> { 183 use crate::status_tracker::OneShotStatusTracker; 184 let store = manifest.to_store()?; 185 Ok(Self::from_store_impl( 186 store, 187 &OneShotStatusTracker::new(), 188 #[cfg(feature = "file_io")] 189 manifest.resources().base_path(), 190 )) 191 } 192 193 /// Generate a Store from a format string and bytes. 194 #[cfg(feature = "v1_api")] 195 pub fn from_bytes(format: &str, image_bytes: &[u8], verify: bool) -> Result<ManifestStore> { 196 let mut validation_log = DetailedStatusTracker::new(); 197 198 Store::load_from_memory(format, image_bytes, verify, &mut validation_log) 199 .map(|store| Self::from_store(store, &validation_log)) 200 } 201 202 /// Generate a Store from a format string and stream. 203 #[async_generic(async_signature( 204 format: &str, 205 mut stream: impl Read + Seek + Send, 206 verify: bool, 207 ))] 208 pub fn from_stream( 209 format: &str, 210 mut stream: impl Read + Seek + Send, 211 verify: bool, 212 ) -> Result<ManifestStore> { 213 let mut validation_log = DetailedStatusTracker::new(); 214 215 let manifest_bytes = Store::load_jumbf_from_stream(format, &mut stream)?; 216 let store = Store::from_jumbf(&manifest_bytes, &mut validation_log)?; 217 if verify { 218 // verify store and claims 219 if _sync { 220 Store::verify_store( 221 &store, 222 &mut ClaimAssetData::Stream(&mut stream, format), 223 &mut validation_log, 224 )?; 225 } else { 226 Store::verify_store_async( 227 &store, 228 &mut ClaimAssetData::Stream(&mut stream, format), 229 &mut validation_log, 230 ) 231 .await?; 232 } 233 } 234 Ok(Self::from_store(store, &validation_log)) 235 } 236 237 #[cfg(feature = "file_io")] 238 /// Loads a ManifestStore from a file 239 /// Example: 240 /// 241 /// ``` 242 /// # use c2pa::Result; 243 /// use c2pa::ManifestStore; 244 /// # fn main() -> Result<()> { 245 /// let manifest_store = ManifestStore::from_file("tests/fixtures/C.jpg")?; 246 /// println!("{}", manifest_store); 247 /// # Ok(()) 248 /// # } 249 /// ``` 250 #[cfg(feature = "v1_api")] 251 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<ManifestStore> { 252 let mut validation_log = DetailedStatusTracker::new(); 253 254 let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; 255 Ok(Self::from_store(store, &validation_log)) 256 } 257 258 #[cfg(feature = "file_io")] 259 /// Loads a ManifestStore from a file adding resources to a folder 260 /// Example: 261 /// 262 /// ``` 263 /// # use c2pa::Result; 264 /// use c2pa::ManifestStore; 265 /// # fn main() -> Result<()> { 266 /// let manifest_store = ManifestStore::from_file_with_resources( 267 /// "tests/fixtures/C.jpg", 268 /// "../target/tmp/manifest_store", 269 /// )?; 270 /// println!("{}", manifest_store); 271 /// # Ok(()) 272 /// # } 273 /// ``` 274 #[allow(dead_code)] 275 pub fn from_file_with_resources<P: AsRef<Path>>( 276 path: P, 277 resource_path: P, 278 ) -> Result<ManifestStore> { 279 let mut validation_log = DetailedStatusTracker::new(); 280 281 let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; 282 Ok(Self::from_store_with_resources( 283 store, 284 &validation_log, 285 resource_path.as_ref(), 286 )) 287 } 288 289 /// Loads a ManifestStore from a file 290 #[allow(dead_code)] 291 pub async fn from_bytes_async( 292 format: &str, 293 image_bytes: &[u8], 294 verify: bool, 295 ) -> Result<ManifestStore> { 296 let mut validation_log = DetailedStatusTracker::new(); 297 298 Store::load_from_memory_async(format, image_bytes, verify, &mut validation_log) 299 .await 300 .map(|store| Self::from_store(store, &validation_log)) 301 } 302 303 /// Loads a ManifestStore from an init segment and fragment. This 304 /// would be used to load and validate fragmented MP4 files that span 305 /// multiple separate assets. 306 pub async fn from_fragment_bytes_async( 307 format: &str, 308 init_bytes: &[u8], 309 fragment_bytes: &[u8], 310 verify: bool, 311 ) -> Result<ManifestStore> { 312 let mut validation_log = DetailedStatusTracker::new(); 313 314 Store::load_fragment_from_memory_async( 315 format, 316 init_bytes, 317 fragment_bytes, 318 verify, 319 &mut validation_log, 320 ) 321 .await 322 .map(|store| Self::from_store(store, &validation_log)) 323 } 324 325 /// Asynchronously loads a manifest from a buffer holding a binary manifest (.c2pa) and validates against an asset buffer 326 /// 327 /// # Example: Creating a manifest store from a .c2pa manifest and validating it against an asset 328 /// ``` 329 /// use c2pa::{Result, ManifestStore}; 330 /// 331 /// # fn main() -> Result<()> { 332 /// # async { 333 /// let asset_bytes = include_bytes!("../tests/fixtures/cloud.jpg"); 334 /// let manifest_bytes = include_bytes!("../tests/fixtures/cloud_manifest.c2pa"); 335 /// 336 /// let manifest_store = ManifestStore::from_manifest_and_asset_bytes_async(manifest_bytes, "image/jpg", asset_bytes) 337 /// .await 338 /// .unwrap(); 339 /// 340 /// println!("{}", manifest_store); 341 /// # }; 342 /// # 343 /// # Ok(()) 344 /// } 345 /// ``` 346 pub async fn from_manifest_and_asset_bytes_async( 347 manifest_bytes: &[u8], 348 format: &str, 349 asset_bytes: &[u8], 350 ) -> Result<ManifestStore> { 351 let mut validation_log = DetailedStatusTracker::new(); 352 let store = Store::from_jumbf(manifest_bytes, &mut validation_log)?; 353 354 Store::verify_store_async( 355 &store, 356 &mut ClaimAssetData::Bytes(asset_bytes, format), 357 &mut validation_log, 358 ) 359 .await?; 360 361 Ok(Self::from_store(store, &validation_log)) 362 } 363 364 /// Synchronously loads a manifest from a buffer holding a binary manifest (.c2pa) and validates against an asset buffer 365 /// 366 /// # Example: Creating a manifest store from a .c2pa manifest and validating it against an asset 367 /// ``` 368 /// use c2pa::{Result, ManifestStore}; 369 /// 370 /// # fn main() -> Result<()> { 371 /// # async { 372 /// let asset_bytes = include_bytes!("../tests/fixtures/cloud.jpg"); 373 /// let manifest_bytes = include_bytes!("../tests/fixtures/cloud_manifest.c2pa"); 374 /// 375 /// let manifest_store = ManifestStore::from_manifest_and_asset_bytes(manifest_bytes, "image/jpg", asset_bytes) 376 /// .unwrap(); 377 /// 378 /// println!("{}", manifest_store); 379 /// # }; 380 /// # 381 /// # Ok(()) 382 /// } 383 pub fn from_manifest_and_asset_bytes( 384 manifest_bytes: &[u8], 385 format: &str, 386 asset_bytes: &[u8], 387 ) -> Result<ManifestStore> { 388 let mut validation_log = DetailedStatusTracker::new(); 389 let store = Store::from_jumbf(manifest_bytes, &mut validation_log)?; 390 391 Store::verify_store( 392 &store, 393 &mut ClaimAssetData::Bytes(asset_bytes, format), 394 &mut validation_log, 395 )?; 396 397 Ok(Self::from_store(store, &validation_log)) 398 } 399 } 400 401 impl Default for ManifestStore { 402 fn default() -> Self { 403 Self::new() 404 } 405 } 406 407 impl std::fmt::Display for ManifestStore { 408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 409 let mut json = serde_json::to_string_pretty(self).unwrap_or_default(); 410 411 fn omit_tag(mut json: String, tag: &str) -> String { 412 while let Some(index) = json.find(&format!("\"{tag}\": [")) { 413 if let Some(idx2) = json[index..].find(']') { 414 json = format!( 415 "{}\"{}\": \"<omitted>\"{}", 416 &json[..index], 417 tag, 418 &json[index + idx2 + 1..] 419 ); 420 } 421 } 422 json 423 } 424 425 // Make a base64 hash from Vec<u8> values. 426 fn b64_tag(mut json: String, tag: &str) -> String { 427 while let Some(index) = json.find(&format!("\"{tag}\": [")) { 428 if let Some(idx2) = json[index..].find(']') { 429 let idx3 = json[index..].find('[').unwrap_or_default(); 430 431 let bytes: Vec<u8> = 432 serde_json::from_slice(json[index + idx3..index + idx2 + 1].as_bytes()) 433 .unwrap_or_default(); 434 435 json = format!( 436 "{}\"{}\": \"{}\"{}", 437 &json[..index], 438 tag, 439 base64::encode(&bytes), 440 &json[index + idx2 + 1..] 441 ); 442 } 443 } 444 445 json 446 } 447 448 json = b64_tag(json, "hash"); 449 json = omit_tag(json, "pad"); 450 451 f.write_str(&json) 452 } 453 } 454 455 #[cfg(test)] 456 mod tests { 457 #![allow(clippy::expect_used)] 458 #![allow(clippy::unwrap_used)] 459 460 #[cfg(target_arch = "wasm32")] 461 use wasm_bindgen_test::*; 462 463 use super::*; 464 use crate::{status_tracker::OneShotStatusTracker, utils::test::create_test_store}; 465 466 #[cfg(target_arch = "wasm32")] 467 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 468 469 // #[cfg_attr(not(target_arch = "wasm32"), test)] 470 // #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 471 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 472 #[test] 473 fn manifest_report() { 474 let store = create_test_store().expect("creating test store"); 475 476 let manifest_store = ManifestStore::from_store(store, &OneShotStatusTracker::new()); 477 assert!(manifest_store.active_manifest.is_some()); 478 assert!(!manifest_store.manifests.is_empty()); 479 let manifest = manifest_store.get_active().unwrap(); 480 assert!(!manifest.ingredients().is_empty()); 481 // make sure we have two different ingredients 482 assert_eq!(manifest.ingredients()[0].format(), "image/jpeg"); 483 assert_eq!(manifest.ingredients()[1].format(), "image/png"); 484 485 let full_report = manifest_store.to_string(); 486 assert!(!full_report.is_empty()); 487 println!("{full_report}"); 488 } 489 490 #[test] 491 #[cfg(feature = "v1_api")] 492 fn manifest_report_image() { 493 let image_bytes = include_bytes!("../tests/fixtures/CA.jpg"); 494 495 let manifest_store = ManifestStore::from_bytes("image/jpeg", image_bytes, true).unwrap(); 496 497 assert!(!manifest_store.manifests.is_empty()); 498 assert!(manifest_store.active_label().is_some()); 499 assert!(manifest_store.get_active().is_some()); 500 assert!(!manifest_store.manifests().is_empty()); 501 assert!(manifest_store.validation_status().is_none()); 502 let manifest = manifest_store.get_active().unwrap(); 503 assert!(!manifest.ingredients().is_empty()); 504 assert_eq!(manifest.issuer().unwrap(), "C2PA Test Signing Cert"); 505 assert!(manifest.time().is_some()); 506 } 507 508 #[cfg_attr(not(target_arch = "wasm32"), actix::test)] 509 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 510 #[cfg(feature = "v1_api")] 511 async fn manifest_report_image_async() { 512 let image_bytes = include_bytes!("../tests/fixtures/CA.jpg"); 513 514 let manifest_store = ManifestStore::from_bytes_async("image/jpeg", image_bytes, true) 515 .await 516 .unwrap(); 517 518 assert!(!manifest_store.manifests.is_empty()); 519 assert!(manifest_store.active_label().is_some()); 520 assert!(manifest_store.get_active().is_some()); 521 assert!(!manifest_store.manifests().is_empty()); 522 assert!(manifest_store.validation_status().is_none()); 523 let manifest = manifest_store.get_active().unwrap(); 524 assert!(!manifest.ingredients().is_empty()); 525 assert_eq!(manifest.issuer().unwrap(), "C2PA Test Signing Cert"); 526 assert!(manifest.time().is_some()); 527 } 528 529 #[test] 530 #[cfg(feature = "file_io")] 531 #[cfg(feature = "v1_api")] 532 fn manifest_report_from_file() { 533 let manifest_store = ManifestStore::from_file("tests/fixtures/CA.jpg").unwrap(); 534 println!("{manifest_store}"); 535 536 assert!(manifest_store.active_label().is_some()); 537 assert!(manifest_store.get_active().is_some()); 538 assert!(!manifest_store.manifests().is_empty()); 539 assert!(manifest_store.validation_status().is_none()); 540 let manifest = manifest_store.get_active().unwrap(); 541 assert!(!manifest.ingredients().is_empty()); 542 assert_eq!(manifest.issuer().unwrap(), "C2PA Test Signing Cert"); 543 assert!(manifest.time().is_some()); 544 } 545 546 #[cfg_attr(not(target_arch = "wasm32"), actix::test)] 547 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 548 #[cfg(feature = "v1_api")] 549 async fn manifest_report_from_manifest_and_asset_bytes_async() { 550 let asset_bytes = include_bytes!("../tests/fixtures/cloud.jpg"); 551 let manifest_bytes = include_bytes!("../tests/fixtures/cloud_manifest.c2pa"); 552 553 let manifest_store = ManifestStore::from_manifest_and_asset_bytes_async( 554 manifest_bytes, 555 "image/jpg", 556 asset_bytes, 557 ) 558 .await 559 .unwrap(); 560 assert!(!manifest_store.manifests().is_empty()); 561 assert!(manifest_store.validation_status().is_none()); 562 println!("{manifest_store}"); 563 } 564 565 #[test] 566 #[cfg(feature = "file_io")] 567 #[cfg(feature = "v1_api")] 568 fn manifest_report_from_file_with_resources() { 569 let manifest_store = ManifestStore::from_file_with_resources( 570 "tests/fixtures/CIE-sig-CA.jpg", 571 "../target/ms", 572 ) 573 .expect("from_store_with_resources"); 574 println!("{manifest_store}"); 575 576 assert!(manifest_store.active_label().is_some()); 577 assert!(manifest_store.get_active().is_some()); 578 assert!(!manifest_store.manifests().is_empty()); 579 assert!(manifest_store.validation_status().is_none()); 580 let manifest = manifest_store.get_active().unwrap(); 581 assert!(!manifest.ingredients().is_empty()); 582 assert_eq!(manifest.issuer().unwrap(), "C2PA Test Signing Cert"); 583 assert!(manifest.time().is_some()); 584 } 585 586 #[test] 587 #[cfg(feature = "v1_api")] 588 fn manifest_report_from_stream() { 589 let image_bytes: &[u8] = include_bytes!("../tests/fixtures/CA.jpg"); 590 let stream = std::io::Cursor::new(image_bytes); 591 let manifest_store = ManifestStore::from_stream("image/jpeg", stream, true).unwrap(); 592 println!("{manifest_store}"); 593 594 assert!(manifest_store.active_label().is_some()); 595 assert!(manifest_store.get_active().is_some()); 596 assert!(!manifest_store.manifests().is_empty()); 597 assert!(manifest_store.validation_status().is_none()); 598 let manifest = manifest_store.get_active().unwrap(); 599 assert!(!manifest.ingredients().is_empty()); 600 assert_eq!(manifest.issuer().unwrap(), "C2PA Test Signing Cert"); 601 assert!(manifest.time().is_some()); 602 } 603 }