use crate::error::{KafkoError, Result};
use std::cell::RefCell;
const ZSTD_LEVEL: i32 = 3;
const DECOMPRESS_MAX_SIZE: usize = 16 * 1024 * 1024;
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) };
}
#[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)),
}
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub(crate) fn compress(self, raw: &[u8], out: &mut Vec<u8>) {
out.clear();
match self {
Compression::None => out.extend_from_slice(raw),
Compression::Lz4 => {
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_flex::compress_into(raw, &mut out[4..]).expect(
"lz4 compress_into should not fail with get_maximum_output_size-sized buffer",
);
out.truncate(4 + written);
}
Compression::Zstd => 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");
}),
}
}
pub(crate) fn decompress(self, compressed: &[u8]) -> Result<Vec<u8>> {
match self {
Compression::None => Ok(compressed.to_vec()),
Compression::Lz4 => {
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)
}
Compression::Zstd => 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(test)]
mod tests {
use super::*;
#[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),
}
}
#[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),
}
}
}
#[test]
fn lz4_decompress_accepts_normally_sized_payload() {
let raw = vec![0xABu8; 4096];
let mut compressed = Vec::new();
Compression::Lz4.compress(&raw, &mut compressed);
let decompressed = Compression::Lz4.decompress(&compressed).unwrap();
assert_eq!(decompressed, raw);
}
#[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),
}
}
}