#![allow(clippy::unwrap_used)]
#[cfg(feature = "file_io")]
use std::path::Path;
use std::{
collections::HashMap,
io::{Cursor, Read, Write},
path::PathBuf,
sync::LazyLock,
};
use env_logger;
use tempfile::TempDir;
use crate::{
assertions::{
labels, Action, Actions, DigitalSourceType, EmbeddedData, Ingredient, Relationship,
ReviewRating, SchemaDotOrg, Thumbnail, User,
},
asset_io::CAIReadWrite,
claim::Claim,
context::Context,
crypto::{cose::CertificateTrustPolicy, raw_signature::SigningAlg},
hash_utils::Hasher,
jumbf_io::get_assetio_handler,
resource_store::UriOrResource,
store::Store,
utils::{io_utils::tempdirectory, mime::extension_to_mime},
AsyncSigner, ClaimGeneratorInfo, Result,
};
pub const TEST_SMALL_JPEG: &str = "earth_apollo17.jpg";
pub const TEST_WEBP: &str = "mars.webp";
pub const TEST_USER_ASSERTION: &str = "test_label";
pub const MANIFEST_STORE_EXT: &str = "c2pa";
pub const TEST_VC: &str = r#"{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"http://schema.org"
],
"type": [
"VerifiableCredential",
"NPPACredential"
],
"issuer": "https://nppa.org/",
"credentialSubject": {
"id": "did:nppa:eb1bb9934d9896a374c384521410c7f14",
"name": "Bob Ross",
"memberOf": "https://nppa.org/"
},
"proof": {
"type": "RsaSignature2018",
"created": "2021-06-18T21:19:10Z",
"proofPurpose": "assertionMethod",
"verificationMethod":
"did:nppa:eb1bb9934d9896a374c384521410c7f14#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A",
"jws": "eyJhbGciOiJQUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19DJBMvvFAIC00nSGB6Tn0XKbbF9XrsaJZREWvR2aONYTQQxnyXirtXnlewJMBBn2h9hfcGZrvnC1b6PgWmukzFJ1IiH1dWgnDIS81BH-IxXnPkbuYDeySorc4QU9MJxdVkY5EL4HYbcIfwKj6X4LBQ2_ZHZIu1jdqLcRZqHcsDF5KKylKc1THn5VRWy5WhYg_gBnyWny8E6Qkrze53MR7OuAmmNJ1m1nN8SxDrG6a08L78J0-Fbas5OjAQz3c17GY8mVuDPOBIOVjMEghBlgl3nOi1ysxbRGhHLEK4s0KKbeRogZdgt1DkQxDFxxn41QWDw_mmMCjs9qxg0zcZzqEJw"
}
}"#;
macro_rules! define_fixtures {
($base_path:expr, $($name:ident => ($file:expr, $format:expr)),* $(,)?) => {
$(
pub const $name: &str = $file;
)*
static EMBEDDED_FIXTURES: LazyLock<HashMap<&'static str, (&'static [u8], &'static str)>> = LazyLock::new(|| {
let mut map = HashMap::new();
$(
// Convert to &[u8] slice to avoid fixed-size array type issues
let bytes: &'static [u8] = include_bytes!(concat!("../../tests/fixtures/", $file));
map.insert($file, (bytes, $format));
)*
map
});
pub fn get_registry() -> &'static HashMap<&'static str, (&'static [u8], &'static str)> {
&EMBEDDED_FIXTURES
}
};
}
define_fixtures!(
"../../tests/fixtures/",
SMALL_JPEG => ("earth_apollo17.jpg", "image/jpeg"),
C_JPEG => ("C.jpg", "image/jpeg"),
CA_JPEG => ("CA.jpg", "image/jpeg"),
XCA_JPEG => ("XCA.jpg", "image/jpeg"),
SAMPLE_PNG => ("libpng-test.png", "image/png"),
SAMPLE_WAV => ("sample1.wav", "audio/wav"),
SAMPLE_WEBP => ("sample1.webp", "image/webp"),
SAMPLE_TIFF => ("TUSCANY.TIF", "image/tiff"),
SAMPLE_AVI => ("test.avi", "video/avi"),
SAMPLE_AVIF => ("sample1.avif", "image/avif"),
SAMPLE_HEIC => ("sample1.heic", "image/heic"),
SAMPLE_HEIF => ("sample1.heif", "image/heif"),
SAMPLE_MP4 => ("video1.mp4", "video/mp4"),
LEGACY_MP4 => ("legacy.mp4", "video/mp4"),
NO_MANIFEST_MP4 => ("video1_no_manifest.mp4", "video/mp4"),
LEGACY_INGREDIENT_HASH => ("legacy_ingredient_hash.jpg", "image/jpeg"),
NO_MANIFEST => ("no_manifest.jpg", "image/jpeg"),
NO_ALG => ("no_alg.jpg", "image/jpeg"),
SAMPLE_BAD_SIGNATURE => ("CIE-sig-CA.jpg", "image/jpeg"),
SAMPLE_PSD => ("Purple Square.psd", "image/vnd.adobe.photoshop"),
TEST_TEXT_PLAIN => ("unsupported_type.txt", "text/plain"),
PRE_RELEASE => ("prerelease.jpg", "image/jpeg"),
C_MOV => ("c.mov", "video/quicktime"),
);
pub fn setup_logger() {
static INIT: std::sync::Once = std::sync::Once::new();
INIT.call_once(|| {
let _ = env_logger::builder().is_test(true).try_init();
});
}
#[allow(clippy::expect_used)]
pub fn test_settings() -> crate::Settings {
crate::Settings::new()
.with_toml(include_str!("../../tests/fixtures/test_settings.toml"))
.expect("built-in test_settings.toml should be valid")
}
#[allow(clippy::expect_used)]
pub fn test_context() -> Context {
Context::new()
.with_settings(test_settings())
.expect("test_settings should always be valid")
}
pub(crate) fn gen_c2pa_uuid() -> String {
let guid = uuid::Uuid::new_v4();
guid.hyphenated()
.encode_lower(&mut uuid::Uuid::encode_buffer())
.to_owned()
}
pub(crate) fn static_test_v1_uuid() -> &'static str {
const TEST_GUID: &str = "urn:uuid:f75ddc48-cdc8-4723-bcfe-77a8d68a5920";
TEST_GUID
}
pub fn create_min_test_claim() -> Result<Claim> {
let mut claim = Claim::new("contentauth unit test", Some("contentauth"), 2);
let mut cg_info = ClaimGeneratorInfo::new("test app");
cg_info.version = Some("2.3.4".to_string());
claim.add_claim_generator_info(cg_info);
let created_action = Action::new("c2pa.created").set_source_type(DigitalSourceType::Empty);
let actions = Actions::new().add_action(created_action);
claim.add_assertion(&actions)?;
Ok(claim)
}
pub fn create_test_claim() -> Result<Claim> {
let mut claim = Claim::new("contentauth unit test", Some("contentauth"), 2);
let icon = EmbeddedData::new(labels::ICON, "image/jpeg", vec![0xde, 0xad, 0xbe, 0xef]);
let icon_ref = claim.add_assertion(&icon)?;
let mut cg_info = ClaimGeneratorInfo::new("test app");
cg_info.version = Some("2.3.4".to_string());
cg_info.icon = Some(UriOrResource::HashedUri(icon_ref));
cg_info.insert("something", "else");
claim.add_claim_generator_info(cg_info);
let claim_thumbnail = EmbeddedData::new(
labels::CLAIM_THUMBNAIL,
"image/jpeg",
vec![0xde, 0xad, 0xbe, 0xef],
);
let _claim_thumbnail_ref = claim.add_assertion(&claim_thumbnail)?;
let ingredient_thumbnail = EmbeddedData::new(
labels::INGREDIENT_THUMBNAIL,
"image/jpeg",
vec![0xde, 0xad, 0xbe, 0xef],
);
let ingredient_thumbnail_ref = claim.add_assertion(&ingredient_thumbnail)?;
let ingredient = Ingredient::new_v3(Relationship::ComponentOf)
.set_title("image_1.jpg")
.set_format("image/jpeg")
.set_thumbnail(Some(&ingredient_thumbnail_ref));
let ingredient_ref = claim.add_assertion(&ingredient)?;
let ingredient2 = Ingredient::new_v3(Relationship::ComponentOf)
.set_title("image_2.jpg")
.set_format("image/png")
.set_thumbnail(Some(&ingredient_thumbnail_ref));
let ingredient_ref2 = claim.add_assertion(&ingredient2)?;
let created_action = Action::new("c2pa.created").set_source_type(DigitalSourceType::Empty);
let placed_action = Action::new("c2pa.placed")
.set_parameter("ingredients", vec![ingredient_ref, ingredient_ref2])?;
let actions = Actions::new()
.add_action(created_action)
.add_action(placed_action);
claim.add_assertion(&actions)?;
Ok(claim)
}
pub fn create_test_claim_v1() -> Result<Claim> {
let mut claim = Claim::new("adobe unit test", Some("adobe"), 1);
let _db_uri = claim.add_databox("text/plain", "this is a test".as_bytes().to_vec(), None)?;
let _db_uri_1 =
claim.add_databox("text/plain", "this is more text".as_bytes().to_vec(), None)?;
let _hu = claim.add_verifiable_credential(TEST_VC)?;
let actions = Actions::new()
.add_action(Action::new("c2pa.created"))
.add_action(
Action::new("c2pa.cropped")
.set_parameter(
"name".to_owned(),
r#"{"left": 0, "right": 2000, "top": 1000, "bottom": 4000}"#,
)
.unwrap(),
)
.add_action(
Action::new("c2pa.filtered")
.set_parameter("name".to_owned(), "gaussian blur")?
.set_when("2015-06-26T16:43:23+0200"),
);
let some_binary_data: Vec<u8> = vec![
0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 0x0b,
0x0e,
];
let user_assertion_data = r#"{
"test_label": "test_value"
}"#;
let cr = r#"{
"@context": "https://schema.org",
"@type": "ClaimReview",
"claimReviewed": "The world is flat",
"reviewRating": {
"@type": "Rating",
"ratingValue": "1",
"bestRating": "5",
"worstRating": "1",
"alternateName": "False"
}
}"#;
let claim_review = SchemaDotOrg::from_json_str(cr)?;
let thumbnail_claim = Thumbnail::new(labels::JPEG_CLAIM_THUMBNAIL, some_binary_data.clone());
let thumbnail_ingred = Thumbnail::new(labels::JPEG_INGREDIENT_THUMBNAIL, some_binary_data);
let user_assertion = User::new(TEST_USER_ASSERTION, user_assertion_data);
claim.add_assertion(&actions)?;
claim.add_assertion(&claim_review)?;
claim.add_assertion(&thumbnail_claim)?;
claim.add_assertion(&user_assertion)?;
let thumb_uri = claim.add_assertion(&thumbnail_ingred)?;
let review = ReviewRating::new(
"a 3rd party plugin was used",
Some("actions.unknownActionsPerformed".to_string()),
1,
);
let ingredient = Ingredient::new(
"image 1.jpg",
"image/jpeg",
"xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c347"),
)
.set_thumbnail(Some(&thumb_uri))
.add_review(review);
let ingredient2 = Ingredient::new(
"image 2.png",
"image/png",
"xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738c",
Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c346"),
)
.set_thumbnail(Some(&thumb_uri));
claim.add_assertion(&ingredient)?;
claim.add_assertion(&ingredient2)?;
Ok(claim)
}
pub fn create_test_store() -> Result<Store> {
let mut store = Store::from_context(&Context::new());
let claim = create_test_claim()?;
store.commit_claim(claim).unwrap();
Ok(store)
}
pub fn create_test_store_v1() -> Result<Store> {
let mut store = Store::from_context(&Context::new());
let claim = create_test_claim_v1()?;
store.commit_claim(claim).unwrap();
Ok(store)
}
pub fn fixture_path(file_name: &str) -> PathBuf {
#[cfg(target_os = "wasi")]
let mut path = PathBuf::from("/");
#[cfg(not(target_os = "wasi"))]
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("tests/fixtures");
path.push(file_name);
path
}
#[allow(clippy::expect_used)]
pub fn create_test_streams(
fixture_name: &str,
) -> (
&'static str,
std::io::Cursor<Vec<u8>>,
std::io::Cursor<Vec<u8>>,
) {
if let Some(fixture) = get_registry().get(fixture_name) {
let data = fixture.0;
let format = fixture.1;
let input_cursor = std::io::Cursor::new(data.to_vec());
let output_cursor = std::io::Cursor::new(Vec::new());
return (format, input_cursor, output_cursor);
}
#[cfg(feature = "file_io")]
{
let input_path = fixture_path(fixture_name);
let input_data = std::fs::read(&input_path).expect("could not read input file");
let format = input_path
.extension()
.and_then(|ext| ext.to_str())
.and_then(extension_to_mime)
.unwrap_or("application/octet-stream");
let input_cursor = std::io::Cursor::new(input_data);
let output_cursor = std::io::Cursor::new(Vec::new());
(format, input_cursor, output_cursor)
}
#[cfg(not(feature = "file_io"))]
{
panic!(
"Fixture '{}' not found in embedded registry and file I/O is disabled",
fixture_name
);
}
}
pub struct TestFileSetup {
pub temp_dir: TempDir,
pub input_path: PathBuf,
pub output_path: PathBuf,
pub format: String,
}
impl TestFileSetup {
#[allow(clippy::expect_used)]
pub fn new(fixture_name: &str) -> Self {
let input_path = fixture_path(fixture_name);
let extension = input_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("bin");
let format = extension_to_mime(extension)
.unwrap_or("application/octet-stream")
.to_string();
let temp_dir = tempdirectory().expect("create temp dir");
let mut output_path = temp_dir.path().join(fixture_name);
output_path.set_extension(extension);
Self {
temp_dir,
input_path,
output_path,
format,
}
}
pub fn temp_dir_path(&self) -> &std::path::Path {
self.temp_dir.path()
}
pub fn temp_path(&self, filename: &str) -> PathBuf {
self.temp_dir.path().join(filename)
}
pub fn sidecar_path(&self) -> PathBuf {
self.output_path.with_extension(MANIFEST_STORE_EXT)
}
pub fn sidecar_url(&self) -> String {
let path_buf = self.sidecar_path(); let path_str = path_buf.to_str().unwrap();
let path_str = path_str.replace('\\', "/");
if path_str.starts_with('/') {
format!("file://{path_str}")
} else {
format!("file:///{path_str}")
}
}
pub fn extension(&self) -> &str {
self.input_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("bin")
}
#[allow(clippy::expect_used)]
pub fn input_stream(&self) -> std::fs::File {
std::fs::File::open(&self.input_path).expect("open input file")
}
#[allow(clippy::expect_used)]
pub fn output_stream(&self) -> std::fs::File {
std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(&self.output_path)
.expect("create output file")
}
pub fn create_streams(&self) -> (&str, std::fs::File, std::fs::File) {
(&self.format, self.input_stream(), self.output_stream())
}
}
pub fn run_file_test<F>(fixture_name: &str, test_fn: F)
where
F: FnOnce(&TestFileSetup),
{
let setup = TestFileSetup::new(fixture_name);
test_fn(&setup);
}
pub fn temp_dir_path(temp_dir: &TempDir, file_name: &str) -> PathBuf {
temp_dir.path().join(file_name)
}
pub fn temp_fixture_path(temp_dir: &TempDir, file_name: &str) -> PathBuf {
let fixture_src = fixture_path(file_name);
let fixture_copy = temp_dir_path(temp_dir, file_name);
std::fs::copy(fixture_src, &fixture_copy).unwrap();
fixture_copy
}
#[cfg(feature = "file_io")]
pub fn temp_signer_file() -> Box<dyn crate::Signer> {
#![allow(clippy::expect_used)]
let mut sign_cert_path = fixture_path("certs");
sign_cert_path.push("ps256");
sign_cert_path.set_extension("pub");
let mut pem_key_path = fixture_path("certs");
pem_key_path.push("ps256");
pem_key_path.set_extension("pem");
crate::create_signer::from_files(&sign_cert_path, &pem_key_path, SigningAlg::Ps256, None)
.expect("get_temp_signer")
}
pub fn test_certificate_acceptance_policy() -> CertificateTrustPolicy {
let mut ctp = CertificateTrustPolicy::default();
ctp.add_trust_anchors(include_bytes!(
"../../tests/fixtures/certs/trust/test_cert_root_bundle.pem"
))
.unwrap();
ctp
}
#[cfg(feature = "file_io")]
pub fn write_jpeg_placeholder_file(
placeholder: &[u8],
input: &Path,
output_file: &mut dyn CAIReadWrite,
hasher: Option<&mut Hasher>,
) -> Result<usize> {
let mut f = std::fs::File::open(input).unwrap();
write_jpeg_placeholder_stream(placeholder, "jpeg", &mut f, output_file, hasher)
}
pub fn write_jpeg_placeholder_stream<R>(
placeholder: &[u8],
format: &str,
input: &mut R,
output_file: &mut dyn CAIReadWrite,
mut hasher: Option<&mut Hasher>,
) -> Result<usize>
where
R: Read + std::io::Seek + Send,
{
let jpeg_io = get_assetio_handler(format).unwrap();
let box_mapper = jpeg_io.asset_box_hash_ref().unwrap();
let boxes = box_mapper.get_box_map(input).unwrap();
let sof = boxes.iter().find(|b| b.names[0] == "SOF0").unwrap();
let outbuf = Vec::new();
let mut out_stream = Cursor::new(outbuf);
input.rewind().unwrap();
let box_len: usize = sof.range_start.try_into()?;
let mut before = vec![0u8; box_len];
input.read_exact(before.as_mut_slice()).unwrap();
if let Some(hasher) = hasher.as_deref_mut() {
hasher.update(&before);
}
out_stream.write_all(&before).unwrap();
out_stream.write_all(placeholder).unwrap();
let mut after_buf = Vec::new();
input.read_to_end(&mut after_buf).unwrap();
if let Some(hasher) = hasher {
hasher.update(&after_buf);
}
out_stream.write_all(&after_buf).unwrap();
output_file.write_all(&out_stream.into_inner()).unwrap();
Ok(box_len)
}
pub fn write_bmff_placeholder_stream<R>(
placeholder: &[u8],
input: &mut R,
output_file: &mut dyn CAIReadWrite,
) -> Result<usize>
where
R: Read + std::io::Seek + Send,
{
input.rewind().unwrap();
let mut size_bytes = [0u8; 4];
input.read_exact(&mut size_bytes).unwrap();
let mut type_bytes = [0u8; 4];
input.read_exact(&mut type_bytes).unwrap();
assert_eq!(
&type_bytes, b"ftyp",
"BMFF stream must start with an ftyp box"
);
let ftyp_size = u32::from_be_bytes(size_bytes) as usize;
let outbuf = Vec::new();
let mut out_stream = Cursor::new(outbuf);
input.rewind().unwrap();
let mut before = vec![0u8; ftyp_size];
input.read_exact(before.as_mut_slice()).unwrap();
out_stream.write_all(&before).unwrap();
out_stream.write_all(placeholder).unwrap();
let mut after_buf = Vec::new();
input.read_to_end(&mut after_buf).unwrap();
out_stream.write_all(&after_buf).unwrap();
output_file.write_all(&out_stream.into_inner()).unwrap();
Ok(ftyp_size)
}
pub(crate) struct TestGoodSigner {}
impl crate::Signer for TestGoodSigner {
fn sign(&self, _data: &[u8]) -> Result<Vec<u8>> {
Ok(b"not a valid signature".to_vec())
}
fn alg(&self) -> SigningAlg {
SigningAlg::Ps256
}
fn certs(&self) -> Result<Vec<Vec<u8>>> {
Ok(Vec::new())
}
fn reserve_size(&self) -> usize {
1024
}
fn send_timestamp_request(&self, _message: &[u8]) -> Option<crate::error::Result<Vec<u8>>> {
Some(Ok(Vec::new()))
}
}
pub(crate) struct AsyncTestGoodSigner {}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AsyncSigner for AsyncTestGoodSigner {
async fn sign(&self, _data: Vec<u8>) -> Result<Vec<u8>> {
Ok(b"not a valid signature".to_vec())
}
fn alg(&self) -> SigningAlg {
SigningAlg::Ps256
}
fn certs(&self) -> Result<Vec<Vec<u8>>> {
Ok(Vec::new())
}
fn reserve_size(&self) -> usize {
1024
}
async fn send_timestamp_request(
&self,
_message: &[u8],
) -> Option<crate::error::Result<Vec<u8>>> {
Some(Ok(Vec::new()))
}
}
#[test]
fn test_create_test_store() {
#[allow(clippy::expect_used)]
let store = create_test_store().expect("create test store");
assert_eq!(store.claims().len(), 1);
}