use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
NotFound,
CorruptedWrapper,
MultipleWrappers,
PayloadTooLarge(usize),
UnrepresentableGap(usize),
MalformedExclusion,
HashMismatch,
UnsupportedAlgorithm(String),
}
impl Error {
pub fn code(&self) -> Option<&'static str> {
Some(match self {
Self::CorruptedWrapper => "manifest.text.corruptedWrapper",
Self::MultipleWrappers => "manifest.text.multipleWrappers",
Self::NotFound => return None,
Self::PayloadTooLarge(_) | Self::UnrepresentableGap(_) => 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)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound => write!(f, "no C2PA text manifest wrapper found"),
Self::CorruptedWrapper => {
write!(f, "wrapper candidate detected but it did not fully decode")
}
Self::MultipleWrappers => write!(f, "more than one valid wrapper found"),
Self::PayloadTooLarge(n) => {
write!(
f,
"payload of {n} bytes exceeds the u32 manifestLength field"
)
}
Self::UnrepresentableGap(n) => {
write!(
f,
"a padding gap of {n} bytes is not expressible as 3a + 4b"
)
}
Self::MalformedExclusion => write!(f, "data hash exclusion range is malformed"),
Self::HashMismatch => write!(f, "data hash does not match the asset 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::CorruptedWrapper,
Error::MultipleWrappers,
Error::PayloadTooLarge(1 << 33),
Error::UnrepresentableGap(5),
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::PayloadTooLarge(4294967296)
.to_string()
.contains("4294967296"));
}
#[test]
fn the_two_wrapper_failure_codes_are_emitted() {
assert_eq!(
Error::CorruptedWrapper.code(),
Some("manifest.text.corruptedWrapper")
);
assert_eq!(
Error::MultipleWrappers.code(),
Some("manifest.text.multipleWrappers")
);
}
#[test]
fn every_code_is_a_registered_identifier() {
for e in all() {
if let Some(code) = e.code() {
assert!(
matches!(
code,
"manifest.text.corruptedWrapper"
| "manifest.text.multipleWrappers"
| "assertion.dataHash.malformed"
| "assertion.dataHash.mismatch"
| "algorithm.unsupported"
),
"{e:?} reports an unregistered code: {code}"
);
}
}
}
#[test]
fn only_an_absent_wrapper_means_unsigned() {
assert_eq!(Error::NotFound.code(), None);
assert!(Error::NotFound.is_no_manifest_located());
for e in [Error::CorruptedWrapper, Error::MultipleWrappers] {
assert!(
!e.is_no_manifest_located(),
"{e:?} must not classify as unsigned"
);
assert!(e.code().is_some(), "{e:?} should report a code");
}
}
#[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");
}
}
}