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

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 }