use super::internal::{CompressMode, DistanceCodes, QualityLevel, WindowBits};
use thiserror::Error;
#[repr(transparent)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Quality(u8);
impl Quality {
pub const Q0: Self = Self(0);
pub const Q1: Self = Self(1);
pub const Q2: Self = Self(2);
pub const Q3: Self = Self(3);
pub const Q4: Self = Self(4);
pub const Q5: Self = Self(5);
pub const Q6: Self = Self(6);
pub const Q7: Self = Self(7);
pub const Q8: Self = Self(8);
pub const Q9: Self = Self(9);
pub const Q10: Self = Self(10);
pub const Q11: Self = Self(11);
pub const MIN: Self = Self::Q0;
pub const MAX: Self = Self::Q11;
#[must_use]
pub const fn get(self) -> u8 {
self.0
}
pub(crate) const fn level(self) -> QualityLevel {
match self.0 {
0 => QualityLevel::Q0,
1 => QualityLevel::Q1,
2 => QualityLevel::Q2,
3 => QualityLevel::Q3,
4 => QualityLevel::Q4,
5 => QualityLevel::Q5,
6 => QualityLevel::Q6,
7 => QualityLevel::Q7,
8 => QualityLevel::Q8,
9 => QualityLevel::Q9,
10 => QualityLevel::Q10,
_ => QualityLevel::Q11,
}
}
}
impl Default for Quality {
fn default() -> Self {
Self::Q11
}
}
impl TryFrom<u8> for Quality {
type Error = ConfigError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if value > Self::MAX.0 {
return Err(ConfigError::Quality { requested: value });
}
Ok(Self(value))
}
}
impl From<Quality> for u8 {
fn from(value: Quality) -> Self {
value.0
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum WindowEncoding {
Standard,
Large,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct Window {
bits: u8,
encoding: WindowEncoding,
}
impl Window {
pub const MIN_BITS: u8 = 10;
pub const MAX_STANDARD_BITS: u8 = 24;
pub const MAX_LARGE_BITS: u8 = 62;
pub const DEFAULT: Self = Self {
bits: 22,
encoding: WindowEncoding::Standard,
};
pub const fn standard(bits: u8) -> Result<Self, ConfigError> {
if bits < Self::MIN_BITS || bits > Self::MAX_STANDARD_BITS {
return Err(ConfigError::StandardWindow { requested: bits });
}
Ok(Self {
bits,
encoding: WindowEncoding::Standard,
})
}
pub const fn large(bits: u8) -> Result<Self, ConfigError> {
if bits < Self::MIN_BITS || bits > Self::MAX_LARGE_BITS {
return Err(ConfigError::LargeWindow { requested: bits });
}
Ok(Self {
bits,
encoding: WindowEncoding::Large,
})
}
#[must_use]
pub const fn bits(self) -> u8 {
self.bits
}
#[must_use]
pub const fn encoding(self) -> WindowEncoding {
self.encoding
}
pub(crate) const fn resolve(self) -> WindowBits {
let outcome = match self.encoding {
WindowEncoding::Standard => WindowBits::standard(self.bits),
WindowEncoding::Large => WindowBits::large(self.bits),
};
match outcome {
Ok(bits) => bits,
Err(_) => WindowBits::DEFAULT,
}
}
}
impl Default for Window {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub enum BlockSize {
#[default]
Auto,
Bits(BlockBits),
}
impl From<BlockBits> for BlockSize {
fn from(value: BlockBits) -> Self {
Self::Bits(value)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct BlockBits(pub(crate) usize);
impl BlockBits {
pub const MIN: Self = Self(16);
pub const MAX: Self = Self(24);
#[must_use]
pub const fn get(self) -> u8 {
self.0 as u8
}
}
impl TryFrom<u8> for BlockBits {
type Error = ConfigError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if usize::from(value) < Self::MIN.0 || usize::from(value) > Self::MAX.0 {
return Err(ConfigError::BlockBits { requested: value });
}
Ok(Self(usize::from(value)))
}
}
impl From<BlockBits> for usize {
fn from(value: BlockBits) -> Self {
value.0
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub enum CompressionMode {
#[default]
Generic,
Text,
Font,
}
impl CompressionMode {
pub(crate) const fn resolve(self) -> CompressMode {
match self {
Self::Generic => CompressMode::Generic,
Self::Text => CompressMode::Text,
Self::Font => CompressMode::Font,
}
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub enum DistanceParams {
#[default]
Auto,
Explicit {
postfix_bits: u8,
direct_codes: u16,
},
}
impl DistanceParams {
pub const MAX_POSTFIX_BITS: u8 = 3;
pub const MAX_DIRECT_CODES: u16 = 120;
pub const fn explicit(postfix_bits: u8, direct_codes: u16) -> Result<Self, ConfigError> {
if postfix_bits > Self::MAX_POSTFIX_BITS {
return Err(ConfigError::DistancePostfixBits {
requested: postfix_bits,
});
}
if direct_codes > Self::MAX_DIRECT_CODES {
return Err(ConfigError::DirectDistanceCodes {
requested: direct_codes,
});
}
let groups = (direct_codes >> postfix_bits) & 0x0F;
if (groups << postfix_bits) != direct_codes {
return Err(ConfigError::MisalignedDistanceCodes {
postfix_bits,
direct_codes,
});
}
Ok(Self::Explicit {
postfix_bits,
direct_codes,
})
}
pub(crate) const fn resolve(self) -> DistanceCodes {
match self {
Self::Auto => DistanceCodes::DEFAULT,
Self::Explicit {
postfix_bits,
direct_codes,
} => DistanceCodes::from_raw(postfix_bits as u32, direct_codes as u32),
}
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub enum LiteralContextMode {
#[default]
Auto,
Enabled,
Disabled,
}
impl LiteralContextMode {
pub(crate) const fn resolve(self) -> bool {
match self {
Self::Auto | Self::Enabled => true,
Self::Disabled => false,
}
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct EncoderConfig {
quality: Quality,
window: Window,
block_size: BlockSize,
mode: CompressionMode,
distance: DistanceParams,
literal_context: LiteralContextMode,
}
impl EncoderConfig {
#[must_use]
pub const fn with_quality(mut self, quality: Quality) -> Self {
self.quality = quality;
self
}
#[must_use]
pub const fn quality(&self) -> Quality {
self.quality
}
#[must_use]
pub const fn with_window(mut self, window: Window) -> Self {
self.window = window;
self
}
#[must_use]
pub const fn window(&self) -> Window {
self.window
}
#[must_use]
pub const fn with_block_size(mut self, block_size: BlockSize) -> Self {
self.block_size = block_size;
self
}
#[must_use]
pub const fn block_size(&self) -> BlockSize {
self.block_size
}
#[must_use]
pub const fn with_mode(mut self, mode: CompressionMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub const fn mode(&self) -> CompressionMode {
self.mode
}
#[must_use]
pub const fn with_distance(mut self, distance: DistanceParams) -> Self {
self.distance = distance;
self
}
#[must_use]
pub const fn distance(&self) -> DistanceParams {
self.distance
}
#[must_use]
pub const fn with_literal_context(mut self, literal_context: LiteralContextMode) -> Self {
self.literal_context = literal_context;
self
}
#[must_use]
pub const fn literal_context(&self) -> LiteralContextMode {
self.literal_context
}
pub(crate) const fn validate(&self) -> Result<(), ConfigError> {
if matches!(self.window.encoding, WindowEncoding::Large) && self.quality.0 <= 2 {
return Err(ConfigError::LargeWindowUnsupportedForQuality {
quality: self.quality,
});
}
Ok(())
}
pub(crate) const fn lower(&self, size_hint: Option<usize>) -> super::internal::CompressParams {
super::internal::CompressParams {
#[cfg(feature = "experimental")]
stream_offset: 0,
quality: self.quality.level(),
lgwin: self.window.resolve(),
lgblock: match self.block_size {
BlockSize::Auto => None,
BlockSize::Bits(bits) => Some(bits),
},
mode: self.mode.resolve(),
size_hint,
distance_codes: self.distance.resolve(),
literal_context_modeling: self.literal_context.resolve(),
}
}
}
#[derive(Error, Debug, Copy, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum ConfigError {
#[error("quality {requested} is outside the 0..=11 the format defines")]
Quality {
requested: u8,
},
#[error("an ordinary window of {requested} bits is outside the 10..=24 RFC 7932 expresses")]
StandardWindow {
requested: u8,
},
#[error("a large window of {requested} bits is outside the 10..=62 RFC 9841 expresses")]
LargeWindow {
requested: u8,
},
#[error("a block size of {requested} bits is outside the 16..=24 the encoder accepts")]
BlockBits {
requested: u8,
},
#[error("{requested} distance postfix bits is more than the 3 RFC 7932 allows")]
DistancePostfixBits {
requested: u8,
},
#[error("{requested} direct distance codes is more than the 120 RFC 7932 allows")]
DirectDistanceCodes {
requested: u16,
},
#[error(
"{direct_codes} direct distance codes is not a whole number of \
1 << {postfix_bits} groups the header can hold"
)]
MisalignedDistanceCodes {
postfix_bits: u8,
direct_codes: u16,
},
#[error("quality {} cannot carry a large window", quality.get())]
LargeWindowUnsupportedForQuality {
quality: Quality,
},
}
#[derive(Error, Debug, Copy, Clone, Eq, PartialEq)]
#[error("the compressed-size bound overflows the address space")]
pub struct SizeOverflow;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_quality_the_format_defines_round_trips() {
for value in 0u8..=11 {
let quality = Quality::try_from(value).expect("a legal quality");
assert_eq!(quality.get(), value);
assert_eq!(u8::from(quality), value);
assert_eq!(usize::from(quality.level()), usize::from(value));
}
for value in [12u8, 13, 255] {
assert_eq!(
Quality::try_from(value),
Err(ConfigError::Quality { requested: value })
);
}
}
#[test]
fn the_quality_constants_are_their_numbers() {
let constants = [
Quality::Q0,
Quality::Q1,
Quality::Q2,
Quality::Q3,
Quality::Q4,
Quality::Q5,
Quality::Q6,
Quality::Q7,
Quality::Q8,
Quality::Q9,
Quality::Q10,
Quality::Q11,
];
for (index, quality) in constants.into_iter().enumerate() {
assert_eq!(usize::from(quality.get()), index);
}
assert_eq!(Quality::MIN, Quality::Q0);
assert_eq!(Quality::MAX, Quality::Q11);
assert_eq!(Quality::default(), Quality::Q11);
}
#[test]
fn a_window_carries_its_header_as_well_as_its_size() {
for bits in 10u8..=24 {
let ordinary = Window::standard(bits).expect("a legal ordinary window");
let large = Window::large(bits).expect("a legal large window");
assert_eq!(ordinary.bits(), large.bits());
assert_ne!(ordinary, large);
assert_eq!(ordinary.encoding(), WindowEncoding::Standard);
assert_eq!(large.encoding(), WindowEncoding::Large);
assert_ne!(ordinary.resolve(), large.resolve());
}
for bits in 25u8..=62 {
assert!(Window::standard(bits).is_err());
assert!(Window::large(bits).is_ok());
}
}
#[test]
fn a_window_outside_its_header_is_refused() {
for bits in 0u8..10 {
assert_eq!(
Window::standard(bits),
Err(ConfigError::StandardWindow { requested: bits })
);
assert_eq!(
Window::large(bits),
Err(ConfigError::LargeWindow { requested: bits })
);
}
assert_eq!(
Window::standard(25),
Err(ConfigError::StandardWindow { requested: 25 })
);
assert_eq!(
Window::large(63),
Err(ConfigError::LargeWindow { requested: 63 })
);
assert_eq!(Window::default(), Window::DEFAULT);
assert_eq!(Window::DEFAULT.bits(), 22);
}
#[test]
fn block_bits_accept_exactly_the_encoders_range() {
for bits in 16u8..=24 {
let block = BlockBits::try_from(bits).expect("a legal block size");
assert_eq!(block.get(), bits);
assert_eq!(usize::from(block), usize::from(bits));
assert_eq!(BlockSize::from(block), BlockSize::Bits(block));
}
for bits in [0u8, 15, 25, 255] {
assert_eq!(
BlockBits::try_from(bits),
Err(ConfigError::BlockBits { requested: bits })
);
}
assert_eq!(BlockBits::MIN.get(), 16);
assert_eq!(BlockBits::MAX.get(), 24);
assert!(BlockBits::MIN < BlockBits::MAX);
assert_eq!(BlockSize::default(), BlockSize::Auto);
}
#[test]
fn an_explicit_distance_layout_is_validated_when_it_is_built() {
for postfix in 0u8..=3 {
for groups in 0u16..16 {
let direct = groups << postfix;
if direct > DistanceParams::MAX_DIRECT_CODES {
continue;
}
assert_eq!(
DistanceParams::explicit(postfix, direct),
Ok(DistanceParams::Explicit {
postfix_bits: postfix,
direct_codes: direct,
})
);
}
}
assert_eq!(
DistanceParams::explicit(4, 0),
Err(ConfigError::DistancePostfixBits { requested: 4 })
);
assert_eq!(
DistanceParams::explicit(0, 121),
Err(ConfigError::DirectDistanceCodes { requested: 121 })
);
assert_eq!(
DistanceParams::explicit(2, 6),
Err(ConfigError::MisalignedDistanceCodes {
postfix_bits: 2,
direct_codes: 6,
})
);
assert_eq!(
DistanceParams::explicit(0, 16),
Err(ConfigError::MisalignedDistanceCodes {
postfix_bits: 0,
direct_codes: 16,
})
);
assert_eq!(DistanceParams::default(), DistanceParams::Auto);
}
#[test]
fn the_literal_context_policy_resolves_the_way_the_reference_does() {
assert!(LiteralContextMode::Auto.resolve());
assert!(LiteralContextMode::Enabled.resolve());
assert!(!LiteralContextMode::Disabled.resolve());
assert_eq!(LiteralContextMode::default(), LiteralContextMode::Auto);
}
#[test]
fn every_mode_lowers_to_its_own_encoder_mode() {
assert_eq!(CompressionMode::Generic.resolve(), CompressMode::Generic);
assert_eq!(CompressionMode::Text.resolve(), CompressMode::Text);
assert_eq!(CompressionMode::Font.resolve(), CompressMode::Font);
assert_eq!(CompressionMode::default(), CompressionMode::Generic);
}
#[test]
fn the_default_configuration_mirrors_the_reference() {
let config = EncoderConfig::default();
assert_eq!(config.quality(), Quality::Q11);
assert_eq!(config.window(), Window::DEFAULT);
assert_eq!(config.block_size(), BlockSize::Auto);
assert_eq!(config.mode(), CompressionMode::Generic);
assert_eq!(config.distance(), DistanceParams::Auto);
assert_eq!(config.literal_context(), LiteralContextMode::Auto);
}
#[test]
fn every_setter_changes_only_its_own_field() {
let base = EncoderConfig::default();
let quality = base.with_quality(Quality::Q1);
assert_eq!(quality.quality(), Quality::Q1);
assert_eq!(quality.window(), base.window());
assert_eq!(quality.mode(), base.mode());
let window = base.with_window(Window::large(30).expect("legal"));
assert_eq!(window.window().bits(), 30);
assert_eq!(window.quality(), base.quality());
let block = base.with_block_size(BlockSize::Bits(BlockBits::MAX));
assert_eq!(block.block_size(), BlockSize::Bits(BlockBits::MAX));
assert_eq!(block.distance(), base.distance());
let mode = base.with_mode(CompressionMode::Font);
assert_eq!(mode.mode(), CompressionMode::Font);
let distance = base.with_distance(DistanceParams::explicit(1, 12).expect("legal"));
assert_eq!(
distance.distance(),
DistanceParams::Explicit {
postfix_bits: 1,
direct_codes: 12,
}
);
let literals = base.with_literal_context(LiteralContextMode::Disabled);
assert_eq!(literals.literal_context(), LiteralContextMode::Disabled);
assert_eq!(literals.quality(), base.quality());
}
#[test]
fn a_large_window_is_refused_only_at_the_qualities_that_cannot_carry_one() {
let large = Window::large(30).expect("legal");
for value in 0u8..=11 {
let quality = Quality::try_from(value).expect("legal");
let config = EncoderConfig::default()
.with_quality(quality)
.with_window(large);
if value <= 2 {
assert_eq!(
config.validate(),
Err(ConfigError::LargeWindowUnsupportedForQuality { quality })
);
} else {
assert_eq!(config.validate(), Ok(()));
}
assert_eq!(
EncoderConfig::default().with_quality(quality).validate(),
Ok(())
);
}
}
#[test]
fn lowering_carries_every_field_and_the_operations_size_hint() {
let config = EncoderConfig::default()
.with_quality(Quality::Q5)
.with_window(Window::large(30).expect("legal"))
.with_block_size(BlockSize::Bits(BlockBits::MAX))
.with_mode(CompressionMode::Text)
.with_distance(DistanceParams::explicit(1, 12).expect("legal"))
.with_literal_context(LiteralContextMode::Disabled);
let lowered = config.lower(Some(4096));
assert_eq!(lowered.quality, QualityLevel::Q5);
assert_eq!(lowered.lgwin, WindowBits::large(30).expect("legal"));
assert_eq!(lowered.lgblock, Some(BlockBits::MAX));
assert_eq!(lowered.mode, CompressMode::Text);
assert_eq!(lowered.size_hint, Some(4096));
assert_eq!(lowered.distance_codes.postfix_bits(), 1);
assert_eq!(lowered.distance_codes.direct_codes(), 12);
assert!(!lowered.literal_context_modeling);
assert_eq!(config.lower(None).size_hint, None);
}
#[test]
fn a_configuration_error_says_what_was_asked_for() {
let messages = [
ConfigError::Quality { requested: 12 }.to_string(),
ConfigError::StandardWindow { requested: 25 }.to_string(),
ConfigError::LargeWindow { requested: 63 }.to_string(),
ConfigError::BlockBits { requested: 15 }.to_string(),
ConfigError::DistancePostfixBits { requested: 4 }.to_string(),
ConfigError::DirectDistanceCodes { requested: 121 }.to_string(),
ConfigError::MisalignedDistanceCodes {
postfix_bits: 2,
direct_codes: 6,
}
.to_string(),
ConfigError::LargeWindowUnsupportedForQuality {
quality: Quality::Q1,
}
.to_string(),
];
assert!(messages[0].contains("12"));
assert!(messages[1].contains("25"));
assert!(messages[2].contains("63"));
assert!(messages[3].contains("15"));
assert!(messages[4].contains('4'));
assert!(messages[5].contains("121"));
assert!(messages[6].contains('6'));
assert!(messages[7].contains('1'));
assert_eq!(
SizeOverflow.to_string(),
"the compressed-size bound overflows the address space"
);
}
}