#[cfg(feature = "file_io")]
use std::path::Path;
use std::{
collections::HashMap,
io::{Read, Seek, Write},
};
use async_generic::async_generic;
#[cfg(feature = "json_schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
claim::ClaimAssetData,
jumbf::labels::manifest_label_from_uri,
status_tracker::{DetailedStatusTracker, StatusTracker},
store::Store,
utils::base64,
validation_status::{status_for_store, ValidationStatus},
Error, Manifest, Result,
};
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
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>>,
#[serde(skip)]
store: Store,
}
impl ManifestStore {
pub fn new() -> Self {
ManifestStore {
active_manifest: None,
manifests: HashMap::<String, Manifest>::new(),
validation_status: None,
store: Store::new(),
}
}
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
}
}
#[cfg(feature = "v1_api")]
pub fn manifests(&self) -> &HashMap<String, Manifest> {
&self.manifests
}
pub fn get(&self, label: &str) -> Option<&Manifest> {
self.manifests.get(label)
}
pub fn get_resource(&self, uri: &str, stream: impl Write + Read + Seek + Send) -> Result<u64> {
let manifest = match manifest_label_from_uri(uri) {
Some(label) => self.get(&label),
None => self.get_active(),
};
if let Some(manifest) = manifest {
let mut resources = manifest.resources();
if !resources.exists(uri) {
for ingredient in manifest.ingredients() {
if ingredient.resources().exists(uri) {
resources = ingredient.resources();
break;
}
}
}
resources.write_stream(uri, stream)
} else {
Err(Error::ResourceNotFound(uri.to_owned()))
}
}
pub fn validation_status(&self) -> Option<&[ValidationStatus]> {
self.validation_status.as_deref()
}
pub(crate) fn from_store(store: Store, validation_log: &impl StatusTracker) -> ManifestStore {
Self::from_store_impl(
store,
validation_log,
#[cfg(feature = "file_io")]
None,
)
}
#[cfg(feature = "file_io")]
pub(crate) fn from_store_with_resources(
store: Store,
validation_log: &impl StatusTracker,
resource_path: &Path,
) -> ManifestStore {
Self::from_store_impl(store, validation_log, Some(resource_path))
}
fn from_store_impl(
store: Store,
validation_log: &impl StatusTracker,
#[cfg(feature = "file_io")] resource_path: Option<&Path>,
) -> ManifestStore {
let mut statuses = status_for_store(&store, validation_log);
let mut manifest_store = ManifestStore::new();
manifest_store.active_manifest = store.provenance_label();
manifest_store.store = store;
let store = &manifest_store.store;
for claim in store.claims() {
let manifest_label = claim.label();
#[cfg(feature = "file_io")]
let result = Manifest::from_store(store, manifest_label, resource_path);
#[cfg(not(feature = "file_io"))]
let result = Manifest::from_store(store, manifest_label);
match result {
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(crate) fn store(&self) -> &Store {
&self.store
}
#[allow(dead_code)]
pub fn from_manifest(manifest: &Manifest) -> Result<Self> {
use crate::status_tracker::OneShotStatusTracker;
let store = manifest.to_store()?;
Ok(Self::from_store_impl(
store,
&OneShotStatusTracker::new(),
#[cfg(feature = "file_io")]
manifest.resources().base_path(),
))
}
#[cfg(feature = "v1_api")]
pub fn from_bytes(format: &str, image_bytes: &[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, &validation_log))
}
#[async_generic(async_signature(
format: &str,
mut stream: impl Read + Seek + Send,
verify: bool,
))]
pub fn from_stream(
format: &str,
mut stream: impl Read + Seek + Send,
verify: bool,
) -> Result<ManifestStore> {
let mut validation_log = DetailedStatusTracker::new();
let manifest_bytes = Store::load_jumbf_from_stream(format, &mut stream)?;
let store = Store::from_jumbf(&manifest_bytes, &mut validation_log)?;
if verify {
if _sync {
Store::verify_store(
&store,
&mut ClaimAssetData::Stream(&mut stream, format),
&mut validation_log,
)?;
} else {
Store::verify_store_async(
&store,
&mut ClaimAssetData::Stream(&mut stream, format),
&mut validation_log,
)
.await?;
}
}
Ok(Self::from_store(store, &validation_log))
}
#[cfg(feature = "file_io")]
#[cfg(feature = "v1_api")]
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, &validation_log))
}
#[cfg(feature = "file_io")]
#[allow(dead_code)]
pub fn from_file_with_resources<P: AsRef<Path>>(
path: P,
resource_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_with_resources(
store,
&validation_log,
resource_path.as_ref(),
))
}
#[allow(dead_code)]
pub async fn from_bytes_async(
format: &str,
image_bytes: &[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, &validation_log))
}
pub async fn from_fragment_bytes_async(
format: &str,
init_bytes: &[u8],
fragment_bytes: &[u8],
verify: bool,
) -> Result<ManifestStore> {
let mut validation_log = DetailedStatusTracker::new();
Store::load_fragment_from_memory_async(
format,
init_bytes,
fragment_bytes,
verify,
&mut validation_log,
)
.await
.map(|store| Self::from_store(store, &validation_log))
}
pub async fn from_manifest_and_asset_bytes_async(
manifest_bytes: &[u8],
format: &str,
asset_bytes: &[u8],
) -> Result<ManifestStore> {
let mut validation_log = DetailedStatusTracker::new();
let store = Store::from_jumbf(manifest_bytes, &mut validation_log)?;
Store::verify_store_async(
&store,
&mut ClaimAssetData::Bytes(asset_bytes, format),
&mut validation_log,
)
.await?;
Ok(Self::from_store(store, &validation_log))
}
pub fn from_manifest_and_asset_bytes(
manifest_bytes: &[u8],
format: &str,
asset_bytes: &[u8],
) -> Result<ManifestStore> {
let mut validation_log = DetailedStatusTracker::new();
let store = Store::from_jumbf(manifest_bytes, &mut validation_log)?;
Store::verify_store(
&store,
&mut ClaimAssetData::Bytes(asset_bytes, format),
&mut validation_log,
)?;
Ok(Self::from_store(store, &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)]
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
use super::*;
use crate::{status_tracker::OneShotStatusTracker, utils::test::create_test_store};
#[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, &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]
#[cfg(feature = "v1_api")]
fn manifest_report_image() {
let image_bytes = include_bytes!("../tests/fixtures/CA.jpg");
let manifest_store = ManifestStore::from_bytes("image/jpeg", image_bytes, 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(not(target_arch = "wasm32"), actix::test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg(feature = "v1_api")]
async fn manifest_report_image_async() {
let image_bytes = include_bytes!("../tests/fixtures/CA.jpg");
let manifest_store = ManifestStore::from_bytes_async("image/jpeg", image_bytes, 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")]
#[cfg(feature = "v1_api")]
fn manifest_report_from_file() {
let manifest_store = ManifestStore::from_file("tests/fixtures/CA.jpg").unwrap();
println!("{manifest_store}");
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(not(target_arch = "wasm32"), actix::test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg(feature = "v1_api")]
async fn manifest_report_from_manifest_and_asset_bytes_async() {
let asset_bytes = include_bytes!("../tests/fixtures/cloud.jpg");
let manifest_bytes = include_bytes!("../tests/fixtures/cloud_manifest.c2pa");
let manifest_store = ManifestStore::from_manifest_and_asset_bytes_async(
manifest_bytes,
"image/jpg",
asset_bytes,
)
.await
.unwrap();
assert!(!manifest_store.manifests().is_empty());
assert!(manifest_store.validation_status().is_none());
println!("{manifest_store}");
}
#[test]
#[cfg(feature = "file_io")]
#[cfg(feature = "v1_api")]
fn manifest_report_from_file_with_resources() {
let manifest_store = ManifestStore::from_file_with_resources(
"tests/fixtures/CIE-sig-CA.jpg",
"../target/ms",
)
.expect("from_store_with_resources");
println!("{manifest_store}");
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 = "v1_api")]
fn manifest_report_from_stream() {
let image_bytes: &[u8] = include_bytes!("../tests/fixtures/CA.jpg");
let stream = std::io::Cursor::new(image_bytes);
let manifest_store = ManifestStore::from_stream("image/jpeg", stream, true).unwrap();
println!("{manifest_store}");
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());
}
}