Skip to main content

c2pa_zip/
error.rs

1use std::fmt;
2
3#[derive(Debug)]
4pub enum Error {
5    /// The end-of-central-directory record was not found.
6    NoEocd,
7    /// A structure extended past the buffer or a length was inconsistent.
8    Truncated,
9    /// A ZIP64 archive; not supported (rejected fail-closed).
10    Zip64Unsupported,
11    /// A central directory entry pointed outside the archive.
12    BadOffset,
13    /// A ZIP entry name was not valid UTF-8.
14    NonUtf8Name,
15}
16
17impl Error {
18    /// The registered C2PA validation status code for this error, or `None`
19    /// when the condition carries no status code.
20    ///
21    /// Always `None` here, and deliberately so. Every variant is an *archive
22    /// parsing* failure — the bytes are not a ZIP this crate can read — which
23    /// happens before any manifest is located and so is not a validation
24    /// outcome.
25    ///
26    /// A ZIP-based asset is bound with a [collection data
27    /// hash](https://crates.io/crates/c2pa-zip), whose failures
28    /// (`assertion.collectionHash.mismatch` and friends) belong to whoever
29    /// computes the hash. This crate reports the byte ranges to hash and does
30    /// not hash them, so those codes are not its to emit.
31    ///
32    /// Every crate in this family exposes this method, so a dispatcher handling
33    /// several embedding methods can ask the same question of any of them.
34    pub fn code(&self) -> Option<&'static str> {
35        None
36    }
37
38    /// Whether this error means the asset carries no provenance at all.
39    ///
40    /// Always `false`: an unreadable archive is not the same as a readable one
41    /// carrying no manifest, which [`crate::read_manifest`] reports as
42    /// `Ok(None)` rather than as an error.
43    pub fn is_no_manifest_located(&self) -> bool {
44        false
45    }
46}
47
48impl fmt::Display for Error {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        let msg = match self {
51            Self::NoEocd => "ZIP end-of-central-directory record not found",
52            Self::Truncated => "ZIP archive is truncated or malformed",
53            Self::Zip64Unsupported => "ZIP64 archives are not supported",
54            Self::BadOffset => "ZIP central directory offset out of range",
55            Self::NonUtf8Name => "ZIP entry name is not valid UTF-8",
56        };
57        f.write_str(msg)
58    }
59}
60
61impl std::error::Error for Error {}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    /// Every variant is an archive-parsing failure, so none may claim a
68    /// validation status code. Guards against a later edit inventing one.
69    #[test]
70    fn no_variant_claims_a_status_code() {
71        for e in [
72            Error::NoEocd,
73            Error::Truncated,
74            Error::Zip64Unsupported,
75            Error::BadOffset,
76            Error::NonUtf8Name,
77        ] {
78            assert_eq!(e.code(), None, "{e:?} claimed a status code");
79            assert!(
80                !e.is_no_manifest_located(),
81                "{e:?} is not an absent manifest"
82            );
83        }
84    }
85
86    /// An unreadable archive is an error; an archive with no manifest is
87    /// `Ok(None)`. Those two outcomes must not collapse into one, or a caller
88    /// cannot tell "corrupt file" from "unsigned file".
89    #[test]
90    fn an_unreadable_archive_is_distinct_from_an_unsigned_one() {
91        let err = crate::read_manifest(b"not a zip at all").unwrap_err();
92        assert!(
93            !err.is_no_manifest_located(),
94            "{err:?} misreported as unsigned"
95        );
96        assert_eq!(err.code(), None);
97    }
98}