use crate::encoding::CompressionLevel;
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Strategy {
Fast,
Dfast,
Greedy,
Lazy,
Lazy2,
Btlazy2,
Btopt,
Btultra,
Btultra2,
}
impl Strategy {
pub const fn ordinal(self) -> u32 {
match self {
Self::Fast => 1,
Self::Dfast => 2,
Self::Greedy => 3,
Self::Lazy => 4,
Self::Lazy2 => 5,
Self::Btlazy2 => 6,
Self::Btopt => 7,
Self::Btultra => 8,
Self::Btultra2 => 9,
}
}
pub const fn from_ordinal(ordinal: u32) -> Option<Self> {
Some(match ordinal {
1 => Self::Fast,
2 => Self::Dfast,
3 => Self::Greedy,
4 => Self::Lazy,
5 => Self::Lazy2,
6 => Self::Btlazy2,
7 => Self::Btopt,
8 => Self::Btultra,
9 => Self::Btultra2,
_ => return None,
})
}
pub(crate) const fn tag(self) -> crate::encoding::strategy::StrategyTag {
use crate::encoding::strategy::StrategyTag;
match self {
Self::Fast => StrategyTag::Fast,
Self::Dfast => StrategyTag::Dfast,
Self::Greedy => StrategyTag::Greedy,
Self::Lazy | Self::Lazy2 => StrategyTag::Lazy,
Self::Btlazy2 => StrategyTag::Btlazy2,
Self::Btopt => StrategyTag::BtOpt,
Self::Btultra => StrategyTag::BtUltra,
Self::Btultra2 => StrategyTag::BtUltra2,
}
}
pub(crate) const fn lazy_depth(self) -> u8 {
match self {
Self::Fast | Self::Dfast | Self::Greedy => 0,
Self::Lazy => 1,
_ => 2,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CParameter {
WindowLog,
HashLog,
ChainLog,
SearchLog,
MinMatch,
TargetLength,
Strategy,
EnableLongDistanceMatching,
LdmHashLog,
LdmMinMatch,
LdmBucketSizeLog,
LdmHashRateLog,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Bounds {
pub lower_bound: i64,
pub upper_bound: i64,
}
impl Bounds {
pub const fn contains(&self, value: i64) -> bool {
value >= self.lower_bound && value <= self.upper_bound
}
}
impl CParameter {
pub const fn bounds(self) -> Bounds {
let (lower_bound, upper_bound) = match self {
Self::WindowLog => (10, 30),
Self::HashLog => (6, 30),
Self::ChainLog => (6, 30),
Self::SearchLog => (1, 30),
Self::MinMatch => (3, 7),
Self::TargetLength => (0, 131_072),
Self::Strategy => (1, 9),
Self::EnableLongDistanceMatching => (0, 1),
Self::LdmHashLog => (6, 30),
Self::LdmMinMatch => (4, 4096),
Self::LdmBucketSizeLog => (1, 8),
Self::LdmHashRateLog => (0, 24),
};
Bounds {
lower_bound,
upper_bound,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParameterError {
OutOfBounds {
parameter: CParameter,
value: i64,
bounds: Bounds,
},
}
impl core::fmt::Display for ParameterError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::OutOfBounds {
parameter,
value,
bounds,
} => write!(
f,
"compression parameter {parameter:?} = {value} out of bounds \
[{}, {}]",
bounds.lower_bound, bounds.upper_bound
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParameterError {}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct LdmOverride {
pub(crate) hash_log: Option<u32>,
pub(crate) min_match: Option<u32>,
pub(crate) bucket_size_log: Option<u32>,
pub(crate) hash_rate_log: Option<u32>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct ParamOverrides {
pub(crate) window_log: Option<u8>,
pub(crate) hash_log: Option<u32>,
pub(crate) chain_log: Option<u32>,
pub(crate) search_log: Option<u32>,
pub(crate) min_match: Option<u32>,
pub(crate) target_length: Option<u32>,
pub(crate) strategy: Option<Strategy>,
pub(crate) ldm: Option<LdmOverride>,
}
impl ParamOverrides {
pub(crate) fn is_empty(&self) -> bool {
self.window_log.is_none()
&& self.hash_log.is_none()
&& self.chain_log.is_none()
&& self.search_log.is_none()
&& self.min_match.is_none()
&& self.target_length.is_none()
&& self.strategy.is_none()
&& self.ldm.is_none()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct CompressionParameters {
level: CompressionLevel,
overrides: ParamOverrides,
}
impl CompressionParameters {
pub fn builder(level: CompressionLevel) -> CompressionParametersBuilder {
CompressionParametersBuilder {
level,
window_log: None,
hash_log: None,
chain_log: None,
search_log: None,
min_match: None,
target_length: None,
strategy: None,
enable_ldm: false,
ldm: LdmOverride::default(),
}
}
pub fn level(&self) -> CompressionLevel {
self.level
}
pub fn long_distance_matching_enabled(&self) -> bool {
self.overrides.ldm.is_some()
}
pub(crate) fn overrides(&self) -> ParamOverrides {
self.overrides
}
}
#[derive(Copy, Clone, Debug)]
pub struct CompressionParametersBuilder {
level: CompressionLevel,
window_log: Option<u32>,
hash_log: Option<u32>,
chain_log: Option<u32>,
search_log: Option<u32>,
min_match: Option<u32>,
target_length: Option<u32>,
strategy: Option<Strategy>,
enable_ldm: bool,
ldm: LdmOverride,
}
impl CompressionParametersBuilder {
pub fn window_log(mut self, value: u32) -> Self {
self.window_log = Some(value);
self
}
pub fn hash_log(mut self, value: u32) -> Self {
self.hash_log = Some(value);
self
}
pub fn chain_log(mut self, value: u32) -> Self {
self.chain_log = Some(value);
self
}
pub fn search_log(mut self, value: u32) -> Self {
self.search_log = Some(value);
self
}
pub fn min_match(mut self, value: u32) -> Self {
self.min_match = Some(value);
self
}
pub fn target_length(mut self, value: u32) -> Self {
self.target_length = Some(value);
self
}
pub fn strategy(mut self, value: Strategy) -> Self {
self.strategy = Some(value);
self
}
pub fn enable_long_distance_matching(mut self, enable: bool) -> Self {
self.enable_ldm = enable;
self
}
pub fn ldm_hash_log(mut self, value: u32) -> Self {
self.enable_ldm = true;
self.ldm.hash_log = Some(value);
self
}
pub fn ldm_min_match(mut self, value: u32) -> Self {
self.enable_ldm = true;
self.ldm.min_match = Some(value);
self
}
pub fn ldm_bucket_size_log(mut self, value: u32) -> Self {
self.enable_ldm = true;
self.ldm.bucket_size_log = Some(value);
self
}
pub fn ldm_hash_rate_log(mut self, value: u32) -> Self {
self.enable_ldm = true;
self.ldm.hash_rate_log = Some(value);
self
}
pub fn build(self) -> Result<CompressionParameters, ParameterError> {
check(CParameter::WindowLog, self.window_log)?;
check(CParameter::HashLog, self.hash_log)?;
check(CParameter::ChainLog, self.chain_log)?;
check(CParameter::SearchLog, self.search_log)?;
check(CParameter::MinMatch, self.min_match)?;
check(CParameter::TargetLength, self.target_length)?;
if let Some(s) = self.strategy {
check(CParameter::Strategy, Some(s.ordinal()))?;
}
let ldm = if self.enable_ldm {
check(CParameter::LdmHashLog, self.ldm.hash_log)?;
check(CParameter::LdmMinMatch, self.ldm.min_match)?;
check(CParameter::LdmBucketSizeLog, self.ldm.bucket_size_log)?;
check(CParameter::LdmHashRateLog, self.ldm.hash_rate_log)?;
Some(self.ldm)
} else {
None
};
Ok(CompressionParameters {
level: self.level,
overrides: ParamOverrides {
window_log: self.window_log.map(|v| v as u8),
hash_log: self.hash_log,
chain_log: self.chain_log,
search_log: self.search_log,
min_match: self.min_match,
target_length: self.target_length,
strategy: self.strategy,
ldm,
},
})
}
}
fn check(parameter: CParameter, value: Option<u32>) -> Result<(), ParameterError> {
if let Some(value) = value {
let bounds = parameter.bounds();
let value = i64::from(value);
if !bounds.contains(value) {
return Err(ParameterError::OutOfBounds {
parameter,
value,
bounds,
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strategy_ordinals_round_trip() {
for ordinal in 1..=9 {
let s = Strategy::from_ordinal(ordinal).expect("valid ordinal");
assert_eq!(s.ordinal(), ordinal);
}
assert_eq!(Strategy::from_ordinal(0), None);
assert_eq!(Strategy::from_ordinal(10), None);
}
#[test]
fn builder_default_overrides_nothing() {
let p = CompressionParameters::builder(CompressionLevel::Level(7))
.build()
.unwrap();
assert!(p.overrides().is_empty());
assert_eq!(p.level(), CompressionLevel::Level(7));
assert!(!p.long_distance_matching_enabled());
}
#[test]
fn builder_records_each_knob() {
let p = CompressionParameters::builder(CompressionLevel::Level(19))
.window_log(22)
.hash_log(23)
.chain_log(24)
.search_log(7)
.min_match(4)
.target_length(256)
.strategy(Strategy::Btultra2)
.build()
.unwrap();
let o = p.overrides();
assert_eq!(o.window_log, Some(22));
assert_eq!(o.hash_log, Some(23));
assert_eq!(o.chain_log, Some(24));
assert_eq!(o.search_log, Some(7));
assert_eq!(o.min_match, Some(4));
assert_eq!(o.target_length, Some(256));
assert_eq!(o.strategy, Some(Strategy::Btultra2));
assert!(!o.is_empty());
}
#[test]
fn enable_ldm_sets_override_block() {
let p = CompressionParameters::builder(CompressionLevel::Level(19))
.enable_long_distance_matching(true)
.build()
.unwrap();
assert!(p.long_distance_matching_enabled());
assert_eq!(p.overrides().ldm, Some(LdmOverride::default()));
}
#[test]
fn ldm_knob_implies_enable() {
let p = CompressionParameters::builder(CompressionLevel::Level(19))
.ldm_hash_log(24)
.ldm_min_match(64)
.ldm_bucket_size_log(4)
.ldm_hash_rate_log(7)
.build()
.unwrap();
assert!(p.long_distance_matching_enabled());
let ldm = p.overrides().ldm.unwrap();
assert_eq!(ldm.hash_log, Some(24));
assert_eq!(ldm.min_match, Some(64));
assert_eq!(ldm.bucket_size_log, Some(4));
assert_eq!(ldm.hash_rate_log, Some(7));
}
#[test]
fn out_of_bounds_window_log_rejected() {
let err = CompressionParameters::builder(CompressionLevel::Default)
.window_log(31)
.build()
.unwrap_err();
match err {
ParameterError::OutOfBounds {
parameter, value, ..
} => {
assert_eq!(parameter, CParameter::WindowLog);
assert_eq!(value, 31);
}
}
}
#[test]
fn out_of_bounds_min_match_rejected() {
let err = CompressionParameters::builder(CompressionLevel::Default)
.min_match(2)
.build()
.unwrap_err();
assert!(matches!(
err,
ParameterError::OutOfBounds {
parameter: CParameter::MinMatch,
..
}
));
}
#[test]
fn ldm_bounds_only_checked_when_enabled() {
let err = CompressionParameters::builder(CompressionLevel::Default)
.ldm_bucket_size_log(9)
.build()
.unwrap_err();
assert!(matches!(
err,
ParameterError::OutOfBounds {
parameter: CParameter::LdmBucketSizeLog,
..
}
));
}
#[test]
fn bounds_match_c_reference() {
assert_eq!(
CParameter::WindowLog.bounds(),
Bounds {
lower_bound: 10,
upper_bound: 30
}
);
assert_eq!(
CParameter::Strategy.bounds(),
Bounds {
lower_bound: 1,
upper_bound: 9
}
);
assert_eq!(
CParameter::TargetLength.bounds(),
Bounds {
lower_bound: 0,
upper_bound: 131_072
}
);
assert!(CParameter::MinMatch.bounds().contains(3));
assert!(CParameter::MinMatch.bounds().contains(7));
assert!(!CParameter::MinMatch.bounds().contains(8));
}
}