Skip to main content

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}