use crate::{
status_tracker::{DetailedStatusTracker, StatusTracker},
store::Store,
validation_status::{status_for_store, ValidationStatus},
Manifest, Result,
};
use serde::Serialize;
use std::collections::HashMap;
#[cfg(feature = "file_io")]
use std::path::Path;
#[derive(Serialize)]
pub struct ManifestStore {
#[serde(skip_serializing_if = "Option::is_none")]
active_manifest: Option<String>,
manifests: HashMap<String, Manifest>,
#[serde(skip_serializing_if = "Option::is_none")]
validation_status: Option<Vec<ValidationStatus>>,
}
impl ManifestStore {
pub(crate) fn new() -> Self {
ManifestStore {
active_manifest: None,
manifests: HashMap::<String, Manifest>::new(),
validation_status: None,
}
}
pub fn active_label(&self) -> Option<&str> {
self.active_manifest.as_deref()
}
pub fn get_active(&self) -> Option<&Manifest> {
if let Some(label) = self.active_manifest.as_ref() {
self.get(label)
} else {
None
}
}
pub fn manifests(&self) -> &HashMap<String, Manifest> {
&self.manifests
}
pub fn get(&self, label: &str) -> Option<&Manifest> {
self.manifests.get(label)
}
pub fn validation_status(&self) -> Option<&[ValidationStatus]> {
self.validation_status.as_deref()
}
pub(crate) fn from_store(
store: &Store,
validation_log: &mut impl StatusTracker,
) -> ManifestStore {
let mut statuses = status_for_store(store, validation_log);
let mut manifest_store = ManifestStore::new();
manifest_store.active_manifest = store.provenance_label();
for claim in store.claims() {
let manifest_label = claim.label();
match Manifest::from_store(store, manifest_label) {
Ok(manifest) => {
manifest_store
.manifests
.insert(manifest_label.to_owned(), manifest);
}
Err(e) => {
statuses.push(ValidationStatus::from_error(&e));
}
};
}
if !statuses.is_empty() {
manifest_store.validation_status = Some(statuses);
}
manifest_store
}
pub fn from_manifest(manifest: &Manifest) -> Result<Self> {
use crate::status_tracker::OneShotStatusTracker;
let store = manifest.to_store()?;
Ok(Self::from_store(&store, &mut OneShotStatusTracker::new()))
}
pub fn from_bytes(format: &str, image_bytes: Vec<u8>, verify: bool) -> Result<ManifestStore> {
let mut validation_log = DetailedStatusTracker::new();
Store::load_from_memory(format, &image_bytes, verify, &mut validation_log)
.map(|store| Self::from_store(&store, &mut validation_log))
}
#[cfg(feature = "file_io")]
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<ManifestStore> {
let mut validation_log = DetailedStatusTracker::new();
let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?;
Ok(Self::from_store(&store, &mut validation_log))
}
pub async fn from_bytes_async(
format: &str,
image_bytes: Vec<u8>,
verify: bool,
) -> Result<ManifestStore> {
let mut validation_log = DetailedStatusTracker::new();
Store::load_from_memory_async(format, &image_bytes, verify, &mut validation_log)
.await
.map(|store| Self::from_store(&store, &mut validation_log))
}
}
impl Default for ManifestStore {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for ManifestStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut json = serde_json::to_string_pretty(self).unwrap_or_default();
fn omit_tag(mut json: String, tag: &str) -> String {
while let Some(index) = json.find(&format!("\"{}\": [", tag)) {
if let Some(idx2) = json[index..].find(']') {
json = format!(
"{}\"{}\": \"<omitted>\"{}",
&json[..index],
tag,
&json[index + idx2 + 1..]
);
}
}
json
}
fn b64_tag(mut json: String, tag: &str) -> String {
while let Some(index) = json.find(&format!("\"{}\": [", tag)) {
if let Some(idx2) = json[index..].find(']') {
let idx3 = json[index..].find('[').unwrap_or_default();
let bytes: Vec<u8> =
serde_json::from_slice(json[index + idx3..index + idx2 + 1].as_bytes())
.unwrap_or_default();
json = format!(
"{}\"{}\": \"{}\"{}",
&json[..index],
tag,
base64::encode(&bytes),
&json[index + idx2 + 1..]
);
}
}
json
}
json = b64_tag(json, "hash");
json = omit_tag(json, "pad");
f.write_str(&json)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use super::*;
use crate::{status_tracker::OneShotStatusTracker, utils::test::create_test_store};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[test]
fn manifest_report() {
let store = create_test_store().expect("creating test store");
let manifest_store = ManifestStore::from_store(&store, &mut OneShotStatusTracker::new());
assert!(manifest_store.active_manifest.is_some());
assert!(!manifest_store.manifests.is_empty());
let manifest = manifest_store.get_active().unwrap();
assert!(!manifest.ingredients().is_empty());
assert_eq!(manifest.ingredients()[0].format(), "image/jpeg");
assert_eq!(manifest.ingredients()[1].format(), "image/png");
let full_report = manifest_store.to_string();
assert!(!full_report.is_empty());
println!("{}", full_report);
}
#[test]
fn manifest_report_image() {
let image_bytes = include_bytes!("../tests/fixtures/CA.jpg");
let manifest_store =
ManifestStore::from_bytes("image/jpeg", image_bytes.to_vec(), true).unwrap();
assert!(!manifest_store.manifests.is_empty());
assert!(manifest_store.active_label().is_some());
assert!(manifest_store.get_active().is_some());
assert!(!manifest_store.manifests().is_empty());
assert!(manifest_store.validation_status().is_none());
let manifest = manifest_store.get_active().unwrap();
assert!(!manifest.ingredients().is_empty());
assert_eq!(manifest.issuer().unwrap(), "C2PA Test Signing Cert");
assert!(manifest.time().is_some());
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[ignore]
#[allow(dead_code)]
async fn manifest_report_image_wasm() {
let image_bytes = include_bytes!("../tests/fixtures/CA.jpg");
let manifest_store =
ManifestStore::from_bytes_async("image/jpeg", image_bytes.to_vec(), true)
.await
.unwrap();
assert!(!manifest_store.manifests.is_empty());
assert!(manifest_store.active_label().is_some());
assert!(manifest_store.get_active().is_some());
assert!(!manifest_store.manifests().is_empty());
assert!(manifest_store.validation_status().is_none());
let manifest = manifest_store.get_active().unwrap();
assert!(!manifest.ingredients().is_empty());
assert_eq!(manifest.issuer().unwrap(), "C2PA Test Signing Cert");
assert!(manifest.time().is_some());
}
#[test]
#[cfg(feature = "file_io")]
fn manifest_report_from_file() {
let manifest_store = ManifestStore::from_file("tests/fixtures/CA.jpg").unwrap();
println!("{}", manifest_store);
assert!(!manifest_store.manifests.is_empty());
assert!(manifest_store.active_label().is_some());
assert!(manifest_store.get_active().is_some());
assert!(!manifest_store.manifests().is_empty());
assert!(manifest_store.validation_status().is_none());
let manifest = manifest_store.get_active().unwrap();
assert!(!manifest.ingredients().is_empty());
assert_eq!(manifest.issuer().unwrap(), "C2PA Test Signing Cert");
assert!(manifest.time().is_some());
}
}