use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
NotFound,
MultipleManifests,
MalformedElement(&'static str),
NoHead,
MalformedExclusion,
HashMismatch,
UnsupportedAlgorithm(String),
}
impl Error {
pub fn code(&self) -> Option<&'static str> {
Some(match self {
Self::MultipleManifests => "manifest.html.multipleManifests",
Self::NotFound | Self::MalformedElement(_) => return None,
Self::NoHead => return None,
Self::MalformedExclusion => "assertion.dataHash.malformed",
Self::HashMismatch => "assertion.dataHash.mismatch",
Self::UnsupportedAlgorithm(_) => "algorithm.unsupported",
})
}
pub fn is_no_manifest_located(&self) -> bool {
matches!(self, Self::NotFound | Self::MalformedElement(_))
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound => write!(f, "no C2PA manifest element found in the document head"),
Self::MultipleManifests => write!(f, "more than one C2PA manifest element found"),
Self::MalformedElement(why) => {
write!(f, "the C2PA manifest element is malformed: {why}")
}
Self::NoHead => write!(f, "the document has no head element to embed into"),
Self::MalformedExclusion => write!(f, "data hash exclusion range is malformed"),
Self::HashMismatch => write!(f, "data hash does not match the document content"),
Self::UnsupportedAlgorithm(a) => write!(f, "unsupported hash algorithm: {a}"),
}
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
fn all() -> Vec<Error> {
vec![
Error::NotFound,
Error::MultipleManifests,
Error::MalformedElement("content is not valid Base64"),
Error::NoHead,
Error::MalformedExclusion,
Error::HashMismatch,
Error::UnsupportedAlgorithm("sha1".into()),
]
}
#[test]
fn display_composes_into_a_sentence_for_every_variant() {
for e in all() {
let s = e.to_string();
assert!(!s.is_empty(), "{e:?} rendered empty");
assert!(!s.ends_with('.'), "{e:?} ends with a period: {s}");
let first = s.chars().next().expect("checked non-empty above");
assert!(!first.is_uppercase(), "{e:?} starts uppercase: {s}");
}
}
#[test]
fn display_carries_the_offending_value() {
assert!(Error::UnsupportedAlgorithm("sha1".into())
.to_string()
.contains("sha1"));
assert!(Error::MalformedElement("link has no href")
.to_string()
.contains("link has no href"));
}
#[test]
fn multiple_manifests_is_a_reportable_failure() {
assert_eq!(
Error::MultipleManifests.code(),
Some("manifest.html.multipleManifests")
);
assert!(!Error::MultipleManifests.is_no_manifest_located());
}
#[test]
fn every_code_is_a_registered_identifier() {
for e in all() {
if let Some(code) = e.code() {
assert!(
matches!(
code,
"manifest.html.multipleManifests"
| "assertion.dataHash.malformed"
| "assertion.dataHash.mismatch"
| "algorithm.unsupported"
),
"{e:?} reports an unregistered code: {code}"
);
}
}
}
#[test]
fn absence_of_provenance_carries_no_code() {
for e in [
Error::NotFound,
Error::MalformedElement("content is not valid Base64"),
] {
assert_eq!(e.code(), None, "{e:?} must not report a status code");
assert!(
e.is_no_manifest_located(),
"{e:?} must classify as unsigned"
);
}
assert_ne!(Error::MalformedElement("x"), Error::NotFound);
}
#[test]
fn binding_failures_are_not_no_manifest_located() {
for e in [
Error::MalformedExclusion,
Error::HashMismatch,
Error::UnsupportedAlgorithm("sha1".into()),
] {
assert!(!e.is_no_manifest_located(), "{e:?} misclassified");
assert!(e.code().is_some(), "{e:?} should report a code");
}
}
#[test]
fn no_head_is_neither_a_location_outcome_nor_a_validation_failure() {
assert_eq!(Error::NoHead.code(), None);
assert!(!Error::NoHead.is_no_manifest_located());
}
}