use core::fmt;
use thiserror::Error;
use crate::FormatVersion;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Level(i32);
impl Level {
pub const MIN: Self = Self(-131_072);
pub const MAX: Self = Self(22);
pub const DEFAULT: Self = Self(6);
#[must_use]
pub const fn get(self) -> i32 {
self.0
}
}
impl Default for Level {
fn default() -> Self {
Self::DEFAULT
}
}
impl TryFrom<i32> for Level {
type Error = ConfigError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value == 0 {
return Ok(Self::DEFAULT);
}
if !(Self::MIN.0..=Self::MAX.0).contains(&value) {
return Err(ConfigError::OutOfRange {
parameter: Parameter::Level,
value: i64::from(value),
});
}
Ok(Self(value))
}
}
impl From<Level> for i32 {
fn from(level: Level) -> Self {
level.0
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ChunkSize(u32);
impl ChunkSize {
pub const MIN: Self = Self(32 << 10);
pub const MAX: Self = Self(i32::MAX as u32);
pub const DEFAULT: Self = Self(16 << 20);
pub(crate) const fn bytes(self) -> usize {
self.0 as usize
}
}
impl Default for ChunkSize {
fn default() -> Self {
Self::DEFAULT
}
}
impl TryFrom<usize> for ChunkSize {
type Error = ConfigError;
fn try_from(value: usize) -> Result<Self, Self::Error> {
if value < Self::MIN.bytes() || value > Self::MAX.bytes() {
return Err(ConfigError::OutOfRange {
parameter: Parameter::ChunkSize,
value: i64::try_from(value).unwrap_or(i64::MAX),
});
}
Ok(Self(value as u32))
}
}
impl From<ChunkSize> for usize {
fn from(size: ChunkSize) -> Self {
size.bytes()
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub enum Chunking {
#[default]
Whole,
Fixed(ChunkSize),
}
impl From<ChunkSize> for Chunking {
fn from(size: ChunkSize) -> Self {
Self::Fixed(size)
}
}
pub(crate) const WHOLE_CHUNK_LIMIT: usize = 1 << 31;
impl Chunking {
pub(crate) const fn limit(self) -> usize {
match self {
Self::Whole => WHOLE_CHUNK_LIMIT,
Self::Fixed(size) => size.bytes(),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct EncoderConfig {
level: Level,
format_version: FormatVersion,
chunking: Chunking,
content_checksum: bool,
compressed_checksum: bool,
decompression_level: u8,
min_stream_size: u32,
store_on_expansion: bool,
permissive: bool,
}
impl EncoderConfig {
pub const DEFAULT: Self = Self {
level: Level::DEFAULT,
format_version: FormatVersion::MAX,
chunking: Chunking::Whole,
content_checksum: true,
compressed_checksum: true,
decompression_level: Self::DEFAULT_DECOMPRESSION_LEVEL,
min_stream_size: Self::DEFAULT_MIN_STREAM_SIZE,
store_on_expansion: true,
permissive: false,
};
pub const DEFAULT_DECOMPRESSION_LEVEL: u8 = 3;
pub const DEFAULT_MIN_STREAM_SIZE: u32 = 10;
#[must_use]
pub const fn with_level(mut self, level: Level) -> Self {
self.level = level;
self
}
#[must_use]
pub const fn with_format_version(mut self, version: FormatVersion) -> Self {
self.format_version = version;
self
}
#[must_use]
pub const fn with_chunking(mut self, chunking: Chunking) -> Self {
self.chunking = chunking;
self
}
#[must_use]
pub const fn with_content_checksum(mut self, enabled: bool) -> Self {
self.content_checksum = enabled;
self
}
#[must_use]
pub const fn with_compressed_checksum(mut self, enabled: bool) -> Self {
self.compressed_checksum = enabled;
self
}
#[must_use]
pub const fn with_decompression_level(mut self, level: u8) -> Self {
self.decompression_level = if level == 0 {
Self::DEFAULT_DECOMPRESSION_LEVEL
} else {
level
};
self
}
#[must_use]
pub const fn with_min_stream_size(mut self, size: u32) -> Self {
self.min_stream_size = if size == 0 {
Self::DEFAULT_MIN_STREAM_SIZE
} else {
size
};
self
}
#[must_use]
pub const fn with_store_on_expansion(mut self, enabled: bool) -> Self {
self.store_on_expansion = enabled;
self
}
#[must_use]
pub const fn with_permissive(mut self, enabled: bool) -> Self {
self.permissive = enabled;
self
}
#[must_use]
pub const fn decompression_level(&self) -> u8 {
self.decompression_level
}
#[must_use]
pub const fn min_stream_size(&self) -> u32 {
self.min_stream_size
}
#[must_use]
pub const fn store_on_expansion(&self) -> bool {
self.store_on_expansion
}
#[must_use]
pub const fn permissive(&self) -> bool {
self.permissive
}
#[must_use]
pub const fn level(&self) -> Level {
self.level
}
#[must_use]
pub const fn format_version(&self) -> FormatVersion {
self.format_version
}
#[must_use]
pub const fn chunking(&self) -> Chunking {
self.chunking
}
#[must_use]
pub const fn content_checksum(&self) -> bool {
self.content_checksum
}
#[must_use]
pub const fn compressed_checksum(&self) -> bool {
self.compressed_checksum
}
}
impl Default for EncoderConfig {
fn default() -> Self {
Self::DEFAULT
}
}
impl From<Level> for EncoderConfig {
fn from(level: Level) -> Self {
Self::DEFAULT.with_level(level)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum Parameter {
Level,
ChunkSize,
}
impl fmt::Display for Parameter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Level => "compression level",
Self::ChunkSize => "chunk size",
})
}
}
#[derive(Error, Debug, Clone, Copy, Eq, PartialEq)]
#[non_exhaustive]
pub enum ConfigError {
#[error("{parameter} {value} is out of range")]
OutOfRange {
parameter: Parameter,
value: i64,
},
#[error("format version {version} predates header comments (22)")]
CommentUnsupported {
version: u32,
},
}
#[derive(Error, Debug, Clone, Copy, Eq, PartialEq)]
#[error("the compressed-size bound overflows usize")]
pub struct SizeOverflow;
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn levels_follow_the_reference_range_and_zero_default() {
assert_eq!(Level::try_from(0), Ok(Level::DEFAULT));
assert_eq!(Level::try_from(-131_072), Ok(Level::MIN));
assert_eq!(Level::try_from(22), Ok(Level::MAX));
assert!(Level::try_from(-131_073).is_err());
assert_eq!(Level::default().get(), 6);
assert_eq!(i32::from(Level::try_from(-5).unwrap()), -5);
}
#[test]
fn chunk_sizes_follow_the_segmenter_range() {
assert_eq!(ChunkSize::try_from(32_768), Ok(ChunkSize::MIN));
assert_eq!(ChunkSize::try_from(i32::MAX as usize), Ok(ChunkSize::MAX));
assert_eq!(
ChunkSize::try_from(32_767),
Err(ConfigError::OutOfRange {
parameter: Parameter::ChunkSize,
value: 32_767,
})
);
assert!(ChunkSize::try_from(1usize << 31).is_err());
assert_eq!(ChunkSize::default(), ChunkSize::DEFAULT);
assert_eq!(
Chunking::from(ChunkSize::MIN),
Chunking::Fixed(ChunkSize::MIN)
);
assert_eq!(Chunking::Whole.limit(), WHOLE_CHUNK_LIMIT);
assert_eq!(Chunking::Fixed(ChunkSize::MIN).limit(), 32_768);
}
#[test]
fn config_builders_set_every_field() {
let config = EncoderConfig::from(Level::MAX)
.with_format_version(FormatVersion::MIN)
.with_chunking(Chunking::Fixed(ChunkSize::MIN))
.with_content_checksum(false)
.with_compressed_checksum(false);
assert_eq!(config.level(), Level::MAX);
assert_eq!(config.format_version(), FormatVersion::MIN);
assert_eq!(config.chunking(), Chunking::Fixed(ChunkSize::MIN));
assert!(!config.content_checksum());
assert!(!config.compressed_checksum());
assert_eq!(EncoderConfig::default(), EncoderConfig::DEFAULT);
}
#[test]
fn engine_parameters_default_to_the_reference_and_zero_means_default() {
let config = EncoderConfig::default();
assert_eq!(config.decompression_level(), 3);
assert_eq!(config.min_stream_size(), 10);
assert!(config.store_on_expansion());
assert!(!config.permissive());
let config = config
.with_decompression_level(1)
.with_min_stream_size(100)
.with_store_on_expansion(false)
.with_permissive(true);
assert_eq!(config.decompression_level(), 1);
assert_eq!(config.min_stream_size(), 100);
assert!(!config.store_on_expansion());
assert!(config.permissive());
let config = config.with_decompression_level(0).with_min_stream_size(0);
assert_eq!(
(config.decompression_level(), config.min_stream_size()),
(3, 10)
);
}
#[test]
fn errors_name_the_parameter() {
let error = ConfigError::OutOfRange {
parameter: Parameter::ChunkSize,
value: 3,
};
assert_eq!(error.to_string(), "chunk size 3 is out of range");
assert_eq!(Parameter::Level.to_string(), "compression level");
assert!(SizeOverflow.to_string().contains("overflows"));
}
}