use std::io::Cursor;
use c2pa::{Reader, Result};
const IMAGE_WITH_MANIFEST: &[u8] = include_bytes!("../fixtures/C.jpg");
fn first_assertions() -> Result<serde_json::Value> {
let reader = Reader::default().with_stream("image/jpeg", Cursor::new(IMAGE_WITH_MANIFEST))?;
let json = reader.to_crjson_value()?;
let manifest = json["manifests"][0].clone();
Ok(manifest["assertions"].clone())
}
#[test]
fn test_hash_data_assertion_structure() -> Result<()> {
let assertions = first_assertions()?;
let obj = assertions
.get("c2pa.hash.data")
.and_then(|v| v.as_object())
.expect("c2pa.hash.data must be present and be an object");
let hash = obj
.get("hash")
.and_then(|v| v.as_str())
.expect("c2pa.hash.data.hash must be a b64'-prefixed string");
assert!(
hash.starts_with("b64'"),
"c2pa.hash.data.hash must start with \"b64'\" prefix, got: {hash:?}"
);
assert!(
hash.len() > "b64'".len(),
"c2pa.hash.data.hash payload must not be empty"
);
let alg = obj
.get("alg")
.and_then(|v| v.as_str())
.expect("c2pa.hash.data.alg must be a string");
assert!(
matches!(alg, "sha256" | "sha384" | "sha512"),
"c2pa.hash.data.alg must be a standard algorithm, got: {alg}"
);
if let Some(pad) = obj.get("pad") {
let pad_str = pad.as_str().unwrap_or_else(|| {
panic!("c2pa.hash.data.pad must be a b64'-prefixed string, not {pad:?}")
});
assert!(
pad_str.starts_with("b64'"),
"c2pa.hash.data.pad must start with \"b64'\" prefix, got: {pad_str:?}"
);
}
Ok(())
}
#[test]
fn test_hash_data_not_filtered() -> Result<()> {
let assertions = first_assertions()?;
assert!(
assertions.get("c2pa.hash.data").is_some(),
"c2pa.hash.data must be included in crJSON output"
);
let standard_reader =
Reader::default().with_stream("image/jpeg", Cursor::new(IMAGE_WITH_MANIFEST))?;
let standard_json = serde_json::to_value(standard_reader)?;
let standard_assertions = standard_json["manifests"]
.as_object()
.and_then(|m| m.values().next())
.and_then(|m| m["assertions"].as_array())
.expect("standard manifests.*.assertions must be an array");
assert!(
!standard_assertions
.iter()
.any(|a| a.get("label").and_then(|l| l.as_str()) == Some("c2pa.hash.data")),
"c2pa.hash.data must be filtered out in standard Reader serialization"
);
Ok(())
}
#[test]
fn test_hash_assertion_versioning() -> Result<()> {
let assertions = first_assertions()?;
for key in assertions
.as_object()
.unwrap()
.keys()
.filter(|k| k.starts_with("c2pa.hash."))
{
let base = key.split("__").next().unwrap_or(key);
let valid = matches!(
base,
"c2pa.hash.data" | "c2pa.hash.bmff" | "c2pa.hash.boxes" | "c2pa.hash.collection.data"
) || base.starts_with("c2pa.hash.data.v")
|| base.starts_with("c2pa.hash.bmff.v")
|| base.starts_with("c2pa.hash.boxes.v")
|| base.starts_with("c2pa.hash.collection.data.v");
assert!(
valid,
"hash assertion key '{key}' does not follow the known versioning pattern"
);
}
Ok(())
}
#[test]
fn test_action_ingredient_hash_is_base64() -> Result<()> {
let assertions = first_assertions()?;
let actions_val = assertions
.get("c2pa.actions.v2")
.or_else(|| assertions.get("c2pa.actions"));
let Some(actions_arr) = actions_val.and_then(|a| a["actions"].as_array()) else {
return Ok(()); };
for (i, action) in actions_arr.iter().enumerate() {
if let Some(hash) = action
.get("parameters")
.and_then(|p| p.get("ingredient"))
.and_then(|ing| ing.get("hash"))
{
let hash_str = hash.as_str().unwrap_or_else(|| {
panic!("action[{i}] ingredient hash must be a b64'-prefixed string, not an array")
});
assert!(
hash_str.starts_with("b64'"),
"action[{i}] ingredient hash must start with \"b64'\" prefix, got: {hash_str:?}"
);
}
}
Ok(())
}