use serde::{Deserialize, Serialize};
use crate::{
assertion::{Assertion, AssertionBase, AssertionCbor},
assertions::{labels, Metadata, ReviewRating},
error::Result,
hashed_uri::HashedUri,
validation_status::ValidationStatus,
};
const ASSERTION_CREATION_VERSION: usize = 1;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum Relationship {
#[serde(rename = "parentOf")]
ParentOf,
#[serde(rename = "componentOf")]
ComponentOf,
}
impl Default for Relationship {
fn default() -> Self {
Relationship::ComponentOf
}
}
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Ingredient {
#[serde(rename = "dc:title")]
pub title: String,
#[serde(rename = "dc:format")]
pub format: String,
#[serde(rename = "documentID", skip_serializing_if = "Option::is_none")]
pub document_id: Option<String>,
#[serde(rename = "instanceID")]
pub instance_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub c2pa_manifest: Option<HashedUri>,
#[serde(rename = "validationStatus", skip_serializing_if = "Option::is_none")]
pub validation_status: Option<Vec<ValidationStatus>>,
pub relationship: Relationship,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail: Option<HashedUri>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
}
impl Ingredient {
pub const LABEL: &'static str = labels::INGREDIENT;
pub fn new(title: &str, format: &str, instance_id: &str, document_id: Option<&str>) -> Self {
Self {
title: title.to_owned(),
format: format.to_owned(),
document_id: document_id.map(|id| id.to_owned()),
instance_id: instance_id.to_owned(),
c2pa_manifest: None,
validation_status: None,
relationship: Relationship::ComponentOf,
thumbnail: None,
metadata: None,
}
}
pub fn set_parent(mut self) -> Self {
self.relationship = Relationship::ParentOf;
self
}
pub fn set_c2pa_manifest_from_hashed_uri(mut self, provenance: Option<HashedUri>) -> Self {
self.c2pa_manifest = provenance;
self
}
pub fn set_thumbnail_hash_link(mut self, thumbnail: Option<&str>) -> Self {
self.thumbnail =
thumbnail.map(|thumb| HashedUri::new(thumb.to_owned(), None, "Hash".as_bytes()));
self
}
pub fn set_thumbnail(mut self, hashed_uri: Option<&HashedUri>) -> Self {
self.thumbnail = hashed_uri.map(|h| h.to_owned());
self
}
pub fn add_review(mut self, review: ReviewRating) -> Self {
let metadata = self.metadata.unwrap_or_else(Metadata::new);
self.metadata = Some(metadata.add_review(review));
self
}
pub fn add_reviews(mut self, reviews: Option<Vec<ReviewRating>>) -> Self {
if let Some(reviews) = reviews {
let metadata = Metadata::new().set_reviews(reviews);
self.metadata = Some(metadata);
};
self
}
pub fn add_validation_status(mut self, status: ValidationStatus) {
match &mut self.validation_status {
None => self.validation_status = Some(vec![status]),
Some(validation_status) => validation_status.push(status),
}
}
}
impl AssertionCbor for Ingredient {}
impl AssertionBase for Ingredient {
const LABEL: &'static str = Self::LABEL;
const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
fn to_assertion(&self) -> Result<Assertion> {
Self::to_cbor_assertion(self)
}
fn from_assertion(assertion: &Assertion) -> Result<Self> {
Self::from_cbor_assertion(assertion)
}
}
#[cfg(test)]
pub mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
use super::*;
use crate::assertion::{AssertionCbor, AssertionData};
#[test]
fn assertion_ingredient() {
let original = Ingredient::new(
"image 1.jpg",
"image/jpeg",
"xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c347"),
)
.set_thumbnail_hash_link(Some("#c2pa.ingredient.thumbnail.jpeg"));
let assertion = original.to_assertion().expect("build_assertion");
assert_eq!(assertion.mime_type(), "application/cbor");
assert_eq!(assertion.label(), Ingredient::LABEL);
let result = Ingredient::from_cbor_assertion(&assertion).expect("from_assertion");
assert_eq!(original.title, result.title);
assert_eq!(original.format, result.format);
assert_eq!(original.document_id, result.document_id);
assert_eq!(original.instance_id, result.instance_id);
assert_eq!(original.thumbnail, result.thumbnail);
}
#[test]
fn test_build_assertion() {
let assertion = Ingredient::new(
"image 1.jpg",
"image/jpeg",
"xmp.did:87d51599-286e-43b2-9478-88c79f49c347",
Some("xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d"),
)
.set_thumbnail_hash_link(Some("#c2pa.ingredient.thumbnail.jpeg"))
.to_assertion()
.unwrap();
println!("assertion label: {}", assertion.label());
let j = assertion.data();
let from_j = Assertion::from_data_cbor(&assertion.label(), j);
let ad_ref = from_j.decode_data();
if let AssertionData::Cbor(ref ad_cbor) = ad_ref {
let orig_d = assertion.decode_data();
if let AssertionData::Cbor(ref orig_cbor) = orig_d {
assert_eq!(orig_cbor, ad_cbor);
} else {
panic!("Couldn't decode orig_d");
}
} else {
panic!("Couldn't decode ad_ref");
}
}
#[test]
fn test_binary_round_trip() {
let assertion = Ingredient::new(
"image 1.jpg",
"image/jpeg",
"xmp.did:87d51599-286e-43b2-9478-88c79f49c347",
Some("xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d"),
)
.set_thumbnail_hash_link(Some("#c2pa.ingredient.thumbnail.jpeg"))
.to_assertion()
.unwrap();
let orig_bytes = assertion.data();
let assertion_from_binary = Assertion::from_data_cbor(&assertion.label(), orig_bytes);
println!(
"Label Match Test {} = {}",
assertion.label(),
assertion_from_binary.label()
);
assert_eq!(assertion.label(), assertion_from_binary.label());
assert_eq!(orig_bytes, assertion_from_binary.data());
println!("Decoded binary matches")
}
#[test]
fn test_assertion_with_reviews() {
let review = ReviewRating::new(
"a 3rd party plugin was used",
Some("actions.unknownActionsPerformed".to_string()),
1,
);
let original = Ingredient::new(
"image 1.jpg",
"image/jpeg",
"xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c347"),
)
.add_review(review);
let assertion = original.to_assertion().expect("build_assertion");
assert_eq!(assertion.mime_type(), "application/cbor");
assert_eq!(assertion.label(), Ingredient::LABEL);
let restored = Ingredient::from_cbor_assertion(&assertion).expect("from_assertion");
assert_eq!(original.title, restored.title);
assert_eq!(original.format, restored.format);
assert_eq!(original.document_id, restored.document_id);
assert_eq!(original.instance_id, restored.instance_id);
assert_eq!(original.thumbnail, restored.thumbnail);
assert!(restored.metadata.is_some());
let metadata = restored.metadata.unwrap();
let date_time = metadata.date_time().unwrap();
let date_time_parsed = chrono::DateTime::parse_from_rfc3339(date_time);
assert!(metadata.reviews().is_some());
assert!(date_time_parsed.is_ok());
let reviews = metadata.reviews().unwrap();
assert_eq!(reviews.len(), 1);
assert_eq!(
reviews[0].code.as_ref().unwrap(),
"actions.unknownActionsPerformed"
);
assert_eq!(reviews[0].explanation, "a 3rd party plugin was used");
assert_eq!(reviews[0].value, 1);
}
}