Skip to main content

c2pa_structured_text/
error.rs

1// Copyright 2026 WritersLogic. All rights reserved.
2// Licensed under the Apache License, Version 2.0 or the MIT license,
3// at your option.
4
5use std::fmt;
6
7#[derive(Debug)]
8pub enum Error {
9    /// No `-----BEGIN/END C2PA MANIFEST-----` block was located.
10    NotFound,
11    /// More than one manifest block was found. Per the specification the asset
12    /// shall then be treated as if no manifests were located.
13    MultipleBlocks,
14    /// The block is present but the reference between the delimiters is empty.
15    EmptyReference,
16    /// The reference is neither a resolvable URI nor a `data:` URI.
17    MalformedReference(String),
18    /// The text uses bare CR (0x0D) line endings, which are unsupported by the
19    /// structured-text binding method because they make line boundaries
20    /// ambiguous. Convert to LF or CRLF before embedding or validating.
21    BareCarriageReturn,
22    /// A `data:application/c2pa;base64,` reference could not be Base64-decoded.
23    ManifestDecode(crate::codec::DecodeError),
24    /// The exclusion ranges of a data hash assertion are malformed: negative,
25    /// out of order, overlapping, or extending past the end of the asset.
26    /// Corresponds to `assertion.dataHash.malformed`.
27    MalformedExclusion,
28    /// The recomputed data hash did not match the value in the assertion.
29    /// Corresponds to `assertion.dataHash.mismatch`.
30    HashMismatch,
31    /// A hash algorithm identifier outside the C2PA allowed list was requested.
32    /// Corresponds to `algorithm.unsupported`.
33    UnsupportedAlgorithm(String),
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    /// The specification defines no status codes specific to structured text —
41    /// unlike HTML or unstructured text, which each have their own — so every
42    /// locating outcome here returns `None`. Only the data-hash conditions map
43    /// to codes, and those are the generic ones shared by every embedding
44    /// method.
45    ///
46    /// Every crate in this family exposes this method, so a dispatcher handling
47    /// several embedding methods can ask the same question of any of them.
48    pub fn code(&self) -> Option<&'static str> {
49        Some(match self {
50            Self::MalformedExclusion => "assertion.dataHash.malformed",
51            Self::HashMismatch => "assertion.dataHash.mismatch",
52            Self::UnsupportedAlgorithm(_) => "algorithm.unsupported",
53            Self::NotFound
54            | Self::MultipleBlocks
55            | Self::EmptyReference
56            | Self::MalformedReference(_)
57            | Self::BareCarriageReturn
58            | Self::ManifestDecode(_) => return None,
59        })
60    }
61
62    /// Whether this error means the asset carries no provenance at all, as
63    /// opposed to provenance that was found and rejected.
64    ///
65    /// [`Error::MultipleBlocks`] counts: the specification requires an asset
66    /// with more than one manifest block to be treated as if no manifests were
67    /// located, rather than reported as a failure.
68    pub fn is_no_manifest_located(&self) -> bool {
69        matches!(self, Self::NotFound | Self::MultipleBlocks)
70    }
71}
72
73impl fmt::Display for Error {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::NotFound => write!(f, "no manifest block found"),
77            Self::MultipleBlocks => write!(f, "multiple manifest blocks found"),
78            Self::EmptyReference => write!(f, "empty manifest reference"),
79            Self::MalformedReference(s) => write!(f, "malformed manifest reference: {s}"),
80            Self::BareCarriageReturn => {
81                write!(
82                    f,
83                    "bare CR line endings are not supported; convert to LF or CRLF"
84                )
85            }
86            Self::ManifestDecode(e) => write!(f, "manifest data URI is not valid base64: {e}"),
87            Self::MalformedExclusion => write!(f, "data hash exclusion range is malformed"),
88            Self::HashMismatch => write!(f, "data hash does not match the asset content"),
89            Self::UnsupportedAlgorithm(a) => write!(f, "unsupported hash algorithm: {a}"),
90        }
91    }
92}
93
94impl std::error::Error for Error {
95    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
96        match self {
97            Self::ManifestDecode(e) => Some(e),
98            _ => None,
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    fn all() -> Vec<Error> {
108        vec![
109            Error::NotFound,
110            Error::MultipleBlocks,
111            Error::EmptyReference,
112            Error::MalformedReference("x".into()),
113            Error::BareCarriageReturn,
114            Error::MalformedExclusion,
115            Error::HashMismatch,
116            Error::UnsupportedAlgorithm("sha1".into()),
117        ]
118    }
119
120    /// Guards against inventing a code. Structured text has no format-specific
121    /// codes, so only the three generic ones may ever appear here.
122    #[test]
123    fn every_code_is_a_registered_identifier() {
124        for e in all() {
125            if let Some(code) = e.code() {
126                assert!(
127                    matches!(
128                        code,
129                        "assertion.dataHash.malformed"
130                            | "assertion.dataHash.mismatch"
131                            | "algorithm.unsupported"
132                    ),
133                    "{e:?} reports an unregistered code: {code}"
134                );
135            }
136        }
137    }
138
139    #[test]
140    fn no_structured_text_specific_code_is_emitted() {
141        // `manifest.structuredText.*` was removed from the specification;
142        // emitting one would be inventing specification.
143        for e in all() {
144            if let Some(code) = e.code() {
145                assert!(!code.starts_with("manifest."), "{e:?} emits {code}");
146            }
147        }
148    }
149
150    #[test]
151    fn locating_outcomes_carry_no_code() {
152        for e in [Error::NotFound, Error::MultipleBlocks] {
153            assert_eq!(e.code(), None, "{e:?} must not report a status code");
154            assert!(
155                e.is_no_manifest_located(),
156                "{e:?} must classify as unsigned"
157            );
158        }
159    }
160
161    #[test]
162    fn binding_failures_are_not_no_manifest_located() {
163        for e in [
164            Error::MalformedExclusion,
165            Error::HashMismatch,
166            Error::UnsupportedAlgorithm("sha1".into()),
167        ] {
168            assert!(!e.is_no_manifest_located(), "{e:?} misclassified");
169            assert!(e.code().is_some(), "{e:?} should report a code");
170        }
171    }
172}