use crate::error::{KafkoError, Result};
#[cfg(any(feature = "compression-lz4", feature = "compression-zstd"))]
use std::cell::RefCell;
#[cfg(feature = "compression-zstd")]
const ZSTD_LEVEL: i32 = 3;
#[allow(dead_code)]
const DECOMPRESS_MAX_SIZE: usize = 16 * 1024 * 1024;
#[cfg(feature = "compression-zstd")]
thread_local! {
static ZSTD_COMPRESSOR: RefCell<Option<zstd::bulk::Compressor<'static>>> =
const { RefCell::new(None) };
static ZSTD_DECOMPRESSOR: RefCell<Option<zstd::bulk::Decompressor<'static>>> =
const { RefCell::new(None) };
}
#[cfg(feature = "compression-lz4")]
thread_local! {
static LZ4_TABLE: RefCell<lz4_flex::block::CompressTable> =
RefCell::new(lz4_flex::block::CompressTable::default());
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Compression {
#[default]
None,
Lz4,
Zstd,
}
impl Compression {
pub(crate) fn flag(self) -> u8 {
match self {
Compression::None => 0,
Compression::Lz4 => 1,
Compression::Zstd => 2,
}
}
pub(crate) fn from_flag(flag: u8) -> Result<Self> {
match flag {
0 => Ok(Compression::None),
1 => Ok(Compression::Lz4),
2 => Ok(Compression::Zstd),
other => Err(KafkoError::UnknownCompression(other)),
}
}
pub fn is_available(self) -> bool {
match self {
Compression::None => true,
Compression::Lz4 => cfg!(feature = "compression-lz4"),
Compression::Zstd => cfg!(feature = "compression-zstd"),
}
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub(crate) fn compress(self, raw: &[u8], out: &mut Vec<u8>) -> Result<()> {
out.clear();
match self {
Compression::None => {
out.extend_from_slice(raw);
Ok(())
}
Compression::Lz4 => lz4_compress(raw, out),
Compression::Zstd => zstd_compress(raw, out),
}
}
pub(crate) fn decompress(self, compressed: &[u8]) -> Result<Vec<u8>> {
match self {
Compression::None => Ok(compressed.to_vec()),
Compression::Lz4 => lz4_decompress(compressed),
Compression::Zstd => zstd_decompress(compressed),
}
}
}
#[cfg(feature = "compression-lz4")]
fn lz4_compress(raw: &[u8], out: &mut Vec<u8>) -> Result<()> {
let max_block_size = lz4_flex::block::get_maximum_output_size(raw.len());
out.resize(4 + max_block_size, 0);
out[..4].copy_from_slice(&(raw.len() as u32).to_le_bytes());
let written = LZ4_TABLE
.with_borrow_mut(|table| {
lz4_flex::block::compress_into_with_table(raw, &mut out[4..], table)
})
.expect(
"lz4 compress_into_with_table should not fail with get_maximum_output_size-sized buffer",
);
out.truncate(4 + written);
Ok(())
}
#[cfg(not(feature = "compression-lz4"))]
fn lz4_compress(_raw: &[u8], _out: &mut Vec<u8>) -> Result<()> {
Err(KafkoError::CompressionUnavailable(Compression::Lz4))
}
#[cfg(feature = "compression-zstd")]
fn zstd_compress(raw: &[u8], out: &mut Vec<u8>) -> Result<()> {
ZSTD_COMPRESSOR.with(|c| {
let mut c = c.borrow_mut();
if c.is_none() {
*c = Some(
zstd::bulk::Compressor::new(ZSTD_LEVEL)
.expect("zstd Compressor::new should not fail at level 3"),
);
}
let bound = zstd::zstd_safe::compress_bound(raw.len());
out.reserve(bound);
c.as_mut()
.expect("compressor initialized by the is_none branch above")
.compress_to_buffer(raw, out)
.expect("zstd compress should not fail with compress_bound-sized buffer");
Ok(())
})
}
#[cfg(not(feature = "compression-zstd"))]
fn zstd_compress(_raw: &[u8], _out: &mut Vec<u8>) -> Result<()> {
Err(KafkoError::CompressionUnavailable(Compression::Zstd))
}
#[cfg(feature = "compression-lz4")]
fn lz4_decompress(compressed: &[u8]) -> Result<Vec<u8>> {
if compressed.len() < 4 {
return Err(KafkoError::DecompressionFailed);
}
let claimed_size = u32::from_le_bytes([
compressed[0],
compressed[1],
compressed[2],
compressed[3],
]) as usize;
if claimed_size > DECOMPRESS_MAX_SIZE {
return Err(KafkoError::DecompressionFailed);
}
lz4_flex::decompress_size_prepended(compressed).map_err(|_| KafkoError::DecompressionFailed)
}
#[cfg(not(feature = "compression-lz4"))]
fn lz4_decompress(_compressed: &[u8]) -> Result<Vec<u8>> {
Err(KafkoError::CompressionUnavailable(Compression::Lz4))
}
#[cfg(feature = "compression-zstd")]
fn zstd_decompress(compressed: &[u8]) -> Result<Vec<u8>> {
ZSTD_DECOMPRESSOR.with(|d| {
let mut d = d.borrow_mut();
if d.is_none() {
*d = Some(
zstd::bulk::Decompressor::new().expect("zstd Decompressor::new should not fail"),
);
}
d.as_mut()
.expect("decompressor initialized by the is_none branch above")
.decompress(compressed, DECOMPRESS_MAX_SIZE)
.map_err(|_| KafkoError::DecompressionFailed)
})
}
#[cfg(not(feature = "compression-zstd"))]
fn zstd_decompress(_compressed: &[u8]) -> Result<Vec<u8>> {
Err(KafkoError::CompressionUnavailable(Compression::Zstd))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "compression-lz4")]
#[test]
fn lz4_decompress_rejects_oversized_size_prefix() {
let payload = [0x55u8, 0xFF, 0xFF, 0xFF, 0x00];
match Compression::Lz4.decompress(&payload) {
Err(KafkoError::DecompressionFailed) => {}
other => panic!("expected DecompressionFailed, got {:?}", other),
}
}
#[cfg(feature = "compression-lz4")]
#[test]
fn lz4_decompress_rejects_payload_shorter_than_size_prefix() {
for len in 0..4 {
let buf = vec![0u8; len];
match Compression::Lz4.decompress(&buf) {
Err(KafkoError::DecompressionFailed) => {}
other => panic!("len={len}: expected DecompressionFailed, got {:?}", other),
}
}
}
#[cfg(feature = "compression-lz4")]
#[test]
fn lz4_decompress_accepts_normally_sized_payload() {
let raw = vec![0xABu8; 4096];
let mut compressed = Vec::new();
Compression::Lz4.compress(&raw, &mut compressed).unwrap();
let decompressed = Compression::Lz4.decompress(&compressed).unwrap();
assert_eq!(decompressed, raw);
}
#[cfg(feature = "compression-zstd")]
#[test]
fn zstd_decompress_rejects_oversized_claim() {
let raw = vec![0u8; DECOMPRESS_MAX_SIZE + 1];
let mut c = zstd::bulk::Compressor::new(ZSTD_LEVEL).unwrap();
let frame = c.compress(&raw).unwrap();
match Compression::Zstd.decompress(&frame) {
Err(KafkoError::DecompressionFailed) => {}
other => panic!("expected DecompressionFailed, got {:?}", other),
}
}
#[cfg(not(feature = "compression-lz4"))]
#[test]
fn lz4_calls_return_unavailable_when_feature_off() {
let mut out = Vec::new();
match Compression::Lz4.compress(b"hello", &mut out) {
Err(KafkoError::CompressionUnavailable(Compression::Lz4)) => {}
other => panic!("expected CompressionUnavailable(Lz4), got {:?}", other),
}
match Compression::Lz4.decompress(b"\x00\x00\x00\x00") {
Err(KafkoError::CompressionUnavailable(Compression::Lz4)) => {}
other => panic!("expected CompressionUnavailable(Lz4), got {:?}", other),
}
}
#[cfg(not(feature = "compression-zstd"))]
#[test]
fn zstd_calls_return_unavailable_when_feature_off() {
let mut out = Vec::new();
match Compression::Zstd.compress(b"hello", &mut out) {
Err(KafkoError::CompressionUnavailable(Compression::Zstd)) => {}
other => panic!("expected CompressionUnavailable(Zstd), got {:?}", other),
}
match Compression::Zstd.decompress(b"\x00\x00\x00\x00") {
Err(KafkoError::CompressionUnavailable(Compression::Zstd)) => {}
other => panic!("expected CompressionUnavailable(Zstd), got {:?}", other),
}
}
#[test]
fn is_available_reports_truth_about_this_build() {
assert!(Compression::None.is_available());
assert_eq!(
Compression::Lz4.is_available(),
cfg!(feature = "compression-lz4")
);
assert_eq!(
Compression::Zstd.is_available(),
cfg!(feature = "compression-zstd")
);
}
}