commit f7d0d145748c6b6ab45691ebe0464a0618a6f4c4
parent 0a07140755363c7c3bbb9bb8ca4576c39dba5a32
Author: Gavin Peacock <gpeacock@adobe.com>
Date: Thu, 25 May 2023 14:00:43 -0700
Add type exports via JSON Schema (#255)
* Add `ResourceStore.resources()` and initial JSON schema support
* Add export schema binary
* Make sure we export the ManifestStore
* Write ManifestStore JSON schema to file
---------
Co-authored-by: Dave Kozma <dkozma@adobe.com>
Diffstat:
17 files changed, 110 insertions(+), 8 deletions(-)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
@@ -9,6 +9,11 @@ updates:
interval: "daily"
- package-ecosystem: "cargo"
+ directory: "export_schema"
+ schedule:
+ interval: "daily"
+
+ - package-ecosystem: "cargo"
directory: "make_test_images"
schedule:
interval: "daily"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
@@ -75,6 +75,7 @@ jobs:
- name: Bump crate versions
run: |
sed -i "s/^version = \"[^\"]*\"$/version = \"$VERSION\"/;" sdk/Cargo.toml
+ sed -i "s/^version = \"[^\"]*\"$/version = \"$VERSION\"/;" export_schema/Cargo.toml
sed -i "s/^version = \"[^\"]*\"$/version = \"$VERSION\"/;" make_test_images/Cargo.toml
sed -i "s/^c2pa = \"[^\"]*\"$/c2pa = \"$VERSION\"/;" README.md
env:
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,3 +1,3 @@
[workspace]
resolver = "2"
-members = ["sdk", "make_test_images"]
+members = ["sdk", "export_schema", "make_test_images"]
diff --git a/Makefile b/Makefile
@@ -48,6 +48,11 @@ doc:
images:
cargo run --release --bin make_test_images
+# Exports JSON schema files so that types can easily be exported to other languages
+# Outputs to release/json-schema
+schema:
+ cargo run --release --bin export_schema
+
# Runs the client example using test image and output to target/tmp/client.jpg
client:
cargo run --example client sdk/tests/fixtures/ca.jpg target/tmp/client.jpg
diff --git a/README.md b/README.md
@@ -39,7 +39,7 @@ The SDK has been tested on the following operating systems:
## Supported file formats
| Extensions | MIME type |
- |---------------| --------------------------------------------------- |
+ | ------------- | --------------------------------------------------- |
| `avi` | `video/msvideo`, `video/avi`, `application-msvideo` |
| `avif` | `image/avif` |
| `c2pa` | `application/x-c2pa-manifest-store`, |
@@ -80,6 +80,7 @@ The Rust SDK crate provides:
* `xmp_write` enables updating XMP on embed with the `dcterms:provenance` field. (Requires [xmp_toolkit](https://crates.io/crates/xmp_toolkit).)
* `no_interleaved_io` forces fully-synchronous I/O; otherwise, the SDK uses threaded I/O for some operations to improve performance.
* `fetch_remote_manifests` enables the verification step to retrieve externally referenced manifest stores. External manifests are only fetched if there is no embedded manifest store and no locally adjacent .c2pa manifest store file of the same name.
+* `json_schema` is used by `make schema` to produce a JSON schema document that represents the `ManifestStore` data structures.
## License
diff --git a/export_schema/Cargo.toml b/export_schema/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "export_schema"
+version = "0.20.1"
+authors = ["Dave Kozma <dkozma@adobe.com>"]
+license = "MIT OR Apache-2.0"
+edition = "2018"
+rust-version = "1.65.0"
+
+[dependencies]
+anyhow = "1.0.40"
+c2pa = { path = "../sdk", features = ["file_io", "json_schema"] }
+schemars = "0.8.12"
+serde_json = "1.0"
diff --git a/export_schema/src/main.rs b/export_schema/src/main.rs
@@ -0,0 +1,19 @@
+use std::{fs, path::Path};
+
+use anyhow::Result;
+use c2pa::ManifestStore;
+use schemars::gen::SchemaSettings;
+
+fn main() -> Result<()> {
+ println!("Exporting JSON schema");
+ let settings = SchemaSettings::draft07();
+ let gen = settings.into_generator();
+ let schema = gen.into_root_schema_for::<ManifestStore>();
+ let output = serde_json::to_string_pretty(&schema).expect("Failed to serialize schema");
+ let output_dir = Path::new("./target/schema");
+ fs::create_dir_all(output_dir).expect("Could not create schema directory");
+ let output_path = output_dir.join("ManifestStore.schema.json");
+ fs::write(&output_path, output).expect("Unable to write schema");
+ println!("Wrote schema to {}", output_path.display());
+ Ok(())
+}
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
@@ -2,7 +2,13 @@
name = "c2pa"
version = "0.22.0"
description = "Rust SDK for C2PA (Coalition for Content Provenance and Authenticity) implementors"
-authors = ["Maurice Fisher <mfisher@adobe.com>", "Gavin Peacock <gpeacock@adobe.com>", "Eric Scouten <scouten@adobe.com>", "Leonard Rosenthol <lrosenth@adobe.com>", "Dave Kozma <dkozma@adobe.com>"]
+authors = [
+ "Maurice Fisher <mfisher@adobe.com>",
+ "Gavin Peacock <gpeacock@adobe.com>",
+ "Eric Scouten <scouten@adobe.com>",
+ "Leonard Rosenthol <lrosenth@adobe.com>",
+ "Dave Kozma <dkozma@adobe.com>",
+]
license = "MIT OR Apache-2.0"
documentation = "https://docs.rs/c2pa"
homepage = "https://contentauthenticity.org"
@@ -27,6 +33,7 @@ xmp_write = ["xmp_toolkit"]
no_interleaved_io = ["file_io"]
fetch_remote_manifests = ["file_io"]
openssl_sign = ["openssl"]
+json_schema = ["dep:schemars"]
# The diagnostics feature is unsupported and might be removed.
# It enables some low-overhead timing features used in our development cycle.
@@ -49,7 +56,7 @@ crate-type = ["lib"]
[dependencies]
asn1-rs = "0.5.2"
-async-trait = { version = "0.1.48"}
+async-trait = { version = "0.1.48" }
atree = "0.5.2"
base64 = "0.13.0"
bcder = "0.7.1"
@@ -57,7 +64,10 @@ blake3 = "1.0.0"
bytes = "1.4.0"
byteorder = { version = "1.4.3", default-features = false }
byteordered = "0.6.0"
-chrono = { version = "0.4.24", default-features = false, features = ["serde", "wasmbind"] }
+chrono = { version = "0.4.24", default-features = false, features = [
+ "serde",
+ "wasmbind",
+] }
ciborium = "0.2.0"
conv = "0.3.3"
coset = "0.3.1"
@@ -74,6 +84,7 @@ png_pong = "0.8.2"
range-set = "0.0.9"
ring = "0.16.20"
riff = "1.0.1"
+schemars = { version = "0.8.12", optional = true }
serde = { version = "1.0.137", features = ["derive"] }
serde_bytes = "0.11.5"
serde_cbor = "0.11.1"
@@ -110,7 +121,13 @@ serde-wasm-bindgen = "0.4.3"
spki = "0.6.0"
wasm-bindgen = "0.2.81"
wasm-bindgen-futures = "0.4.31"
-web-sys = { version = "0.3.58", features = ["Crypto", "SubtleCrypto", "CryptoKey", "Window", "WorkerGlobalScope"] }
+web-sys = { version = "0.3.58", features = [
+ "Crypto",
+ "SubtleCrypto",
+ "CryptoKey",
+ "Window",
+ "WorkerGlobalScope",
+] }
[dev-dependencies]
anyhow = "1.0.40"
diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs
@@ -11,6 +11,8 @@
// specific language governing permissions and limitations under
// each license.
+#[cfg(feature = "json_schema")]
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
@@ -25,6 +27,7 @@ const ASSERTION_CREATION_VERSION: usize = 1;
// Used to differentiate a parent from a component
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub enum Relationship {
#[serde(rename = "parentOf")]
ParentOf,
diff --git a/sdk/src/assertions/metadata.rs b/sdk/src/assertions/metadata.rs
@@ -14,6 +14,8 @@
use std::collections::HashMap;
use chrono::{SecondsFormat, Utc};
+#[cfg(feature = "json_schema")]
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -28,6 +30,7 @@ const ASSERTION_CREATION_VERSION: usize = 1;
/// The Metadata structure can be used as part of other assertions or on its own to reference others
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct Metadata {
#[serde(rename = "reviewRatings", skip_serializing_if = "Option::is_none")]
reviews: Option<Vec<ReviewRating>>,
@@ -156,6 +159,7 @@ pub mod c2pa_source {
/// A description of the source for assertion data
#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq, Eq)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct DataSource {
/// A value from among the enumerated list indicating the source of the assertion.
#[serde(rename = "type")]
@@ -194,6 +198,7 @@ impl DataSource {
/// Identifies a person responsible for an action.
#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq, Eq)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct Actor {
/// An identifier for a human actor, used when the "type" is `humanEntry.identified`.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -242,6 +247,7 @@ pub enum ReviewCode {
///
/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_claim_review>.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct ReviewRating {
pub explanation: String,
#[serde(skip_serializing_if = "Option::is_none")]
diff --git a/sdk/src/hashed_uri.rs b/sdk/src/hashed_uri.rs
@@ -13,16 +13,20 @@
use std::fmt;
+#[cfg(feature = "json_schema")]
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Hashed Uri stucture as defined by C2PA spec
/// It is annotated to produce the correctly tagged cbor serialization
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct HashedUri {
url: String, // URI stored as tagged cbor
#[serde(skip_serializing_if = "Option::is_none")]
alg: Option<String>,
#[serde(with = "serde_bytes")]
+ #[cfg_attr(feature = "json_schema", schemars(with = "Vec<u8>"))]
hash: Vec<u8>, // hash stored as cbor byte string
// salt used to generate hash
diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs
@@ -17,6 +17,8 @@ use std::path::{Path, PathBuf};
use std::{borrow::Cow, io::Cursor};
use log::{debug, error};
+#[cfg(feature = "json_schema")]
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -35,7 +37,9 @@ use crate::{
utils::xmp_inmemory_utils::XmpInfo,
validation_status::{self, status_for_store, ValidationStatus},
};
+
#[derive(Debug, Default, Deserialize, Serialize)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
/// An `Ingredient` is any external asset that has been used in the creation of an image.
pub struct Ingredient {
/// A human-readable title, generally source filename.
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -16,6 +16,8 @@ use std::{borrow::Cow, collections::HashMap, io::Cursor};
use std::{fs::create_dir_all, path::Path};
use log::{debug, error, warn};
+#[cfg(feature = "json_schema")]
+use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
@@ -37,6 +39,7 @@ use crate::{
/// A Manifest represents all the information in a c2pa manifest
#[derive(Debug, Default, Deserialize, Serialize)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct Manifest {
/// Optional prefix added to the generated Manifest Label
/// This is typically Internet domain name for the vendor (i.e. `adobe`)
@@ -67,7 +70,7 @@ pub struct Manifest {
thumbnail: Option<ResourceRef>,
/// A List of ingredients
- #[serde(default = "default_vec")]
+ #[serde(default = "default_vec::<Ingredient>")]
ingredients: Vec<Ingredient>,
/// A List of verified credentials
@@ -75,7 +78,7 @@ pub struct Manifest {
credentials: Option<Vec<Value>>,
/// A list of assertions
- #[serde(default = "default_vec")]
+ #[serde(default = "default_vec::<ManifestAssertion>")]
assertions: Vec<ManifestAssertion>,
/// A list of redactions - URIs to a redacted assertions
@@ -1008,6 +1011,7 @@ impl std::fmt::Display for Manifest {
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
/// Holds information about a signature
pub struct SignatureInfo {
/// human readable issuing authority for this signature
diff --git a/sdk/src/manifest_assertion.rs b/sdk/src/manifest_assertion.rs
@@ -1,3 +1,5 @@
+#[cfg(feature = "json_schema")]
+use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize, Serialize}; //, Deserializer, Serializer};
use serde_json::Value;
@@ -8,6 +10,7 @@ use crate::{
/// Assertions in C2PA can be stored in several formats
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub enum ManifestAssertionKind {
Cbor,
Json,
@@ -16,6 +19,7 @@ pub enum ManifestAssertionKind {
}
#[derive(Debug, Deserialize, Serialize, Clone)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[serde(untagged)]
enum ManifestData {
Json(Value), // { label: String, instance: usize, data: Value },
@@ -23,6 +27,7 @@ enum ManifestData {
}
#[derive(Debug, Deserialize, Serialize, Clone)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
/// A labeled container for an Assertion value in a Manifest
pub struct ManifestAssertion {
/// An assertion label in reverse domain format
diff --git a/sdk/src/manifest_store.rs b/sdk/src/manifest_store.rs
@@ -15,6 +15,8 @@ use std::collections::HashMap;
#[cfg(feature = "file_io")]
use std::path::Path;
+#[cfg(feature = "json_schema")]
+use schemars::JsonSchema;
use serde::Serialize;
use crate::{
@@ -26,6 +28,7 @@ use crate::{
};
#[derive(Serialize)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
/// A Container for a set of Manifests and a ValidationStatus list
pub struct ManifestStore {
#[serde(skip_serializing_if = "Option::is_none")]
diff --git a/sdk/src/resource_store.rs b/sdk/src/resource_store.rs
@@ -15,6 +15,8 @@
use std::path::{Path, PathBuf};
use std::{borrow::Cow, collections::HashMap};
+#[cfg(feature = "json_schema")]
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{Error, Result};
@@ -28,6 +30,7 @@ pub(crate) fn skip_serializing_resources(_: &ResourceStore) -> bool {
/// A reference to a resource to be used in JSON serialization
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct ResourceRef {
pub format: String,
pub identifier: String,
@@ -44,9 +47,11 @@ impl ResourceRef {
/// Resource store to contain binary objects referenced from JSON serializable structures
#[derive(Debug, Serialize)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct ResourceStore {
resources: HashMap<String, Vec<u8>>,
#[cfg(feature = "file_io")]
+ #[serde(skip_serializing_if = "Option::is_none")]
base_path: Option<PathBuf>,
}
@@ -125,6 +130,10 @@ impl ResourceStore {
Ok(())
}
+ pub fn resources(&self) -> &HashMap<String, Vec<u8>> {
+ &self.resources
+ }
+
/// Returns a copy on write reference to the resource if found.
///
/// returns Error::NotFound if it cannot find a resource matching that id
diff --git a/sdk/src/validation_status.rs b/sdk/src/validation_status.rs
@@ -18,6 +18,8 @@
#![deny(missing_docs)]
use log::debug;
+#[cfg(feature = "json_schema")]
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
@@ -34,6 +36,7 @@ use crate::{
///
/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_existing_manifests>.
#[derive(Clone, Debug, Deserialize, Serialize)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct ValidationStatus {
code: String,