use ::core::fmt;
use thiserror::Error;
use crate::dictionary::DictionaryId;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Error)]
#[non_exhaustive]
pub enum DecodeConfigError {
#[error("window log limit must be 10..=31, got {log}")]
WindowLog {
log: u8,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Error)]
#[non_exhaustive]
pub enum InvalidDataKind {
#[error("frame magic number")]
Magic,
#[error("frame header")]
FrameHeader,
#[error("skippable frame")]
SkippableFrame,
#[error("block type")]
BlockType,
#[error("block size")]
BlockSize,
#[error("literals section")]
LiteralsSection,
#[error("Huffman tree description")]
HuffmanTable,
#[error("Huffman bitstream")]
HuffmanStream,
#[error("FSE table description")]
FseTable,
#[error("sequences section")]
SequencesSection,
#[error("sequence bitstream")]
SequenceStream,
#[error("literal length")]
LiteralsOverrun,
#[error("match offset")]
Offset,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DecodeError {
#[error("invalid Zstandard {kind}")]
InvalidData {
#[source]
kind: InvalidDataKind,
},
#[error("unexpected end of compressed input")]
UnexpectedEndOfInput,
#[error("trailing data at byte {offset}")]
TrailingData {
offset: u64,
},
#[error("output slice is full after {written} bytes")]
OutputTooSmall {
written: usize,
},
#[error("content checksum mismatch: stored {expected:#010x}, computed {computed:#010x}")]
ChecksumMismatch {
expected: u32,
computed: u32,
},
#[error("frame declares {declared} content bytes, decoded {decoded}")]
ContentSizeMismatch {
declared: u64,
decoded: u64,
},
#[error("frame requires dictionary {id}, and none was supplied")]
DictionaryUnsupported {
id: u32,
},
#[error("frame requires dictionary {frame}, but {}", Supplied(*.expected))]
DictionaryMismatch {
expected: Option<DictionaryId>,
frame: DictionaryId,
},
#[error("frame window of {window_size} bytes exceeds the {limit}-byte limit")]
WindowLimitExceeded {
window_size: u64,
limit: u64,
},
#[error("compressed input exceeds {limit} bytes")]
InputLimitExceeded {
limit: u64,
},
#[error("decompressed output exceeds {limit} bytes")]
OutputLimitExceeded {
limit: u64,
},
#[error("decoder allocation failed")]
AllocationFailed,
#[error("invalid decoder state")]
InvalidState,
}
struct Supplied(Option<DictionaryId>);
impl fmt::Display for Supplied {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Some(id) => write!(formatter, "dictionary {id} was supplied"),
None => formatter.write_str("the dictionary declares no ID"),
}
}
}
impl From<InvalidDataKind> for DecodeError {
fn from(kind: InvalidDataKind) -> Self {
Self::InvalidData { kind }
}
}
#[cfg(not(feature = "no_std"))]
impl From<DecodeError> for std::io::Error {
fn from(error: DecodeError) -> Self {
use std::io::ErrorKind;
let kind = match &error {
DecodeError::InvalidData { .. }
| DecodeError::TrailingData { .. }
| DecodeError::ChecksumMismatch { .. }
| DecodeError::ContentSizeMismatch { .. }
| DecodeError::DictionaryUnsupported { .. }
| DecodeError::DictionaryMismatch { .. } => ErrorKind::InvalidData,
DecodeError::UnexpectedEndOfInput => ErrorKind::UnexpectedEof,
DecodeError::OutputTooSmall { .. } | DecodeError::InvalidState => {
ErrorKind::InvalidInput
}
DecodeError::AllocationFailed => ErrorKind::OutOfMemory,
DecodeError::WindowLimitExceeded { .. }
| DecodeError::InputLimitExceeded { .. }
| DecodeError::OutputLimitExceeded { .. } => ErrorKind::Other,
};
Self::new(kind, error)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ::core::error::Error as _;
use alloc::string::ToString;
#[test]
fn invalid_data_keeps_its_kind_as_the_source() {
let error = DecodeError::from(InvalidDataKind::Offset);
assert_eq!(error.to_string(), "invalid Zstandard match offset");
let source = error.source().expect("a source");
assert_eq!(source.to_string(), "match offset");
}
#[test]
fn messages_name_the_values_involved() {
let checksum = DecodeError::ChecksumMismatch {
expected: 1,
computed: 0xabcd,
};
assert_eq!(
checksum.to_string(),
"content checksum mismatch: stored 0x00000001, computed 0x0000abcd"
);
let window = DecodeError::WindowLimitExceeded {
window_size: 1 << 28,
limit: 1 << 27,
};
assert_eq!(
window.to_string(),
"frame window of 268435456 bytes exceeds the 134217728-byte limit"
);
assert_eq!(
DecodeConfigError::WindowLog { log: 40 }.to_string(),
"window log limit must be 10..=31, got 40"
);
let frame = DictionaryId::new(7).expect("non-zero");
let mismatch = DecodeError::DictionaryMismatch {
expected: DictionaryId::new(9),
frame,
};
assert_eq!(
mismatch.to_string(),
"frame requires dictionary 7, but dictionary 9 was supplied"
);
let missing = DecodeError::DictionaryMismatch {
expected: None,
frame,
};
assert_eq!(
missing.to_string(),
"frame requires dictionary 7, but the dictionary declares no ID"
);
assert_eq!(
DecodeError::DictionaryUnsupported { id: 7 }.to_string(),
"frame requires dictionary 7, and none was supplied"
);
}
#[cfg(not(feature = "no_std"))]
#[test]
fn io_errors_classify_and_keep_the_decode_error() {
use std::io::ErrorKind;
let cases = [
(
DecodeError::from(InvalidDataKind::Magic),
ErrorKind::InvalidData,
),
(
DecodeError::TrailingData { offset: 1 },
ErrorKind::InvalidData,
),
(
DecodeError::ChecksumMismatch {
expected: 0,
computed: 1,
},
ErrorKind::InvalidData,
),
(
DecodeError::ContentSizeMismatch {
declared: 0,
decoded: 1,
},
ErrorKind::InvalidData,
),
(
DecodeError::DictionaryUnsupported { id: 1 },
ErrorKind::InvalidData,
),
(
DecodeError::DictionaryMismatch {
expected: None,
frame: DictionaryId::new(1).expect("non-zero"),
},
ErrorKind::InvalidData,
),
(DecodeError::UnexpectedEndOfInput, ErrorKind::UnexpectedEof),
(
DecodeError::OutputTooSmall { written: 0 },
ErrorKind::InvalidInput,
),
(DecodeError::InvalidState, ErrorKind::InvalidInput),
(DecodeError::AllocationFailed, ErrorKind::OutOfMemory),
(
DecodeError::WindowLimitExceeded {
window_size: 2,
limit: 1,
},
ErrorKind::Other,
),
(
DecodeError::InputLimitExceeded { limit: 1 },
ErrorKind::Other,
),
(
DecodeError::OutputLimitExceeded { limit: 1 },
ErrorKind::Other,
),
];
for (error, kind) in cases {
let io = std::io::Error::from(error);
assert_eq!(io.kind(), kind);
assert!(io.get_ref().is_some_and(|inner| inner.is::<DecodeError>()));
}
}
}