@@ -22,6 +22,8 @@ use crate::{
22
22
Error ,
23
23
} ;
24
24
25
+ const ASSERTION_CREATION_VERSION : usize = 1 ;
26
+
25
27
/// A `Metadata` assertion provides structured metadata using JSON-LD format for
26
28
/// both standardized C2PA metadata and custom metadata schemas.
27
29
///
@@ -38,21 +40,29 @@ pub struct Metadata {
38
40
/// Metadata fields with namespace prefixes.
39
41
#[ serde( flatten) ]
40
42
pub value : HashMap < String , Value > ,
41
- /// Assertion label (not serialized).
43
+
44
+ /// Custom assertion label (not serialized into content).
42
45
#[ serde( skip) ]
43
- pub label : String ,
46
+ custom_metadata_label : Option < String > ,
44
47
}
45
48
46
49
impl Metadata {
47
50
/// 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 > {
49
52
let metadata = serde_json:: from_slice :: < Metadata > ( jsonld. as_bytes ( ) )
50
53
. map_err ( |e| Error :: BadParam ( format ! ( "Invalid JSON format: {e}" ) ) ) ?;
51
54
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
+
52
62
Ok ( Self {
53
63
context : metadata. context ,
54
64
value : metadata. value ,
55
- label : label . to_owned ( ) ,
65
+ custom_metadata_label ,
56
66
} )
57
67
}
58
68
@@ -67,11 +77,18 @@ impl Metadata {
67
77
return false ;
68
78
}
69
79
70
- if self . label == labels:: METADATA {
80
+ if self . label ( ) == labels:: METADATA {
71
81
for ( namespace, uri) in & self . context {
72
82
if let Some ( expected_uri) = ALLOWED_SCHEMAS . get ( namespace. as_str ( ) ) {
73
83
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
+ }
75
92
}
76
93
}
77
94
}
@@ -83,19 +100,30 @@ impl Metadata {
83
100
return false ;
84
101
}
85
102
}
86
- if self . label == labels:: METADATA && !ALLOWED_FIELDS . contains ( & label. as_str ( ) ) {
103
+ if self . label ( ) == labels:: METADATA && !ALLOWED_FIELDS . contains ( & label. as_str ( ) ) {
87
104
return false ;
88
105
}
89
106
}
90
107
true
91
108
}
109
+
110
+ /// Get the label for the metadata
111
+ pub fn get_label ( & self ) -> & str {
112
+ self . label ( )
113
+ }
92
114
}
93
115
94
116
impl AssertionJson for Metadata { }
95
117
96
118
impl AssertionBase for Metadata {
119
+ const LABEL : & ' static str = labels:: METADATA ;
120
+ const VERSION : Option < usize > = Some ( ASSERTION_CREATION_VERSION ) ;
121
+
97
122
fn label ( & self ) -> & str {
98
- & self . label
123
+ match & self . custom_metadata_label {
124
+ Some ( cm) => cm,
125
+ None => Self :: LABEL ,
126
+ }
99
127
}
100
128
101
129
fn to_assertion ( & self ) -> Result < Assertion , Error > {
@@ -104,7 +132,10 @@ impl AssertionBase for Metadata {
104
132
105
133
fn from_assertion ( assertion : & Assertion ) -> Result < Self , Error > {
106
134
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
+
108
139
Ok ( metadata)
109
140
}
110
141
}
@@ -122,14 +153,21 @@ lazy_static! {
122
153
( "dc" , "http://purl.org/dc/elements/1.1/" ) ,
123
154
( "Iptc4xmpExt" , "http://iptc.org/std/Iptc4xmpExt/2008-02-29/" ) ,
124
155
( "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/" ) ,
126
157
( "photoshop" , "http://ns.adobe.com/photoshop/1.0/" ) ,
127
158
( "tiff" , "http://ns.adobe.com/tiff/1.0/" ) ,
128
159
( "xmpDM" , "http://ns.adobe.com/xmp/1.0/DynamicMedia/" ) ,
129
160
( "plus" , "http://ns.useplus.org/ldf/xmp/1.0/" ) ,
130
161
]
131
162
. into_iter( )
132
163
. 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( ) ;
133
171
}
134
172
135
173
/// The c2pa.metadata assertion shall only contain certain fields.
@@ -459,7 +497,7 @@ pub mod tests {
459
497
const SPEC_EXAMPLE : & str = r#"{
460
498
"@context" : {
461
499
"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/",
463
501
"tiff": "http://ns.adobe.com/tiff/1.0/",
464
502
"Iptc4xmpExt": "http://iptc.org/std/Iptc4xmpExt/2008-02-29/",
465
503
"photoshop" : "http://ns.adobe.com/photoshop/1.0/"
@@ -537,6 +575,25 @@ pub mod tests {
537
575
}
538
576
"# ;
539
577
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
+
540
597
#[ test]
541
598
fn metadata_from_json ( ) {
542
599
let metadata = Metadata :: new ( METADATA , SPEC_EXAMPLE ) . unwrap ( ) ;
@@ -551,12 +608,26 @@ pub mod tests {
551
608
assert_eq ! ( metadata, result) ;
552
609
}
553
610
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
+
554
625
#[ test]
555
626
fn test_custom_validation ( ) {
556
627
let mut metadata = Metadata :: new ( "custom.metadata" , CUSTOM_METADATA ) . unwrap ( ) ;
557
628
assert ! ( metadata. is_valid( ) ) ;
558
629
// c2pa.metadata has restrictions on fields
559
- metadata. label = METADATA . to_owned ( ) ;
630
+ metadata. custom_metadata_label = Some ( METADATA . to_owned ( ) ) ;
560
631
assert ! ( !metadata. is_valid( ) ) ;
561
632
}
562
633
@@ -570,7 +641,7 @@ pub mod tests {
570
641
fn test_field_not_in_context ( ) {
571
642
let mut metadata = Metadata :: new ( "custom.metadata" , MISSING_CONTEXT ) . unwrap ( ) ;
572
643
assert ! ( !metadata. is_valid( ) ) ;
573
- metadata. label = METADATA . to_owned ( ) ;
644
+ metadata. custom_metadata_label = Some ( METADATA . to_owned ( ) ) ;
574
645
assert ! ( !metadata. is_valid( ) ) ;
575
646
}
576
647
@@ -579,7 +650,7 @@ pub mod tests {
579
650
let mut metadata = Metadata :: new ( METADATA , MISMATCH_URI ) . unwrap ( ) ;
580
651
assert ! ( !metadata. is_valid( ) ) ;
581
652
// 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 ( ) ) ;
583
654
assert ! ( metadata. is_valid( ) ) ;
584
655
}
585
656
0 commit comments