use crate::CompressionError;
const GZIP_LEVELS: LevelRange = LevelRange {
min: 1,
max: 9,
default: -1,
};
const LZ4_LEVELS: LevelRange = LevelRange {
min: 1,
max: 17,
default: 9,
};
const ZSTD_LEVELS: LevelRange = LevelRange {
min: -131_072,
max: 22,
default: 3,
};
#[derive(Debug, Clone, Copy)]
struct LevelRange {
min: i32,
max: i32,
default: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
#[non_exhaustive]
pub enum CompressionType {
None = 0,
Gzip = 1,
Snappy = 2,
Lz4 = 3,
Zstd = 4,
}
impl CompressionType {
#[must_use]
pub fn from_attribute_bits(b: u8) -> Option<Self> {
match b & 0b0000_0111 {
0 => Some(Self::None),
1 => Some(Self::Gzip),
2 => Some(Self::Snappy),
3 => Some(Self::Lz4),
4 => Some(Self::Zstd),
_ => None,
}
}
#[must_use]
pub fn as_attribute_bits(self) -> u8 {
self as u8
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::None => "none",
Self::Gzip => "gzip",
Self::Snappy => "snappy",
Self::Lz4 => "lz4",
Self::Zstd => "zstd",
}
}
#[must_use]
pub const fn default_level(self) -> Option<i32> {
match self.levels() {
Some(levels) => Some(levels.default),
None => None,
}
}
#[must_use]
pub const fn min_level(self) -> Option<i32> {
match self.levels() {
Some(levels) => Some(levels.min),
None => None,
}
}
#[must_use]
pub const fn max_level(self) -> Option<i32> {
match self.levels() {
Some(levels) => Some(levels.max),
None => None,
}
}
pub fn check_level(self, level: i32) -> Result<(), CompressionError> {
let invalid = |reason: String| CompressionError::InvalidLevel {
codec: self.name(),
level,
reason,
};
match (self, self.levels()) {
(_, None) => Err(invalid(format!(
"Compression levels are not defined for this compression type: {}",
self.name()
))),
(Self::Gzip, Some(levels)) => {
if level > levels.max || (level < levels.min && level != levels.default) {
Err(invalid(format!(
"Value must be between {} and {} or equal to {}",
levels.min, levels.max, levels.default
)))
} else {
Ok(())
}
}
(_, Some(levels)) => {
if level < levels.min {
Err(invalid(format!("Value must be at least {}", levels.min)))
} else if level > levels.max {
Err(invalid(format!(
"Value must be no more than {}",
levels.max
)))
} else {
Ok(())
}
}
}
}
const fn levels(self) -> Option<LevelRange> {
match self {
Self::None | Self::Snappy => None,
Self::Gzip => Some(GZIP_LEVELS),
Self::Lz4 => Some(LZ4_LEVELS),
Self::Zstd => Some(ZSTD_LEVELS),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attribute_bits_roundtrip() {
for (_name, compression) in [
("none", CompressionType::None),
("gzip", CompressionType::Gzip),
("snappy", CompressionType::Snappy),
("lz4", CompressionType::Lz4),
("zstd", CompressionType::Zstd),
] {
assert2::assert!(
CompressionType::from_attribute_bits(compression.as_attribute_bits())
== Some(compression)
);
}
}
#[test]
fn levels_follow_kafka_compression_type() {
let reasons = |codec: CompressionType, levels: &[i32]| -> Vec<Option<String>> {
levels
.iter()
.map(|&level| match codec.check_level(level) {
Ok(()) => None,
Err(CompressionError::InvalidLevel { reason, .. }) => Some(reason),
Err(other) => Some(format!("unexpected error {other}")),
})
.collect()
};
let gzip_reason = Some("Value must be between 1 and 9 or equal to -1".to_owned());
let none_levels = |name: &str| {
Some(format!(
"Compression levels are not defined for this compression type: {name}"
))
};
let actual = [
(
"none",
CompressionType::None.default_level(),
CompressionType::None.min_level(),
CompressionType::None.max_level(),
reasons(CompressionType::None, &[0]),
),
(
"gzip",
CompressionType::Gzip.default_level(),
CompressionType::Gzip.min_level(),
CompressionType::Gzip.max_level(),
reasons(CompressionType::Gzip, &[-2, -1, 0, 1, 9, 10]),
),
(
"snappy",
CompressionType::Snappy.default_level(),
CompressionType::Snappy.min_level(),
CompressionType::Snappy.max_level(),
reasons(CompressionType::Snappy, &[1]),
),
(
"lz4",
CompressionType::Lz4.default_level(),
CompressionType::Lz4.min_level(),
CompressionType::Lz4.max_level(),
reasons(CompressionType::Lz4, &[0, 1, 17, 18]),
),
(
"zstd",
CompressionType::Zstd.default_level(),
CompressionType::Zstd.min_level(),
CompressionType::Zstd.max_level(),
reasons(CompressionType::Zstd, &[-131_073, -131_072, 22, 23]),
),
];
let expected = [
("none", None, None, None, vec![none_levels("none")]),
(
"gzip",
Some(-1),
Some(1),
Some(9),
vec![
gzip_reason.clone(),
None,
gzip_reason.clone(),
None,
None,
gzip_reason,
],
),
("snappy", None, None, None, vec![none_levels("snappy")]),
(
"lz4",
Some(9),
Some(1),
Some(17),
vec![
Some("Value must be at least 1".to_owned()),
None,
None,
Some("Value must be no more than 17".to_owned()),
],
),
(
"zstd",
Some(3),
Some(-131_072),
Some(22),
vec![
Some("Value must be at least -131072".to_owned()),
None,
None,
Some("Value must be no more than 22".to_owned()),
],
),
];
assert2::assert!(actual == expected);
}
#[test]
fn attribute_bits_mask() {
assert2::assert!(
CompressionType::from_attribute_bits(0b1111_1000 | 0b0000_0001)
== Some(CompressionType::Gzip)
);
}
#[test]
fn attribute_bits_unknown() {
for (_name, bits) in [("reserved five", 5), ("reserved seven", 7)] {
assert2::assert!(CompressionType::from_attribute_bits(bits) == None);
}
}
}