use core::ffi::c_void;
use crate::{
ZSTD_compress, ZSTD_compressBound, ZSTD_createDCtx, ZSTD_decompress, ZSTD_decompressDCtx,
ZSTD_freeDCtx, ZSTD_getErrorName, ZSTD_getFrameContentSize, ZSTD_isError, ZSTD_DCtx,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Error(pub usize);
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let name = unsafe { core::ffi::CStr::from_ptr(ZSTD_getErrorName(self.0)) };
write!(f, "zstd error: {}", name.to_string_lossy())
}
}
impl std::error::Error for Error {}
pub type Result<T> = core::result::Result<T, Error>;
#[inline]
fn check(code: usize) -> Result<usize> {
if unsafe { ZSTD_isError(code) } != 0 {
Err(Error(code))
} else {
Ok(code)
}
}
#[inline]
#[must_use]
pub fn compress_bound(src_len: usize) -> usize {
unsafe { ZSTD_compressBound(src_len) }
}
#[inline]
#[must_use]
pub fn frame_content_size(src: &[u8]) -> Option<usize> {
let s = unsafe { ZSTD_getFrameContentSize(src.as_ptr() as *const c_void, src.len()) };
if s == u64::MAX || s == u64::MAX - 1 {
None
} else {
Some(s as usize)
}
}
pub fn compress(src: &[u8], level: i32) -> Result<Vec<u8>> {
let cap = compress_bound(src.len());
let mut dst = vec![0u8; cap];
let n = check(unsafe {
ZSTD_compress(
dst.as_mut_ptr() as *mut c_void,
cap,
src.as_ptr() as *const c_void,
src.len(),
level,
)
})?;
dst.truncate(n);
Ok(dst)
}
pub fn decompress(src: &[u8]) -> Result<Vec<u8>> {
let cap = frame_content_size(src).ok_or(Error(0))?;
let mut dst = vec![0u8; cap];
let n = decompress_into(src, &mut dst)?;
debug_assert_eq!(n, cap);
dst.truncate(n);
Ok(dst)
}
pub fn decompress_into(src: &[u8], dst: &mut [u8]) -> Result<usize> {
check(unsafe {
ZSTD_decompress(
dst.as_mut_ptr() as *mut c_void,
dst.len(),
src.as_ptr() as *const c_void,
src.len(),
)
})
}
pub struct Decompressor {
dctx: *mut ZSTD_DCtx,
}
impl Decompressor {
#[must_use]
pub fn new() -> Self {
let dctx = unsafe { ZSTD_createDCtx() };
assert!(!dctx.is_null(), "ZSTD_createDCtx returned null (OOM)");
Self { dctx }
}
pub fn decompress_into(&mut self, src: &[u8], dst: &mut [u8]) -> Result<usize> {
check(unsafe {
ZSTD_decompressDCtx(
self.dctx,
dst.as_mut_ptr() as *mut c_void,
dst.len(),
src.as_ptr() as *const c_void,
src.len(),
)
})
}
}
impl Default for Decompressor {
fn default() -> Self {
Self::new()
}
}
impl Drop for Decompressor {
fn drop(&mut self) {
unsafe { ZSTD_freeDCtx(self.dctx) };
}
}
unsafe impl Send for Decompressor {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_oneshot() {
let data = b"the quick brown fox jumps over the lazy dog".repeat(1000);
let comp = compress(&data, 3).unwrap();
assert!(comp.len() < data.len());
assert_eq!(frame_content_size(&comp), Some(data.len()));
assert_eq!(decompress(&comp).unwrap(), data);
}
#[test]
fn roundtrip_zero_copy_reused_ctx() {
let frames: Vec<Vec<u8>> = (0..16)
.map(|i| compress(format!("frame {i} ").repeat(500).as_bytes(), 3).unwrap())
.collect();
let mut dctx = Decompressor::new();
let mut buf = vec![0u8; 64 * 1024];
for (i, f) in frames.iter().enumerate() {
let n = dctx.decompress_into(f, &mut buf).unwrap();
assert_eq!(&buf[..n], format!("frame {i} ").repeat(500).as_bytes());
}
}
#[test]
fn error_displays() {
assert!(decompress_into(b"not a zstd frame", &mut [0u8; 16]).is_err());
}
}