use std::io::{Cursor, Seek};
use c2pa::{assertions::DigitalSourceType, Builder, BuilderIntent, Reader, Result, Signer};
mod common;
fn test_context() -> std::sync::Arc<c2pa::Context> {
common::test_context().into_shared()
}
#[test]
fn test_nested_ingredients_reconstruction_from_store() -> Result<()> {
let context = test_context();
let format = "image/jpeg";
let mut base_image = Cursor::new(include_bytes!("fixtures/no_manifest.jpg"));
let mut level1_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
builder.sign(
context.signer()?,
format,
&mut base_image,
&mut level1_output,
)?;
level1_output.rewind()?;
let mut level2_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Edit);
builder.sign(
context.signer()?,
format,
&mut level1_output,
&mut level2_output,
)?;
level2_output.rewind()?;
let mut level3_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Edit);
builder.sign(
context.signer()?,
format,
&mut level2_output,
&mut level3_output,
)?;
level3_output.rewind()?;
let reader = Reader::from_shared_context(&context).with_stream(format, &mut level3_output)?;
let active_manifest = reader
.active_manifest()
.expect("Should have active manifest");
assert!(
!active_manifest.ingredients().is_empty(),
"Level 3 should have ingredients"
);
assert_eq!(
active_manifest.ingredients().len(),
1,
"Level 3 should have exactly 1 ingredient"
);
let level2_ingredient = &active_manifest.ingredients()[0];
let level2_active_manifest = level2_ingredient
.active_manifest()
.expect("Level 2 ingredient should have active manifest");
let level2_manifest = reader
.get_manifest(level2_active_manifest)
.expect("Should be able to get level 2 ingredient's manifest");
assert!(
!level2_manifest.ingredients().is_empty(),
"Level 2 ingredient's manifest should have its own ingredient (nested ingredient from level 1)"
);
assert_eq!(
level2_manifest.ingredients().len(),
1,
"Level 2 ingredient should have exactly 1 nested ingredient"
);
assert!(
!level2_manifest.ingredients().is_empty(),
"Level 2's manifest should have at least one ingredient (level 1)"
);
Ok(())
}
#[test]
fn test_reader_to_builder_preserves_nested_ingredients() -> Result<()> {
let context = test_context();
let format = "image/jpeg";
let mut base_image = Cursor::new(include_bytes!("fixtures/no_manifest.jpg"));
let mut level1_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
builder.sign(
context.signer()?,
format,
&mut base_image,
&mut level1_output,
)?;
base_image.rewind()?;
level1_output.rewind()?;
let mut level2_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Edit);
builder.add_ingredient_from_stream(
serde_json::json!({"title": "L1"}).to_string(),
format,
&mut level1_output,
)?;
builder.sign(
context.signer()?,
format,
&mut base_image,
&mut level2_output,
)?;
base_image.rewind()?;
level2_output.rewind()?;
let mut level3_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Edit);
builder.add_ingredient_from_stream(
serde_json::json!({"title": "L2"}).to_string(),
format,
&mut level2_output,
)?;
builder.sign(
context.signer()?,
format,
&mut base_image,
&mut level3_output,
)?;
level3_output.rewind()?;
let reader = Reader::from_shared_context(&context).with_stream(format, &mut level3_output)?;
let mut builder_from_reader = reader.into_builder()?;
base_image.rewind()?;
let mut level4_output = Cursor::new(Vec::new());
builder_from_reader.sign(
context.signer()?,
format,
&mut base_image,
&mut level4_output,
)?;
level4_output.rewind()?;
let reader = Reader::from_shared_context(&context).with_stream(format, &mut level4_output)?;
let active_manifest = reader
.active_manifest()
.expect("Should have active manifest");
assert!(!active_manifest.ingredients().is_empty());
let level2_ingredient = &active_manifest.ingredients()[0];
let level2_active_manifest = level2_ingredient
.active_manifest()
.expect("Level 2 ingredient should have active manifest");
let level2_manifest = reader
.get_manifest(level2_active_manifest)
.expect("Should be able to get level 2 ingredient's manifest");
assert!(
!level2_manifest.ingredients().is_empty(),
"Nested ingredients should be preserved through Reader to Builder conversion"
);
Ok(())
}
#[test]
fn test_ingredient_manifest_data_includes_nested_ingredients() -> Result<()> {
let context = test_context();
let format = "image/jpeg";
let mut base_image = Cursor::new(include_bytes!("fixtures/no_manifest.jpg"));
let mut level1_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
builder.sign(
context.signer()?,
format,
&mut base_image,
&mut level1_output,
)?;
base_image.rewind()?;
level1_output.rewind()?;
let mut level2_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Edit);
builder.add_ingredient_from_stream(
serde_json::json!({"title": "Test ingredient"}).to_string(),
format,
&mut level1_output,
)?;
builder.sign(
context.signer()?,
format,
&mut base_image,
&mut level2_output,
)?;
level2_output.rewind()?;
let reader = Reader::from_shared_context(&context).with_stream(format, &mut level2_output)?;
let builder = reader.into_builder()?;
let manifest_def = &builder.definition;
assert!(!manifest_def.ingredients.is_empty());
let ingredient = &manifest_def.ingredients[0];
assert!(
ingredient.manifest_data().is_some(),
"Ingredient should have manifest_data"
);
let manifest_data_bytes = ingredient.manifest_data();
assert!(
manifest_data_bytes.is_some() && !manifest_data_bytes.unwrap().is_empty(),
"Ingredient's manifest_data should contain valid bytes"
);
Ok(())
}
#[test]
fn test_deeply_nested_ingredients() -> Result<()> {
let context = test_context();
let format = "image/jpeg";
let mut base_image = Cursor::new(include_bytes!("fixtures/no_manifest.jpg"));
let mut current_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
builder.sign(
context.signer()?,
format,
&mut base_image,
&mut current_output,
)?;
for level in 1..=4 {
base_image.rewind()?;
current_output.rewind()?;
let mut next_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Edit);
builder.add_ingredient_from_stream(
serde_json::json!({"title": format!("Level {}", level)}).to_string(),
format,
&mut current_output,
)?;
builder.sign(context.signer()?, format, &mut base_image, &mut next_output)?;
current_output = next_output;
}
current_output.rewind()?;
let reader = Reader::from_shared_context(&context).with_stream(format, &mut current_output)?;
let mut current_manifest = reader
.active_manifest()
.expect("Should have active manifest");
for level in (1..=4).rev() {
assert!(
!current_manifest.ingredients().is_empty(),
"Should have ingredient at depth {level}"
);
let ingredient = ¤t_manifest.ingredients()[0];
let expected_title = format!("Level {level}");
assert_eq!(
ingredient.title(),
Some(expected_title.as_str()),
"Ingredient at level {level} should have correct title"
);
if level > 1 {
let active_manifest_label = ingredient.active_manifest().unwrap_or_else(|| {
panic!("Ingredient at level {level} should have active manifest")
});
current_manifest = reader
.get_manifest(active_manifest_label)
.unwrap_or_else(|| panic!("Should be able to get manifest at level {level}"));
}
}
Ok(())
}
#[test]
fn test_ingredient_without_nested_ingredients() -> Result<()> {
let context = test_context();
let format = "image/jpeg";
let mut base_image = Cursor::new(include_bytes!("fixtures/no_manifest.jpg"));
let mut level1_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
builder.sign(
context.signer()?,
format,
&mut base_image,
&mut level1_output,
)?;
base_image.rewind()?;
level1_output.rewind()?;
let mut level2_output = Cursor::new(Vec::new());
let mut builder = Builder::from_shared_context(&context);
builder.set_intent(BuilderIntent::Edit);
builder.add_ingredient_from_stream(
serde_json::json!({"title": "Simple ingredient"}).to_string(),
format,
&mut level1_output,
)?;
builder.sign(
context.signer()?,
format,
&mut base_image,
&mut level2_output,
)?;
level2_output.rewind()?;
let reader = Reader::from_shared_context(&context).with_stream(format, &mut level2_output)?;
let active_manifest = reader
.active_manifest()
.expect("Should have active manifest");
assert!(!active_manifest.ingredients().is_empty());
let ingredient = &active_manifest.ingredients()[0];
if let Some(active_label) = ingredient.active_manifest() {
if let Some(ing_manifest) = reader.get_manifest(active_label) {
assert!(
ing_manifest.ingredients().is_empty(),
"Simple ingredient should have no nested ingredients"
);
}
}
Ok(())
}
fn test_sign_manifest(
builder: &mut Builder,
format: &str,
source: &mut Cursor<&[u8]>,
signer: &dyn Signer,
) -> Result<Cursor<Vec<u8>>> {
let mut output = Cursor::new(Vec::new());
source.rewind()?;
builder.sign(signer, format, source, &mut output)?;
output.rewind()?;
Ok(output)
}
#[test]
fn test_diamond_topology_read() -> Result<()> {
let context = test_context();
let signer = context.signer()?;
let format = "image/jpeg";
let mut source = Cursor::new(include_bytes!("fixtures/no_manifest.jpg").as_slice());
let mut builder_a = Builder::from_shared_context(&context);
builder_a.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
let mut output_a = test_sign_manifest(&mut builder_a, format, &mut source, signer)?;
let mut builder_b = Builder::from_shared_context(&context);
builder_b.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
output_a.rewind()?;
builder_b.add_ingredient_from_stream(
serde_json::json!({"title": "A via B"}).to_string(),
format,
&mut output_a,
)?;
let mut output_b = test_sign_manifest(&mut builder_b, format, &mut source, signer)?;
let mut builder_c = Builder::from_shared_context(&context);
builder_c.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
output_a.rewind()?;
builder_c.add_ingredient_from_stream(
serde_json::json!({"title": "A via C"}).to_string(),
format,
&mut output_a,
)?;
let mut output_c = test_sign_manifest(&mut builder_c, format, &mut source, signer)?;
let mut builder_d = Builder::from_shared_context(&context);
builder_d.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
output_b.rewind()?;
builder_d.add_ingredient_from_stream(
serde_json::json!({"title": "B"}).to_string(),
format,
&mut output_b,
)?;
output_c.rewind()?;
builder_d.add_ingredient_from_stream(
serde_json::json!({"title": "C"}).to_string(),
format,
&mut output_c,
)?;
let mut output_d = test_sign_manifest(&mut builder_d, format, &mut source, signer)?;
let reader = Reader::from_shared_context(&context).with_stream(format, &mut output_d)?;
let active = reader
.active_manifest()
.expect("should have active manifest");
assert_eq!(active.ingredients().len(), 2, "D should have 2 ingredients");
for ingredient in active.ingredients() {
assert!(
ingredient.manifest_data().is_some(),
"Ingredient {:?} should have manifest_data populated",
ingredient.title()
);
if let Some(label) = ingredient.active_manifest() {
let manifest = reader
.get_manifest(label)
.unwrap_or_else(|| panic!("should find manifest for {label}"));
assert_eq!(
manifest.ingredients().len(),
1,
"B and C should each have 1 ingredient (A)"
);
}
}
Ok(())
}
#[test]
fn test_mixed_v2_v3_ingredient_versions() -> Result<()> {
let context = test_context();
let signer = context.signer()?;
let format = "image/jpeg";
let mut source = Cursor::new(include_bytes!("fixtures/no_manifest.jpg").as_slice());
let mut builder_a = Builder::from_shared_context(&context);
builder_a.definition.claim_version = Some(1);
builder_a.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
let mut output_a = test_sign_manifest(&mut builder_a, format, &mut source, signer)?;
let mut builder_b = Builder::from_shared_context(&context);
builder_b.definition.claim_version = Some(1);
builder_b.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
output_a.rewind()?;
builder_b.add_ingredient_from_stream(
serde_json::json!({"title": "A via B"}).to_string(),
format,
&mut output_a,
)?;
let mut output_b = test_sign_manifest(&mut builder_b, format, &mut source, signer)?;
let mut builder_c = Builder::from_shared_context(&context);
builder_c.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
output_a.rewind()?;
builder_c.add_ingredient_from_stream(
serde_json::json!({"title": "A via C"}).to_string(),
format,
&mut output_a,
)?;
let mut output_c = test_sign_manifest(&mut builder_c, format, &mut source, signer)?;
let mut builder_d = Builder::from_shared_context(&context);
builder_d.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
output_b.rewind()?;
builder_d.add_ingredient_from_stream(
serde_json::json!({"title": "B"}).to_string(),
format,
&mut output_b,
)?;
output_c.rewind()?;
builder_d.add_ingredient_from_stream(
serde_json::json!({"title": "C"}).to_string(),
format,
&mut output_c,
)?;
let mut output_d = test_sign_manifest(&mut builder_d, format, &mut source, signer)?;
let reader = Reader::from_shared_context(&context).with_stream(format, &mut output_d)?;
let mut builder_e = reader.into_builder()?;
builder_e.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
let mut output_e = test_sign_manifest(&mut builder_e, format, &mut source, signer)?;
let reader_e = Reader::from_shared_context(&context).with_stream(format, &mut output_e)?;
let active_e = reader_e
.active_manifest()
.expect("E should have active manifest");
assert_eq!(
active_e.ingredients().len(),
2,
"E should have 2 ingredients after round-trip with mixed versions"
);
for ingredient in active_e.ingredients() {
if let Some(label) = ingredient.active_manifest() {
let manifest = reader_e
.get_manifest(label)
.unwrap_or_else(|| panic!("should find manifest for {label}"));
assert_eq!(
manifest.ingredients().len(),
1,
"Each ingredient should have 1 nested ingredient (A) despite mixed versions"
);
}
}
Ok(())
}
#[test]
fn test_diamond_topology_into_builder_round_trip() -> Result<()> {
let context = test_context();
let signer = context.signer()?;
let format = "image/jpeg";
let mut source = Cursor::new(include_bytes!("fixtures/no_manifest.jpg").as_slice());
let mut builder_a = Builder::from_shared_context(&context);
builder_a.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
let mut output_a = test_sign_manifest(&mut builder_a, format, &mut source, signer)?;
let mut builder_b = Builder::from_shared_context(&context);
builder_b.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
output_a.rewind()?;
builder_b.add_ingredient_from_stream(
serde_json::json!({"title": "A via B"}).to_string(),
format,
&mut output_a,
)?;
let mut output_b = test_sign_manifest(&mut builder_b, format, &mut source, signer)?;
let mut builder_c = Builder::from_shared_context(&context);
builder_c.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
output_a.rewind()?;
builder_c.add_ingredient_from_stream(
serde_json::json!({"title": "A via C"}).to_string(),
format,
&mut output_a,
)?;
let mut output_c = test_sign_manifest(&mut builder_c, format, &mut source, signer)?;
let mut builder_d = Builder::from_shared_context(&context);
builder_d.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
output_b.rewind()?;
builder_d.add_ingredient_from_stream(
serde_json::json!({"title": "B"}).to_string(),
format,
&mut output_b,
)?;
output_c.rewind()?;
builder_d.add_ingredient_from_stream(
serde_json::json!({"title": "C"}).to_string(),
format,
&mut output_c,
)?;
let mut output_d = test_sign_manifest(&mut builder_d, format, &mut source, signer)?;
let reader = Reader::from_shared_context(&context).with_stream(format, &mut output_d)?;
let mut builder_e = reader.into_builder()?;
builder_e.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
let mut output_e = test_sign_manifest(&mut builder_e, format, &mut source, signer)?;
let reader_e = Reader::from_shared_context(&context).with_stream(format, &mut output_e)?;
let active_e = reader_e
.active_manifest()
.expect("E should have active manifest");
assert_eq!(
active_e.ingredients().len(),
2,
"E should have 2 ingredients after round-trip"
);
for ingredient in active_e.ingredients() {
if let Some(label) = ingredient.active_manifest() {
let manifest = reader_e
.get_manifest(label)
.unwrap_or_else(|| panic!("should find manifest for {label}"));
assert_eq!(
manifest.ingredients().len(),
1,
"Each ingredient should still have 1 nested ingredient (A) after round-trip"
);
}
}
Ok(())
}