c2pa_http/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/// Errors from locating a C2PA Manifest Store via an HTTP `Link` header.
8///
9/// # What carries a status code
10///
11/// Locating a manifest by reference is a prerequisite to validation. A response
12/// with no `c2pa-manifest` link simply has no provenance to check, which is not
13/// a failure.
14///
15/// The one registered code that belongs to this crate is
16/// `manifest.inaccessible`: the specification requires it when a manifest "was
17/// documented to exist in a remote location, but is not present there, or the
18/// location is not currently available (such as in an offline scenario)". This
19/// crate performs no network I/O, so it never raises that itself — it is
20/// exposed as [`Error::Inaccessible`] for the caller that does the fetching to
21/// report through the same type.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum Error {
24 /// No `Link` header with `rel="c2pa-manifest"` was present.
25 NotFound,
26 /// More than one distinct `c2pa-manifest` target was advertised.
27 ///
28 /// The specification describes retrieving "a" Manifest Store from "that URI
29 /// reference" and defines no precedence between competing links, so picking
30 /// one would be inventing a rule. Duplicate links naming the *same* target
31 /// are not an error.
32 MultipleLinks,
33 /// A `Link` field value could not be parsed as RFC 8288.
34 Malformed(&'static str),
35 /// A manifest was advertised but could not be retrieved.
36 ///
37 /// Reported as `manifest.inaccessible`. Raised by the caller performing the
38 /// fetch, not by this crate.
39 Inaccessible,
40}
41
42impl Error {
43 /// The registered C2PA validation status code for this error, or `None`
44 /// when the condition carries no status code.
45 pub fn code(&self) -> Option<&'static str> {
46 match self {
47 Self::Inaccessible => Some("manifest.inaccessible"),
48 Self::NotFound | Self::MultipleLinks | Self::Malformed(_) => None,
49 }
50 }
51
52 /// Whether this means the response advertised no provenance at all, as
53 /// opposed to provenance that was advertised and could not be used.
54 pub fn is_no_manifest_located(&self) -> bool {
55 matches!(
56 self,
57 Self::NotFound | Self::MultipleLinks | Self::Malformed(_)
58 )
59 }
60}
61
62impl fmt::Display for Error {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 Self::NotFound => write!(f, "no c2pa-manifest Link header found"),
66 Self::MultipleLinks => {
67 write!(f, "more than one c2pa-manifest target was advertised")
68 }
69 Self::Malformed(why) => write!(f, "malformed Link header: {why}"),
70 Self::Inaccessible => {
71 write!(f, "the advertised manifest could not be retrieved")
72 }
73 }
74 }
75}
76
77impl std::error::Error for Error {}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 fn all() -> Vec<Error> {
84 vec![
85 Error::NotFound,
86 Error::MultipleLinks,
87 Error::Malformed("unterminated target"),
88 Error::Inaccessible,
89 ]
90 }
91
92 #[test]
93 fn display_composes_into_a_sentence_for_every_variant() {
94 for e in all() {
95 let s = e.to_string();
96 assert!(!s.is_empty(), "{e:?} rendered empty");
97 assert!(!s.ends_with('.'), "{e:?} ends with a period: {s}");
98 let first = s.chars().next().expect("checked non-empty above");
99 assert!(!first.is_uppercase(), "{e:?} starts uppercase: {s}");
100 }
101 }
102
103 #[test]
104 fn only_inaccessible_carries_a_code() {
105 assert_eq!(Error::Inaccessible.code(), Some("manifest.inaccessible"));
106 for e in [Error::NotFound, Error::MultipleLinks, Error::Malformed("x")] {
107 assert_eq!(e.code(), None, "{e:?} must not report a status code");
108 assert!(
109 e.is_no_manifest_located(),
110 "{e:?} must classify as unsigned"
111 );
112 }
113 }
114
115 #[test]
116 fn inaccessible_is_not_an_absence_of_provenance() {
117 // Something was advertised; it just could not be fetched.
118 assert!(!Error::Inaccessible.is_no_manifest_located());
119 }
120
121 #[test]
122 fn every_code_is_a_registered_identifier() {
123 for e in all() {
124 if let Some(code) = e.code() {
125 assert_eq!(code, "manifest.inaccessible", "{e:?} invented a code");
126 }
127 }
128 }
129}