Skip to content
141 changes: 141 additions & 0 deletions sdk/src/assertions/asset_reference.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// 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 serde::{Deserialize, Serialize};

use crate::{
assertion::{Assertion, AssertionBase, AssertionCbor},
assertions::labels,
error::Result,
};

/// An `AssetReference` assertion provides information on one or more locations of
/// where a copy of the asset may be obtained.
///
/// This assertion contains a list of references, each one declaring a location expressed as a URI and
/// optionally a description. The URI may be either a single asset or it may reference a directory.
///
/// <https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_asset_reference>
#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub struct AssetReference {
pub references: Vec<Reference>,
}

impl AssetReference {
pub const LABEL: &'static str = labels::ASSET_REFERENCE;

/// Creates an AssetReference to a location.
pub fn new(uri: &str, description: Option<&str>) -> Self {
Self {
references: vec![Reference::new(uri, description)],
}
}

/// Adds an [`AssetReference`] to this assertion's list of references.
pub fn add_reference(mut self, uri: &str, description: Option<&str>) -> Self {
self.references.push(Reference::new(uri, description));
self
}
}

/// Defines a single location of where a copy of the asset may be obtained.
#[derive(Deserialize, Serialize, Debug, Default, PartialEq, Eq)]
pub struct Reference {
pub reference: ReferenceUri,

#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}

impl Reference {
/// Creates a new reference to a location, and optionally a description.
pub fn new(uri: &str, description: Option<&str>) -> Self {
Reference {
reference: ReferenceUri {
uri: uri.to_owned(),
},
description: description.map(String::from),
}
}
}

#[derive(Deserialize, Serialize, Debug, Default, PartialEq, Eq)]
pub struct ReferenceUri {
pub uri: String,
}

impl AssertionCbor for AssetReference {}

impl AssertionBase for AssetReference {
const LABEL: &'static str = Self::LABEL;

fn to_assertion(&self) -> Result<Assertion> {
Self::to_cbor_assertion(self)
}

fn from_assertion(assertion: &Assertion) -> Result<Self> {
Self::from_cbor_assertion(assertion)
}
}

#[cfg(test)]
pub mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]

use crate::{assertion::AssertionBase, assertions::AssetReference};

#[test]
fn assertion_references() {
let original = AssetReference::new(
"https://some.storage.us/foo",
Some("A copy of the asset on the web"),
)
.add_reference("ipfs://cid", Some("A copy of the asset on IPFS"));

assert_eq!(original.references.len(), 2);

let assertion = original.to_assertion().unwrap();
assert_eq!(assertion.mime_type(), "application/cbor");
assert_eq!(assertion.label(), AssetReference::LABEL);

let result = AssetReference::from_assertion(&assertion).unwrap();
assert_eq!(result, original)
}

#[test]
fn test_json_round_trip() {
let json = serde_json::json!({
"references": [
{
"description": "A copy of the asset on the web",
"reference": {
"uri": "https://some.storage.us/foo"
}
},
{
"description": "A copy of the asset on IPFS",
"reference": {
"uri": "ipfs://cid"
}
}
]
});

let original: AssetReference = serde_json::from_value(json).unwrap();
let assertion = original.to_assertion().unwrap();
let result = AssetReference::from_assertion(&assertion).unwrap();

assert_eq!(result, original);
}
}
5 changes: 5 additions & 0 deletions sdk/src/assertions/labels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ pub(crate) const ASSERTION_STORE: &str = "c2pa.assertions";
// Databoxes label
pub(crate) const DATABOX_STORE: &str = "c2pa.databoxes";

/// Label prefix for asset reference assertion.
///
/// See <https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_asset_reference>
pub const ASSET_REFERENCE: &str = "c2pa.asset-ref";

/// Return the version suffix from an assertion label if it exists.
///
/// When an assertion's schema is changed in a backwards-compatible manner,
Expand Down
3 changes: 3 additions & 0 deletions sdk/src/assertions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ mod actions;
pub(crate) use actions::V2_DEPRECATED_ACTIONS;
pub use actions::{c2pa_action, Action, ActionTemplate, Actions, SoftwareAgent};

mod asset_reference;
pub use asset_reference::AssetReference;

mod asset_types;
pub use asset_types::{AssetTypeEnum, AssetTypes};

Expand Down
66 changes: 65 additions & 1 deletion sdk/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ mod integration_1 {
use std::{io, path::PathBuf};

use c2pa::{
assertions::{c2pa_action, Action, Actions},
assertions::{c2pa_action, Action, Actions, AssetReference},
create_signer,
crypto::raw_signature::SigningAlg,
settings::load_settings_from_str,
Expand Down Expand Up @@ -289,6 +289,70 @@ mod integration_1 {
Ok(())
}

#[test]
#[cfg(feature = "file_io")]
fn test_asset_reference_assertion() -> Result<()> {
// set up parent and destination paths
let dir = tempdirectory()?;
let output_path = dir.path().join("test_file.jpg");
#[cfg(target_os = "wasi")]
let mut parent_path = PathBuf::from("/");
#[cfg(not(target_os = "wasi"))]
let mut parent_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
parent_path.push("tests/fixtures/earth_apollo17.jpg");
#[cfg(target_os = "wasi")]
let mut ingredient_path = PathBuf::from("/");
#[cfg(not(target_os = "wasi"))]
let mut ingredient_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
ingredient_path.push("tests/fixtures/libpng-test.png");

let config = include_bytes!("../tests/fixtures/certs/trust/store.cfg");
let priv_trust = include_bytes!("../tests/fixtures/certs/trust/test_cert_root_bundle.pem");

// Configure before first use so that trust settings are used for all calls.
// In production code you should check that the file is indeed UTF-8 text.
configure_trust(
Some(String::from_utf8_lossy(priv_trust).to_string()),
None,
Some(String::from_utf8_lossy(config).to_string()),
)?;

let generator = ClaimGeneratorInfo::new("app");
// create a new Manifest
let mut builder = Builder::new();
builder.set_claim_generator_info(generator);

// allocate references
let references = AssetReference::new(
"https://some.storage.us/foo",
Some("A copy of the asset on the web"),
)
.add_reference("ipfs://cid", Some("A copy of the asset on IPFS"));

// add references assertion
builder.add_assertion(AssetReference::LABEL, &references)?;

// sign and embed into the target file
let signer = get_temp_signer();
builder.sign_file(signer.as_ref(), &parent_path, &output_path)?;

// read our new file with embedded manifest
let reader = Reader::from_file(&output_path)?;

println!("{reader}");

assert!(reader.active_manifest().is_some());
if let Some(manifest) = reader.active_manifest() {
assert!(manifest.title().is_some());
assert_eq!(manifest.assertions().len(), 1);
let assertion_ref: AssetReference = manifest.assertions()[0].to_assertion()?;
assert_eq!(assertion_ref, references);
} else {
panic!("no manifest in store");
}
Ok(())
}

#[cfg(feature = "v1_api")]
struct PlacedCallback {
path: String,
Expand Down
Loading