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}