use anyhow::{Result, anyhow};
#[cfg(not(feature = "openzl"))]
const NO_CODEC: &str = "znippy-common was built without the `openzl` feature, so the OpenZL codec \
is not linked: compressed blobs cannot be read or written. Stored (uncompressed) \
entries, the Arrow index and the metadata sub-index are unaffected. Enable the \
`openzl` feature to link the codec (note: it builds the vendored OpenZL C sources \
and needs a C compiler at compile time).";
pub struct CompressCtx {
#[cfg(feature = "openzl")]
cctx: openzl_sys_rs::ZlCCtx,
}
unsafe impl Send for CompressCtx {}
#[cfg(not(feature = "openzl"))]
impl CompressCtx {
pub fn new(_compression_level: i32) -> Result<Self> {
Err(anyhow!(NO_CODEC))
}
pub fn compress(&mut self, _input: &[u8]) -> Result<Vec<u8>> {
Err(anyhow!(NO_CODEC))
}
pub fn compress_into(&mut self, _input: &[u8], _out: &mut Vec<u8>) -> Result<usize> {
Err(anyhow!(NO_CODEC))
}
}
#[cfg(not(feature = "openzl"))]
pub fn decompress_frame(_compressed: &[u8]) -> Result<Vec<u8>> {
Err(anyhow!(NO_CODEC))
}
#[cfg(not(feature = "openzl"))]
pub fn decompress_into(_compressed: &[u8], _out: &mut Vec<u8>) -> Result<usize> {
Err(anyhow!(NO_CODEC))
}
#[cfg(feature = "openzl")]
impl CompressCtx {
pub fn new(compression_level: i32) -> Result<Self> {
use openzl_sys_rs::*;
let mut cctx = ZlCCtx::new().ok_or_else(|| anyhow!("ZL_CCtx_create failed"))?;
let version = unsafe { ZL_getDefaultEncodingVersion() } as i32;
cctx.set_parameter(ZL_CParam_ZL_CParam_stickyParameters, 1)
.map_err(|e| anyhow!(e))?;
cctx.set_parameter(ZL_CParam_ZL_CParam_formatVersion, version)
.map_err(|e| anyhow!(e))?;
cctx.set_parameter(ZL_CParam_ZL_CParam_compressionLevel, compression_level)
.map_err(|e| anyhow!(e))?;
Ok(Self { cctx })
}
pub fn compress(&mut self, input: &[u8]) -> Result<Vec<u8>> {
use openzl_sys_rs::zl_compress_bound;
let bound = zl_compress_bound(input.len());
let mut output = vec![0u8; bound];
let compressed_size = self.cctx.compress(&mut output, input)
.map_err(|e| anyhow!(e))?;
output.truncate(compressed_size);
Ok(output)
}
pub fn compress_into(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<usize> {
use openzl_sys_rs::zl_compress_bound;
let bound = zl_compress_bound(input.len());
if out.len() < bound {
out.resize(bound, 0);
}
let compressed_size = self
.cctx
.compress(out.as_mut_slice(), input)
.map_err(|e| anyhow!(e))?;
out.truncate(compressed_size);
Ok(compressed_size)
}
}
#[cfg(feature = "openzl")]
pub fn decompress_frame(compressed: &[u8]) -> Result<Vec<u8>> {
let mut out = Vec::new();
decompress_into(compressed, &mut out)?;
Ok(out)
}
#[cfg(feature = "openzl")]
pub fn decompress_into(compressed: &[u8], out: &mut Vec<u8>) -> Result<usize> {
use openzl_sys_rs::*;
let decompressed_size = zl_get_decompressed_size(compressed)
.map_err(|e| anyhow!("OpenZL getDecompressedSize: {}", e))?;
if out.len() < decompressed_size {
out.resize(decompressed_size, 0);
}
let written = zl_decompress(&mut out[..decompressed_size], compressed)
.map_err(|e| anyhow!("OpenZL decompress: {}", e))?;
out.truncate(written);
Ok(written)
}
#[cfg(all(test, not(feature = "openzl")))]
mod no_codec_tests {
use super::*;
fn assert_names_the_feature(err: anyhow::Error) {
let msg = err.to_string();
assert!(
msg.contains("`openzl` feature"),
"error must name the feature that is missing, got: {msg}"
);
}
#[test]
fn compress_ctx_refuses_to_exist() {
assert_names_the_feature(
CompressCtx::new(3).err().expect("CompressCtx::new must fail with no codec linked"),
);
}
#[test]
fn decompress_refuses_and_does_not_touch_the_buffer() {
let frame = [0x5Bu8, 0x2A, 0x4D, 0x18, 0x00, 0x00, 0x00, 0x00];
let mut out = vec![0xAAu8; 4];
assert_names_the_feature(
decompress_into(&frame, &mut out).err().expect("decompress_into must fail"),
);
assert_eq!(out, vec![0xAAu8; 4], "buffer must be untouched on refusal");
assert_names_the_feature(
decompress_frame(&frame).err().expect("decompress_frame must fail"),
);
}
}
#[cfg(all(test, feature = "openzl"))]
mod tests {
use super::*;
#[test]
fn test_roundtrip() {
let mut ctx = CompressCtx::new(3).unwrap();
let input = b"Hello world! This is a test of compression roundtrip. Repeated data helps compression. Repeated data helps compression. Repeated data helps compression.";
let compressed = ctx.compress(input).unwrap();
println!("Compressed {} -> {} bytes", input.len(), compressed.len());
let decompressed = decompress_frame(&compressed).unwrap();
assert_eq!(&decompressed[..], &input[..]);
}
#[test]
fn test_multi_compress_same_ctx() {
let mut ctx = CompressCtx::new(3).unwrap();
for i in 0..10 {
let input: Vec<u8> = (0..4096).map(|x| ((x + i) % 251) as u8).collect();
let compressed = ctx.compress(&input).unwrap();
let decompressed = decompress_frame(&compressed).unwrap();
assert_eq!(decompressed, input, "Failed at iteration {}", i);
}
println!("10 sequential compress calls OK");
}
#[test]
fn test_parallel_contexts() {
let handles: Vec<_> = (0..8).map(|t| {
std::thread::spawn(move || {
let mut ctx = CompressCtx::new(3).unwrap();
for i in 0..5 {
let input: Vec<u8> = (0..8192).map(|x| ((x + i + t*100) % 251) as u8).collect();
let compressed = ctx.compress(&input).unwrap();
let decompressed = decompress_frame(&compressed).unwrap();
assert_eq!(decompressed, input);
}
})
}).collect();
for h in handles {
h.join().unwrap();
}
println!("8 parallel contexts x 5 calls each OK");
}
}