Skip to content

Commit 9fc179e

Browse files
fix: Fix label handling for custom metadata (#1354)
* Fix label handling for custom metadata * fix integration test * Fix builder metadata usage to allow custom metadata to pass through * Fix wasm warning * Code simplification * Update to the corrected exifEx namespace * More exifEx fixes * Add backcompat support since there has been some cleanup of the namespaces
1 parent 82c6a70 commit 9fc179e

File tree

6 files changed

+108
-29
lines changed

6 files changed

+108
-29
lines changed

sdk/src/assertions/exif.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ impl Exif {
4444
Self {
4545
object_context: Some(json!({
4646
"dc": "http://purl.org/dc/elements/1.1/",
47-
"exifEX": "http://cipa.jp/exif/2.32/",
47+
"exifEX": "http://cipa.jp/exif/1.0/",
4848
"exif": "http://ns.adobe.com/exif/1.0/",
4949
"tiff": "http://ns.adobe.com/tiff/1.0/",
5050
"xmp": "http://ns.adobe.com/xap/1.0/",
@@ -138,7 +138,7 @@ pub mod tests {
138138
const SPEC_EXAMPLE: &str = r#"{
139139
"@context" : {
140140
"dc": "http://purl.org/dc/elements/1.1/",
141-
"exifEX": "http://cipa.jp/exif/2.32/",
141+
"exifEX": "http://cipa.jp/exif/1.0/",
142142
"exif": "http://ns.adobe.com/exif/1.0/",
143143
"tiff": "http://ns.adobe.com/tiff/1.0/",
144144
"xmp": "http://ns.adobe.com/xap/1.0/",

sdk/src/assertions/metadata.rs

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ use crate::{
2222
Error,
2323
};
2424

25+
const ASSERTION_CREATION_VERSION: usize = 1;
26+
2527
/// A `Metadata` assertion provides structured metadata using JSON-LD format for
2628
/// both standardized C2PA metadata and custom metadata schemas.
2729
///
@@ -38,21 +40,29 @@ pub struct Metadata {
3840
/// Metadata fields with namespace prefixes.
3941
#[serde(flatten)]
4042
pub value: HashMap<String, Value>,
41-
/// Assertion label (not serialized).
43+
44+
/// Custom assertion label (not serialized into content).
4245
#[serde(skip)]
43-
pub label: String,
46+
custom_metadata_label: Option<String>,
4447
}
4548

4649
impl Metadata {
4750
/// Creates a new metadata assertion from a JSON-LD string.
48-
pub fn new(label: &str, jsonld: &str) -> Result<Self, Error> {
51+
pub fn new(metadata_label: &str, jsonld: &str) -> Result<Self, Error> {
4952
let metadata = serde_json::from_slice::<Metadata>(jsonld.as_bytes())
5053
.map_err(|e| Error::BadParam(format!("Invalid JSON format: {e}")))?;
5154

55+
// is this a standard c2pa.metadata assertion or a custom field
56+
let custom_metadata_label = if metadata_label != labels::METADATA {
57+
Some(metadata_label.to_owned())
58+
} else {
59+
None
60+
};
61+
5262
Ok(Self {
5363
context: metadata.context,
5464
value: metadata.value,
55-
label: label.to_owned(),
65+
custom_metadata_label,
5666
})
5767
}
5868

@@ -67,11 +77,18 @@ impl Metadata {
6777
return false;
6878
}
6979

70-
if self.label == labels::METADATA {
80+
if self.label() == labels::METADATA {
7181
for (namespace, uri) in &self.context {
7282
if let Some(expected_uri) = ALLOWED_SCHEMAS.get(namespace.as_str()) {
7383
if uri != expected_uri {
74-
return false;
84+
// check the backcompat list
85+
if let Some(bcl) = BACKCOMPAT_LIST.get(namespace.as_str()) {
86+
if !bcl.iter().any(|v| v == uri) {
87+
return false;
88+
}
89+
} else {
90+
return false;
91+
}
7592
}
7693
}
7794
}
@@ -83,19 +100,30 @@ impl Metadata {
83100
return false;
84101
}
85102
}
86-
if self.label == labels::METADATA && !ALLOWED_FIELDS.contains(&label.as_str()) {
103+
if self.label() == labels::METADATA && !ALLOWED_FIELDS.contains(&label.as_str()) {
87104
return false;
88105
}
89106
}
90107
true
91108
}
109+
110+
/// Get the label for the metadata
111+
pub fn get_label(&self) -> &str {
112+
self.label()
113+
}
92114
}
93115

94116
impl AssertionJson for Metadata {}
95117

96118
impl AssertionBase for Metadata {
119+
const LABEL: &'static str = labels::METADATA;
120+
const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
121+
97122
fn label(&self) -> &str {
98-
&self.label
123+
match &self.custom_metadata_label {
124+
Some(cm) => cm,
125+
None => Self::LABEL,
126+
}
99127
}
100128

101129
fn to_assertion(&self) -> Result<Assertion, Error> {
@@ -104,7 +132,10 @@ impl AssertionBase for Metadata {
104132

105133
fn from_assertion(assertion: &Assertion) -> Result<Self, Error> {
106134
let mut metadata = Self::from_json_assertion(assertion)?;
107-
metadata.label = assertion.label().to_owned();
135+
136+
metadata.custom_metadata_label =
137+
(assertion.label() != labels::METADATA).then(|| assertion.label().to_owned());
138+
108139
Ok(metadata)
109140
}
110141
}
@@ -122,14 +153,21 @@ lazy_static! {
122153
("dc", "http://purl.org/dc/elements/1.1/"),
123154
("Iptc4xmpExt", "http://iptc.org/std/Iptc4xmpExt/2008-02-29/"),
124155
("exif", "http://ns.adobe.com/exif/1.0/"),
125-
("exifEX", "http://cipa.jp/exif/1.0/exifEX"),
156+
("exifEX", "http://cipa.jp/exif/1.0/"),
126157
("photoshop", "http://ns.adobe.com/photoshop/1.0/"),
127158
("tiff", "http://ns.adobe.com/tiff/1.0/"),
128159
("xmpDM", "http://ns.adobe.com/xmp/1.0/DynamicMedia/"),
129160
("plus", "http://ns.useplus.org/ldf/xmp/1.0/"),
130161
]
131162
.into_iter()
132163
.collect();
164+
165+
// list is to support versions that have changed since the current spec
166+
static ref BACKCOMPAT_LIST: HashMap<&'static str, Vec<&'static str>> = vec![
167+
("exifEX", vec!["http://cipa.jp/exif/1.0/exifEX", "http://cipa.jp/exif/2.32/"])
168+
]
169+
.into_iter()
170+
.collect();
133171
}
134172

135173
/// The c2pa.metadata assertion shall only contain certain fields.
@@ -459,7 +497,7 @@ pub mod tests {
459497
const SPEC_EXAMPLE: &str = r#"{
460498
"@context" : {
461499
"exif": "http://ns.adobe.com/exif/1.0/",
462-
"exifEX": "http://cipa.jp/exif/1.0/exifEX",
500+
"exifEX": "http://cipa.jp/exif/1.0/",
463501
"tiff": "http://ns.adobe.com/tiff/1.0/",
464502
"Iptc4xmpExt": "http://iptc.org/std/Iptc4xmpExt/2008-02-29/",
465503
"photoshop" : "http://ns.adobe.com/photoshop/1.0/"
@@ -537,6 +575,25 @@ pub mod tests {
537575
}
538576
"#;
539577

578+
const BACKCOMPAT: &str = r#" {
579+
"@context" : {
580+
"exif": "http://ns.adobe.com/exif/1.0/",
581+
"exifEX": "http://cipa.jp/exif/2.32/",
582+
"tiff": "http://ns.adobe.com/tiff/1.0/",
583+
"Iptc4xmpExt": "http://iptc.org/std/Iptc4xmpExt/2008-02-29/",
584+
"photoshop" : "http://ns.adobe.com/photoshop/1.0/"
585+
},
586+
"photoshop:DateCreated": "Aug 31, 2022",
587+
"Iptc4xmpExt:DigitalSourceType": "https://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture",
588+
"exif:GPSVersionID": "2.2.0.0",
589+
"exif:GPSLatitude": "39,21.102N",
590+
"exif:GPSLongitude": "74,26.5737W",
591+
"exif:GPSAltitudeRef": 0,
592+
"exif:GPSAltitude": "100963/29890",
593+
"exifEX:LensSpecification": { "@list": [ 1.55, 4.2, 1.6, 2.4 ] }
594+
}
595+
"#;
596+
540597
#[test]
541598
fn metadata_from_json() {
542599
let metadata = Metadata::new(METADATA, SPEC_EXAMPLE).unwrap();
@@ -551,12 +608,26 @@ pub mod tests {
551608
assert_eq!(metadata, result);
552609
}
553610

611+
#[test]
612+
fn backcompat() {
613+
let metadata = Metadata::new(METADATA, BACKCOMPAT).unwrap();
614+
assert!(metadata.is_valid());
615+
}
616+
617+
#[test]
618+
fn assertion_custom_round_trip() {
619+
let metadata = Metadata::new("custom.metadata", CUSTOM_METADATA).unwrap();
620+
let assertion = metadata.to_assertion().unwrap();
621+
let result = Metadata::from_assertion(&assertion).unwrap();
622+
assert_eq!(metadata, result);
623+
}
624+
554625
#[test]
555626
fn test_custom_validation() {
556627
let mut metadata = Metadata::new("custom.metadata", CUSTOM_METADATA).unwrap();
557628
assert!(metadata.is_valid());
558629
// c2pa.metadata has restrictions on fields
559-
metadata.label = METADATA.to_owned();
630+
metadata.custom_metadata_label = Some(METADATA.to_owned());
560631
assert!(!metadata.is_valid());
561632
}
562633

@@ -570,7 +641,7 @@ pub mod tests {
570641
fn test_field_not_in_context() {
571642
let mut metadata = Metadata::new("custom.metadata", MISSING_CONTEXT).unwrap();
572643
assert!(!metadata.is_valid());
573-
metadata.label = METADATA.to_owned();
644+
metadata.custom_metadata_label = Some(METADATA.to_owned());
574645
assert!(!metadata.is_valid());
575646
}
576647

@@ -579,7 +650,7 @@ pub mod tests {
579650
let mut metadata = Metadata::new(METADATA, MISMATCH_URI).unwrap();
580651
assert!(!metadata.is_valid());
581652
// custom metadata does not have restriction on uris
582-
metadata.label = "custom.metadata".to_owned();
653+
metadata.custom_metadata_label = Some("custom.metadata".to_owned());
583654
assert!(metadata.is_valid());
584655
}
585656

sdk/src/builder.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,14 @@ use serde_with::skip_serializing_none;
2626
use uuid::Uuid;
2727
use zip::{write::SimpleFileOptions, ZipArchive, ZipWriter};
2828

29+
use crate::assertion::AssertionBase;
2930
#[allow(deprecated)]
3031
use crate::{
3132
assertion::AssertionDecodeError,
3233
assertions::{
33-
c2pa_action,
34-
labels::{self, METADATA_LABEL_REGEX},
35-
Action, ActionTemplate, Actions, AssertionMetadata, BmffHash, BoxHash, CreativeWork,
36-
DataHash, DigitalSourceType, EmbeddedData, Exif, Metadata, SoftwareAgent, Thumbnail, User,
37-
UserCbor,
34+
c2pa_action, labels, Action, ActionTemplate, Actions, AssertionMetadata, BmffHash, BoxHash,
35+
CreativeWork, DataHash, DigitalSourceType, EmbeddedData, Exif, Metadata, SoftwareAgent,
36+
Thumbnail, User, UserCbor,
3837
},
3938
cbor_types::value_cbor_to_type,
4039
claim::Claim,
@@ -1006,7 +1005,8 @@ impl Builder {
10061005
let bmff_hash: BmffHash = manifest_assertion.to_assertion()?;
10071006
claim.add_assertion_with_salt(&bmff_hash, &salt)
10081007
}
1009-
l if METADATA_LABEL_REGEX.is_match(l) => {
1008+
Metadata::LABEL => {
1009+
// user metadata will go through the fallback path
10101010
let metadata: Metadata = manifest_assertion.to_assertion()?;
10111011
claim.add_gathered_assertion_with_salt(&metadata, &salt)
10121012
}

sdk/src/utils/io_utils.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use std::{
2222
#[allow(unused)] // different code path for WASI
2323
use tempfile::{tempdir, Builder, NamedTempFile, SpooledTempFile, TempDir};
2424

25-
use crate::{asset_io::rename_or_move, settings::get_settings_value, Error, Result};
25+
use crate::{asset_io::rename_or_move, Error, Result};
2626
// Replace data at arbitrary location and len in a file.
2727
// start_location is where the replacement data will start
2828
// replace_len is how many bytes from source to replaced starting a start_location
@@ -127,7 +127,7 @@ fn stream_with_fs_fallback_wasm(
127127

128128
#[cfg(not(target_arch = "wasm32"))]
129129
fn stream_with_fs_fallback_file_io(threshold_override: Option<usize>) -> Result<SpooledTempFile> {
130-
let threshold = threshold_override.unwrap_or(get_settings_value::<usize>(
130+
let threshold = threshold_override.unwrap_or(crate::settings::get_settings_value::<usize>(
131131
"core.backing_store_memory_threshold_in_mb",
132132
)?);
133133

@@ -452,8 +452,10 @@ mod tests {
452452
assert!(!stream.is_rolled(), "data still in memory");
453453

454454
let large_data = vec![0; 1024 * 1024]; // 1MB.
455-
let threshold =
456-
get_settings_value::<usize>("core.backing_store_memory_threshold_in_mb").unwrap();
455+
let threshold = crate::settings::get_settings_value::<usize>(
456+
"core.backing_store_memory_threshold_in_mb",
457+
)
458+
.unwrap();
457459

458460
for _ in 0..threshold {
459461
stream.write_all(&large_data).unwrap();

sdk/tests/integration.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,14 @@ mod integration_1 {
279279
let custom_metadata_assertion = Metadata::new("custom.foo.metadata", CUSTOM_METADATA)?;
280280

281281
// add metadata assertions
282-
builder.add_assertion_json(&c2pa_metadata_assertion.label, &c2pa_metadata_assertion)?;
283-
builder.add_assertion_json(&custom_metadata_assertion.label, &custom_metadata_assertion)?;
282+
builder.add_assertion_json(
283+
c2pa_metadata_assertion.get_label(),
284+
&c2pa_metadata_assertion,
285+
)?;
286+
builder.add_assertion_json(
287+
custom_metadata_assertion.get_label(),
288+
&custom_metadata_assertion,
289+
)?;
284290

285291
// sign and embed into the target file
286292
let signer = Settings::signer()?;

sdk/tests/v2_api_integration.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ mod integration_v2 {
9696
"data": {
9797
"@context" : {
9898
"exif": "http://ns.adobe.com/exif/1.0/",
99-
"exifEX": "http://cipa.jp/exif/2.32/",
99+
"exifEX": "http://cipa.jp/exif/1.0/",
100100
"tiff": "http://ns.adobe.com/tiff/1.0/",
101101
"Iptc4xmpExt": "http://iptc.org/std/Iptc4xmpExt/2008-02-29/",
102102
"photoshop" : "http://ns.adobe.com/photoshop/1.0/"

0 commit comments

Comments
 (0)