#![cfg(not(target_os = "wasi"))]
use std::{
error::Error,
fs::{self, create_dir_all},
path::PathBuf,
process::Command,
};
use assert_cmd::{cargo, prelude::*};
use httpmock::{prelude::*, Mock};
use predicate::str;
use predicates::prelude::*;
use serde_json::Value;
use tempfile::tempdir;
const TEST_IMAGE: &str = "earth_apollo17.jpg";
const TEST_IMAGE_WITH_MANIFEST: &str = "C.jpg";
fn fixture_path(name: &str) -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("tests/fixtures");
path.push(name);
fs::canonicalize(path).expect("canonicalize")
}
fn temp_path(name: &str) -> PathBuf {
let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
create_dir_all(&path).ok();
path.join(name)
}
#[test]
fn tool_not_found() -> Result<(), Box<dyn Error>> {
let mut cmd = Command::new(cargo::cargo_bin!("c2patool"));
cmd.arg("test/file/notfound.jpg");
cmd.assert().failure().stderr(str::contains("os error"));
Ok(())
}
#[test]
fn tool_not_found_info() -> Result<(), Box<dyn Error>> {
let mut cmd = Command::new(cargo::cargo_bin!("c2patool"));
cmd.arg("test/file/notfound.jpg").arg("--info");
cmd.assert()
.failure()
.stderr(str::contains("file not found"));
Ok(())
}
#[test]
fn tool_jpeg_no_report() -> Result<(), Box<dyn Error>> {
let mut cmd = Command::new(cargo::cargo_bin!("c2patool"));
cmd.arg(fixture_path(TEST_IMAGE));
cmd.assert()
.failure()
.stderr(str::contains("No claim found"));
Ok(())
}
#[test]
fn tool_info() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("--info")
.assert()
.success()
.stdout(str::contains(
"Provenance URI = self#jumbf=/c2pa/contentauth:urn:uuid:",
))
.stdout(str::contains("Manifest store size = 51217"));
Ok(())
}
#[test]
fn tool_embed_jpeg_report() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE))
.arg("-m")
.arg("sample/test.json")
.arg("-p")
.arg(fixture_path(TEST_IMAGE))
.arg("-o")
.arg(temp_path("out.jpg"))
.arg("-f")
.assert()
.success() .stdout(str::contains("My Title"));
Ok(())
}
#[test]
fn tool_fs_output_report() -> Result<(), Box<dyn Error>> {
let path = temp_path("output_dir");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path("verify.jpeg"))
.arg("-o")
.arg(&path)
.arg("-f")
.assert()
.success()
.stdout(str::contains(format!(
"Manifest report written to the directory {path:?}"
)));
let manifest_json = path.join("manifest_store.json");
let contents = fs::read_to_string(manifest_json)?;
let json: Value = serde_json::from_str(&contents)?;
assert_eq!(
json.as_object()
.unwrap()
.get("active_manifest")
.unwrap()
.as_str()
.unwrap(),
"adobe:urn:uuid:df1d2745-5beb-4d6c-bd99-3527e29c7df0",
);
Ok(())
}
#[test]
fn tool_fs_output_report_supports_detailed_flag() -> Result<(), Box<dyn Error>> {
let path = temp_path("./output_detailed");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path("verify.jpeg"))
.arg("-o")
.arg(&path)
.arg("-f")
.arg("-d")
.assert()
.success()
.stdout(str::contains(format!(
"Manifest report written to the directory {path:?}"
)));
let manifest_json = path.join("detailed.json");
let contents = fs::read_to_string(manifest_json)?;
let json: Value = serde_json::from_str(&contents)?;
assert!(json
.as_object()
.unwrap()
.get("validation_results")
.is_some());
Ok(())
}
#[test]
fn tool_fs_output_fails_when_output_exists() -> Result<(), Box<dyn Error>> {
let path = temp_path("./output_conflict");
create_dir_all(&path)?;
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path("C.jpg"))
.arg("-o")
.arg(&path)
.assert()
.failure()
.stderr(str::contains(
"Error: Output already exists; use -f/force to force write",
));
Ok(())
}
#[test]
fn tool_test_manifest_folder() -> Result<(), Box<dyn std::error::Error>> {
let out_path = temp_path("manifest_test");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("-o")
.arg(&out_path)
.arg("-f")
.assert()
.success()
.stdout(str::contains("Manifest report written"));
let json =
std::fs::read_to_string(out_path.join("manifest_store.json")).expect("read manifest");
dbg!(&json);
assert!(json.contains("make_test_images"));
Ok(())
}
#[test]
fn tool_test_ingredient_folder() -> Result<(), Box<dyn std::error::Error>> {
let out_path = temp_path("ingredient_test");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("-o")
.arg(&out_path)
.arg("--ingredient")
.arg("-f")
.assert()
.success()
.stdout(str::contains("Ingredient report written"));
let json = std::fs::read_to_string(out_path.join("ingredient.json")).expect("read manifest");
assert!(json.contains("manifest_data"));
Ok(())
}
#[test]
fn tool_test_manifest_ingredient_json() -> Result<(), Box<dyn std::error::Error>> {
let out_path = temp_path("ingredient_json");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("-o")
.arg(&out_path)
.arg("--ingredient")
.arg("-f")
.assert()
.success()
.stdout(str::contains("Ingredient report written"));
let json_path = out_path.join("ingredient.json");
let parent = json_path.to_string_lossy().to_string();
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE))
.arg("-p")
.arg(parent)
.arg("-m")
.arg("sample/test.json")
.arg("-o")
.arg(temp_path("out_2.jpg"))
.arg("-f")
.assert()
.success()
.stdout(str::contains("My Title"));
Ok(())
}
#[test]
fn tool_embed_jpeg_with_ingredients_report() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE))
.arg("-m")
.arg(fixture_path("ingredient_test.json"))
.arg("-o")
.arg(temp_path("ingredients.jpg"))
.arg("-f")
.assert()
.success()
.stdout(str::contains("ingredients.jpg"))
.stdout(str::contains("test ingredient"))
.stdout(str::contains("temporal"))
.stdout(str::contains("earth_apollo17.jpg"));
Ok(())
}
#[test]
fn tool_extensions_do_not_match() -> Result<(), Box<dyn Error>> {
let path = temp_path("./foo.png");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path("C.jpg"))
.arg("-m")
.arg(fixture_path("ingredient_test.json"))
.arg("-o")
.arg(&path)
.assert()
.failure()
.stderr(str::contains("Output type must match source type"));
Ok(())
}
#[test]
fn tool_similar_extensions_match() -> Result<(), Box<dyn Error>> {
let path = temp_path("./similar.JpEg");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path("C.jpg"))
.arg("-m")
.arg(fixture_path("ingredient_test.json"))
.arg("-o")
.arg(&path)
.arg("-f")
.assert()
.success()
.stdout(str::contains("similar."));
Ok(())
}
#[test]
fn tool_fail_if_thumbnail_missing() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE))
.arg("-c")
.arg("{\"thumbnail\": {\"identifier\": \"thumb.jpg\",\"format\": \"image/jpeg\"}}")
.arg("-o")
.arg(temp_path("out_thumb.jpg"))
.arg("-f")
.assert()
.failure()
.stderr(str::contains("resource not found"));
Ok(())
}
#[test]
fn tool_sign_to_same_file_with_force() -> Result<(), Box<dyn Error>> {
let tmp_dir = tempdir()?;
let file_path = tmp_dir.path().join("same_image.jpg");
fs::copy(fixture_path(TEST_IMAGE), &file_path)?;
Command::new(cargo::cargo_bin!("c2patool"))
.arg(&file_path)
.arg("-m")
.arg(fixture_path("ingredient_test.json"))
.arg("-o")
.arg(&file_path)
.arg("-f")
.assert()
.success()
.stdout(str::contains("same_image.jpg"))
.stdout(str::contains("test ingredient"))
.stdout(str::contains("temporal"))
.stdout(str::contains("earth_apollo17.jpg"));
Ok(())
}
#[test]
fn tool_sign_to_same_file_no_force() -> Result<(), Box<dyn Error>> {
let tmp_dir = tempdir()?;
let file_path = tmp_dir.path().join("same_image.jpg");
fs::copy(fixture_path(TEST_IMAGE), &file_path)?;
Command::new(cargo::cargo_bin!("c2patool"))
.arg(&file_path)
.arg("-m")
.arg(fixture_path("ingredient_test.json"))
.arg("-o")
.arg(&file_path)
.assert()
.failure()
.stderr(str::contains(
"Error: Output already exists; use -f/force to force write",
));
Ok(())
}
#[test]
fn test_sign_using_c2patool_as_subprocess_signer() -> Result<(), Box<dyn Error>> {
let output = temp_path("output_subprocess_signer.jpg");
let signer_cmd = format!("{} test-signer", cargo::cargo_bin!("c2patool").display());
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE))
.arg("--signer-path")
.arg(&signer_cmd)
.arg("--manifest")
.arg("sample/test.json")
.arg("-o")
.arg(&output)
.arg("-f")
.assert()
.success();
Ok(())
}
#[test]
fn test_sign_cawg_using_c2patool_as_identity_signer() -> Result<(), Box<dyn Error>> {
let output = temp_path("output_cawg_identity_signer.jpg");
let signer_cmd = format!("{} test-signer", cargo::cargo_bin!("c2patool").display());
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE))
.arg("--signer-path")
.arg(&signer_cmd)
.arg("--identity-signer-path")
.arg(&signer_cmd)
.arg("--manifest")
.arg("sample/test.json")
.arg("-o")
.arg(&output)
.arg("-f")
.assert()
.success()
.stdout(str::contains("cawg.identity"));
Ok(())
}
#[test]
fn test_fails_for_not_found_external_signer() -> Result<(), Box<dyn Error>> {
let output = temp_path("output_not_found_signer.jpg");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE))
.arg("--signer-path")
.arg("./nonexistent-signer-binary-xyz")
.arg("--manifest")
.arg("sample/test.json")
.arg("-o")
.arg(&output)
.arg("-f")
.assert()
.failure()
.stderr(str::contains("Failed to run"));
Ok(())
}
#[test]
fn test_fails_for_external_signer_failure() -> Result<(), Box<dyn Error>> {
let output = temp_path("output_failing_signer.jpg");
let signer_cmd = format!(
"{} test-signer --fail",
cargo::cargo_bin!("c2patool").display()
);
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE))
.arg("--signer-path")
.arg(&signer_cmd)
.arg("--manifest")
.arg("sample/test.json")
.arg("-o")
.arg(&output)
.arg("-f")
.assert()
.failure()
.stderr(str::contains("deliberately failed"));
Ok(())
}
#[test]
fn rust_log_unset_suppresses_trust_debug_messages() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("trust")
.arg("--trust_anchors")
.arg(fixture_path("trust/anchors.pem"))
.arg("--trust_config")
.arg(fixture_path("trust/store.cfg"))
.env_remove("RUST_LOG")
.assert()
.success()
.stderr(str::contains("Using trust anchors").not());
Ok(())
}
#[test]
fn rust_log_debug_shows_trust_debug_messages() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("trust")
.arg("--trust_anchors")
.arg(fixture_path("trust/anchors.pem"))
.arg("--trust_config")
.arg(fixture_path("trust/store.cfg"))
.env("RUST_LOG", "debug")
.assert()
.success()
.stderr(str::contains("Using trust"));
Ok(())
}
#[test]
fn tool_load_trust_settings_from_file_trusted() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("trust")
.arg("--trust_anchors")
.arg(fixture_path("trust/anchors.pem"))
.arg("--trust_config")
.arg(fixture_path("trust/store.cfg"))
.assert()
.success()
.stdout(str::contains("C2PA Test Signing Cert"))
.stdout(str::contains("signingCredential.untrusted").not());
Ok(())
}
#[test]
fn tool_load_trust_settings_from_file_untrusted() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("trust")
.arg("--trust_anchors")
.arg(fixture_path("trust/no-match.pem"))
.arg("--trust_config")
.arg(fixture_path("trust/store.cfg"))
.assert()
.success()
.stdout(str::contains("C2PA Test Signing Cert"))
.stdout(str::contains("signingCredential.untrusted"));
Ok(())
}
fn create_mock_server<'a>(
server: &'a MockServer,
anchor_source: &str,
config_source: &str,
) -> Vec<Mock<'a>> {
let anchor_path = fixture_path(anchor_source).to_str().unwrap().to_owned();
let trust_mock = server.mock(|when, then| {
when.method(GET).path("/trust/anchors.pem");
then.status(200)
.header("content-type", "text/plain")
.body_from_file(anchor_path);
});
let config_path = fixture_path(config_source).to_str().unwrap().to_owned();
let config_mock = server.mock(|when, then| {
when.method(GET).path("/trust/store.cfg");
then.status(200)
.header("content-type", "text/plain")
.body_from_file(config_path);
});
vec![trust_mock, config_mock]
}
#[test]
fn tool_load_trust_settings_from_url_arg_trusted() -> Result<(), Box<dyn Error>> {
let server = MockServer::start();
let mocks = create_mock_server(&server, "trust/anchors.pem", "trust/store.cfg");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("trust")
.arg("--trust_anchors")
.arg(server.url("/trust/anchors.pem"))
.arg("--trust_config")
.arg(server.url("/trust/store.cfg"))
.assert()
.success()
.stdout(str::contains("C2PA Test Signing Cert"))
.stdout(str::contains("signingCredential.untrusted").not());
mocks.iter().for_each(|m| m.assert());
Ok(())
}
#[test]
fn tool_load_trust_settings_from_url_arg_untrusted() -> Result<(), Box<dyn Error>> {
let server = MockServer::start();
let mocks = create_mock_server(&server, "trust/no-match.pem", "trust/store.cfg");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("trust")
.arg("--trust_anchors")
.arg(server.url("/trust/anchors.pem"))
.arg("--trust_config")
.arg(server.url("/trust/store.cfg"))
.assert()
.success()
.stdout(str::contains("C2PA Test Signing Cert"))
.stdout(str::contains("signingCredential.untrusted"));
mocks.iter().for_each(|m| m.assert());
Ok(())
}
#[test]
fn tool_load_trust_settings_from_url_env_trusted() -> Result<(), Box<dyn Error>> {
let server = MockServer::start();
let mocks = create_mock_server(&server, "trust/anchors.pem", "trust/store.cfg");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("trust")
.env("C2PATOOL_TRUST_ANCHORS", server.url("/trust/anchors.pem"))
.env("C2PATOOL_TRUST_CONFIG", server.url("/trust/store.cfg"))
.assert()
.success()
.stdout(str::contains("C2PA Test Signing Cert"))
.stdout(str::contains("signingCredential.untrusted").not());
mocks.iter().for_each(|m| m.assert());
Ok(())
}
#[test]
fn tool_load_trust_settings_from_url_env_untrusted() -> Result<(), Box<dyn Error>> {
let server = MockServer::start();
let mocks = create_mock_server(&server, "trust/no-match.pem", "trust/store.cfg");
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("trust")
.env("C2PATOOL_TRUST_ANCHORS", server.url("/trust/anchors.pem"))
.env("C2PATOOL_TRUST_CONFIG", server.url("/trust/store.cfg"))
.assert()
.success()
.stdout(str::contains("C2PA Test Signing Cert"))
.stdout(str::contains("signingCredential.untrusted"));
mocks.iter().for_each(|m| m.assert());
Ok(())
}
#[test]
fn tool_tree() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.arg("--tree")
.assert()
.success()
.stdout(str::contains("Asset:C.jpg, Manifest:contentauth:urn:uuid:"))
.stdout(str::contains("Assertion:c2pa.actions"));
Ok(())
}
#[test]
fn tool_read_image_with_cawg_data() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg("--settings")
.arg(fixture_path("trust/cawg_test_settings.toml"))
.arg(fixture_path("C_with_CAWG_data.jpg"))
.assert()
.success()
.stdout(str::contains("cawg.identity"))
.stdout(str::contains("c2pa.assertions/cawg.training-mining"))
.stdout(str::contains("cawg.identity.well-formed"));
Ok(())
}
#[test]
fn tool_read_image_with_details_with_cawg_data() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg("--settings")
.arg(fixture_path("trust/cawg_test_settings.toml"))
.arg(fixture_path("C_with_CAWG_data.jpg"))
.arg("--detailed")
.assert()
.success()
.stdout(str::contains("assertion_store"))
.stdout(str::contains("cawg.identity"))
.stdout(str::contains("c2pa.assertions/cawg.training-mining"))
.stdout(str::contains("cawg.identity.well-formed"));
Ok(())
}
#[test]
fn tool_sign_image_with_cawg_data() -> Result<(), Box<dyn Error>> {
let tmp_dir = tempdir()?;
let file_path = tmp_dir.path().join("same_image.jpg");
fs::copy(fixture_path(TEST_IMAGE), &file_path)?;
let output_path = tmp_dir.path().join("same_image_cawg_signed.jpg");
Command::new(cargo::cargo_bin!("c2patool"))
.arg("--settings")
.arg(fixture_path("trust/cawg_sign_settings.toml"))
.arg(&file_path)
.arg("-m")
.arg(fixture_path("ingredient_test.json"))
.arg("-o")
.arg(&output_path)
.arg("-f")
.assert()
.success();
Command::new(cargo::cargo_bin!("c2patool"))
.arg("--settings")
.arg(fixture_path("trust/cawg_sign_settings.toml"))
.arg(&output_path)
.assert()
.success()
.stdout(str::contains("cawg.identity"))
.stdout(str::contains("c2pa.assertions/cawg.training-mining"));
Ok(())
}
#[test]
fn tool_read_image_crjson() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg("--crjson")
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
.assert()
.success()
.stdout(str::contains("\"jsonGenerator\""))
.stdout(str::contains("https://c2pa.org/crjson"));
Ok(())
}
const MINIMAL_MANIFEST: &str = r#"{"assertions": []}"#;
#[test]
fn intent_create_adds_created_action() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE))
.arg("--create")
.arg("digitalCapture")
.arg("-c")
.arg(MINIMAL_MANIFEST)
.arg("-o")
.arg(temp_path("intent_create_out.jpg"))
.arg("-f")
.assert()
.success()
.stdout(str::contains("c2pa.created"))
.stdout(str::contains("digitalCapture"))
.stdout(str::contains("parentOf").not());
Ok(())
}
#[test]
fn intent_create_rejects_parent_flag() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE))
.arg("--create")
.arg("digitalCapture")
.arg("--parent")
.arg(fixture_path(TEST_IMAGE))
.arg("-c")
.arg(MINIMAL_MANIFEST)
.arg("-o")
.arg(temp_path("intent_create_parent_out.jpg"))
.arg("-f")
.assert()
.failure()
.stderr(str::contains("cannot be used with"));
Ok(())
}
#[test]
fn intent_edit_default_adds_parent_and_opened_action() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE))
.arg("-c")
.arg(MINIMAL_MANIFEST)
.arg("-o")
.arg(temp_path("intent_edit_out.jpg"))
.arg("-f")
.assert()
.success()
.stdout(str::contains("c2pa.opened"))
.stdout(str::contains("parentOf"));
Ok(())
}
#[test]
fn intent_update_adds_parent_and_opened_action() -> Result<(), Box<dyn Error>> {
Command::new(cargo::cargo_bin!("c2patool"))
.arg(fixture_path(TEST_IMAGE_WITH_MANIFEST)) .arg("--update")
.arg("-c")
.arg(MINIMAL_MANIFEST)
.arg("-o")
.arg(temp_path("intent_update_out.jpg"))
.arg("-f")
.assert()
.success()
.stdout(str::contains("c2pa.opened"))
.stdout(str::contains("parentOf"));
Ok(())
}