Skip to main content

c2pa_ml/
error.rs

1use crate::format::Format;
2use std::fmt;
3
4/// Errors returned by the crate.
5#[derive(Debug)]
6pub enum Error {
7    /// The bytes did not match any supported ML model container format.
8    UnknownFormat,
9    /// The container was structurally malformed for its detected format.
10    Malformed(String),
11    /// No C2PA manifest was present in the model.
12    NotFound,
13    /// A manifest source carried neither an embedded store nor a remote URI.
14    EmptySource,
15    /// A stored manifest reference could not be decoded (e.g. invalid Base64).
16    MalformedReference(String),
17    /// More than one `c2pa:manifest` entry was present.
18    ///
19    /// Only one shall be present per file. The specification assigns a distinct
20    /// failure code per format; see [`Error::code`].
21    MultipleManifests(Format),
22    /// The exclusion ranges are malformed: out of order, overlapping, extending
23    /// past the end of the file, or not matching the located manifest value.
24    ///
25    /// For SafeTensors this also covers an 8-byte header length field that
26    /// disagrees with the actual JSON header length, which the specification
27    /// requires a validator to reject with `assertion.dataHash.malformed`.
28    MalformedExclusion(String),
29    /// The recomputed data hash did not match the value in the assertion.
30    HashMismatch,
31    /// A hash algorithm identifier outside the C2PA allowed list was requested.
32    UnsupportedAlgorithm(String),
33    Io(std::io::Error),
34}
35
36impl Error {
37    /// The registered C2PA validation status code for this error, or `None`
38    /// when the condition carries no status code.
39    ///
40    /// Reading, embedding, and format-detection errors are not validation
41    /// outcomes and return `None`.
42    pub fn code(&self) -> Option<&'static str> {
43        Some(match self {
44            Self::MultipleManifests(Format::Onnx) => "manifest.onnx.multipleManifests",
45            Self::MultipleManifests(Format::SafeTensors) => {
46                "manifest.safetensors.multipleManifests"
47            }
48            // GGUF embedding is a crate extension with no specified codes.
49            Self::MultipleManifests(Format::Gguf) => return None,
50            Self::MalformedExclusion(_) => "assertion.dataHash.malformed",
51            Self::HashMismatch => "assertion.dataHash.mismatch",
52            Self::UnsupportedAlgorithm(_) => "algorithm.unsupported",
53            Self::UnknownFormat
54            | Self::Malformed(_)
55            | Self::NotFound
56            | Self::EmptySource
57            | Self::MalformedReference(_)
58            | Self::Io(_) => return None,
59        })
60    }
61}
62
63impl fmt::Display for Error {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            Self::UnknownFormat => {
67                write!(f, "unrecognized ML model container format")
68            }
69            Self::Malformed(s) => write!(f, "malformed model container: {s}"),
70            Self::NotFound => write!(f, "no C2PA manifest found in model"),
71            Self::EmptySource => {
72                write!(f, "manifest source has neither an embedded store nor a URI")
73            }
74            Self::MalformedReference(s) => write!(f, "malformed manifest reference: {s}"),
75            Self::MultipleManifests(container) => {
76                write!(
77                    f,
78                    "more than one c2pa:manifest entry in the {} container",
79                    container.name()
80                )
81            }
82            Self::MalformedExclusion(s) => write!(f, "data hash exclusion is malformed: {s}"),
83            Self::HashMismatch => write!(f, "data hash does not match the model content"),
84            Self::UnsupportedAlgorithm(a) => write!(f, "unsupported hash algorithm: {a}"),
85            Self::Io(e) => write!(f, "I/O error: {e}"),
86        }
87    }
88}
89
90impl std::error::Error for Error {}
91
92impl From<std::io::Error> for Error {
93    fn from(e: std::io::Error) -> Self {
94        Self::Io(e)
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    fn all() -> Vec<Error> {
103        vec![
104            Error::UnknownFormat,
105            Error::Malformed("x".into()),
106            Error::NotFound,
107            Error::EmptySource,
108            Error::MalformedReference("x".into()),
109            Error::MultipleManifests(Format::Gguf),
110            Error::MultipleManifests(Format::Onnx),
111            Error::MultipleManifests(Format::SafeTensors),
112            Error::MalformedExclusion("x".into()),
113            Error::HashMismatch,
114            Error::UnsupportedAlgorithm("sha1".into()),
115        ]
116    }
117
118    #[test]
119    fn display_composes_into_a_sentence_for_every_variant() {
120        for e in all() {
121            let s = e.to_string();
122            assert!(!s.is_empty(), "{e:?} rendered empty");
123            assert!(!s.ends_with('.'), "{e:?} ends with a period: {s}");
124            let first = s.chars().next().expect("checked non-empty above");
125            assert!(!first.is_uppercase(), "{e:?} starts uppercase: {s}");
126        }
127    }
128
129    #[test]
130    fn multiplicity_codes_are_per_format_and_as_registered() {
131        assert_eq!(
132            Error::MultipleManifests(Format::Onnx).code(),
133            Some("manifest.onnx.multipleManifests")
134        );
135        assert_eq!(
136            Error::MultipleManifests(Format::SafeTensors).code(),
137            Some("manifest.safetensors.multipleManifests")
138        );
139        // GGUF is not a specified container, so inventing a code for it would be
140        // inventing specification.
141        assert_eq!(Error::MultipleManifests(Format::Gguf).code(), None);
142    }
143
144    #[test]
145    fn every_code_is_a_registered_identifier() {
146        for e in all() {
147            if let Some(code) = e.code() {
148                assert!(
149                    matches!(
150                        code,
151                        "manifest.onnx.multipleManifests"
152                            | "manifest.safetensors.multipleManifests"
153                            | "assertion.dataHash.malformed"
154                            | "assertion.dataHash.mismatch"
155                            | "algorithm.unsupported"
156                    ),
157                    "{e:?} reports an unregistered code: {code}"
158                );
159            }
160        }
161    }
162
163    #[test]
164    fn reading_and_embedding_errors_carry_no_code() {
165        for e in [
166            Error::UnknownFormat,
167            Error::Malformed("x".into()),
168            Error::NotFound,
169            Error::EmptySource,
170            Error::MalformedReference("x".into()),
171        ] {
172            assert_eq!(e.code(), None, "{e:?} must not report a status code");
173        }
174    }
175
176    #[test]
177    fn format_names_itself_in_the_message() {
178        assert!(Error::MultipleManifests(Format::Onnx)
179            .to_string()
180            .contains("ONNX"));
181        assert!(Error::MultipleManifests(Format::SafeTensors)
182            .to_string()
183            .contains("SafeTensors"));
184    }
185}