1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! All error types used in this crate

use crate::parse::Method;

use super::encoding;

/// Any zip-related error, from invalid archives to encoding problems.
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// Not a valid zip file, or a variant that is unsupported.
    #[error("format: {0}")]
    Format(#[from] FormatError),

    /// Something is not supported by this crate
    #[error("unsupported: {0}")]
    Unsupported(#[from] UnsupportedError),

    /// Invalid UTF-8, Shift-JIS, or any problem encountered while decoding text in general.
    #[error("encoding: {0:?}")]
    Encoding(#[from] encoding::DecodingError),

    /// I/O-related error
    #[error("io: {0}")]
    IO(#[from] std::io::Error),

    /// Decompression-related error
    #[error("{method:?} decompression error: {msg}")]
    Decompression {
        /// The compression method that failed
        method: Method,
        /// Additional information
        msg: String,
    },

    /// Could not read as a zip because size could not be determined
    #[error("size must be known to open zip file")]
    UnknownSize,
}

impl Error {
    /// Create a new error indicating that the given method is not supported.
    pub fn method_not_supported(method: Method) -> Self {
        Self::Unsupported(UnsupportedError::MethodNotSupported(method))
    }

    /// Create a new error indicating that the given method is not enabled.
    pub fn method_not_enabled(method: Method) -> Self {
        Self::Unsupported(UnsupportedError::MethodNotEnabled(method))
    }
}

/// Some part of the zip format is not supported by this crate.
#[derive(Debug, thiserror::Error)]
pub enum UnsupportedError {
    /// The compression method is not supported.
    #[error("compression method not supported: {0:?}")]
    MethodNotSupported(Method),

    /// The compression method is supported, but not enabled in this build.
    #[error("compression method supported, but not enabled in this build: {0:?}")]
    MethodNotEnabled(Method),

    /// The zip file uses a version of LZMA that is not supported.
    #[error("only LZMA2.0 is supported, found LZMA{minor}.{major}")]
    LzmaVersionUnsupported {
        /// major version read from LZMA properties header, cf. appnote 5.8.8
        major: u8,
        /// minor version read from LZMA properties header, cf. appnote 5.8.8
        minor: u8,
    },

    /// The LZMA properties header is not the expected size.
    #[error("LZMA properties header wrong size: expected {expected} bytes, got {actual} bytes")]
    LzmaPropertiesHeaderWrongSize {
        /// expected size in bytes
        expected: u16,
        /// actual size in bytes, read from a u16, cf. appnote 5.8.8
        actual: u16,
    },
}

/// Specific zip format errors, mostly due to invalid zip archives but that could also stem from
/// implementation shortcomings.
#[derive(Debug, thiserror::Error)]
pub enum FormatError {
    /// The end of central directory record was not found.
    ///
    /// This usually indicates that the file being read is not a zip archive.
    #[error("end of central directory record not found")]
    DirectoryEndSignatureNotFound,

    /// The zip64 end of central directory record could not be parsed.
    ///
    /// This is only returned when a zip64 end of central directory *locator* was found,
    /// so the archive should be zip64, but isn't.
    #[error("zip64 end of central directory record not found")]
    Directory64EndRecordInvalid,

    /// Corrupted/partial zip file: the offset we found for the central directory
    /// points outside of the current file.
    #[error("directory offset points outside of file")]
    DirectoryOffsetPointsOutsideFile,

    /// The central record is corrupted somewhat.
    ///
    /// This can happen when the end of central directory record advertises
    /// a certain number of files, but we weren't able to read the same number of central directory
    /// headers.
    #[error("invalid central record: expected to read {expected} files, got {actual}")]
    InvalidCentralRecord {
        /// expected number of files
        expected: u16,
        /// actual number of files
        actual: u16,
    },

    /// An extra field (that we support) was not decoded correctly.
    ///
    /// This can indicate an invalid zip archive, or an implementation error in this crate.
    #[error("could not decode extra field")]
    InvalidExtraField,

    /// The header offset of an entry is invalid.
    ///
    /// This can indicate an invalid zip archive, or an invalid user-provided global offset
    #[error("invalid header offset")]
    InvalidHeaderOffset,

    /// End of central directory record claims an impossible number of files.
    ///
    /// Each entry takes a minimum amount of size, so if the overall archive size is smaller than
    /// claimed_records_count * minimum_entry_size, we know it's not a valid zip file.
    #[error("impossible number of files: claims to have {claimed_records_count}, but zip size is {zip_size}")]
    ImpossibleNumberOfFiles {
        /// number of files claimed in the end of central directory record
        claimed_records_count: u64,
        /// total size of the zip file
        zip_size: u64,
    },

    /// The local file header (before the file data) could not be parsed correctly.
    #[error("invalid local file header")]
    InvalidLocalHeader,

    /// The data descriptor (after the file data) could not be parsed correctly.
    #[error("invalid data descriptor")]
    InvalidDataDescriptor,

    /// The uncompressed size didn't match
    #[error("uncompressed size didn't match: expected {expected}, got {actual}")]
    WrongSize {
        /// expected size in bytes (from the local header, data descriptor, etc.)
        expected: u64,
        /// actual size in bytes (from decompressing the entry)
        actual: u64,
    },

    /// The CRC-32 checksum didn't match.
    #[error("checksum didn't match: expected {expected:x?}, got {actual:x?}")]
    WrongChecksum {
        /// expected checksum (from the data descriptor, etc.)
        expected: u32,
        /// actual checksum (from decompressing the entry)
        actual: u32,
    },
}

impl From<Error> for std::io::Error {
    fn from(e: Error) -> Self {
        match e {
            Error::IO(e) => e,
            e => std::io::Error::new(std::io::ErrorKind::Other, e),
        }
    }
}