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}