commit 67c206e317131b0270ffdea9a510ef3883b72dc5
parent 36493ab66c398d3a8eb70a945d04919a8f4fe49f
Author: Gavin Peacock <gpeacock@adobe.com>
Date: Thu, 2 Jun 2022 08:52:17 -0700
Convert make_tests into a scriptable engine; rename to make_test_images (#29)
* Move make_tests to bin and drive it from a config file
* doc updates, change tsa to ta
* add dependabot support for make_tests
* remove unused dependencies
* Add a make_tests unit test
* Change a short name to longer awkward one
Diffstat:
11 files changed, 501 insertions(+), 410 deletions(-)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
@@ -12,3 +12,8 @@ updates:
directory: "c2patool"
schedule:
interval: "daily"
+
+ - package-ecosystem: "cargo"
+ directory: "make_test_images"
+ schedule:
+ interval: "daily"
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,2 +1,2 @@
[workspace]
-members = ["sdk", "c2patool"]
+members = ["sdk", "c2patool", "make_test_images"]
diff --git a/Makefile b/Makefile
@@ -80,10 +80,10 @@ endif
doc:
cargo doc --no-deps --open
-# Builds a set of test images using the make_tests example
+# Builds a set of test images using the make_test_images example
# Outputs to release/test-images
images:
- cargo run --release --example make_tests
+ cargo run --release --example make_test_images
# Runs the client example using test image and output to target/tmp/client.jpg
client:
diff --git a/make_test_images/Cargo.toml b/make_test_images/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "make_test_images"
+version = "0.1.0"
+authors = ["Gavin Peacock <gpeacock@adobe.com>"]
+license = "MIT OR Apache-2.0"
+edition = "2018"
+rust-version = "1.58.0"
+
+[dependencies]
+anyhow = "1.0"
+c2pa = { path="../sdk", features = ["file_io"] }
+env_logger = "0.9"
+log = "0.4"
+tempfile = "3.3"
+image = "0.23.10"
+nom = "7.1.1"
+regex = "1.5.6"
+serde = "1.0.137"
+serde_json = "1.0.81"
+twoway = "0.2.2"
+
+
diff --git a/make_test_images/src/main.rs b/make_test_images/src/main.rs
@@ -0,0 +1,38 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+//! This generates a set of test images with a wide variety of configurations
+//! To run this, use the following command in a terminal
+//! cargo run --release --bin make_test_images
+//!
+mod make_test_images;
+use anyhow::{Context, Result};
+
+fn main() -> Result<()> {
+ let args: Vec<String> = std::env::args().collect();
+ let path = if args.len() > 1 {
+ args[1].as_ref()
+ } else {
+ "make_test_images/tests.json"
+ };
+ let buf = std::fs::read_to_string(&path).context(format!("Reading {}", path))?;
+ let config: make_test_images::Config =
+ serde_json::from_str(&buf).context("Config file format")?;
+
+ // set RUST_LOG=debug to get detailed debug logging
+ env_logger::init();
+
+ make_test_images::MakeTestImages::new(config).run()?;
+
+ Ok(())
+}
diff --git a/make_test_images/src/make_test_images.rs b/make_test_images/src/make_test_images.rs
@@ -0,0 +1,400 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+//! Constructs a set of test images using a configuration script
+//!
+use c2pa::{
+ assertions::{c2pa_action, Action, Actions, CreativeWork, SchemaDotOrgPerson},
+ jumbf_io,
+ openssl::temp_signer::get_signer_by_alg,
+ Error, Ingredient, IngredientOptions, Manifest, ManifestStore,
+};
+
+use anyhow::{Context, Result};
+use image::GenericImageView;
+use nom::AsBytes;
+use serde::Deserialize;
+use std::{
+ fs,
+ path::{Path, PathBuf},
+};
+use tempfile::tempdir;
+use twoway::find_bytes;
+
+const IMAGE_WIDTH: u32 = 2048;
+const IMAGE_HEIGHT: u32 = 1365;
+
+/// Defines an operation for creating a test image
+#[derive(Debug, Deserialize)]
+pub struct Recipe {
+ /// The operation to perform:
+ ///
+ /// One of: "copy", "make", "ogp", "dat", "sig", "uri", "clm", "prv"
+ pub op: String,
+ /// Path or filename of parent
+ ///
+ /// Assumes output folder if no path
+ /// Will add default extension if non specified
+ pub parent: Option<String>,
+ /// A list of Ingredient paths
+ ///
+ /// Assumes output folder if no path
+ /// Will add default extension if non specified
+ pub ingredients: Option<Vec<String>>,
+ /// The folder to write files to, will create if it does not exist
+ pub output: String,
+}
+
+/// Configuration
+#[derive(Debug, Deserialize)]
+#[serde(default)]
+pub struct Config {
+ /// The signing algorithm to use
+ pub alg: String,
+ /// A url to a time authority if desired
+ pub ta: Option<String>,
+ /// The output folder for the generated files
+ pub output_path: String,
+ /// Extension to add to filenames if none was given
+ pub default_ext: String,
+ /// A name for a Creative Work Author assertion
+ pub author: Option<String>,
+ /// A list of recipes for test files
+ pub recipes: Vec<Recipe>,
+}
+
+// Defaults for Config
+impl Default for Config {
+ fn default() -> Self {
+ Self {
+ alg: "ps256".to_owned(),
+ ta: None,
+ output_path: "target/images".to_owned(),
+ default_ext: "jpg".to_owned(),
+ author: None,
+ recipes: Vec::new(),
+ }
+ }
+}
+
+/// Tool for building test case images for C2PA
+pub struct MakeTestImages {
+ config: Config,
+ output_dir: PathBuf,
+}
+
+impl MakeTestImages {
+ pub fn new(config: Config) -> Self {
+ let output = config.output_path.to_owned();
+ Self {
+ config,
+ output_dir: PathBuf::from(output),
+ }
+ }
+
+ /// Makes a full path from a filename or path
+ ///
+ /// If there is no parent, prepend the output path
+ /// If there is no extension, use the default
+ fn make_path(&self, s: &str) -> PathBuf {
+ let mut path_buf = PathBuf::from(s);
+ // parent() tends to return an empty string instead of None
+ let has_path = match path_buf.parent() {
+ Some(p) => p.to_string_lossy().len() > 0,
+ None => false,
+ };
+ // if we just have a filename, then assume it is in the output folder
+ if !has_path {
+ path_buf = PathBuf::from(&self.output_dir);
+ path_buf.push(s);
+ }
+ // add the default extension is none is supplied
+ if path_buf.extension().is_none() {
+ path_buf.set_extension(&self.config.default_ext);
+ }
+ path_buf
+ }
+
+ /// Patches new content into a file
+ ///
+ /// # Parameters
+ /// path - path to file to be patched
+ /// search_bytes - bytes to be replaced
+ /// replace_bytes - replacement bytes
+ fn patch_file(path: &std::path::Path, search_bytes: &[u8], replace_bytes: &[u8]) -> Result<()> {
+ let mut buf = fs::read(path)?;
+
+ if let Some(splice_start) = find_bytes(&buf, search_bytes) {
+ buf.splice(
+ splice_start..splice_start + search_bytes.len(),
+ replace_bytes.iter().cloned(),
+ );
+ } else {
+ return Err(Error::NotFound.into());
+ }
+
+ fs::write(path, &buf)?;
+
+ Ok(())
+ }
+
+ /// Creates a test image with optional source and ingredients, out to dest
+ fn make_image(&self, recipe: &Recipe) -> Result<PathBuf> {
+ let src = recipe.parent.as_deref();
+ let dst_path = self.make_path(&recipe.output);
+ println!("Creating {:?}", dst_path);
+ // keep track of all actions here
+ let mut actions = Actions::new();
+
+ let options = IngredientOptions {
+ make_hash: true,
+ title: None,
+ };
+
+ let generator = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
+ let mut manifest = Manifest::new(generator);
+ manifest.set_vendor("contentauth".to_owned()); // needed for generating error cases below
+
+ if let Some(user) = self.config.author.as_ref() {
+ let creative_work = CreativeWork::new()
+ .add_author(SchemaDotOrgPerson::new().set_name(user.to_owned())?)?;
+
+ manifest.add_assertion(&creative_work)?;
+ }
+
+ // process parent first
+ let mut img = match src {
+ Some(src) => {
+ let src_path = &self.make_path(src);
+
+ let parent = Ingredient::from_file_with_options(src_path, &options)?;
+ actions.add_action(
+ Action::new(c2pa_action::OPENED)
+ .set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?,
+ );
+ manifest.set_parent(parent)?;
+
+ // load the image for editing
+ let mut img =
+ image::open(&src_path).context(format!("opening parent {:?}", src_path))?;
+
+ // adjust brightness to show we made an edit
+ img = img.brighten(30);
+ actions.add_action(
+ Action::new(c2pa_action::COLOR_ADJUSTMENTS)
+ .set_parameter("name".to_owned(), "brightnesscontrast")?,
+ );
+ img
+ }
+ None => {
+ // create a default image with a gradient
+ let mut img = image::DynamicImage::new_rgb8(IMAGE_WIDTH, IMAGE_HEIGHT);
+ if let Some(img_ref) = img.as_mut_rgb8() {
+ // fill image with a gradient
+ for (x, y, pixel) in img_ref.enumerate_pixels_mut() {
+ let r = (0.3 * x as f32) as u8;
+ let b = (0.3 * y as f32) as u8;
+ *pixel = image::Rgb([r, 100, b]);
+ }
+ }
+ actions
+ .add_action(Action::new(c2pa_action::CREATED))
+ .add_action(
+ Action::new(c2pa_action::DRAWING)
+ .set_parameter("name".to_owned(), "gradient")?,
+ );
+
+ img
+ }
+ };
+
+ // then add all ingredients
+ if let Some(ing_vec) = &recipe.ingredients {
+ // scale ingredients to paste in top row of the image
+ let width = match ing_vec.len() as u32 {
+ 0 | 1 => img.width() / 2,
+ _ => img.width() / ing_vec.len() as u32,
+ };
+ let height = img.height() as u32 / 2;
+
+ let mut x = 0;
+ for ing in ing_vec {
+ let ing_path = &self.make_path(ing);
+
+ // get the bits of the ingredient, resize it and overlay it on the base image
+ let img_ingredient =
+ image::open(&ing_path).context(format!("opening ingredient {:?}", ing_path))?;
+ let img_small = img_ingredient.thumbnail(width, height);
+ image::imageops::overlay(&mut img, &img_small, x, 0);
+
+ // create and add the ingredient
+ let ingredient = Ingredient::from_file_with_options(ing_path, &options)?;
+ actions.add_action(
+ Action::new(c2pa_action::PLACED).set_parameter(
+ "identifier".to_owned(),
+ ingredient.instance_id().to_owned(),
+ )?,
+ );
+ manifest.add_ingredient(ingredient);
+
+ x += width;
+ }
+ // record what we did as an action (only need to record this once)
+ actions.add_action(Action::new(c2pa_action::RESIZED));
+ }
+
+ // save the changes to the image as our target file
+ img.save(&dst_path)?;
+
+ // add all our actions as an assertion now.
+ manifest.add_assertion(&actions)?; // extra get required here, since actions is an array
+
+ // now create store; sign claim and embed in target
+ let temp_dir = tempdir()?;
+ let (signer, _) =
+ get_signer_by_alg(&temp_dir.path(), &self.config.alg, self.config.ta.clone());
+
+ manifest.embed(&dst_path, &dst_path, signer.as_ref())?;
+
+ Ok(dst_path)
+ }
+
+ /// makes an off the golden path image from an existing image with a claim
+ fn make_ogp(&self, recipe: &Recipe) -> Result<PathBuf> {
+ let src = recipe.parent.as_deref().unwrap_or_default();
+ let src_path = &self.make_path(src);
+ let dst_path = self.make_path(recipe.output.as_str());
+ println!("Creating OGP {:?}", dst_path);
+
+ let jumbf = jumbf_io::load_jumbf_from_file(&PathBuf::from(src_path))
+ .context(format!("loading OGP {:?}", src_path))?;
+ // save the edited image to our destination file
+ let mut img = image::open(&Path::new(src_path))
+ .context(format!("loading OGP image{:?}", src_path))?;
+ img = img.grayscale();
+ img.save(&dst_path)
+ .context(format!("saving OGP image{:?}", &dst_path))?;
+ // write the original claim data to the edited image
+ jumbf_io::save_jumbf_to_file(&jumbf, &PathBuf::from(&dst_path), Some(&dst_path))
+ .context(format!("OGP save_jumbf_to_file {:?}", &dst_path))?;
+ // The image library does not preserve any metadata so we have to write it ourselves.
+ // todo: should preserve all metadata and update instanceId.
+ Ok(dst_path)
+ }
+
+ /// Generates various error conditions
+ fn make_err(&self, recipe: &Recipe) -> Result<PathBuf> {
+ let op = recipe.op.as_str();
+ let src = recipe.parent.as_deref().unwrap_or_default();
+ let dst_path = self.make_path(recipe.output.as_str());
+ println!("Creating Error op={} {:?}", op, dst_path);
+
+ let (search_bytes, replace_bytes) = match op {
+ // modify the XMP (change xmp magic id value) - this should cause a data hash mismatch (OTGP)
+ "dat" => (
+ b"W5M0MpCehiHzreSzNTczkc9d".as_bytes(),
+ b"W5M0MpCehiHzreSzdeadbeef".as_bytes(),
+ ),
+ // modify the claim_generator value inside the claim, the claim hash will no longer match the signature
+ "sig" => (
+ b"make_test_images".as_bytes(),
+ b"make_test_xxxxxx".as_bytes(),
+ ),
+ // modify a value inside an actions assertion, the assertion hash will fail
+ "uri" => (
+ b"brightnesscontrast".as_bytes(),
+ b"brightnessdeadbeef".as_bytes(),
+ ),
+ // modify a uri to a manifest so the manifest cannot be found (missing manifest)
+ "clm" => (
+ b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth".as_bytes(),
+ b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentbeef".as_bytes(),
+ ),
+ // modify the provenance uri so that is references a non-existing manifest
+ "prv" => (
+ b"dcterms:provenance=\"self#jumbf=/c2pa/contentauth".as_bytes(),
+ b"dcterms:provenance=\"self#jumbf=/c2pa/contentbeef".as_bytes(),
+ ),
+ _ => panic!("bad parameter"),
+ };
+
+ std::fs::copy(&self.make_path(src), &dst_path).context("copying for make_err")?;
+
+ Self::patch_file(&dst_path, search_bytes, replace_bytes)
+ .context(format!("patching {}", op))?;
+
+ Ok(dst_path)
+ }
+
+ /// copies a file from the parent to the output
+ fn make_copy(&self, recipe: &Recipe) -> Result<PathBuf> {
+ let dst_path = self.make_path(recipe.output.as_str());
+ println!("Copying {:?}", dst_path);
+ let src = recipe.parent.as_deref().unwrap_or_default();
+ let dst = recipe.output.as_str();
+ std::fs::copy(src, &dst_path).context(format!("copying {} to {}", src, dst))?;
+ Ok(dst_path)
+ }
+
+ /// Runs a list of recipes
+ pub fn run(&self) -> Result<()> {
+ if !self.output_dir.exists() {
+ std::fs::create_dir_all(&self.output_dir).context("Can't create output folder")?;
+ };
+
+ let recipes = &self.config.recipes;
+ for recipe in recipes {
+ let dst_path = match recipe.op.as_str() {
+ "make" => self.make_image(recipe)?,
+ "ogp" => self.make_ogp(recipe)?,
+ "dat" | "sig" | "uri" | "clm" | "prv" => self.make_err(recipe)?,
+ "copy" => self.make_copy(recipe)?,
+ _ => return Err(Error::BadParam(recipe.op.to_string()).into()),
+ };
+ let manifest_store = ManifestStore::from_file(&dst_path);
+
+ if recipe.op.as_str() != "copy" {
+ println!("{}", manifest_store?);
+ }
+ }
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+
+ use super::*;
+ const TESTS: &str = r#"{
+ "alg": "ps256",
+ "tsa": "http://timestamp.digicert.com",
+ "output_path": "../target/tmp",
+ "default_ext": "jpg",
+ "author": "Gavin Peacock",
+ "recipes": [
+ { "op": "copy", "parent": "../sdk/tests/fixtures/IMG_0003.jpg", "output": "A.jpg" },
+ { "op": "make", "output": "C" },
+ { "op": "ogp", "parent": "C", "output": "XC" },
+ { "op": "sig", "parent": "C", "output": "E-sig-C" }
+ ]
+ }"#;
+
+ #[test]
+ fn test_make_images() {
+ let config: Config = serde_json::from_str(TESTS)
+ .context("Config file format")
+ .expect("serde_json");
+ MakeTestImages::new(config).run().expect("running");
+ }
+}
diff --git a/make_test_images/tests.json b/make_test_images/tests.json
@@ -0,0 +1,31 @@
+{
+ "alg": "ps256",
+ "tsa": "http://timestamp.digicert.com",
+ "output_path": "target/images",
+ "default_ext": "jpg",
+ "author": "Gavin Peacock",
+ "recipes": [
+ { "op": "copy", "parent": "sdk/tests/fixtures/IMG_0003.jpg", "output": "A.jpg" },
+ { "op": "copy", "parent": "sdk/tests/fixtures/P1000827.jpg", "output": "I.jpg" },
+ { "op": "make", "output": "C" },
+ { "op": "make", "parent": "A.jpg", "output": "CA" },
+ { "op": "make", "parent": "CA", "output": "CACA" },
+ { "op": "make", "ingredients": ["I.jpg"], "output": "CI" },
+ { "op": "make", "ingredients": ["I.jpg","I.jpg"], "output": "CII" },
+ { "op": "make", "parent": "A.jpg", "ingredients": ["I.jpg"], "output": "CAI" },
+ { "op": "make", "parent": "A.jpg", "ingredients": ["CA"], "output": "CAICA" },
+ { "op": "make", "ingredients": ["CA"], "output": "CICA" },
+ { "op": "make", "parent": "A.jpg", "ingredients": ["CAI"], "output": "CAICAI" },
+ { "op": "make", "parent": "CAICA", "ingredients": ["CICA"], "output": "CACAICAICICA" },
+ { "op": "make", "ingredients": ["CA","CA","CA"], "output": "CICACACA" },
+ { "op": "ogp", "parent": "CA", "output": "XCA" },
+ { "op": "ogp", "parent": "CI", "output": "XCI" },
+ { "op": "dat", "parent": "CA", "output": "E-dat-CA" },
+ { "op": "sig", "parent": "CA", "output": "E-sig-CA" },
+ { "op": "uri", "parent": "CA", "output": "E-uri-CA" },
+ { "op": "clm", "parent": "CAICAI", "output": "E-clm-CAICAI" },
+ { "op": "make", "ingredient": ["E-sig-CA"], "output": "CIE-sig-CA" },
+ { "op": "uri", "parent": "CIE-sig-CA", "output": "E-uri-CIE-sig-CA" },
+ { "op": "make", "parent": "A.jpg", "ingredients": ["C", "A.jpg", "I.jpg", "CA", "CI", "CAI", "CICA"], "output": "CAIAIIICAICIICAIICICA" }
+ ]
+}
+\ No newline at end of file
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
@@ -81,7 +81,6 @@ web-sys = { version = "0.3.54", features = ["Crypto", "SubtleCrypto", "CryptoKey
[dev-dependencies]
anyhow = "1.0.40"
-env_logger = "0.7.1"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3.0"
diff --git a/sdk/examples/make_tests/main.rs b/sdk/examples/make_tests/main.rs
@@ -1,43 +0,0 @@
-// Copyright 2022 Adobe. All rights reserved.
-// This file is licensed to you under the Apache License,
-// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
-// or the MIT license (http://opensource.org/licenses/MIT),
-// at your option.
-
-// Unless required by applicable law or agreed to in writing,
-// this software is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
-// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
-// specific language governing permissions and limitations under
-// each license.
-
-//! This generates a set of test images with a wide variety of configurations
-//! To run this, use the following command in a terminal
-//! cargo run --release --example make_tests
-//!
-use anyhow::Result;
-use std::path::PathBuf;
-// The make_tests sample is not designed to work with a wasm build
-// so we provide a wasm stub here and only include the module for non wasm
-#[cfg(not(target_arch = "wasm32"))]
-mod make_tests;
-#[cfg(not(target_arch = "wasm32"))]
-use crate::make_tests::make_tests;
-
-#[cfg(target_arch = "wasm32")]
-fn make_tests(_output_folder: &std::path::Path, _alg: &str, _tsa: Option<String>) -> Result<()> {
- panic!("Not implemented for wasm");
-}
-
-const TARGET_FOLDER: &str = "target/test_images";
-fn main() -> Result<()> {
- // set RUST_LOG=debug to get detailed debug logging
- env_logger::init();
-
- // choose a timestamp service authority
- let tsa = Some("http://timestamp.digicert.com".to_string());
-
- make_tests(&PathBuf::from(TARGET_FOLDER), "ps256", tsa)?;
-
- Ok(())
-}
diff --git a/sdk/examples/make_tests/make_tests.rs b/sdk/examples/make_tests/make_tests.rs
@@ -1,361 +0,0 @@
-// Copyright 2022 Adobe. All rights reserved.
-// This file is licensed to you under the Apache License,
-// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
-// or the MIT license (http://opensource.org/licenses/MIT),
-// at your option.
-
-// Unless required by applicable law or agreed to in writing,
-// this software is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
-// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
-// specific language governing permissions and limitations under
-// each license.
-
-use anyhow::{Context, Result};
-
-use c2pa::{
- assertions::{c2pa_action, Action, Actions, CreativeWork, SchemaDotOrgPerson},
- jumbf_io,
- openssl::temp_signer::get_signer_by_alg,
- Error, Ingredient, IngredientOptions, Manifest, ManifestStore,
-};
-
-use image::GenericImageView;
-use nom::AsBytes;
-use tempfile::tempdir;
-use twoway::find_bytes;
-
-use std::{
- fs,
- path::{Path, PathBuf},
-};
-
-const GENERATOR: &str = "make_tests";
-const USER: &str = "Joe Bloggs";
-
-const IMAGE_WIDTH: u32 = 2048;
-const IMAGE_HEIGHT: u32 = 1365;
-
-/**
-Patch new content into a file
-path - path to file to be patched
-search_bytes - bytes to be replaced
-replace_bytes - replacement bytes
-*/
-pub fn patch_file(path: &std::path::Path, search_bytes: &[u8], replace_bytes: &[u8]) -> Result<()> {
- let mut buf = fs::read(path)?;
-
- if let Some(splice_start) = find_bytes(&buf, search_bytes) {
- buf.splice(
- splice_start..splice_start + search_bytes.len(),
- replace_bytes.iter().cloned(),
- );
- } else {
- return Err(Error::NotFound.into());
- }
-
- fs::write(path, &buf)?;
-
- Ok(())
-}
-
-pub struct MakeTests {
- output_dir: PathBuf,
-}
-
-impl MakeTests {
- pub fn new(path: &Path) -> Self {
- Self {
- output_dir: PathBuf::from(path),
- }
- }
-
- pub fn output_dir(&self) -> PathBuf {
- self.output_dir.to_owned()
- }
-
- fn make_path(&self, s: &str) -> PathBuf {
- //let output_dir = unsafe { OUTPUT_FOLDER.as_ref().unwrap().lock().unwrap().to_string() };
- let mut path_buf = PathBuf::from(&self.output_dir);
- path_buf.push(s);
- if path_buf.extension().is_none() {
- path_buf.set_extension("jpg");
- }
- path_buf
- }
-
- // create a test image with optional source and ingredients, out to dest
- pub fn make_image(
- &self,
- src: Option<&str>,
- ing: Option<&Vec<&str>>,
- dst: &str,
- alg: &str,
- tsa: Option<String>,
- ) -> Result<()> {
- let dst_path = &self.make_path(dst);
- println!("creating {:?}", dst_path);
- // keep track of all actions here
- let mut actions = Actions::new();
-
- let options = IngredientOptions {
- make_hash: true,
- title: None,
- };
-
- let mut manifest = Manifest::new(GENERATOR.to_string());
- manifest.set_vendor("contentauth".to_owned()); // needed for generating error cases below
-
- let creative_work =
- CreativeWork::new().add_author(SchemaDotOrgPerson::new().set_name(USER.to_owned())?)?;
-
- manifest.add_assertion(&creative_work)?;
-
- // process parent first
- let mut img = match src {
- Some(src) => {
- let src_path = &self.make_path(src);
-
- let parent = Ingredient::from_file_with_options(src_path, &options)?;
- actions.add_action(
- Action::new(c2pa_action::OPENED)
- .set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?,
- );
- manifest.set_parent(parent)?;
-
- // load the image for editing
- let mut img =
- image::open(&src_path).context(format!("opening image A {:?}", src_path))?;
-
- // adjust brightness to show we made an edit
- img = img.brighten(30);
- actions.add_action(
- Action::new(c2pa_action::COLOR_ADJUSTMENTS)
- .set_parameter("name".to_owned(), "brightnesscontrast")?,
- );
- img
- }
- None => {
- // create a default image with a gradient
- let mut img = image::DynamicImage::new_rgb8(IMAGE_WIDTH, IMAGE_HEIGHT);
- if let Some(img_ref) = img.as_mut_rgb8() {
- // fill image with a gradient
- for (x, y, pixel) in img_ref.enumerate_pixels_mut() {
- let r = (0.3 * x as f32) as u8;
- let b = (0.3 * y as f32) as u8;
- *pixel = image::Rgb([r, 100, b]);
- }
- }
- actions
- .add_action(Action::new(c2pa_action::CREATED))
- .add_action(
- Action::new(c2pa_action::DRAWING)
- .set_parameter("name".to_owned(), "gradient")?,
- );
-
- img
- }
- };
-
- // then add all ingredients
- if let Some(ing_vec) = ing {
- // scale ingredients to paste in top row of the image
- let width = match ing_vec.len() as u32 {
- 0 | 1 => img.width() / 2,
- _ => img.width() / ing_vec.len() as u32,
- };
- let height = img.height() as u32 / 2;
-
- let mut x = 0;
- for ing in ing_vec {
- let ing_path = &self.make_path(ing);
-
- // get the bits of the ingredient, resize it and overlay it on the base image
- let img_ingredient =
- image::open(&ing_path).context(format!("opening image I {:?}", ing_path))?;
- let img_small = img_ingredient.thumbnail(width, height);
- image::imageops::overlay(&mut img, &img_small, x, 0);
-
- // create and add the ingredient
- let ingredient = Ingredient::from_file_with_options(ing_path, &options)?;
- actions.add_action(
- Action::new(c2pa_action::PLACED).set_parameter(
- "identifier".to_owned(),
- ingredient.instance_id().to_owned(),
- )?,
- );
- manifest.add_ingredient(ingredient);
-
- x += width;
- }
- // record what we did as an action (only need to record this once)
- actions.add_action(Action::new(c2pa_action::RESIZED));
- }
-
- // save the changes to the image as our target file
- img.save(dst_path)?;
-
- // add all our actions as an assertion now.
- manifest.add_assertion(&actions)?; // extra get required here, since actions is an array
-
- // now create store; sign claim and embed in target
- let temp_dir = tempdir()?;
- let (signer, _) = get_signer_by_alg(&temp_dir.path(), alg, tsa);
-
- manifest.embed(dst_path, dst_path, signer.as_ref())?;
-
- println!("{}", ManifestStore::from_file(dst_path)?);
-
- Ok(())
- }
-
- // make an off the golden path image from an existing image with a claim
- fn make_ogp(&self, src: &str, dst: &str) -> Result<()> {
- println!("creating OGP {}", dst);
- let src_path = &self.make_path(src);
- let dst_path = &self.make_path(dst);
- let jumbf = jumbf_io::load_jumbf_from_file(&PathBuf::from(src_path))
- .context(format!("loading OGP {:?}", src_path))?;
- // save the edited image to our destination file
- let mut img = image::open(&Path::new(src_path))
- .context(format!("loading OGP image{:?}", src_path))?;
- img = img.grayscale();
- img.save(dst_path)
- .context(format!("saving OGP image{:?}", dst_path))?;
- // write the original claim data to the edited image
- jumbf_io::save_jumbf_to_file(
- &jumbf,
- &PathBuf::from(dst_path),
- Some(&PathBuf::from(dst_path)),
- )
- .context(format!("OGP save_jumbf_to_file {:?}", dst_path))?;
- // The image library does not preserve any metadata so we have to write it ourselves.
- // todo: should preserve all metadata and update instanceId.
- Ok(())
- }
-
- fn make_err(&self, src: &str, err: &str) -> Result<()> {
- let (search_bytes, replace_bytes) = match err {
- // modify the XMP (change xmp magic id value) - this should cause a data hash mismatch (OTGP)
- "dat" => (
- b"W5M0MpCehiHzreSzNTczkc9d".as_bytes(),
- b"W5M0MpCehiHzreSzdeadbeef".as_bytes(),
- ),
- // modify the claim_generator value inside the claim, the claim hash will no longer match the signature
- "sig" => (b"make_tests".as_bytes(), b"make_xxxxx".as_bytes()),
- // modify a value inside an actions assertion, the assertion hash will fail
- "uri" => (
- b"brightnesscontrast".as_bytes(),
- b"brightnessdeadbeef".as_bytes(),
- ),
- // modify a uri to a manifest so the manifest cannot be found (missing manifest)
- "clm" => (
- b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth".as_bytes(),
- b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth".as_bytes(),
- ),
- // modify the provenance uri so that is references a non-existing manifest
- "prv" => (
- b"dcterms:provenance=\"self#jumbf=/c2pa/contentauth".as_bytes(),
- b"dcterms:provenance=\"self#jumbf=/c2pa/contentauth".as_bytes(),
- ),
- _ => panic!("bad parameter"),
- };
-
- let dst = format!("E-{}-{}", err, src);
- std::fs::copy(&self.make_path(src), &self.make_path(&dst))
- .context("copying for make_err")?;
- patch_file(&self.make_path(&dst), search_bytes, replace_bytes)
- .context(format!("patching {}", err))?;
-
- Ok(())
- }
-}
-
-pub fn make_tests(output_folder: &Path, alg: &str, tsa: Option<String>) -> Result<()> {
- // destination folder for this content
- let mt = MakeTests::new(output_folder);
- let data_dir = mt.output_dir();
-
- if !data_dir.exists() {
- std::fs::create_dir_all(&data_dir).expect("Can't create C2PA data directory");
- };
-
- // copy A and I source images into destination folder
- // these images should have no claims
- std::fs::copy("sdk/tests/fixtures/IMG_0003.jpg", &mt.make_path("A.jpg"))
- .context("error copying A")?;
- std::fs::copy("sdk/tests/fixtures/P1000827.jpg", &mt.make_path("I.jpg"))
- .context("error copying I")?;
-
- // --------------------------------------------------------------------
-
- //make_cai(None, Some(&vec!["PS.svg"]), "CIPS")?;
- mt.make_image(None, None, "C", alg, tsa.clone())?;
- mt.make_image(Some("A"), None, "CA", alg, tsa.clone())?;
- mt.make_image(Some("CA"), None, "CACA", alg, tsa.clone())?;
- mt.make_image(None, Some(&vec!["I"]), "CI", alg, tsa.clone())?;
- mt.make_image(None, Some(&vec!["I", "I"]), "CII", alg, tsa.clone())?;
- mt.make_image(
- None,
- Some(&vec!["I", "I", "I", "I", "I"]),
- "CIIIII",
- alg,
- tsa.clone(),
- )?;
- mt.make_image(Some("A"), Some(&vec!["I"]), "CAI", alg, tsa.clone())?;
- mt.make_image(Some("A"), Some(&vec!["CA"]), "CAICA", alg, tsa.clone())?;
- mt.make_image(None, Some(&vec!["CA"]), "CICA", alg, tsa.clone())?;
- mt.make_image(Some("CA"), Some(&vec!["CAI"]), "CAICAI", alg, tsa.clone())?;
- mt.make_image(None, Some(&vec!["CA"]), "CICA", alg, tsa.clone())?;
- mt.make_image(
- Some("CAICA"),
- Some(&vec!["CICA"]),
- "CACAICAICICA",
- alg,
- tsa.clone(),
- )?;
- mt.make_image(
- None,
- Some(&vec!["CA", "CA", "CA"]),
- "CICACACA",
- alg,
- tsa.clone(),
- )?;
-
- mt.make_ogp("CA", "XCA")?;
- mt.make_ogp("CI", "XCI")?;
- mt.make_image(Some("CA"), Some(&vec!["XCI"]), "CAIXCI", alg, tsa.clone())?;
- mt.make_image(
- Some("XCA"),
- Some(&vec!["XCI"]),
- "CAXCAIXCI",
- alg,
- tsa.clone(),
- )?;
-
- mt.make_err("CA", "dat")?;
- mt.make_err("CA", "sig")?;
- mt.make_err("CA", "uri")?;
- mt.make_err("CAICAI", "clm")?;
- mt.make_err("CA", "prv")?;
- mt.make_image(
- None,
- Some(&vec!["E-sig-CA"]),
- "CIE-sig-CA",
- alg,
- tsa.clone(),
- )?;
- // inject an assertion error into a claim that has an accepted error
- mt.make_err("CIE-sig-CA", "uri")?;
-
- mt.make_image(
- Some("A"),
- Some(&vec!["C", "A", "I", "CA", "CI", "CAI", "CICA"]),
- "CAIAIIICAICIICAIICICA",
- alg,
- tsa,
- )?;
- // // save the changes to the image and add the claim
- println!("done");
- Ok(())
-}
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
@@ -76,8 +76,7 @@ pub use error::{Error, Result};
mod ingredient;
pub use ingredient::{Ingredient, IngredientOptions};
-pub mod jumbf_io; // used by make_tests
-
+pub mod jumbf_io;
mod manifest;
pub use manifest::{Manifest, ManifestAssertion};