Skip to main content

c2pa/
assertion.rs

1// Copyright 2022 Adobe. All rights reserved.
2// This file is licensed to you under the Apache License,
3// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
4// or the MIT license (http://opensource.org/licenses/MIT),
5// at your option.
6
7// Unless required by applicable law or agreed to in writing,
8// this software is distributed on an "AS IS" BASIS, WITHOUT
9// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
10// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
11// specific language governing permissions and limitations under
12// each license.
13
14use std::fmt;
15
16use serde::{de::DeserializeOwned, Deserialize, Serialize};
17use serde_bytes::ByteBuf;
18use serde_json::Value;
19use thiserror::Error;
20
21use crate::{
22    assertions::labels,
23    error::{Error, Result},
24};
25
26/// Check to see if this a label whose string can vary, if so return the root of the label and version if available
27fn get_mutable_label(var_label: &str) -> (String, Option<usize>) {
28    if var_label.starts_with(labels::SCHEMA_ORG) {
29        (var_label.to_string(), None)
30    } else {
31        // is it a type of thumbnail
32        let tn = get_thumbnail_type(var_label);
33
34        if tn == "none" {
35            let components: Vec<&str> = var_label.split('.').collect();
36            match components.last() {
37                Some(last) => {
38                    // check for a valid version number
39                    if last.len() > 1 {
40                        let (ver, ver_inst_str) = last.split_at(1);
41                        if ver == "v" {
42                            if let Ok(ver_inst) = ver_inst_str.parse::<usize>() {
43                                let ver_trim = format!(".{last}");
44                                let root_label = var_label.trim_end_matches(&ver_trim);
45                                return (root_label.to_string(), Some(ver_inst));
46                            }
47                        }
48                    }
49                    (var_label.to_string(), None)
50                }
51                None => (var_label.to_string(), None),
52            }
53        } else {
54            (tn, None)
55        }
56    }
57}
58
59pub fn get_thumbnail_type(thumbnail_label: &str) -> String {
60    if thumbnail_label.starts_with(labels::CLAIM_THUMBNAIL) {
61        return labels::CLAIM_THUMBNAIL.to_string();
62    }
63    if thumbnail_label.starts_with(labels::INGREDIENT_THUMBNAIL) {
64        return labels::INGREDIENT_THUMBNAIL.to_string();
65    }
66    "none".to_string()
67}
68
69pub fn get_thumbnail_image_type(thumbnail_label: &str) -> Option<String> {
70    let components: Vec<&str> = thumbnail_label.split('.').collect();
71
72    if thumbnail_label.contains("thumbnail") && components.len() >= 4 {
73        let image_type: Vec<&str> = components[3].split('_').collect(); // strip and other label adornments
74        Some(image_type[0].to_ascii_lowercase())
75    } else {
76        None
77    }
78}
79
80pub fn get_thumbnail_instance(label: &str) -> Option<usize> {
81    let label_type = get_thumbnail_type(label);
82    // only ingredients thumbs store ids in the label, so use placeholder ids for the others
83    match label_type.as_ref() {
84        labels::INGREDIENT_THUMBNAIL => {
85            // extract id from underscore separated part of the full label
86            let components: Vec<&str> = label.split("__").collect();
87            if components.len() == 2 {
88                let subparts: Vec<&str> = components[1].split('.').collect();
89                subparts[0].parse::<usize>().ok()
90            } else {
91                Some(0)
92            }
93        }
94        _ => None,
95    }
96}
97
98/// The core required trait for all assertions.
99///
100/// This defines the label and version for the assertion
101/// and supplies the to/from converters for C2PA assertion format.
102pub trait AssertionBase
103where
104    Self: Sized,
105{
106    /// The label for this assertion (reverse domain format)
107    const LABEL: &'static str = "unknown";
108
109    /// The version for this assertion (if any) Defaults to None/1
110    const VERSION: Option<usize> = None;
111
112    /// Returns a label for this assertion.
113    fn label(&self) -> &str {
114        Self::LABEL
115    }
116
117    fn version(&self) -> Option<usize> {
118        Self::VERSION
119    }
120
121    /// Convert this instance to an Assertion
122    fn to_assertion(&self) -> Result<Assertion>;
123
124    /// Returns Self or AssertionDecode Result from an assertion
125    fn from_assertion(assertion: &Assertion) -> Result<Self>;
126}
127
128/// Trait to handle default Cbor encoding/decoding of Assertions
129pub trait AssertionCbor: Serialize + DeserializeOwned + AssertionBase {
130    fn to_cbor_assertion(&self) -> Result<Assertion> {
131        let data = AssertionData::Cbor(
132            c2pa_cbor::to_vec(self).map_err(|err| Error::AssertionEncoding(err.to_string()))?,
133        );
134        Ok(Assertion::new(self.label(), self.version(), data).set_content_type("application/cbor"))
135    }
136
137    fn from_cbor_assertion(assertion: &Assertion) -> Result<Self> {
138        assertion.check_max_version(Self::VERSION)?;
139
140        match assertion.decode_data() {
141            AssertionData::Cbor(data) => Ok(c2pa_cbor::from_slice(data).map_err(|e| {
142                Error::AssertionDecoding(AssertionDecodeError::from_assertion_and_cbor_err(
143                    assertion, e,
144                ))
145            })?),
146
147            data => Err(AssertionDecodeError::from_assertion_unexpected_data_type(
148                assertion, data, "cbor",
149            )
150            .into()),
151        }
152    }
153}
154
155/// Trait to handle default Json encoding/decoding of Assertions
156pub trait AssertionJson: Serialize + DeserializeOwned + AssertionBase {
157    fn to_json_assertion(&self) -> Result<Assertion> {
158        let data = AssertionData::Json(
159            serde_json::to_string(self).map_err(|err| Error::AssertionEncoding(err.to_string()))?,
160        );
161        Ok(Assertion::new(self.label(), self.version(), data).set_content_type("application/json"))
162    }
163
164    fn from_json_assertion(assertion: &Assertion) -> Result<Self> {
165        assertion.check_max_version(Self::VERSION)?;
166
167        match assertion.decode_data() {
168            AssertionData::Json(data) => Ok(serde_json::from_str(data)
169                .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(assertion, e))?),
170            data => Err(Error::AssertionDecoding(
171                AssertionDecodeError::from_assertion_unexpected_data_type(assertion, data, "json"),
172            )),
173        }
174    }
175}
176
177/// Assertion data as binary CBOR or JSON depending upon
178/// the Assertion type (see spec).
179/// For JSON assertions the data is a JSON string and a Vec of u8 values for
180/// binary data and JSON data to be CBOR encoded.
181#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Hash)]
182pub enum AssertionData {
183    Json(String),          // json encoded data
184    Binary(Vec<u8>),       // binary data
185    Cbor(Vec<u8>),         // binary cbor encoded data
186    Uuid(String, Vec<u8>), // user defined content (uuid, data)
187}
188
189impl From<AssertionData> for Vec<u8> {
190    fn from(ad: AssertionData) -> Self {
191        match ad {
192            AssertionData::Json(s) => s.into_bytes(), // json encoded data
193            AssertionData::Binary(x) | AssertionData::Uuid(_, x) => x, // binary data
194            AssertionData::Cbor(x) => x,
195        }
196    }
197}
198
199impl fmt::Debug for AssertionData {
200    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
201        match self {
202            Self::Json(s) => write!(f, "{s:?}"), // json encoded data
203            Self::Binary(_) => write!(f, "<omitted>"),
204            Self::Uuid(uuid, _) => {
205                write!(f, "uuid: {uuid}, <omitted>")
206            }
207            Self::Cbor(s) => {
208                let buf: Vec<u8> = Vec::new();
209                let mut from = c2pa_cbor::Deserializer::from_slice(s);
210                let mut to = serde_json::Serializer::pretty(buf);
211
212                serde_transcode::transcode(&mut from, &mut to).map_err(|_err| fmt::Error)?;
213                let buf2 = to.into_inner();
214
215                let decoded: Value = serde_json::from_slice(&buf2).map_err(|_err| fmt::Error)?;
216
217                write!(f, "{:?}", decoded.to_string())
218            }
219        }
220    }
221}
222
223/// Internal Assertion structure
224///
225/// Each assertion type will
226/// contain its AssertionData.  For the User Assertion type we
227/// allow a String to set the label. The AssertionData contains
228/// the data payload for the assertion and the version number for its schema (if supported).
229#[derive(Clone, Debug, PartialEq, Eq, Hash)]
230pub struct Assertion {
231    label: String,
232    version: Option<usize>,
233    data: AssertionData,
234    content_type: String,
235}
236
237impl Assertion {
238    pub(crate) fn new(label: &str, version: Option<usize>, data: AssertionData) -> Self {
239        Self {
240            label: label.to_owned(),
241            version,
242            content_type: "application/cbor".to_owned(),
243            data,
244        }
245    }
246
247    pub(crate) fn set_content_type(mut self, content_type: &str) -> Self {
248        content_type.clone_into(&mut self.content_type);
249        self
250    }
251
252    /// return content_type for the the data enclosed in the Assertion
253    pub(crate) fn content_type(&self) -> &str {
254        self.content_type.as_str()
255    }
256
257    // pub(crate) fn set_data(mut self, data: &AssertionData) -> Self {
258    //     self.data = data.to_owned();
259    //     self
260    // }
261
262    // Return version string of known assertion if available
263    pub(crate) fn get_ver(&self) -> usize {
264        self.version.unwrap_or(1)
265    }
266
267    pub fn version(&self) -> usize {
268        self.version.unwrap_or(1)
269    }
270
271    // pub fn check_version(&self, max_version: usize) -> AssertionDecodeResult<()> {
272    //     match self.version {
273    //         Some(version) if version > max_version => Err(AssertionDecodeError {
274    //             label: self.label.clone(),
275    //             version: self.version,
276    //             content_type: self.content_type.clone(),
277    //             source: AssertionDecodeErrorCause::AssertionTooNew {
278    //                 max: max_version,
279    //                 found: version,
280    //             },
281    //         }),
282    //         _ => Ok(()),
283    //     }
284    // }
285
286    /// Return a reference to the AssertionData bound to this Assertion
287    pub(crate) fn decode_data(&self) -> &AssertionData {
288        &self.data
289    }
290
291    /// return mimetype for the the data enclosed in the Assertion
292    // Todo: deprecate this in favor of content_type()
293    pub(crate) fn mime_type(&self) -> String {
294        self.content_type.clone()
295    }
296
297    /// Test to see if the Assertions are of the same variant
298    pub(crate) fn assertions_eq(a: &Assertion, b: &Assertion) -> bool {
299        a.label_root() == b.label_root()
300    }
301
302    /// Return the CAI label for this Assertion (no version)
303    pub(crate) fn label_root(&self) -> String {
304        let label = get_mutable_label(&self.label).0;
305        // thumbnails need the image_type added
306        match get_thumbnail_image_type(&self.label) {
307            None => label,
308            Some(image_type) => format!("{label}.{image_type}"),
309        }
310    }
311
312    /// Return the CAI label for this Assertion with version string if available
313    pub(crate) fn label(&self) -> String {
314        let base_label = self.label_root();
315        let v = self.get_ver();
316        if v > 1 {
317            // c2pa does not include v1 labels
318            format!("{base_label}.v{v}")
319        } else {
320            base_label
321        }
322    }
323
324    /// Return a reference to the data as a byte array
325    pub(crate) fn data(&self) -> &[u8] {
326        // return bytes of the assertion data
327        match self.decode_data() {
328            AssertionData::Json(x) => x.as_bytes(), // json encoded data
329            AssertionData::Binary(x) | AssertionData::Uuid(_, x) => x, // binary data
330            AssertionData::Cbor(x) => x,
331        }
332    }
333
334    /// Return assertion as serde_json Object
335    /// this may have loss of cbor structure if unsupported in conversion to json
336    /// It should always do the correct thing when using the correct tagged CBOR types
337    pub(crate) fn as_json_object(&self) -> AssertionDecodeResult<Value> {
338        match self.decode_data() {
339            AssertionData::Json(x) => serde_json::from_str(x)
340                .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e)),
341
342            AssertionData::Cbor(x) => {
343                let buf: Vec<u8> = Vec::new();
344                let mut from = c2pa_cbor::Deserializer::from_slice(x);
345                let mut to = serde_json::Serializer::new(buf);
346
347                serde_transcode::transcode(&mut from, &mut to)
348                    .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))?;
349
350                let buf2 = to.into_inner();
351                serde_json::from_slice(&buf2)
352                    .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))
353            }
354
355            AssertionData::Binary(x) => {
356                let binary_bytes = ByteBuf::from(x.clone());
357                let binary_str = serde_json::to_string(&binary_bytes)
358                    .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))?;
359
360                serde_json::from_str(&binary_str)
361                    .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))
362            }
363            AssertionData::Uuid(uuid, x) => {
364                #[derive(Serialize)]
365                struct TmpObj<'a> {
366                    uuid: &'a str,
367                    data: ByteBuf,
368                }
369
370                let v = TmpObj {
371                    uuid,
372                    data: ByteBuf::from(x.clone()),
373                };
374
375                let binary_str = serde_json::to_string(&v)
376                    .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))?;
377
378                serde_json::from_str(&binary_str)
379                    .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))
380            }
381        }
382    }
383
384    fn from_assertion_data(label: &str, content_type: &str, data: AssertionData) -> Assertion {
385        use crate::claim::Claim;
386        let version = labels::version(label);
387        let (label, instance) = Claim::assertion_label_from_link(label);
388        let label = Claim::label_with_instance(&label, instance);
389
390        Self {
391            label,
392            version: if version == 1 { None } else { Some(version) },
393            data,
394            content_type: content_type.to_owned(),
395        }
396    }
397
398    /// create an assertion from binary data
399    pub(crate) fn from_data_binary(label: &str, mime_type: &str, binary_data: &[u8]) -> Assertion {
400        Self::from_assertion_data(
401            label,
402            mime_type,
403            AssertionData::Binary(binary_data.to_vec()),
404        )
405    }
406
407    /// Deconstruct a binary assertion, moving the Vec<u8> out without copying
408    pub(crate) fn binary_deconstruct(
409        assertion: Assertion,
410    ) -> Result<(String, Option<usize>, String, Vec<u8>)> {
411        match assertion.data {
412            AssertionData::Binary(data) => Ok((
413                assertion.label,
414                assertion.version,
415                assertion.content_type,
416                data,
417            )),
418            _ => Err(AssertionDecodeError::from_assertion_unexpected_data_type(
419                &assertion,
420                assertion.decode_data(),
421                "binary",
422            )
423            .into()),
424        }
425    }
426
427    /// create an assertion from user binary data
428    pub(crate) fn from_data_uuid(label: &str, uuid_str: &str, binary_data: &[u8]) -> Assertion {
429        Self::from_assertion_data(
430            label,
431            "application/octet-stream",
432            AssertionData::Uuid(uuid_str.to_owned(), binary_data.to_vec()),
433        )
434    }
435
436    pub(crate) fn from_data_cbor(label: &str, binary_data: &[u8]) -> Assertion {
437        Self::from_assertion_data(
438            label,
439            "application/cbor",
440            AssertionData::Cbor(binary_data.to_vec()),
441        )
442    }
443
444    pub(crate) fn from_data_json(
445        label: &str,
446        binary_data: &[u8],
447    ) -> AssertionDecodeResult<Assertion> {
448        let json = String::from_utf8(binary_data.to_vec()).map_err(|_| AssertionDecodeError {
449            label: label.to_string(),
450            version: None, // TODO: Can we get this info?
451            content_type: "application/json".to_string(),
452            source: AssertionDecodeErrorCause::BinaryDataNotUtf8,
453        })?;
454
455        Ok(Self::from_assertion_data(
456            label,
457            "application/json",
458            AssertionData::Json(json),
459        ))
460    }
461
462    // Check assertion label against a target label.
463    pub(crate) fn check_version_from_label(
464        &self,
465        desired_version: usize,
466    ) -> AssertionDecodeResult<()> {
467        let base_version = labels::version(&self.label);
468        if desired_version > base_version {
469            return Err(AssertionDecodeError {
470                label: self.label.clone(),
471                version: self.version,
472                content_type: self.content_type.clone(),
473                source: AssertionDecodeErrorCause::AssertionTooNew {
474                    max: desired_version,
475                    found: base_version,
476                },
477            });
478        }
479
480        Ok(())
481    }
482
483    fn check_max_version(&self, max_version: Option<usize>) -> AssertionDecodeResult<()> {
484        if let Some(data_version) = self.version {
485            if let Some(max_version) = max_version {
486                if data_version > max_version {
487                    return Err(AssertionDecodeError {
488                        label: self.label.clone(),
489                        version: self.version,
490                        content_type: self.content_type.clone(),
491                        source: AssertionDecodeErrorCause::AssertionTooNew {
492                            max: max_version,
493                            found: data_version,
494                        },
495                    });
496                }
497            }
498        }
499        Ok(())
500    }
501}
502
503/// This error type is returned when an assertion can not be decoded.
504#[non_exhaustive]
505pub struct AssertionDecodeError {
506    pub label: String,
507    pub version: Option<usize>,
508    pub content_type: String,
509    pub source: AssertionDecodeErrorCause,
510}
511
512impl AssertionDecodeError {
513    fn fmt_internal(&self, f: &mut fmt::Formatter) -> fmt::Result {
514        write!(
515            f,
516            "could not decode assertion {} (version {}, content type {}): {}",
517            self.label,
518            self.version
519                .map_or("(no version)".to_string(), |v| v.to_string()),
520            self.content_type,
521            self.source
522        )
523    }
524
525    pub(crate) fn from_assertion_and_cbor_err(
526        assertion: &Assertion,
527        source: c2pa_cbor::error::Error,
528    ) -> Self {
529        Self {
530            label: assertion.label.clone(),
531            version: assertion.version,
532            content_type: assertion.content_type.clone(),
533            source: source.into(),
534        }
535    }
536
537    pub(crate) fn from_assertion_and_json_err(
538        assertion: &Assertion,
539        source: serde_json::error::Error,
540    ) -> Self {
541        Self {
542            label: assertion.label.clone(),
543            version: assertion.version,
544            content_type: assertion.content_type.clone(),
545            source: source.into(),
546        }
547    }
548
549    pub(crate) fn from_assertion_unexpected_data_type(
550        assertion: &Assertion,
551        assertion_data: &AssertionData,
552        expected: &str,
553    ) -> Self {
554        Self {
555            label: assertion.label.clone(),
556            version: assertion.version,
557            content_type: assertion.content_type.clone(),
558            source: AssertionDecodeErrorCause::UnexpectedDataType {
559                expected: expected.to_string(),
560                found: Self::data_type_from_assertion_data(assertion_data),
561            },
562        }
563    }
564
565    fn data_type_from_assertion_data(assertion_data: &AssertionData) -> String {
566        match assertion_data {
567            AssertionData::Json(_) => "json".to_string(),
568            AssertionData::Binary(_) => "binary".to_string(),
569            AssertionData::Cbor(_) => "cbor".to_string(),
570            AssertionData::Uuid(_, _) => "uuid".to_string(),
571        }
572    }
573
574    pub(crate) fn from_json_err(
575        label: String,
576        version: Option<usize>,
577        content_type: String,
578        source: serde_json::error::Error,
579    ) -> Self {
580        Self {
581            label,
582            version,
583            content_type,
584            source: source.into(),
585        }
586    }
587
588    pub(crate) fn from_err<S: Into<AssertionDecodeErrorCause>>(
589        label: String,
590        version: Option<usize>,
591        content_type: String,
592        source: S,
593    ) -> Self {
594        Self {
595            label,
596            version,
597            content_type,
598            source: source.into(),
599        }
600    }
601}
602
603impl std::fmt::Debug for AssertionDecodeError {
604    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
605        self.fmt_internal(f)
606    }
607}
608
609impl std::fmt::Display for AssertionDecodeError {
610    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
611        self.fmt_internal(f)
612    }
613}
614
615impl std::error::Error for AssertionDecodeError {
616    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
617        Some(&self.source)
618    }
619}
620
621/// This error type is used inside `AssertionDecodeError` to describe the
622/// root cause for the decoding error.
623#[derive(Debug, Error)]
624#[non_exhaustive]
625pub enum AssertionDecodeErrorCause {
626    /// The assertion had an unexpected data type.
627    #[error("the assertion had an unexpected data type: expected {expected}, found {found}")]
628    UnexpectedDataType { expected: String, found: String },
629
630    /// The assertion has a version that is newer that this toolkit can understand.
631    #[error("the assertion version is too new: expected no later than {max}, found {found}")]
632    AssertionTooNew { max: usize, found: usize },
633
634    /// Binary data could not be interpreted as UTF-8.
635    #[error("binary data could not be interpreted as UTF-8")]
636    BinaryDataNotUtf8,
637
638    /// Assertion data did not match hash link.
639    #[error("the assertion data did not match the hash embedded in the link")]
640    AssertionDataIncorrect,
641
642    #[error(transparent)]
643    JsonError(#[from] serde_json::Error),
644
645    #[error(transparent)]
646    CborError(#[from] c2pa_cbor::Error),
647
648    /// There was a problem decoding field.
649    #[error("the assertion had a mandatory field: {expected} that could not be decoded")]
650    FieldDecoding { expected: String },
651}
652
653pub(crate) type AssertionDecodeResult<T> = std::result::Result<T, AssertionDecodeError>;
654
655#[cfg(test)]
656pub mod tests {
657    #![allow(clippy::unwrap_used)]
658
659    use super::*;
660    use crate::assertions::{Action, Actions};
661
662    #[test]
663    fn test_version_label() {
664        let test_json = r#"{
665            "left": 0,
666            "right": 2000,
667            "top": 1000,
668            "bottom": 4000
669        }"#;
670        let json = AssertionData::Json(test_json.to_string());
671        let json2 = AssertionData::Json(test_json.to_string());
672
673        let a = Assertion::new(Actions::LABEL, Some(2), json);
674        let a_no_ver = Assertion::new(Actions::LABEL, None, json2);
675
676        assert_eq!(a.get_ver(), 2);
677        assert_eq!(a_no_ver.get_ver(), 1);
678        assert_eq!(a.label(), format!("{}.{}", Actions::LABEL, "v2"));
679        assert_eq!(a.label_root(), Actions::LABEL);
680        assert_eq!(a_no_ver.label(), Actions::LABEL);
681    }
682
683    #[test]
684    fn test_cbor_conversion() {
685        let action = Actions::new()
686            .add_action(
687                Action::new("c2pa.cropped")
688                    .set_parameter(
689                        "coordinate".to_owned(),
690                        serde_json::json!({"left": 0,"right": 2000,"top": 1000,"bottom": 4000}),
691                    )
692                    .unwrap(),
693            )
694            .add_action(
695                Action::new("c2pa.filtered")
696                    .set_parameter("name".to_owned(), "gaussian blur")
697                    .unwrap()
698                    .set_software_agent("Photoshop")
699                    .set_when("2015-06-26T16:43:23+0200"),
700            )
701            .to_assertion()
702            .unwrap();
703
704        let action_cbor = action.data();
705
706        let action_restored = Assertion::from_data_cbor(&action.label(), action_cbor);
707
708        assert!(Assertion::assertions_eq(&action, &action_restored));
709
710        let action_obj = action.as_json_object().unwrap();
711        let action_restored_obj = action_restored.as_json_object().unwrap();
712
713        assert_eq!(action_obj, action_restored_obj);
714    }
715}