Skip to main content

zstd_pure_rs/common/
error.rs

1//! Translation of `lib/common/error_private.{h,c}` and the public enum
2//! from `lib/zstd_errors.h`.
3//!
4//! Upstream uses `size_t` return values, with errors encoded as
5//! `(size_t)-ZSTD_error_foo` (very large numbers). We preserve that
6//! calling convention in the Rust port because it is exposed through
7//! the public C-style API (`ZSTD_isError`, `ZSTD_getErrorName`), and
8//! keeping it bit-identical means FFI consumers of the original C API
9//! could swap implementations without any source change.
10
11/// ZSTD error code. Upstream uses a C enum, which can carry unnamed
12/// integer values after casts. `Unknown` preserves those raw values
13/// without creating invalid Rust enum discriminants.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum ErrorCode {
16    NoError,
17    Generic,
18    PrefixUnknown,
19    VersionUnsupported,
20    FrameParameterUnsupported,
21    FrameParameterWindowTooLarge,
22    CorruptionDetected,
23    ChecksumWrong,
24    LiteralsHeaderWrong,
25    DictionaryCorrupted,
26    DictionaryWrong,
27    DictionaryCreationFailed,
28    ParameterUnsupported,
29    ParameterCombinationUnsupported,
30    ParameterOutOfBound,
31    TableLogTooLarge,
32    MaxSymbolValueTooLarge,
33    MaxSymbolValueTooSmall,
34    CannotProduceUncompressedBlock,
35    StabilityConditionNotRespected,
36    StageWrong,
37    InitMissing,
38    MemoryAllocation,
39    WorkSpaceTooSmall,
40    DstSizeTooSmall,
41    SrcSizeWrong,
42    DstBufferNull,
43    NoForwardProgressDestFull,
44    NoForwardProgressInputEmpty,
45    FrameIndexTooLarge,
46    SeekableIO,
47    DstBufferWrong,
48    SrcBufferWrong,
49    SequenceProducerFailed,
50    ExternalSequencesInvalid,
51    MaxCode,
52    Unknown(i32),
53}
54
55impl ErrorCode {
56    /// Preserve the raw numeric value, matching a C enum cast.
57    pub fn from_raw(code: i32) -> Self {
58        match code {
59            0 => Self::NoError,
60            1 => Self::Generic,
61            10 => Self::PrefixUnknown,
62            12 => Self::VersionUnsupported,
63            14 => Self::FrameParameterUnsupported,
64            16 => Self::FrameParameterWindowTooLarge,
65            20 => Self::CorruptionDetected,
66            22 => Self::ChecksumWrong,
67            24 => Self::LiteralsHeaderWrong,
68            30 => Self::DictionaryCorrupted,
69            32 => Self::DictionaryWrong,
70            34 => Self::DictionaryCreationFailed,
71            40 => Self::ParameterUnsupported,
72            41 => Self::ParameterCombinationUnsupported,
73            42 => Self::ParameterOutOfBound,
74            44 => Self::TableLogTooLarge,
75            46 => Self::MaxSymbolValueTooLarge,
76            48 => Self::MaxSymbolValueTooSmall,
77            49 => Self::CannotProduceUncompressedBlock,
78            50 => Self::StabilityConditionNotRespected,
79            60 => Self::StageWrong,
80            62 => Self::InitMissing,
81            64 => Self::MemoryAllocation,
82            66 => Self::WorkSpaceTooSmall,
83            70 => Self::DstSizeTooSmall,
84            72 => Self::SrcSizeWrong,
85            74 => Self::DstBufferNull,
86            80 => Self::NoForwardProgressDestFull,
87            82 => Self::NoForwardProgressInputEmpty,
88            100 => Self::FrameIndexTooLarge,
89            102 => Self::SeekableIO,
90            104 => Self::DstBufferWrong,
91            105 => Self::SrcBufferWrong,
92            106 => Self::SequenceProducerFailed,
93            107 => Self::ExternalSequencesInvalid,
94            120 => Self::MaxCode,
95            raw => Self::Unknown(raw),
96        }
97    }
98
99    pub fn as_i32(self) -> i32 {
100        match self {
101            Self::NoError => 0,
102            Self::Generic => 1,
103            Self::PrefixUnknown => 10,
104            Self::VersionUnsupported => 12,
105            Self::FrameParameterUnsupported => 14,
106            Self::FrameParameterWindowTooLarge => 16,
107            Self::CorruptionDetected => 20,
108            Self::ChecksumWrong => 22,
109            Self::LiteralsHeaderWrong => 24,
110            Self::DictionaryCorrupted => 30,
111            Self::DictionaryWrong => 32,
112            Self::DictionaryCreationFailed => 34,
113            Self::ParameterUnsupported => 40,
114            Self::ParameterCombinationUnsupported => 41,
115            Self::ParameterOutOfBound => 42,
116            Self::TableLogTooLarge => 44,
117            Self::MaxSymbolValueTooLarge => 46,
118            Self::MaxSymbolValueTooSmall => 48,
119            Self::CannotProduceUncompressedBlock => 49,
120            Self::StabilityConditionNotRespected => 50,
121            Self::StageWrong => 60,
122            Self::InitMissing => 62,
123            Self::MemoryAllocation => 64,
124            Self::WorkSpaceTooSmall => 66,
125            Self::DstSizeTooSmall => 70,
126            Self::SrcSizeWrong => 72,
127            Self::DstBufferNull => 74,
128            Self::NoForwardProgressDestFull => 80,
129            Self::NoForwardProgressInputEmpty => 82,
130            Self::FrameIndexTooLarge => 100,
131            Self::SeekableIO => 102,
132            Self::DstBufferWrong => 104,
133            Self::SrcBufferWrong => 105,
134            Self::SequenceProducerFailed => 106,
135            Self::ExternalSequencesInvalid => 107,
136            Self::MaxCode => 120,
137            Self::Unknown(raw) => raw,
138        }
139    }
140}
141
142/// Rich error type for Rust-idiomatic APIs in this crate. The C-compatible
143/// layer still uses `size_t`-encoded codes via `ERROR()` / `ERR_isError()`.
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub struct ZstdError(pub ErrorCode);
146
147impl core::fmt::Display for ZstdError {
148    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
149        f.write_str(ERR_getErrorString(self.0))
150    }
151}
152
153#[cfg(feature = "std")]
154impl std::error::Error for ZstdError {}
155
156/// Translate an `ErrorCode` into upstream's `(size_t)-code` encoding.
157///
158/// Counterpart to the `ZSTD_ERROR(name)` macro expansion.
159#[inline]
160pub fn ERROR(code: ErrorCode) -> usize {
161    (0usize).wrapping_sub(code.as_i32() as usize)
162}
163
164/// Mirror of upstream `ERR_isError(size_t code)`: any numeric return value
165/// greater than `ERROR(maxCode)` is interpreted as an error.
166#[inline]
167pub fn ERR_isError(code: usize) -> bool {
168    code > ERROR(ErrorCode::MaxCode)
169}
170
171/// Mirror of upstream `ERR_getErrorCode(size_t code)`: decode a `(size_t)-err`
172/// return value into its `ErrorCode`. Non-error inputs decode to `NoError`.
173#[inline]
174pub fn ERR_getErrorCode(code: usize) -> ErrorCode {
175    if !ERR_isError(code) {
176        return ErrorCode::NoError;
177    }
178    // -code in 32-bit signed arithmetic.
179    let raw = 0i32.wrapping_sub(code as i32);
180    ErrorCode::from_raw(raw)
181}
182
183/// Mirror of upstream `ERR_getErrorName(size_t code)`: stringify a
184/// `(size_t)-err` code.
185#[inline]
186pub fn ERR_getErrorName(code: usize) -> &'static str {
187    ERR_getErrorString(ERR_getErrorCode(code))
188}
189
190/// Mirror of upstream `ERR_getErrorString(ERR_enum)`: a stable human-readable
191/// label per error code. Strings are byte-for-byte the same as upstream's
192/// messages to preserve compatibility with tooling that scrapes them.
193pub fn ERR_getErrorString(code: ErrorCode) -> &'static str {
194    match code {
195        ErrorCode::NoError => "No error detected",
196        ErrorCode::Generic => "Error (generic)",
197        ErrorCode::PrefixUnknown => "Unknown frame descriptor",
198        ErrorCode::VersionUnsupported => "Version not supported",
199        ErrorCode::FrameParameterUnsupported => "Unsupported frame parameter",
200        ErrorCode::FrameParameterWindowTooLarge => "Frame requires too much memory for decoding",
201        ErrorCode::CorruptionDetected => "Data corruption detected",
202        ErrorCode::ChecksumWrong => "Restored data doesn't match checksum",
203        ErrorCode::LiteralsHeaderWrong => {
204            "Header of Literals' block doesn't respect format specification"
205        }
206        ErrorCode::ParameterUnsupported => "Unsupported parameter",
207        ErrorCode::ParameterCombinationUnsupported => "Unsupported combination of parameters",
208        ErrorCode::ParameterOutOfBound => "Parameter is out of bound",
209        ErrorCode::InitMissing => "Context should be init first",
210        ErrorCode::MemoryAllocation => "Allocation error : not enough memory",
211        ErrorCode::WorkSpaceTooSmall => "workSpace buffer is not large enough",
212        ErrorCode::StageWrong => "Operation not authorized at current processing stage",
213        ErrorCode::TableLogTooLarge => "tableLog requires too much memory : unsupported",
214        ErrorCode::MaxSymbolValueTooLarge => "Unsupported max Symbol Value : too large",
215        ErrorCode::MaxSymbolValueTooSmall => "Specified maxSymbolValue is too small",
216        ErrorCode::CannotProduceUncompressedBlock => {
217            "This mode cannot generate an uncompressed block"
218        }
219        ErrorCode::StabilityConditionNotRespected => {
220            "pledged buffer stability condition is not respected"
221        }
222        ErrorCode::DictionaryCorrupted => "Dictionary is corrupted",
223        ErrorCode::DictionaryWrong => "Dictionary mismatch",
224        ErrorCode::DictionaryCreationFailed => "Cannot create Dictionary from provided samples",
225        ErrorCode::DstSizeTooSmall => "Destination buffer is too small",
226        ErrorCode::SrcSizeWrong => "Src size is incorrect",
227        ErrorCode::DstBufferNull => "Operation on NULL destination buffer",
228        ErrorCode::NoForwardProgressDestFull => {
229            "Operation made no progress over multiple calls, due to output buffer being full"
230        }
231        ErrorCode::NoForwardProgressInputEmpty => {
232            "Operation made no progress over multiple calls, due to input being empty"
233        }
234        ErrorCode::FrameIndexTooLarge => "Frame index is too large",
235        ErrorCode::SeekableIO => "An I/O error occurred when reading/seeking",
236        ErrorCode::DstBufferWrong => "Destination buffer is wrong",
237        ErrorCode::SrcBufferWrong => "Source buffer is wrong",
238        ErrorCode::SequenceProducerFailed => {
239            "Block-level external sequence producer returned an error code"
240        }
241        ErrorCode::ExternalSequencesInvalid => "External sequences are not valid",
242        _ => "Unspecified error code",
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    const UPSTREAM_ERRORS: &[(ErrorCode, i32, &str)] = &[
251        (ErrorCode::NoError, 0, "No error detected"),
252        (ErrorCode::Generic, 1, "Error (generic)"),
253        (ErrorCode::PrefixUnknown, 10, "Unknown frame descriptor"),
254        (ErrorCode::VersionUnsupported, 12, "Version not supported"),
255        (
256            ErrorCode::FrameParameterUnsupported,
257            14,
258            "Unsupported frame parameter",
259        ),
260        (
261            ErrorCode::FrameParameterWindowTooLarge,
262            16,
263            "Frame requires too much memory for decoding",
264        ),
265        (
266            ErrorCode::CorruptionDetected,
267            20,
268            "Data corruption detected",
269        ),
270        (
271            ErrorCode::ChecksumWrong,
272            22,
273            "Restored data doesn't match checksum",
274        ),
275        (
276            ErrorCode::LiteralsHeaderWrong,
277            24,
278            "Header of Literals' block doesn't respect format specification",
279        ),
280        (
281            ErrorCode::DictionaryCorrupted,
282            30,
283            "Dictionary is corrupted",
284        ),
285        (ErrorCode::DictionaryWrong, 32, "Dictionary mismatch"),
286        (
287            ErrorCode::DictionaryCreationFailed,
288            34,
289            "Cannot create Dictionary from provided samples",
290        ),
291        (ErrorCode::ParameterUnsupported, 40, "Unsupported parameter"),
292        (
293            ErrorCode::ParameterCombinationUnsupported,
294            41,
295            "Unsupported combination of parameters",
296        ),
297        (
298            ErrorCode::ParameterOutOfBound,
299            42,
300            "Parameter is out of bound",
301        ),
302        (
303            ErrorCode::TableLogTooLarge,
304            44,
305            "tableLog requires too much memory : unsupported",
306        ),
307        (
308            ErrorCode::MaxSymbolValueTooLarge,
309            46,
310            "Unsupported max Symbol Value : too large",
311        ),
312        (
313            ErrorCode::MaxSymbolValueTooSmall,
314            48,
315            "Specified maxSymbolValue is too small",
316        ),
317        (
318            ErrorCode::CannotProduceUncompressedBlock,
319            49,
320            "This mode cannot generate an uncompressed block",
321        ),
322        (
323            ErrorCode::StabilityConditionNotRespected,
324            50,
325            "pledged buffer stability condition is not respected",
326        ),
327        (
328            ErrorCode::StageWrong,
329            60,
330            "Operation not authorized at current processing stage",
331        ),
332        (ErrorCode::InitMissing, 62, "Context should be init first"),
333        (
334            ErrorCode::MemoryAllocation,
335            64,
336            "Allocation error : not enough memory",
337        ),
338        (
339            ErrorCode::WorkSpaceTooSmall,
340            66,
341            "workSpace buffer is not large enough",
342        ),
343        (
344            ErrorCode::DstSizeTooSmall,
345            70,
346            "Destination buffer is too small",
347        ),
348        (ErrorCode::SrcSizeWrong, 72, "Src size is incorrect"),
349        (
350            ErrorCode::DstBufferNull,
351            74,
352            "Operation on NULL destination buffer",
353        ),
354        (
355            ErrorCode::NoForwardProgressDestFull,
356            80,
357            "Operation made no progress over multiple calls, due to output buffer being full",
358        ),
359        (
360            ErrorCode::NoForwardProgressInputEmpty,
361            82,
362            "Operation made no progress over multiple calls, due to input being empty",
363        ),
364        (
365            ErrorCode::FrameIndexTooLarge,
366            100,
367            "Frame index is too large",
368        ),
369        (
370            ErrorCode::SeekableIO,
371            102,
372            "An I/O error occurred when reading/seeking",
373        ),
374        (
375            ErrorCode::DstBufferWrong,
376            104,
377            "Destination buffer is wrong",
378        ),
379        (ErrorCode::SrcBufferWrong, 105, "Source buffer is wrong"),
380        (
381            ErrorCode::SequenceProducerFailed,
382            106,
383            "Block-level external sequence producer returned an error code",
384        ),
385        (
386            ErrorCode::ExternalSequencesInvalid,
387            107,
388            "External sequences are not valid",
389        ),
390        (ErrorCode::MaxCode, 120, "Unspecified error code"),
391    ];
392
393    #[test]
394    fn error_encoding_roundtrip() {
395        let code = ErrorCode::CorruptionDetected;
396        let enc = ERROR(code);
397        assert!(ERR_isError(enc));
398        assert_eq!(ERR_getErrorCode(enc), code);
399    }
400
401    #[test]
402    fn zero_and_small_values_are_not_errors() {
403        assert!(!ERR_isError(0));
404        assert!(!ERR_isError(1));
405        assert!(!ERR_isError(1 << 20));
406    }
407
408    #[test]
409    fn boundary_maxcode_is_not_error() {
410        // `ERR_isError` uses strict > comparison against ERROR(maxCode).
411        assert!(!ERR_isError(ERROR(ErrorCode::MaxCode)));
412        // Anything "more negative" (a smaller enum value) yields a larger
413        // size_t and IS an error.
414        assert!(ERR_isError(ERROR(ErrorCode::CorruptionDetected)));
415    }
416
417    #[test]
418    fn error_strings_match_upstream_literals() {
419        for &(code, _raw, name) in UPSTREAM_ERRORS {
420            assert_eq!(ERR_getErrorString(code), name, "{code:?}");
421        }
422    }
423
424    #[test]
425    fn error_name_via_size_t() {
426        let enc = ERROR(ErrorCode::ChecksumWrong);
427        assert_eq!(
428            ERR_getErrorName(enc),
429            "Restored data doesn't match checksum"
430        );
431    }
432
433    #[test]
434    fn from_raw_preserves_extreme_and_negative_inputs() {
435        // C enum casts preserve the numeric value even when it isn't a
436        // named enumerator.
437        assert_eq!(ErrorCode::from_raw(i32::MIN).as_i32(), i32::MIN);
438        assert_eq!(ErrorCode::from_raw(-1).as_i32(), -1);
439        assert_eq!(ErrorCode::from_raw(-42).as_i32(), -42);
440        assert_eq!(ErrorCode::from_raw(i32::MAX).as_i32(), i32::MAX);
441        assert_eq!(ErrorCode::from_raw(999_999).as_i32(), 999_999);
442        // Known-valid code still round-trips.
443        assert_eq!(ErrorCode::from_raw(22), ErrorCode::ChecksumWrong);
444    }
445
446    #[test]
447    fn getErrorName_on_non_error_returns_no_error_string() {
448        // Contract: calling `ERR_getErrorName` on a code that isn't
449        // actually an error (i.e. a small successful size like 100)
450        // must return the NoError string, not garbage. This protects
451        // the common pattern where a caller logs the error name
452        // unconditionally after a call.
453        assert_eq!(ERR_getErrorName(0), "No error detected");
454        assert_eq!(ERR_getErrorName(100), "No error detected");
455        assert_eq!(ERR_getErrorName(1_000_000), "No error detected");
456        // Symmetrically for ERR_getErrorCode.
457        assert_eq!(ERR_getErrorCode(0), ErrorCode::NoError);
458        assert_eq!(ERR_getErrorCode(1_000_000), ErrorCode::NoError);
459    }
460
461    #[test]
462    fn from_raw_roundtrips_every_named_variant() {
463        // Regression gate: if someone adds a new ErrorCode variant
464        // but forgets to wire it up in `from_raw`, this catches it.
465        for &(code, raw, _name) in UPSTREAM_ERRORS {
466            assert_eq!(code.as_i32(), raw, "raw value drifted for {code:?}");
467            assert_eq!(
468                ErrorCode::from_raw(raw),
469                code,
470                "from_raw({raw}) did not round-trip for {code:?}"
471            );
472        }
473        // Unknown numeric codes preserve their raw C enum value.
474        assert_eq!(ErrorCode::from_raw(99_999).as_i32(), 99_999);
475    }
476
477    #[test]
478    fn unknown_encoded_error_name_matches_upstream_default() {
479        let encoded_unknown_error = (0usize).wrapping_sub(2);
480        assert!(ERR_isError(encoded_unknown_error));
481        assert_eq!(ERR_getErrorCode(encoded_unknown_error).as_i32(), 2);
482        assert_eq!(
483            ERR_getErrorString(ErrorCode::Unknown(2)),
484            "Unspecified error code"
485        );
486        assert_eq!(
487            ERR_getErrorName(encoded_unknown_error),
488            "Unspecified error code"
489        );
490    }
491
492    #[test]
493    fn size_t_error_threshold_matches_upstream() {
494        assert!(ERR_isError((0usize).wrapping_sub(1)));
495        assert!(ERR_isError((0usize).wrapping_sub(119)));
496        assert!(!ERR_isError(ERROR(ErrorCode::MaxCode)));
497        assert!(!ERR_isError((0usize).wrapping_sub(121)));
498    }
499}