use std::{borrow::Cow, collections::HashMap, io::Cursor};
#[cfg(feature = "file_io")]
use std::{fs::create_dir_all, path::Path};
use async_generic::async_generic;
use log::{debug, error};
#[cfg(feature = "json_schema")]
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use crate::{
assertion::{AssertionBase, AssertionData},
assertions::{
labels, Actions, CreativeWork, DataHash, Exif, SoftwareAgent, Thumbnail, User, UserCbor,
},
asset_io::{CAIRead, CAIReadWrite},
claim::{Claim, RemoteManifest},
error::{Error, Result},
ingredient::Ingredient,
jumbf,
manifest_assertion::ManifestAssertion,
resource_store::{skip_serializing_resources, ResourceRef, ResourceStore},
salt::DefaultSalt,
store::Store,
AsyncSigner, ClaimGeneratorInfo, HashRange, ManifestAssertionKind, RemoteSigner, Signer,
SigningAlg,
};
#[derive(Debug, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct Manifest {
#[serde(skip_serializing_if = "Option::is_none")]
vendor: Option<String>,
#[serde(default = "default_claim_generator")]
pub claim_generator: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub claim_generator_info: Option<Vec<ClaimGeneratorInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(default = "default_format")]
format: String,
#[serde(default = "default_instance_id")]
instance_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
claim_generator_hints: Option<HashMap<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
thumbnail: Option<ResourceRef>,
#[serde(default = "default_vec::<Ingredient>")]
ingredients: Vec<Ingredient>,
#[serde(skip_serializing_if = "Option::is_none")]
credentials: Option<Vec<Value>>,
#[serde(default = "default_vec::<ManifestAssertion>")]
assertions: Vec<ManifestAssertion>,
#[serde(skip_serializing_if = "Option::is_none")]
redactions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
signature_info: Option<SignatureInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
label: Option<String>,
#[serde(skip)]
remote_manifest: Option<RemoteManifest>,
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "skip_serializing_resources")]
resources: ResourceStore,
}
fn default_claim_generator() -> String {
format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
}
fn default_instance_id() -> String {
format!("xmp:iid:{}", Uuid::new_v4())
}
fn default_format() -> String {
"application/octet-stream".to_owned()
}
fn default_vec<T>() -> Vec<T> {
Vec::new()
}
impl Manifest {
pub fn new<S: Into<String>>(claim_generator: S) -> Self {
Self {
claim_generator: claim_generator.into(),
format: default_format(),
instance_id: default_instance_id(),
..Default::default()
}
}
pub fn claim_generator(&self) -> &str {
self.claim_generator.as_str()
}
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
pub fn format(&self) -> &str {
&self.format
}
pub fn instance_id(&self) -> &str {
&self.instance_id
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn thumbnail(&self) -> Option<(&str, Cow<Vec<u8>>)> {
self.thumbnail
.as_ref()
.and_then(|t| Some(t.format.as_str()).zip(self.resources.get(&t.identifier).ok()))
}
pub fn thumbnail_ref(&self) -> Option<&ResourceRef> {
self.thumbnail.as_ref()
}
pub fn ingredients(&self) -> &[Ingredient] {
&self.ingredients
}
pub fn ingredients_mut(&mut self) -> &mut [Ingredient] {
&mut self.ingredients
}
pub fn assertions(&self) -> &[ManifestAssertion] {
&self.assertions
}
pub fn credentials(&self) -> Option<&[Value]> {
self.credentials.as_deref()
}
pub fn remote_manifest_url(&self) -> Option<&str> {
match self.remote_manifest.as_ref() {
Some(RemoteManifest::Remote(url)) => Some(url.as_str()),
Some(RemoteManifest::EmbedWithRemote(url)) => Some(url.as_str()),
_ => None,
}
}
pub fn set_vendor<S: Into<String>>(&mut self, vendor: S) -> &mut Self {
self.vendor = Some(vendor.into());
self
}
pub fn set_label<S: Into<String>>(&mut self, label: S) -> &mut Self {
self.label = Some(label.into());
self
}
pub fn set_claim_generator<S: Into<String>>(&mut self, generator: S) -> &mut Self {
self.claim_generator = generator.into();
self
}
pub fn set_format<S: Into<String>>(&mut self, format: S) -> &mut Self {
self.format = format.into();
self
}
pub fn set_instance_id<S: Into<String>>(&mut self, instance_id: S) -> &mut Self {
self.instance_id = instance_id.into();
self
}
pub fn set_title<S: Into<String>>(&mut self, title: S) -> &mut Self {
self.title = Some(title.into());
self
}
pub fn set_thumbnail_ref(&mut self, thumbnail: ResourceRef) -> Result<&mut Self> {
if thumbnail.format != "none" && !self.resources.exists(&thumbnail.identifier) {
return Err(Error::NotFound);
};
self.thumbnail = Some(thumbnail);
Ok(self)
}
pub fn set_thumbnail<S: Into<String>, B: Into<Vec<u8>>>(
&mut self,
format: S,
thumbnail: B,
) -> Result<&mut Self> {
let base_id = self
.label()
.unwrap_or_else(|| self.instance_id())
.to_string();
self.thumbnail = Some(
self.resources
.add_with(&base_id, &format.into(), thumbnail)?,
);
Ok(self)
}
pub fn set_sidecar_manifest(&mut self) -> &mut Self {
self.remote_manifest = Some(RemoteManifest::SideCar);
self
}
pub fn set_remote_manifest<S: Into<String>>(&mut self, remote_url: S) -> &mut Self {
self.remote_manifest = Some(RemoteManifest::Remote(remote_url.into()));
self
}
pub fn set_embedded_manifest_with_remote_ref<S: Into<String>>(
&mut self,
remote_url: S,
) -> &mut Self {
self.remote_manifest = Some(RemoteManifest::EmbedWithRemote(remote_url.into()));
self
}
pub fn signature_info(&self) -> Option<&SignatureInfo> {
self.signature_info.as_ref()
}
pub fn parent(&self) -> Option<&Ingredient> {
self.ingredients.iter().find(|i| i.is_parent())
}
pub fn set_parent(&mut self, mut ingredient: Ingredient) -> Result<&mut Self> {
if self.parent().is_some() {
error!("parent already added");
return Err(Error::BadParam("Parent parent already added".to_owned()));
}
ingredient.set_is_parent();
self.ingredients.insert(0, ingredient);
Ok(self)
}
pub fn add_ingredient(&mut self, ingredient: Ingredient) -> &mut Self {
self.ingredients.push(ingredient);
self
}
pub fn add_labeled_assertion<S: Into<String>, T: Serialize>(
&mut self,
label: S,
data: &T,
) -> Result<&mut Self> {
self.assertions
.push(ManifestAssertion::from_labeled_assertion(label, data)?);
Ok(self)
}
pub fn add_assertion<T: Serialize + AssertionBase>(&mut self, data: &T) -> Result<&mut Self> {
self.assertions
.push(ManifestAssertion::from_assertion(data)?);
Ok(self)
}
pub fn find_assertion<T: DeserializeOwned>(&self, label: &str) -> Result<T> {
if let Some(manifest_assertion) = self.assertions.iter().find(|a| a.label() == label) {
manifest_assertion.to_assertion()
} else {
Err(Error::NotFound)
}
}
pub fn find_assertion_with_instance<T: DeserializeOwned>(
&self,
label: &str,
instance: usize,
) -> Result<T> {
if let Some(manifest_assertion) = self
.assertions
.iter()
.find(|a| a.label() == label && a.instance() == instance)
{
manifest_assertion.to_assertion()
} else {
Err(Error::NotFound)
}
}
pub fn add_redaction<S: Into<String>>(&mut self, label: S) -> Result<&mut Self> {
match self.redactions.as_mut() {
Some(redactions) => redactions.push(label.into()),
None => self.redactions = Some([label.into()].to_vec()),
}
Ok(self)
}
pub fn add_verifiable_credential<T: Serialize>(&mut self, data: &T) -> Result<&mut Self> {
let value = serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?;
match self.credentials.as_mut() {
Some(credentials) => credentials.push(value),
None => self.credentials = Some([value].to_vec()),
}
Ok(self)
}
pub fn issuer(&self) -> Option<String> {
self.signature_info.to_owned().and_then(|sig| sig.issuer)
}
pub fn time(&self) -> Option<String> {
self.signature_info.to_owned().and_then(|sig| sig.time)
}
pub fn resources(&self) -> &ResourceStore {
&self.resources
}
pub fn resources_mut(&mut self) -> &mut ResourceStore {
&mut self.resources
}
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_slice(json.as_bytes()).map_err(Error::JsonError)
}
#[cfg(feature = "file_io")]
pub fn with_base_path<P: AsRef<Path>>(&mut self, base_path: P) -> Result<&Self> {
create_dir_all(&base_path)?;
self.resources.set_base_path(base_path.as_ref());
for i in 0..self.ingredients.len() {
self.ingredients[i].with_base_path(base_path.as_ref())?;
}
Ok(self)
}
pub(crate) fn from_store(
store: &Store,
manifest_label: &str,
#[cfg(feature = "file_io")] resource_path: Option<&Path>,
) -> Result<Self> {
let claim = store
.get_claim(manifest_label)
.ok_or_else(|| Error::ClaimMissing {
label: manifest_label.to_owned(),
})?;
let claim_generator = claim.claim_generator().to_owned();
let mut manifest = Manifest::new(claim_generator);
#[cfg(feature = "file_io")]
if let Some(base_path) = resource_path {
manifest.with_base_path(base_path)?;
}
if let Some(info_vec) = claim.claim_generator_info() {
let mut generators = Vec::new();
for claim_info in info_vec {
let mut info = claim_info.to_owned();
if let Some(icon) = claim_info.icon.as_ref() {
info.set_icon(icon.to_resource_ref(manifest.resources_mut(), claim)?);
}
generators.push(info);
}
manifest.claim_generator_info = Some(generators);
}
manifest.set_label(claim.label());
manifest.resources.set_label(claim.label()); manifest.claim_generator_hints = claim.get_claim_generator_hint_map().cloned();
let credentials: Vec<Value> = claim
.get_verifiable_credentials()
.iter()
.filter_map(|d| match d {
AssertionData::Json(s) => serde_json::from_str(s).ok(),
_ => None,
})
.collect();
if !credentials.is_empty() {
manifest.credentials = Some(credentials);
}
manifest.redactions = claim.redactions().map(|rs| {
rs.iter()
.filter_map(|r| jumbf::labels::assertion_label_from_uri(r))
.collect()
});
if let Some(title) = claim.title() {
manifest.set_title(title);
}
manifest.set_format(claim.format());
manifest.set_instance_id(claim.instance_id());
for assertion in claim.assertions() {
let claim_assertion = store.get_claim_assertion_from_uri(
&jumbf::labels::to_absolute_uri(claim.label(), &assertion.url()),
)?;
let assertion = claim_assertion.assertion();
let label = claim_assertion.label();
let base_label = assertion.label();
debug!("assertion = {}", &label);
match base_label.as_ref() {
base if base.starts_with(labels::ACTIONS) => {
let mut actions = Actions::from_assertion(assertion)?;
for action in actions.actions_mut() {
if let Some(SoftwareAgent::ClaimGeneratorInfo(info)) =
action.software_agent_mut()
{
if let Some(icon) = info.icon.as_mut() {
let icon = icon.to_resource_ref(manifest.resources_mut(), claim)?;
info.set_icon(icon);
}
}
}
if let Some(templates) = actions.templates.as_mut() {
for template in templates {
template.icon = match template.icon.take() {
Some(icon) => {
Some(icon.to_resource_ref(manifest.resources_mut(), claim)?)
}
None => None,
};
template.software_agent = match template.software_agent.take() {
Some(SoftwareAgent::ClaimGeneratorInfo(mut info)) => {
if let Some(icon) = info.icon.as_mut() {
let icon =
icon.to_resource_ref(manifest.resources_mut(), claim)?;
info.set_icon(icon);
}
Some(SoftwareAgent::ClaimGeneratorInfo(info))
}
agent => agent,
};
}
}
let manifest_assertion = ManifestAssertion::from_assertion(&actions)?
.set_instance(claim_assertion.instance());
manifest.assertions.push(manifest_assertion);
}
base if base.starts_with(labels::INGREDIENT) => {
let assertion_uri = jumbf::labels::to_assertion_uri(claim.label(), &label);
let ingredient = Ingredient::from_ingredient_uri(
store,
manifest_label,
&assertion_uri,
#[cfg(feature = "file_io")]
resource_path,
)?;
manifest.add_ingredient(ingredient);
}
labels::DATA_HASH | labels::BMFF_HASH | labels::BOX_HASH => {
}
label if label.starts_with(labels::CLAIM_THUMBNAIL) => {
let thumbnail = Thumbnail::from_assertion(assertion)?;
let id = jumbf::labels::to_assertion_uri(claim.label(), label);
let id = jumbf::labels::to_relative_uri(&id);
manifest.thumbnail = Some(manifest.resources.add_uri(
&id,
&thumbnail.content_type,
thumbnail.data,
)?);
}
_ => {
match assertion.decode_data() {
AssertionData::Cbor(_) => {
let value = assertion.as_json_object()?;
let ma = ManifestAssertion::new(base_label, value)
.set_instance(claim_assertion.instance());
manifest.assertions.push(ma);
}
AssertionData::Json(_) => {
let value = assertion.as_json_object()?;
let ma = ManifestAssertion::new(base_label, value)
.set_instance(claim_assertion.instance())
.set_kind(ManifestAssertionKind::Json);
manifest.assertions.push(ma);
}
AssertionData::Binary(_x) => {}
AssertionData::Uuid(_, _) => {}
}
}
}
}
manifest.signature_info = match claim.signature_info() {
Some(signature_info) => Some(SignatureInfo {
alg: signature_info.alg,
issuer: signature_info.issuer_org,
time: signature_info.date.map(|d| d.to_rfc3339()),
cert_serial_number: signature_info.cert_serial_number.map(|s| s.to_string()),
cert_chain: String::from_utf8(signature_info.cert_chain)
.map_err(|_e| Error::CoseInvalidCert)?,
revocation_status: signature_info.revocation_status,
}),
None => None,
};
Ok(manifest)
}
#[cfg(feature = "file_io")]
pub fn set_asset_from_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let ingredient = Ingredient::from_file_info(path.as_ref());
self.set_format(ingredient.format());
self.set_instance_id(ingredient.instance_id());
if self.title().is_none() {
self.set_title(ingredient.title());
}
if self.thumbnail_ref().is_none() {
#[cfg(feature = "add_thumbnails")]
if let Ok((format, image)) = crate::utils::thumbnail::make_thumbnail(path.as_ref()) {
let base_path = self.resources_mut().take_base_path();
self.set_thumbnail(format, image)?;
if let Some(path) = base_path {
self.resources_mut().set_base_path(path)
}
}
}
Ok(())
}
pub(crate) fn to_claim(&self) -> Result<Claim> {
let generator = format!(
"{} {}/{}",
&self.claim_generator,
crate::NAME,
crate::VERSION
);
let mut claim = match self.label() {
Some(label) => Claim::new_with_user_guid(&generator, &label.to_string()),
None => Claim::new(&generator, self.vendor.as_deref()),
};
if let Some(info_vec) = self.claim_generator_info.as_ref() {
for info in info_vec {
let mut claim_info = info.to_owned();
if let Some(icon) = claim_info.icon.as_ref() {
claim_info.icon = Some(icon.to_hashed_uri(self.resources(), &mut claim)?);
}
claim.add_claim_generator_info(claim_info);
}
}
if let Some(remote_op) = &self.remote_manifest {
match remote_op {
RemoteManifest::NoRemote => (),
RemoteManifest::SideCar => claim.set_external_manifest(),
RemoteManifest::Remote(r) => claim.set_remote_manifest(r)?,
RemoteManifest::EmbedWithRemote(r) => claim.set_embed_remote_manifest(r)?,
};
}
if let Some(title) = self.title() {
claim.set_title(Some(title.to_owned()));
}
self.format().clone_into(&mut claim.format);
self.instance_id().clone_into(&mut claim.instance_id);
if let Some(thumb_ref) = self.thumbnail_ref() {
if thumb_ref.format != "none" {
let data = self.resources.get(&thumb_ref.identifier)?;
claim.add_assertion(&Thumbnail::new(
&labels::add_thumbnail_format(labels::CLAIM_THUMBNAIL, &thumb_ref.format),
data.into_owned(),
))?;
}
}
let mut vc_table = HashMap::new();
if let Some(verified_credentials) = self.credentials.as_ref() {
for vc in verified_credentials {
let vc_str = &vc.to_string();
let id = Claim::vc_id(vc_str)?;
vc_table.insert(id, claim.add_verifiable_credential(vc_str)?);
}
}
let mut ingredient_map = HashMap::new();
for ingredient in &self.ingredients {
let uri = ingredient.add_to_claim(&mut claim, self.redactions.clone(), None)?;
ingredient_map.insert(ingredient.instance_id(), uri);
}
let salt = DefaultSalt::default();
for manifest_assertion in &self.assertions {
match manifest_assertion.label() {
l if l.starts_with(Actions::LABEL) => {
let version = labels::version(l);
let mut actions: Actions = manifest_assertion.to_assertion()?;
let ingredients_key = match version {
None | Some(1) => "ingredient",
Some(2) => "ingredients",
_ => return Err(Error::AssertionUnsupportedVersion),
};
let needs_ingredient: Vec<(usize, crate::assertions::Action)> = actions
.actions()
.iter()
.enumerate()
.filter_map(|(i, a)| {
if a.instance_id().is_some()
&& a.get_parameter(ingredients_key).is_none()
{
Some((i, a.clone()))
} else {
None
}
})
.collect();
for (index, action) in needs_ingredient {
if let Some(id) = action.instance_id() {
if let Some(hash_url) = ingredient_map.get(id) {
let update = match ingredients_key {
"ingredient" => {
action.set_parameter(ingredients_key, hash_url.clone())
}
_ => {
action.set_parameter(ingredients_key, [hash_url.clone()])
}
}?;
actions = actions.update_action(index, update);
}
}
}
if let Some(templates) = actions.templates.as_mut() {
for template in templates {
template.icon = match template.icon.take() {
Some(icon) => {
Some(icon.to_hashed_uri(self.resources(), &mut claim)?)
}
None => None,
};
template.software_agent = match template.software_agent.take() {
Some(SoftwareAgent::ClaimGeneratorInfo(mut info)) => {
if let Some(icon) = info.icon.as_mut() {
let icon =
icon.to_hashed_uri(self.resources(), &mut claim)?;
info.set_icon(icon);
}
Some(SoftwareAgent::ClaimGeneratorInfo(info))
}
agent => agent,
};
}
}
let actions_mut = actions.actions_mut();
#[allow(clippy::needless_range_loop)]
for index in 0..actions_mut.len() {
let action = &actions_mut[index];
if let Some(SoftwareAgent::ClaimGeneratorInfo(info)) =
action.software_agent()
{
if let Some(icon) = info.icon.as_ref() {
let mut info = info.to_owned();
let icon_uri = icon.to_hashed_uri(self.resources(), &mut claim)?;
let update = info.set_icon(icon_uri);
let mut action = action.to_owned();
action = action.set_software_agent(update.to_owned());
actions_mut[index] = action;
}
}
}
claim.add_assertion(&actions)
}
CreativeWork::LABEL => {
let mut cw: CreativeWork = manifest_assertion.to_assertion()?;
if let Some(cw_authors) = cw.author() {
let mut authors = Vec::new();
for a in cw_authors {
authors.push(
a.identifier()
.and_then(|i| {
vc_table
.get(&i)
.map(|uri| a.clone().add_credential(uri.clone()))
})
.unwrap_or_else(|| Ok(a.clone()))?,
);
}
cw = cw.set_author(&authors)?;
}
claim.add_assertion_with_salt(&cw, &salt)
}
Exif::LABEL => {
let exif: Exif = manifest_assertion.to_assertion()?;
claim.add_assertion_with_salt(&exif, &salt)
}
_ => match manifest_assertion.kind() {
ManifestAssertionKind::Cbor => claim.add_assertion_with_salt(
&UserCbor::new(
manifest_assertion.label(),
serde_cbor::to_vec(&manifest_assertion.value()?)?,
),
&salt,
),
ManifestAssertionKind::Json => claim.add_assertion_with_salt(
&User::new(
manifest_assertion.label(),
&serde_json::to_string(&manifest_assertion.value()?)?,
),
&salt,
),
ManifestAssertionKind::Binary => {
return Err(Error::AssertionEncoding);
}
ManifestAssertionKind::Uri => {
return Err(Error::AssertionEncoding);
}
},
}?;
}
Ok(claim)
}
pub(crate) fn to_store(&self) -> Result<Store> {
let claim = self.to_claim()?;
let mut store = Store::new();
let _provenance = store.commit_claim(claim)?;
Ok(store)
}
#[cfg(feature = "file_io")]
fn embed_prep<P: AsRef<Path>>(&mut self, source_path: P, dest_path: P) -> Result<P> {
let mut copied = false;
if !source_path.as_ref().exists() {
let path = source_path.as_ref().to_string_lossy().into_owned();
return Err(Error::FileNotFound(path));
}
if !dest_path.as_ref().exists() {
if let Some(output_dir) = dest_path.as_ref().parent() {
create_dir_all(output_dir)?;
}
std::fs::copy(&source_path, &dest_path)?;
copied = true;
}
self.set_asset_from_path(dest_path.as_ref())?;
if copied {
Ok(dest_path)
} else {
Ok(source_path)
}
}
#[cfg(feature = "file_io")]
pub fn embed<P: AsRef<Path>>(
&mut self,
source_path: P,
dest_path: P,
signer: &dyn Signer,
) -> Result<Vec<u8>> {
let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?;
let mut store = self.to_store()?;
store.save_to_asset(source_path.as_ref(), signer, dest_path.as_ref())
}
#[async_generic(async_signature(
&mut self,
format: &str,
asset: &[u8],
signer: &dyn AsyncSigner,
))]
pub fn embed_from_memory(
&mut self,
format: &str,
asset: &[u8],
signer: &dyn Signer,
) -> Result<Vec<u8>> {
let asset = asset.to_vec();
let mut stream = std::io::Cursor::new(asset);
let mut output_stream = Cursor::new(Vec::new());
if _sync {
self.embed_to_stream(format, &mut stream, &mut output_stream, signer)?;
} else {
self.embed_to_stream_async(format, &mut stream, &mut output_stream, signer)
.await?;
}
Ok(output_stream.into_inner())
}
#[deprecated(since = "0.27.2", note = "use embed_to_stream instead")]
pub fn embed_stream(
&mut self,
format: &str,
stream: &mut dyn CAIRead,
signer: &dyn Signer,
) -> Result<Vec<u8>> {
let output_vec: Vec<u8> = Vec::new();
let mut output_stream = Cursor::new(output_vec);
self.embed_to_stream(format, stream, &mut output_stream, signer)?;
Ok(output_stream.into_inner())
}
#[async_generic(async_signature(
&mut self,
format: &str,
source: &mut dyn CAIRead,
dest: &mut dyn CAIReadWrite,
signer: &dyn AsyncSigner,
))]
pub fn embed_to_stream(
&mut self,
format: &str,
source: &mut dyn CAIRead,
dest: &mut dyn CAIReadWrite,
signer: &dyn Signer,
) -> Result<Vec<u8>> {
self.set_format(format);
self.set_instance_id(format!("xmp:iid:{}", Uuid::new_v4()));
#[cfg(feature = "add_thumbnails")]
{
if self.thumbnail_ref().is_none() {
if let Ok((format, image)) =
crate::utils::thumbnail::make_thumbnail_from_stream(format, source)
{
self.set_thumbnail(format, image)?;
}
}
}
let mut store = self.to_store()?;
if _sync {
store.save_to_stream(format, source, dest, signer)
} else {
store
.save_to_stream_async(format, source, dest, signer)
.await
}
}
pub async fn embed_from_memory_remote_signed(
&mut self,
format: &str,
asset: &[u8],
signer: &dyn RemoteSigner,
) -> Result<(Vec<u8>, Vec<u8>)> {
self.set_format(format);
self.set_instance_id(format!("xmp:iid:{}", Uuid::new_v4()));
#[allow(unused_mut)] let mut stream = std::io::Cursor::new(asset);
#[cfg(feature = "add_thumbnails")]
{
if self.thumbnail_ref().is_none() {
if let Ok((format, image)) =
crate::utils::thumbnail::make_thumbnail_from_stream(format, &mut stream)
{
self.set_thumbnail(format, image)?;
}
}
}
let asset = stream.into_inner();
let mut store = self.to_store()?;
let (output_asset, output_manifest) = store
.save_to_memory_remote_signed(format, asset, signer)
.await?;
Ok((output_asset, output_manifest))
}
#[cfg(feature = "file_io")]
pub async fn embed_async_signed<P: AsRef<Path>>(
&mut self,
source_path: P,
dest_path: P,
signer: &dyn AsyncSigner,
) -> Result<Vec<u8>> {
let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?;
let mut store = self.to_store()?;
store
.save_to_asset_async(source_path.as_ref(), signer, dest_path.as_ref())
.await
}
#[cfg(feature = "file_io")]
pub async fn embed_remote_signed<P: AsRef<Path>>(
&mut self,
source_path: P,
dest_path: P,
signer: &dyn RemoteSigner,
) -> Result<Vec<u8>> {
let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?;
let mut store = self.to_store()?;
store
.save_to_asset_remote_signed(source_path.as_ref(), signer, dest_path.as_ref())
.await
}
#[cfg(feature = "file_io")]
pub fn remove_manifest<P: AsRef<Path>>(asset_path: P) -> Result<()> {
use crate::jumbf_io::remove_jumbf_from_file;
remove_jumbf_from_file(asset_path.as_ref())
}
pub fn data_hash_placeholder(&mut self, reserve_size: usize, format: &str) -> Result<Vec<u8>> {
let dh: Result<DataHash> = self.find_assertion(DataHash::LABEL);
if dh.is_err() {
let mut ph = DataHash::new("jumbf manifest", "sha256");
for _ in 0..10 {
ph.add_exclusion(HashRange::new(0, 2));
}
self.add_assertion(&ph)?;
}
let mut store = self.to_store()?;
let placeholder = store.get_data_hashed_manifest_placeholder(reserve_size, format)?;
Ok(placeholder)
}
#[async_generic(async_signature(
&mut self,
dh: &DataHash,
signer: &dyn AsyncSigner,
format: &str,
mut asset_reader: Option<&mut dyn CAIRead>,
))]
pub fn data_hash_embeddable_manifest(
&mut self,
dh: &DataHash,
signer: &dyn Signer,
format: &str,
mut asset_reader: Option<&mut dyn CAIRead>,
) -> Result<Vec<u8>> {
let mut store = self.to_store()?;
if let Some(asset_reader) = asset_reader.as_deref_mut() {
asset_reader.rewind()?;
}
if _sync {
store.get_data_hashed_embeddable_manifest(dh, signer, format, asset_reader)
} else {
store
.get_data_hashed_embeddable_manifest_async(dh, signer, format, asset_reader)
.await
}
}
pub async fn data_hash_embeddable_manifest_remote(
&mut self,
dh: &DataHash,
signer: &dyn RemoteSigner,
format: &str,
mut asset_reader: Option<&mut dyn CAIRead>,
) -> Result<Vec<u8>> {
let mut store = self.to_store()?;
if let Some(asset_reader) = asset_reader.as_deref_mut() {
asset_reader.rewind()?;
}
store
.get_data_hashed_embeddable_manifest_remote(dh, signer, format, asset_reader)
.await
}
#[async_generic(async_signature(
&mut self,
signer: &dyn AsyncSigner,
format: Option<&str>,
))]
pub fn box_hash_embeddable_manifest(
&mut self,
signer: &dyn Signer,
format: Option<&str>,
) -> Result<Vec<u8>> {
let mut store = self.to_store()?;
let mut cm = if _sync {
store.get_box_hashed_embeddable_manifest(signer)
} else {
store.get_box_hashed_embeddable_manifest_async(signer).await
}?;
if let Some(format) = format {
cm = Store::get_composed_manifest(&cm, format)?;
}
Ok(cm)
}
pub fn composed_manifest(manifest_bytes: &[u8], format: &str) -> Result<Vec<u8>> {
Store::get_composed_manifest(manifest_bytes, format)
}
}
impl std::fmt::Display for Manifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let json = serde_json::to_string_pretty(self).unwrap_or_default();
f.write_str(&json)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct SignatureInfo {
#[serde(skip_serializing_if = "Option::is_none")]
alg: Option<SigningAlg>,
#[serde(skip_serializing_if = "Option::is_none")]
issuer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
cert_serial_number: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
time: Option<String>,
#[serde(skip)] cert_chain: String,
#[serde(skip_serializing_if = "Option::is_none")]
revocation_status: Option<bool>,
}
impl SignatureInfo {
pub fn cert_chain(&self) -> &str {
&self.cert_chain
}
}
#[cfg(test)]
pub(crate) mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use std::io::Cursor;
#[cfg(feature = "file_io")]
use tempfile::tempdir;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
use crate::{
assertions::{c2pa_action, Action, Actions},
ingredient::Ingredient,
reader::Reader,
utils::test::{temp_remote_signer, temp_signer, TEST_VC},
Manifest, Result,
};
#[cfg(feature = "file_io")]
use crate::{
assertions::{labels::ACTIONS, DataHash},
error::Error,
hash_utils::HashRange,
resource_store::ResourceRef,
status_tracker::{DetailedStatusTracker, StatusTracker},
store::Store,
utils::test::{
fixture_path, temp_dir_path, temp_fixture_path, write_jpeg_placeholder_file,
TEST_SMALL_JPEG,
},
validation_status,
};
#[derive(serde::Serialize)]
#[allow(dead_code)] struct MyStruct {
l1: String,
l2: u32,
}
fn test_manifest() -> Manifest {
Manifest::new("test".to_owned())
}
#[test]
#[cfg(feature = "file_io")]
fn from_file() {
let mut manifest = test_manifest();
let source_path = fixture_path(TEST_SMALL_JPEG);
manifest
.set_vendor("vendor".to_owned())
.set_parent(Ingredient::from_file(&source_path).expect("from_file"))
.expect("set_parent");
let vc: serde_json::Value = serde_json::from_str(TEST_VC).unwrap();
manifest
.add_verifiable_credential(&vc)
.expect("verifiable_credential");
manifest
.add_labeled_assertion(
"my.assertion",
&MyStruct {
l1: "some data".to_owned(),
l2: 5,
},
)
.expect("add_assertion");
let actions = Actions::new().add_action(
Action::new(c2pa_action::EDITED)
.set_parameter("name".to_owned(), "gaussian_blur")
.unwrap(),
);
manifest.add_assertion(&actions).expect("add_assertion");
manifest.add_ingredient(Ingredient::from_file(&source_path).expect("from_file"));
let mut json = serde_json::to_string_pretty(&manifest).expect("error to json");
while let Some(index) = json.find("\"thumbnail\": [") {
if let Some(idx2) = json[index..].find(']') {
json = format!(
"{}\"thumbnail\": \"<omitted>\"{}",
&json[..index],
&json[index + idx2 + 1..]
);
}
}
let dir = tempdir().expect("temp dir");
let test_output = dir.path().join("wc_embed_test.jpg");
let signer = temp_signer();
let _store = manifest
.embed(&source_path, &test_output, signer.as_ref())
.expect("embed");
assert_eq!(manifest.format(), "image/jpeg");
assert_eq!(manifest.title(), Some("wc_embed_test.jpg"));
if cfg!(feature = "add_thumbnails") {
assert!(manifest.thumbnail().is_some());
} else {
assert!(manifest.thumbnail().is_none());
}
let ingredient = Ingredient::from_file(&test_output).expect("load_from_asset");
assert!(ingredient.active_manifest().is_some());
}
#[test]
#[cfg(feature = "file_io")]
fn ws_bad_assertion() {
let ap = fixture_path(TEST_SMALL_JPEG);
let temp_dir = tempdir().expect("temp dir");
let test_output = temp_dir_path(&temp_dir, "ws_bad_assertion.jpg");
std::fs::copy(ap, test_output).expect("copy");
let mut manifest = test_manifest();
manifest
.add_labeled_assertion(
"c2pa.actions",
&MyStruct {
l1: "some data".to_owned(),
l2: 5,
},
)
.expect("add_assertion");
let result = manifest.to_store();
println!("{result:?}");
assert!(result.is_err())
}
#[test]
#[cfg(feature = "file_io")]
fn ws_valid_labeled_assertion() {
let ap = fixture_path(TEST_SMALL_JPEG);
let temp_dir = tempdir().expect("temp dir");
let test_output = temp_dir_path(&temp_dir, "ws_bad_assertion.jpg");
std::fs::copy(ap, test_output).expect("copy");
let mut manifest = test_manifest();
manifest
.add_labeled_assertion(
"c2pa.actions",
&serde_json::json!({
"actions": [
{
"action": "c2pa.edited",
"parameters": {
"description": "gradient",
"name": "any value"
},
"softwareAgent": "TestApp"
},
{
"action": "c2pa.dubbed",
"changes": [
{
"description": "translated to klingon",
"region": [
{
"type": "temporal",
"time": {}
},
{
"type": "identified",
"item": {
"identifier": "https://bioportal.bioontology.org/ontologies/FMA",
"value": "lips"
}
}
]
}
]
}
]
}),
)
.expect("add_assertion");
let store = manifest.to_store().expect("valid action to_store");
let m2 = Manifest::from_store(&store, &store.provenance_label().unwrap(), None)
.expect("from_store");
let actions: Actions = m2
.find_assertion("c2pa.actions.v2")
.expect("find_assertion");
assert_eq!(actions.actions()[0].action(), "c2pa.edited");
assert_eq!(actions.actions()[1].action(), "c2pa.dubbed");
}
#[test]
fn test_verifiable_credential() {
let mut manifest = test_manifest();
let vc: serde_json::Value = serde_json::from_str(TEST_VC).unwrap();
manifest
.add_verifiable_credential(&vc)
.expect("verifiable_credential");
let store = manifest.to_store().expect("to_store");
let claim = store.provenance_claim().unwrap();
assert!(!claim.get_verifiable_credentials().is_empty());
}
#[test]
fn test_assertion_user_cbor() {
use crate::{assertions::UserCbor, Manifest};
const LABEL: &str = "org.cai.test";
const DATA: &str = r#"{ "l1":"some data", "l2":"some other data" }"#;
let json: serde_json::Value = serde_json::from_str(DATA).unwrap();
let data = serde_cbor::to_vec(&json).unwrap();
let cbor = UserCbor::new(LABEL, data);
let mut manifest = test_manifest();
manifest.add_assertion(&cbor).expect("add_assertion");
manifest.add_assertion(&cbor).expect("add_assertion");
let store = manifest.to_store().expect("to_store");
let _manifest2 = Manifest::from_store(
&store,
&store.provenance_label().unwrap(),
#[cfg(feature = "file_io")]
None,
)
.expect("from_store");
println!("{store}");
println!("{_manifest2:?}");
let cbor2: UserCbor = manifest.find_assertion(LABEL).expect("get_assertion");
assert_eq!(cbor, cbor2);
}
#[test]
#[cfg(feature = "file_io")]
fn test_redaction() {
const ASSERTION_LABEL: &str = "stds.schema-org.CreativeWork";
let temp_dir = tempdir().expect("temp dir");
let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let output2 = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let mut manifest = test_manifest();
manifest
.add_labeled_assertion(
ASSERTION_LABEL,
&serde_json::json! (
{
"@context": "https://schema.org",
"@type": "CreativeWork",
"author": [
{
"@type": "Person",
"name": "Joe Bloggs"
},
]
}),
)
.expect("add_assertion");
let signer = temp_signer();
let c2pa_data = manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
let mut validation_log = DetailedStatusTracker::new();
let store1 = Store::load_from_memory("c2pa", &c2pa_data, true, &mut validation_log)
.expect("load from memory");
let claim1_label = store1.provenance_label().unwrap();
let claim = store1.provenance_claim().unwrap();
assert!(claim.get_claim_assertion(ASSERTION_LABEL, 0).is_some());
let mut manifest2 = test_manifest();
manifest2
.set_parent(Ingredient::from_file(&output).expect("from_file"))
.expect("set_parent");
manifest2
.add_redaction(ASSERTION_LABEL)
.expect("add_redaction");
let signer = temp_signer();
let _store2 = manifest2
.embed(&output2, &output2, signer.as_ref())
.expect("embed");
let mut report = DetailedStatusTracker::new();
let store3 = Store::load_from_asset(&output2, true, &mut report).unwrap();
let claim2 = store3.provenance_claim().unwrap();
assert!(claim2.redactions().is_some());
assert!(!claim2.redactions().unwrap().is_empty());
assert!(!report.get_log().is_empty());
let redacted_uri = &claim2.redactions().unwrap()[0];
let claim1 = store3.get_claim(&claim1_label).unwrap();
assert!(claim1.get_claim_assertion(redacted_uri, 0).is_none());
}
#[test]
#[cfg(feature = "file_io")]
fn test_action_assertion_redaction_error() {
let temp_dir = tempdir().expect("temp dir");
let parent_output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let mut parent_manifest = test_manifest();
let actions = Actions::new().add_action(
Action::new(c2pa_action::FILTERED)
.set_parameter("name".to_owned(), "gaussian blur")
.unwrap()
.set_when("2015-06-26T16:43:23+0200"),
);
parent_manifest
.add_assertion(&actions)
.expect("add_assertion");
let signer = temp_signer();
parent_manifest
.embed(&parent_output, &parent_output, signer.as_ref())
.expect("embed");
let mut manifest = test_manifest();
manifest
.set_parent(Ingredient::from_file(&parent_output).expect("from_file"))
.expect("set_parent");
assert!(manifest.add_redaction(ACTIONS).is_ok());
let redact_output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let embed_result = manifest.embed(&redact_output, &redact_output, signer.as_ref());
assert!(matches!(
embed_result.err().unwrap(),
Error::AssertionInvalidRedaction
));
}
#[test]
fn manifest_assertion_instances() {
let mut manifest = Manifest::new("test".to_owned());
let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED));
manifest.add_assertion(&actions).expect("add_assertion");
manifest.add_assertion(&actions).expect("add_assertion");
manifest.add_assertion(&actions).expect("add_assertion");
let store = manifest.to_store().expect("to_store");
println!("{store}");
let active_label = store.provenance_label().unwrap();
let manifest2 = Manifest::from_store(
&store,
&active_label,
#[cfg(feature = "file_io")]
None,
)
.expect("from_store");
println!("{manifest2}");
let action2: Result<Actions> = manifest2.find_assertion_with_instance(Actions::LABEL, 2);
assert!(action2.is_ok());
assert_eq!(action2.unwrap().actions()[0].action(), c2pa_action::EDITED);
}
#[cfg(all(feature = "file_io", feature = "openssl_sign"))]
#[actix::test]
async fn test_embed_async_sign() {
let temp_dir = tempdir().expect("temp dir");
let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let async_signer =
crate::openssl::temp_signer_async::AsyncSignerAdapter::new(crate::SigningAlg::Ps256);
let mut manifest = test_manifest();
manifest
.embed_async_signed(&output, &output, &async_signer)
.await
.expect("embed");
let reader = Reader::from_file(&output).expect("from_file");
assert_eq!(
reader.active_manifest().unwrap().title().unwrap(),
TEST_SMALL_JPEG
);
}
#[cfg(all(feature = "file_io", feature = "openssl_sign"))]
#[actix::test]
async fn test_embed_remote_sign() {
let temp_dir = tempdir().expect("temp dir");
let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let remote_signer = temp_remote_signer();
let mut manifest = test_manifest();
manifest
.embed_remote_signed(&output, &output, remote_signer.as_ref())
.await
.expect("embed");
let manifest_store = Reader::from_file(&output).expect("from_file");
assert_eq!(
manifest_store.active_manifest().unwrap().title().unwrap(),
TEST_SMALL_JPEG
);
}
#[cfg(feature = "file_io")]
#[test]
fn test_embed_user_label() {
let temp_dir = tempdir().expect("temp dir");
let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let signer = temp_signer();
let mut manifest = test_manifest();
manifest.set_label("MyLabel");
manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
let reader = Reader::from_file(&output).expect("from_file");
assert_eq!(
reader.active_manifest().unwrap().title().unwrap(),
TEST_SMALL_JPEG
);
}
#[cfg(feature = "file_io")]
#[test]
fn test_embed_sidecar_user_label() {
let temp_dir = tempdir().expect("temp dir");
let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let sidecar = output.with_extension("c2pa");
let fp = format!("file:/{}", sidecar.to_str().unwrap());
let url = url::Url::parse(&fp).unwrap();
let signer = temp_signer();
let mut manifest = test_manifest();
manifest.set_label("MyLabel");
manifest.set_remote_manifest(url);
let c2pa_data = manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
let manifest_store =
Reader::from_stream("application/c2pa", Cursor::new(c2pa_data)).expect("from_bytes");
assert_eq!(
manifest_store.active_manifest().unwrap().title().unwrap(),
TEST_SMALL_JPEG
);
}
#[cfg_attr(not(target_arch = "wasm32"), actix::test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
async fn test_embed_jpeg_stream_wasm() {
use crate::assertions::User;
let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg");
let mut manifest = Manifest::new("my_app".to_owned());
manifest.set_title("EmbedStream");
manifest
.add_assertion(&User::new(
"org.contentauth.mylabel",
r#"{"my_tag":"Anything I want"}"#,
))
.unwrap();
let mut ingredient = Ingredient::from_memory_async("jpeg", image)
.await
.expect("from_stream_async");
ingredient.set_title("parent.jpg");
manifest.set_parent(ingredient).expect("set_parent");
let signer = temp_remote_signer();
let (out_vec, _out_manifest) = manifest
.embed_from_memory_remote_signed("jpeg", image, signer.as_ref())
.await
.expect("embed_stream");
let manifest_store = Reader::from_stream("image/jpeg", Cursor::new(out_vec)).unwrap();
println!("It worked: {manifest_store}\n");
}
#[cfg_attr(not(target_arch = "wasm32"), actix::test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
async fn test_embed_png_stream_wasm() {
use crate::assertions::User;
let image = include_bytes!("../tests/fixtures/libpng-test.png");
let mut manifest = Manifest::new("my_app".to_owned());
manifest.set_title("EmbedStream");
manifest
.add_assertion(&User::new(
"org.contentauth.mylabel",
r#"{"my_tag":"Anything I want"}"#,
))
.unwrap();
let signer = temp_remote_signer();
let (out_vec, _out_manifest) = manifest
.embed_from_memory_remote_signed("png", image, signer.as_ref())
.await
.expect("embed_stream");
let manifest_store = Reader::from_stream("image/png", Cursor::new(out_vec)).unwrap();
println!("It worked: {manifest_store}\n");
}
#[cfg_attr(not(target_arch = "wasm32"), actix::test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
async fn test_embed_webp_stream_wasm() {
use crate::assertions::User;
let image = include_bytes!("../tests/fixtures/mars.webp");
let mut manifest = Manifest::new("my_app".to_owned());
manifest.set_title("EmbedStream");
manifest
.add_assertion(&User::new(
"org.contentauth.mylabel",
r#"{"my_tag":"Anything I want"}"#,
))
.unwrap();
let signer = temp_remote_signer();
let (out_vec, _out_manifest) = manifest
.embed_from_memory_remote_signed("image/webp", image, signer.as_ref())
.await
.expect("embed_stream");
let manifest_store = Reader::from_stream("image/webp", Cursor::new(out_vec)).unwrap();
println!("It worked: {manifest_store}\n");
}
#[test]
fn test_embed_stream() {
use crate::assertions::User;
let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg");
let mut stream = std::io::Cursor::new(image.to_vec());
let mut manifest = Manifest::new("my_app".to_owned());
manifest.set_title("EmbedStream");
manifest
.add_assertion(&User::new(
"org.contentauth.mylabel",
r#"{"my_tag":"Anything I want"}"#,
))
.unwrap();
let signer = temp_signer();
let mut output = Cursor::new(Vec::new());
manifest
.embed_to_stream("jpeg", &mut stream, &mut output, signer.as_ref())
.expect("embed_stream");
stream.set_position(0);
let reader = Reader::from_stream("jpeg", &mut output).expect("from_bytes");
assert_eq!(
reader.active_manifest().unwrap().title().unwrap(),
"EmbedStream"
);
#[cfg(feature = "add_thumbnails")]
assert!(reader.active_manifest().unwrap().thumbnail().is_some());
}
#[cfg(any(target_arch = "wasm32", feature = "openssl_sign"))]
#[cfg_attr(feature = "openssl_sign", actix::test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
async fn test_embed_from_memory_async() {
use crate::{assertions::User, utils::test::temp_async_signer};
let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg");
let mut stream = std::io::Cursor::new(image.to_vec());
let mut manifest = Manifest::new("my_app".to_owned());
manifest.set_title("EmbedStream");
manifest
.add_assertion(&User::new(
"org.contentauth.mylabel",
r#"{"my_tag":"Anything I want"}"#,
))
.unwrap();
let signer = temp_async_signer();
let mut output = Cursor::new(Vec::new());
manifest
.embed_to_stream_async("jpeg", &mut stream, &mut output, signer.as_ref())
.await
.expect("embed_stream");
let manifest_store = crate::ManifestStore::from_bytes("jpeg", &output.into_inner(), true)
.expect("from_bytes");
assert_eq!(
manifest_store.get_active().unwrap().title().unwrap(),
"EmbedStream"
);
#[cfg(feature = "add_thumbnails")]
assert!(manifest_store.get_active().unwrap().thumbnail().is_some());
}
#[cfg(feature = "file_io")]
#[actix::test]
async fn test_embed_with_ingredient_error() {
let temp_dir = tempdir().expect("temp dir");
let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let signer = temp_signer();
let mut manifest = test_manifest();
let ingredient =
Ingredient::from_file(fixture_path("XCA.jpg")).expect("getting ingredient");
assert!(ingredient.validation_status().is_some());
assert_eq!(
ingredient.validation_status().unwrap()[0].code(),
validation_status::ASSERTION_DATAHASH_MISMATCH
);
manifest.add_ingredient(ingredient);
manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
let manifest_store = Reader::from_file(&output).expect("from_file");
println!("{manifest_store}");
let manifest = manifest_store.active_manifest().unwrap();
let ingredient_status = manifest.ingredients()[0].validation_status();
assert_eq!(
ingredient_status.unwrap()[0].code(),
validation_status::ASSERTION_DATAHASH_MISMATCH
);
assert_eq!(manifest.title().unwrap(), TEST_SMALL_JPEG);
assert!(manifest_store.validation_status().is_none())
}
#[cfg(feature = "file_io")]
#[test]
fn test_embed_sidecar_with_parent_manifest() {
let temp_dir = tempdir().expect("temp dir");
let source = fixture_path("XCA.jpg");
let output = temp_dir.path().join("XCAplus.jpg");
let sidecar = output.with_extension("c2pa");
let fp = format!("file:/{}", sidecar.to_str().unwrap());
let url = url::Url::parse(&fp).unwrap();
let signer = temp_signer();
let parent = Ingredient::from_file(fixture_path("XCA.jpg")).expect("getting parent");
let mut manifest = test_manifest();
manifest.set_parent(parent).expect("setting parent");
manifest.set_remote_manifest(url.clone());
let _c2pa_data = manifest
.embed(&source, &output, signer.as_ref())
.expect("embed");
assert_eq!(manifest.remote_manifest_url().unwrap(), url.to_string());
let manifest_store = Reader::from_file(&output).expect("from_file");
assert_eq!(
manifest_store.active_manifest().unwrap().title().unwrap(),
"XCAplus.jpg"
);
}
#[cfg(feature = "file_io")]
#[test]
fn test_embed_user_thumbnail() {
let temp_dir = tempdir().expect("temp dir");
let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let signer = temp_signer();
let mut manifest = test_manifest();
let thumb_data = vec![1, 2, 3];
manifest
.set_thumbnail("image/jpeg", thumb_data.clone())
.expect("set_thumbnail");
manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
let manifest_store = Reader::from_file(&output).expect("from_file");
let active_manifest = manifest_store.active_manifest().unwrap();
let (format, image) = active_manifest.thumbnail().unwrap();
assert_eq!(format, "image/jpeg");
assert_eq!(image.into_owned(), thumb_data);
}
#[cfg(feature = "file_io")]
const MANIFEST_JSON: &str = r#"{
"claim_generator": "test",
"claim_generator_info": [
{
"name": "test",
"version": "1.0",
"icon": {
"format": "image/svg+xml",
"identifier": "sample1.svg"
}
}
],
"format" : "image/jpeg",
"thumbnail": {
"format": "image/jpeg",
"identifier": "IMG_0003.jpg"
},
"assertions": [
{
"label": "c2pa.actions.v2",
"data": {
"actions": [
{
"action": "c2pa.opened",
"instanceId": "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
"parameters": {
"description": "import"
},
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia",
"softwareAgent": {
"name": "TestApp",
"version": "1.0",
"icon": {
"format": "image/svg+xml",
"identifier": "sample1.svg"
},
"something": "else"
},
"changes": [
{
"region" : [
{
"type" : "temporal",
"time" : {}
},
{
"type" : "identified",
"item" : {
"identifier" : "https://bioportal.bioontology.org/ontologies/FMA",
"value" : "lips"
}
}
],
"description": "lip synced area"
}
]
}
],
"templates": [
{
"action": "c2pa.opened",
"softwareAgent": {
"name": "TestApp",
"version": "1.0",
"icon": {
"format": "image/svg+xml",
"identifier": "sample1.svg"
},
"something": "else"
},
"icon": {
"format": "image/svg+xml",
"identifier": "sample1.svg"
}
}
]
}
}
],
"ingredients": [{
"title": "A.jpg",
"format": "image/jpeg",
"document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
"relationship": "parentOf",
"thumbnail": {
"format": "image/png",
"identifier": "exp-test1.png"
}
},
{
"title": "prompt",
"format": "text/plain",
"relationship": "inputTo",
"data": {
"format": "text/plain",
"identifier": "prompt.txt",
"data_types": [
{
"type": "c2pa.types.generator.prompt"
}
]
}
}
]
}"#;
#[test]
#[cfg(feature = "openssl_sign")]
fn from_json_with_stream() {
use crate::assertions::Relationship;
let mut manifest = Manifest::from_json(MANIFEST_JSON).unwrap();
manifest
.resources_mut()
.add("IMG_0003.jpg", *b"my value")
.unwrap()
.add("sample1.svg", *b"my value")
.expect("add resource");
manifest.ingredients_mut()[0]
.resources_mut()
.add("exp-test1.png", *b"my value")
.expect("add_resource");
manifest.ingredients_mut()[1]
.resources_mut()
.add("prompt.txt", *b"pirate with bird on shoulder")
.expect("add_resource");
println!("{manifest}");
let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg");
let mut input = std::io::Cursor::new(image.to_vec());
let signer = temp_signer();
let mut output = Cursor::new(Vec::new());
manifest
.embed_to_stream("jpeg", &mut input, &mut output, signer.as_ref())
.expect("embed_stream");
output.set_position(0);
let reader = Reader::from_stream("jpeg", &mut output).expect("from_bytes");
println!("manifest_store = {reader}");
let m = reader.active_manifest().unwrap();
assert!(m.thumbnail().is_some());
let (format, image) = m.thumbnail().unwrap();
assert_eq!(format, "image/jpeg");
assert_eq!(image.to_vec(), b"my value");
assert_eq!(m.ingredients().len(), 2);
assert_eq!(m.ingredients()[1].relationship(), &Relationship::InputTo);
assert!(m.ingredients()[1].data_ref().is_some());
assert_eq!(m.ingredients()[1].data_ref().unwrap().format, "text/plain");
let id = m.ingredients()[1].data_ref().unwrap().identifier.as_str();
assert_eq!(
m.ingredients()[1].resources().get(id).unwrap().into_owned(),
b"pirate with bird on shoulder"
);
}
#[test]
#[cfg(feature = "openssl_sign")]
fn from_json_with_memory() {
use crate::assertions::Relationship;
let mut manifest = Manifest::from_json(MANIFEST_JSON).unwrap();
manifest
.resources_mut()
.add("IMG_0003.jpg", *b"my value")
.unwrap()
.add("sample1.svg", *b"my value")
.expect("add resource");
manifest.ingredients_mut()[0]
.resources_mut()
.add("exp-test1.png", *b"my value")
.expect("add_resource");
manifest.ingredients_mut()[1]
.resources_mut()
.add("prompt.txt", *b"pirate with bird on shoulder")
.expect("add_resource");
println!("{manifest}");
let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg");
let signer = temp_signer();
let output_image = manifest
.embed_from_memory("jpeg", image, signer.as_ref())
.expect("embed_stream");
let reader = Reader::from_stream("jpeg", Cursor::new(output_image)).expect("from_bytes");
println!("manifest_store = {reader}");
let m = reader.active_manifest().unwrap();
assert!(m.thumbnail().is_some());
let (format, image) = m.thumbnail().unwrap();
assert_eq!(format, "image/jpeg");
assert_eq!(image.to_vec(), b"my value");
assert_eq!(m.ingredients().len(), 2);
assert_eq!(m.ingredients()[1].relationship(), &Relationship::InputTo);
assert!(m.ingredients()[1].data_ref().is_some());
assert_eq!(m.ingredients()[1].data_ref().unwrap().format, "text/plain");
let id = m.ingredients()[1].data_ref().unwrap().identifier.as_str();
assert_eq!(
m.ingredients()[1].resources().get(id).unwrap().into_owned(),
b"pirate with bird on shoulder"
);
}
#[test]
#[cfg(feature = "file_io")]
fn from_json_with_files() {
let mut manifest = Manifest::from_json(MANIFEST_JSON).unwrap();
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("tests/fixtures"); manifest.with_base_path(path).expect("with_files");
let store = manifest.to_store().expect("to store");
let mut resource_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
resource_path.push("../target/tmp/manifest");
let m2 = Manifest::from_store(
&store,
&store.provenance_label().unwrap(),
Some(&resource_path),
)
.expect("from store");
println!("{m2}");
assert!(m2.thumbnail().is_some());
assert!(m2.ingredients()[0].thumbnail().is_some());
}
#[cfg(feature = "file_io")]
#[test]
fn test_embed_from_json() {
let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
fixtures.push("tests/fixtures");
let temp_dir = tempdir().expect("temp dir");
let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let signer = temp_signer();
let mut manifest = Manifest::from_json(MANIFEST_JSON).expect("from_json");
manifest.with_base_path(fixtures).expect("with_base");
manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
let reader = Reader::from_file(&output).expect("from_file");
println!("{reader}");
let active_manifest = reader.active_manifest().unwrap();
let (format, _) = active_manifest.thumbnail().unwrap();
assert_eq!(format, "image/jpeg");
}
#[cfg(feature = "file_io")]
#[test]
fn test_embed_webp_from_json() {
use crate::utils::test::TEST_WEBP;
let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
fixtures.push("tests/fixtures");
let temp_dir = tempdir().expect("temp dir");
let output = temp_fixture_path(&temp_dir, TEST_WEBP);
let signer = temp_signer();
let mut manifest = Manifest::from_json(MANIFEST_JSON).expect("from_json");
manifest.with_base_path(fixtures).expect("with_base");
manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
let manifest_store = Reader::from_file(&output).expect("from_file");
println!("{manifest_store}");
let active_manifest = manifest_store.active_manifest().unwrap();
let (format, _) = active_manifest.thumbnail().unwrap();
assert_eq!(format, "image/jpeg");
}
#[test]
#[cfg(feature = "file_io")]
fn test_create_file_based_ingredient() {
let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
fixtures.push("tests/fixtures");
let temp_dir = tempdir().expect("temp dir");
let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let mut manifest = Manifest::new("claim_generator");
manifest.with_base_path(fixtures).expect("with_base");
assert!(manifest
.set_thumbnail_ref(ResourceRef::new("image/jpg", "foo"))
.is_err());
assert!(manifest.thumbnail_ref().is_none());
assert!(manifest
.set_thumbnail_ref(ResourceRef::new("image/jpeg", "C.jpg"))
.is_ok());
assert!(manifest.thumbnail_ref().is_some());
let signer = temp_signer();
manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
}
#[test]
#[cfg(all(feature = "file_io", feature = "add_thumbnails"))]
fn test_create_no_claim_thumbnail() {
let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
fixtures.push("tests/fixtures");
let temp_dir = tempdir().expect("temp dir");
let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
let mut manifest = Manifest::new("claim_generator");
assert!(manifest
.set_thumbnail_ref(ResourceRef::new("none", "none"))
.is_ok());
assert!(manifest.thumbnail_ref().is_some());
assert!(manifest.thumbnail().is_none());
let signer = temp_signer();
manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
let manifest_store = Reader::from_file(&output).expect("from_file");
println!("{manifest_store}");
let active_manifest = manifest_store.active_manifest().unwrap();
assert!(active_manifest.thumbnail_ref().is_none());
assert!(active_manifest.thumbnail().is_none());
}
#[test]
fn test_missing_thumbnail() {
const MANIFEST_JSON: &str = r#"
{
"claim_generator": "test",
"format" : "image/jpeg",
"thumbnail": {
"format": "image/jpeg",
"identifier": "does_not_exist.jpg"
}
}
"#;
let mut manifest = Manifest::from_json(MANIFEST_JSON).expect("from_json");
let mut source = std::io::Cursor::new(vec![1, 2, 3]);
let mut dest = std::io::Cursor::new(Vec::new());
let signer = temp_signer();
let result =
manifest.embed_to_stream("image/jpeg", &mut source, &mut dest, signer.as_ref());
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("resource not found: does_not_exist.jpg"));
}
#[test]
#[cfg(feature = "file_io")]
fn test_data_hash_embeddable_manifest() {
let ap = fixture_path("cloud.jpg");
let signer = temp_signer();
let mut manifest = Manifest::new("claim_generator");
let placeholder = manifest
.data_hash_placeholder(signer.reserve_size(), "jpeg")
.unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let output = temp_dir_path(&temp_dir, "boxhash-out.jpg");
let mut output_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&output)
.unwrap();
let offset =
write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, None).unwrap();
let exclusion = HashRange::new(offset, placeholder.len());
let exclusions = vec![exclusion];
let mut dh = DataHash::new("source_hash", "sha256");
dh.exclusions = Some(exclusions);
let signed_manifest = manifest
.data_hash_embeddable_manifest(
&dh,
signer.as_ref(),
"image/jpeg",
Some(&mut output_file),
)
.unwrap();
use std::io::{Seek, SeekFrom, Write};
output_file.seek(SeekFrom::Start(offset as u64)).unwrap();
output_file.write_all(&signed_manifest).unwrap();
let manifest_store = Reader::from_file(&output).expect("from_file");
println!("{manifest_store}");
assert!(manifest_store.validation_status().is_none());
}
#[cfg(all(feature = "file_io", feature = "openssl_sign"))]
#[actix::test]
async fn test_data_hash_embeddable_manifest_remote_signed() {
let ap = fixture_path("cloud.jpg");
let signer = temp_remote_signer();
let mut manifest = Manifest::new("claim_generator");
let placeholder = manifest
.data_hash_placeholder(signer.reserve_size(), "jpeg")
.unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let output = temp_dir_path(&temp_dir, "boxhash-out.jpg");
let mut output_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&output)
.unwrap();
let offset =
write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, None).unwrap();
let exclusion = HashRange::new(offset, placeholder.len());
let exclusions = vec![exclusion];
let mut dh = DataHash::new("source_hash", "sha256");
dh.exclusions = Some(exclusions);
let signed_manifest = manifest
.data_hash_embeddable_manifest_remote(
&dh,
signer.as_ref(),
"c2pa", Some(&mut output_file),
)
.await
.unwrap();
let signed_manifest =
Manifest::composed_manifest(&signed_manifest, "image/jpeg").expect("composed_manifest");
use std::io::{Seek, SeekFrom, Write};
output_file.seek(SeekFrom::Start(offset as u64)).unwrap();
output_file.write_all(&signed_manifest).unwrap();
let manifest_store = Reader::from_file(&output).expect("from_file");
println!("{manifest_store}");
assert!(manifest_store.validation_status().is_none());
}
#[test]
#[cfg(feature = "file_io")]
fn test_box_hash_embeddable_manifest() {
let asset_bytes = include_bytes!("../tests/fixtures/boxhash.jpg");
let box_hash_data = include_bytes!("../tests/fixtures/boxhash.json");
let box_hash: crate::assertions::BoxHash = serde_json::from_slice(box_hash_data).unwrap();
let mut manifest = Manifest::new("test_app".to_owned());
manifest.set_title("BoxHashTest").set_format("image/jpeg");
manifest
.add_labeled_assertion(crate::assertions::labels::BOX_HASH, &box_hash)
.unwrap();
let signer = temp_signer();
let embeddable = manifest
.box_hash_embeddable_manifest(signer.as_ref(), None)
.expect("embeddable_manifest");
let reader = Reader::from_manifest_data_and_stream(
&embeddable,
"image/jpeg",
Cursor::new(asset_bytes),
)
.unwrap();
println!("{reader}");
assert!(reader.active_manifest().is_some());
assert!(reader.validation_status().is_none());
}
}