Skip to main content

compress_lib/
error.rs

1//! The error type every codec returns.
2
3use core::fmt;
4
5/// What went wrong, as a stable, matchable value.
6///
7/// Every kind says what to do in its [`message`](ErrorKind::message) and has a
8/// stable [`code`](ErrorKind::code) for logs and metrics. New kinds may be
9/// added in minor releases, so match with a wildcard arm.
10///
11/// # Examples
12///
13/// ```
14/// use compress_lib::ErrorKind;
15///
16/// assert_eq!(ErrorKind::OutputLimit.code(), "compress.output_limit");
17/// ```
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum ErrorKind {
21    /// A DEFLATE block header names block type 3, which does not exist.
22    InvalidBlockType,
23    /// A stored block's length and its one's complement disagree.
24    InvalidStoredLength,
25    /// A Huffman code description is invalid: over-subscribed, incomplete
26    /// where the format forbids it, a repeat with no previous length, or a
27    /// repeat that runs past the end of the table.
28    InvalidCodeLengths,
29    /// A code decodes to a symbol the format reserves (length symbols 286
30    /// and 287, distance symbols 30 and 31) or to no symbol at all.
31    InvalidSymbol,
32    /// A back-reference points before the start of the data (and any preset
33    /// dictionary) or further back than the window allows.
34    DistanceTooFar,
35    /// The input ended inside the stream: the data is truncated.
36    UnexpectedEnd,
37    /// The output reached the configured `max_output` before the stream
38    /// ended.
39    OutputLimit,
40    /// The output grew past the configured ratio to the input consumed.
41    RatioLimit,
42    /// Bytes follow the end of the stream where none are allowed.
43    TrailingData,
44    /// A window size outside 8 to 15 bits was asked for, or a zlib header
45    /// names a window larger than the decoder allows.
46    InvalidWindowBits,
47    /// A compression level outside 0 to 9 was asked for.
48    InvalidLevel,
49    /// A zlib or gzip header is malformed.
50    InvalidHeader,
51    /// A checksum (Adler-32, CRC-32, or the gzip header CRC) does not match
52    /// the data.
53    ChecksumMismatch,
54    /// The gzip trailer's length does not match the data.
55    LengthMismatch,
56    /// The gzip header's optional fields are larger than the configured cap.
57    HeaderTooLarge,
58    /// A gzip stream has more members than the configured cap.
59    TooManyMembers,
60    /// A zlib stream needs a preset dictionary that was not given.
61    DictionaryRequired,
62    /// The preset dictionary given is not the one the zlib stream names.
63    WrongDictionary,
64    /// The call is not valid in the engine's current state.
65    InvalidState,
66    /// The allocator refused memory for output.
67    AllocationFailed,
68}
69
70impl ErrorKind {
71    /// A stable identifier for logs and metrics, never changed once
72    /// released.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use compress_lib::ErrorKind;
78    ///
79    /// assert_eq!(ErrorKind::DistanceTooFar.code(), "compress.distance_too_far");
80    /// ```
81    #[must_use]
82    pub const fn code(self) -> &'static str {
83        match self {
84            Self::InvalidBlockType => "compress.invalid_block_type",
85            Self::InvalidStoredLength => "compress.invalid_stored_length",
86            Self::InvalidCodeLengths => "compress.invalid_code_lengths",
87            Self::InvalidSymbol => "compress.invalid_symbol",
88            Self::DistanceTooFar => "compress.distance_too_far",
89            Self::UnexpectedEnd => "compress.unexpected_end",
90            Self::OutputLimit => "compress.output_limit",
91            Self::RatioLimit => "compress.ratio_limit",
92            Self::TrailingData => "compress.trailing_data",
93            Self::InvalidWindowBits => "compress.invalid_window_bits",
94            Self::InvalidLevel => "compress.invalid_level",
95            Self::InvalidHeader => "compress.invalid_header",
96            Self::ChecksumMismatch => "compress.checksum_mismatch",
97            Self::LengthMismatch => "compress.length_mismatch",
98            Self::HeaderTooLarge => "compress.header_too_large",
99            Self::TooManyMembers => "compress.too_many_members",
100            Self::DictionaryRequired => "compress.dictionary_required",
101            Self::WrongDictionary => "compress.wrong_dictionary",
102            Self::InvalidState => "compress.invalid_state",
103            Self::AllocationFailed => "compress.allocation_failed",
104        }
105    }
106
107    /// What happened and what to do, in one sentence.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use compress_lib::ErrorKind;
113    ///
114    /// assert!(ErrorKind::OutputLimit.message().contains("max_output"));
115    /// ```
116    #[must_use]
117    pub const fn message(self) -> &'static str {
118        match self {
119            Self::InvalidBlockType => {
120                "invalid DEFLATE block type 3: the data is not DEFLATE or is corrupt"
121            }
122            Self::InvalidStoredLength => {
123                "stored block length check failed: the data is corrupt or truncated"
124            }
125            Self::InvalidCodeLengths => {
126                "invalid Huffman code lengths: the data is corrupt or not DEFLATE"
127            }
128            Self::InvalidSymbol => "invalid or reserved symbol: the data is corrupt",
129            Self::DistanceTooFar => {
130                "back-reference beyond the data or the window: the data is corrupt, \
131                 needs a preset dictionary, or uses a larger window than configured"
132            }
133            Self::UnexpectedEnd => "input ended inside the stream: the data is truncated",
134            Self::OutputLimit => {
135                "output reached max_output: raise the limit only if the source is trusted"
136            }
137            Self::RatioLimit => {
138                "output exceeded the ratio limit: likely a decompression bomb; \
139                 raise max_ratio only if the source is trusted"
140            }
141            Self::TrailingData => {
142                "bytes follow the end of the stream: strip them or allow trailing data"
143            }
144            Self::InvalidWindowBits => "window bits must be 8 to 15 (and within the decoder's)",
145            Self::InvalidLevel => "compression level must be 0 to 9",
146            Self::InvalidHeader => "invalid zlib or gzip header: the data is not in that format",
147            Self::ChecksumMismatch => "checksum mismatch: the data is corrupt",
148            Self::LengthMismatch => "gzip length mismatch: the data is corrupt or truncated",
149            Self::HeaderTooLarge => {
150                "gzip header fields exceed the configured cap: raise max_header if trusted"
151            }
152            Self::TooManyMembers => {
153                "gzip stream has more members than max_members: raise it if trusted"
154            }
155            Self::DictionaryRequired => {
156                "the zlib stream needs a preset dictionary: call set_dictionary first"
157            }
158            Self::WrongDictionary => "wrong preset dictionary: use the one the zlib stream names",
159            Self::InvalidState => "call not valid in the engine's state: reset it or finish first",
160            Self::AllocationFailed => "out of memory for output: lower max_output or free memory",
161        }
162    }
163}
164
165impl fmt::Display for ErrorKind {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        f.write_str(self.message())
168    }
169}
170
171/// A codec error: its [`ErrorKind`] and the input offset where it was found.
172///
173/// # Examples
174///
175/// ```
176/// use compress_lib::{Error, ErrorKind};
177///
178/// fn check(e: Error) -> bool {
179///     matches!(e.kind(), ErrorKind::OutputLimit | ErrorKind::RatioLimit)
180/// }
181/// # let _ = check;
182/// ```
183#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
184pub struct Error {
185    kind: ErrorKind,
186    offset: u64,
187}
188
189impl Error {
190    /// An error of `kind` found at input byte `offset`.
191    #[must_use]
192    pub(crate) const fn new(kind: ErrorKind, offset: u64) -> Self {
193        Self { kind, offset }
194    }
195
196    /// An error not tied to a place in the input (configuration, state).
197    #[must_use]
198    pub(crate) const fn config(kind: ErrorKind) -> Self {
199        Self { kind, offset: 0 }
200    }
201
202    /// What went wrong.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// use compress_lib::{ErrorKind, Level};
208    ///
209    /// assert_eq!(Level::new(10).unwrap_err().kind(), ErrorKind::InvalidLevel);
210    /// ```
211    #[must_use]
212    pub const fn kind(&self) -> ErrorKind {
213        self.kind
214    }
215
216    /// The offset in the compressed input (bytes since the engine started or
217    /// was last reset) where the problem was found; 0 for configuration
218    /// errors.
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// use compress_lib::Level;
224    ///
225    /// assert_eq!(Level::new(10).unwrap_err().offset(), 0);
226    /// ```
227    #[must_use]
228    pub const fn offset(&self) -> u64 {
229        self.offset
230    }
231
232    /// The stable identifier of the kind, as [`ErrorKind::code`].
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use compress_lib::Level;
238    ///
239    /// assert_eq!(Level::new(10).unwrap_err().code(), "compress.invalid_level");
240    /// ```
241    #[must_use]
242    pub const fn code(&self) -> &'static str {
243        self.kind.code()
244    }
245}
246
247impl fmt::Display for Error {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        if self.offset == 0 {
250            f.write_str(self.kind.message())
251        } else {
252            write!(f, "{} (at input byte {})", self.kind.message(), self.offset)
253        }
254    }
255}
256
257impl core::error::Error for Error {}
258
259impl From<Error> for ErrorKind {
260    fn from(e: Error) -> Self {
261        e.kind
262    }
263}
264
265#[cfg(feature = "std")]
266impl From<Error> for std::io::Error {
267    fn from(e: Error) -> Self {
268        let kind = match e.kind {
269            ErrorKind::UnexpectedEnd => std::io::ErrorKind::UnexpectedEof,
270            ErrorKind::InvalidWindowBits | ErrorKind::InvalidLevel | ErrorKind::InvalidState => {
271                std::io::ErrorKind::InvalidInput
272            }
273            ErrorKind::AllocationFailed => std::io::ErrorKind::OutOfMemory,
274            _ => std::io::ErrorKind::InvalidData,
275        };
276        std::io::Error::new(kind, e)
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use alloc::{collections::BTreeSet, string::ToString};
283
284    use super::*;
285
286    const ALL: [ErrorKind; 20] = [
287        ErrorKind::InvalidBlockType,
288        ErrorKind::InvalidStoredLength,
289        ErrorKind::InvalidCodeLengths,
290        ErrorKind::InvalidSymbol,
291        ErrorKind::DistanceTooFar,
292        ErrorKind::UnexpectedEnd,
293        ErrorKind::OutputLimit,
294        ErrorKind::RatioLimit,
295        ErrorKind::TrailingData,
296        ErrorKind::InvalidWindowBits,
297        ErrorKind::InvalidLevel,
298        ErrorKind::InvalidHeader,
299        ErrorKind::ChecksumMismatch,
300        ErrorKind::LengthMismatch,
301        ErrorKind::HeaderTooLarge,
302        ErrorKind::TooManyMembers,
303        ErrorKind::DictionaryRequired,
304        ErrorKind::WrongDictionary,
305        ErrorKind::InvalidState,
306        ErrorKind::AllocationFailed,
307    ];
308
309    #[test]
310    fn test_error_codes_unique_and_prefixed() {
311        let codes: BTreeSet<_> = ALL.iter().map(|k| k.code()).collect();
312        assert_eq!(codes.len(), ALL.len());
313        assert!(codes.iter().all(|c| c.starts_with("compress.")));
314    }
315
316    #[test]
317    fn test_error_messages_say_what_to_do() {
318        for k in ALL {
319            let m = k.message();
320            assert!(m.contains(':') || m.contains("must"), "{k:?}: {m}");
321            assert_eq!(k.to_string(), m);
322        }
323    }
324
325    #[test]
326    fn test_error_display_includes_offset_when_known() {
327        let e = Error::new(ErrorKind::InvalidBlockType, 17);
328        assert!(e.to_string().ends_with("(at input byte 17)"));
329        assert_eq!(e.offset(), 17);
330        assert_eq!(e.kind(), ErrorKind::InvalidBlockType);
331        assert_eq!(ErrorKind::from(e), ErrorKind::InvalidBlockType);
332        let c = Error::config(ErrorKind::InvalidLevel);
333        assert_eq!(c.to_string(), ErrorKind::InvalidLevel.message());
334        assert_eq!(c.code(), "compress.invalid_level");
335    }
336
337    #[cfg(feature = "std")]
338    #[test]
339    fn test_error_into_io_error_maps_kind() {
340        let io: std::io::Error = Error::new(ErrorKind::UnexpectedEnd, 3).into();
341        assert_eq!(io.kind(), std::io::ErrorKind::UnexpectedEof);
342        let io: std::io::Error = Error::config(ErrorKind::InvalidLevel).into();
343        assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput);
344        let io: std::io::Error = Error::new(ErrorKind::ChecksumMismatch, 9).into();
345        assert_eq!(io.kind(), std::io::ErrorKind::InvalidData);
346    }
347}