use bytes::{BufMut, Bytes, BytesMut};
use krabka_units::prelude::{ByteSize, ByteSizeExt as _, kibibytes};
use crate::CompressionError;
const XERIAL_HEADER: [u8; 16] = [
0x82, b'S', b'N', b'A', b'P', b'P', b'Y', 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, ];
const XERIAL_CHUNK: ByteSize = kibibytes(32);
pub fn compress(data: &[u8]) -> Result<Bytes, CompressionError> {
let chunks = data.len().div_ceil(XERIAL_CHUNK.bytes_usize());
let capacity = XERIAL_HEADER
.len()
.saturating_add(data.len())
.saturating_add(data.len() / 6)
.saturating_add(chunks.saturating_mul(36));
let mut out = BytesMut::with_capacity(capacity);
out.put_slice(&XERIAL_HEADER);
let mut encoder = snap::raw::Encoder::new();
for chunk in data.chunks(XERIAL_CHUNK.bytes_usize()) {
let max = snap::raw::max_compress_len(chunk.len());
let header = out.len();
out.put_u32(0);
let start = out.len();
out.resize(start + max, 0);
let n = encoder
.compress(chunk, &mut out[start..])
.map_err(|e| CompressionError::InvalidData(format!("snappy encode: {e}")))?;
out[header..start]
.copy_from_slice(&u32::try_from(n).expect("chunk size fits u32").to_be_bytes());
out.truncate(start + n);
}
Ok(out.freeze())
}
pub fn decompress(data: &[u8], max_output: usize) -> Result<Bytes, CompressionError> {
if data.len() < XERIAL_HEADER.len() {
return Err(CompressionError::InvalidData(
"snappy payload too short for xerial header".into(),
));
}
if data[..8] != XERIAL_HEADER[..8] {
return Err(CompressionError::InvalidData(
"snappy missing xerial magic".into(),
));
}
let mut rest = &data[XERIAL_HEADER.len()..];
let mut out = BytesMut::new();
let mut decoder = snap::raw::Decoder::new();
while !rest.is_empty() {
if rest.len() < 4 {
return Err(CompressionError::InvalidData(
"snappy chunk header truncated".into(),
));
}
let len = u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]) as usize;
rest = &rest[4..];
if rest.len() < len {
return Err(CompressionError::InvalidData(
"snappy chunk body truncated".into(),
));
}
let (block, tail) = rest.split_at(len);
rest = tail;
let max_out = snap::raw::decompress_len(block)
.map_err(|e| CompressionError::InvalidData(format!("snappy decode_len: {e}")))?;
let end = out
.len()
.checked_add(max_out)
.ok_or(CompressionError::TooLarge { limit: max_output })?;
if end > max_output {
return Err(CompressionError::TooLarge { limit: max_output });
}
if out.capacity() == 0 {
out = BytesMut::with_capacity(end.max(data.len().saturating_mul(2).min(max_output)));
}
let start = out.len();
out.resize(end, 0);
let n = decoder
.decompress(block, &mut out[start..])
.map_err(|e| CompressionError::InvalidData(format!("snappy decode: {e}")))?;
out.truncate(start + n);
}
Ok(out.freeze())
}
#[cfg(test)]
mod tests {
use super::*;
const HELLO: &[u8] = b"hello kafka, this is a moderately repetitive payload to compress";
const BIG_CAP: usize = 256 * 1024 * 1024;
#[test]
fn roundtrip() {
let z = compress(HELLO).unwrap();
let back = decompress(&z, BIG_CAP).unwrap();
assert2::assert!(back.as_ref() == HELLO);
}
#[test]
fn decompress_truncated_header() {
assert2::assert!(matches!(
decompress(&XERIAL_HEADER[..4], BIG_CAP),
Err(CompressionError::InvalidData(_))
));
}
#[test]
fn decompress_missing_magic() {
let bytes = [0u8; 20];
assert2::assert!(matches!(
decompress(&bytes, BIG_CAP),
Err(CompressionError::InvalidData(_))
));
}
#[test]
fn decompress_truncated_chunk() {
let mut bytes = XERIAL_HEADER.to_vec();
bytes.extend_from_slice(&[0, 0, 0, 100]); bytes.push(0); assert2::assert!(matches!(
decompress(&bytes, BIG_CAP),
Err(CompressionError::InvalidData(_))
));
}
#[test]
fn decompression_bomb_rejected() {
let bomb = vec![0u8; 64 * 1024 * 1024];
let z = compress(&bomb).unwrap();
assert2::assert!(matches!(
decompress(&z, 1024),
Err(CompressionError::TooLarge { limit: 1024 })
));
let back = decompress(&z, BIG_CAP).unwrap();
assert2::assert!(back.as_ref() == bomb.as_slice());
}
#[test]
fn decompress_at_exact_cap_succeeds() {
let z = compress(HELLO).unwrap();
let back = decompress(&z, HELLO.len()).unwrap();
assert2::assert!(back.as_ref() == HELLO);
assert2::assert!(matches!(
decompress(&z, HELLO.len() - 1),
Err(CompressionError::TooLarge { limit }) if limit == HELLO.len() - 1
));
}
fn chunk_count(compressed: &[u8]) -> usize {
let mut rest = &compressed[XERIAL_HEADER.len()..];
let mut chunks = 0;
while rest.len() >= 4 {
let len = u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]) as usize;
rest = &rest[4 + len..];
chunks += 1;
}
chunks
}
#[test]
fn compress_splits_exactly_at_the_xerial_chunk_boundary() {
let chunk = XERIAL_CHUNK.bytes_usize();
for (payload_len, expected_chunks) in [
(4096, 1),
(chunk - 1, 1),
(chunk, 1),
(chunk + 1, 2),
(2 * chunk, 2),
(2 * chunk + 1, 3),
] {
let payload: Vec<u8> = (0..payload_len)
.map(|i| u8::try_from(i % 251).unwrap())
.collect();
let z = compress(&payload).unwrap();
assert2::check!(
chunk_count(&z) == expected_chunks,
"payload of {payload_len} bytes"
);
}
}
#[test]
fn decompress_bare_header_is_empty() {
let z = compress(b"").unwrap();
assert2::assert!(z.len() == XERIAL_HEADER.len());
let back = decompress(&z, 1024).unwrap();
assert2::assert!(back.is_empty());
}
#[test]
fn decompress_four_byte_chunk_header_reads_length() {
let mut bytes = XERIAL_HEADER.to_vec();
bytes.extend_from_slice(&[0, 0, 0, 5]); let Err(CompressionError::InvalidData(msg)) = decompress(&bytes, 1024) else {
panic!("expected InvalidData");
};
assert2::assert!(msg.contains("body"));
}
}