Skip to main content

zstd_sys_rs/
safe.rs

1//! Safe, allocation-aware wrappers over the raw libzstd 1.5.7 FFI.
2//!
3//! The crate root is bindgen-generated FFI (`ZSTD_*`); this module adds the
4//! minimum safe surface most callers need — one-shot compress/decompress plus a
5//! **zero-extra-allocation** path: a reusable [`Decompressor`] (amortizes the
6//! `ZSTD_DCtx` across many frames) that decompresses straight into a caller-owned
7//! buffer via [`Decompressor::decompress_into`]. That's the shape hot loops want
8//! (decode N frames, reuse one context + one output buffer, no per-frame `Vec`).
9
10use core::ffi::c_void;
11
12use crate::{
13    ZSTD_compress, ZSTD_compressBound, ZSTD_createDCtx, ZSTD_decompress, ZSTD_decompressDCtx,
14    ZSTD_freeDCtx, ZSTD_getErrorName, ZSTD_getFrameContentSize, ZSTD_isError, ZSTD_DCtx,
15};
16
17/// A libzstd error (the raw `size_t` code; name via `Display`).
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct Error(pub usize);
20
21impl core::fmt::Display for Error {
22    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
23        // SAFETY: ZSTD_getErrorName returns a static NUL-terminated C string.
24        let name = unsafe { core::ffi::CStr::from_ptr(ZSTD_getErrorName(self.0)) };
25        write!(f, "zstd error: {}", name.to_string_lossy())
26    }
27}
28
29impl std::error::Error for Error {}
30
31pub type Result<T> = core::result::Result<T, Error>;
32
33#[inline]
34fn check(code: usize) -> Result<usize> {
35    // SAFETY: pure predicate over the return code.
36    if unsafe { ZSTD_isError(code) } != 0 {
37        Err(Error(code))
38    } else {
39        Ok(code)
40    }
41}
42
43/// Maximum compressed size for `src_len` input bytes.
44#[inline]
45#[must_use]
46pub fn compress_bound(src_len: usize) -> usize {
47    // SAFETY: pure function of the length.
48    unsafe { ZSTD_compressBound(src_len) }
49}
50
51/// Decompressed size declared in the frame header, if present and known.
52/// `None` for "unknown" (streaming-written) or malformed frames.
53#[inline]
54#[must_use]
55pub fn frame_content_size(src: &[u8]) -> Option<usize> {
56    // SAFETY: reads only `src.len()` bytes from `src`.
57    let s = unsafe { ZSTD_getFrameContentSize(src.as_ptr() as *const c_void, src.len()) };
58    // ZSTD_CONTENTSIZE_UNKNOWN = (0ULL - 1), ZSTD_CONTENTSIZE_ERROR = (0ULL - 2).
59    if s == u64::MAX || s == u64::MAX - 1 {
60        None
61    } else {
62        Some(s as usize)
63    }
64}
65
66/// One-shot compress into a fresh `Vec` at the given level (1..=22).
67pub fn compress(src: &[u8], level: i32) -> Result<Vec<u8>> {
68    let cap = compress_bound(src.len());
69    let mut dst = vec![0u8; cap];
70    // SAFETY: dst has `cap` writable bytes; src has `src.len()` readable bytes.
71    let n = check(unsafe {
72        ZSTD_compress(
73            dst.as_mut_ptr() as *mut c_void,
74            cap,
75            src.as_ptr() as *const c_void,
76            src.len(),
77            level,
78        )
79    })?;
80    dst.truncate(n);
81    Ok(dst)
82}
83
84/// One-shot decompress into a fresh `Vec` sized from the frame header.
85/// Errors if the frame doesn't declare its content size (use streaming/a known
86/// bound for those).
87pub fn decompress(src: &[u8]) -> Result<Vec<u8>> {
88    let cap = frame_content_size(src).ok_or(Error(0))?;
89    let mut dst = vec![0u8; cap];
90    let n = decompress_into(src, &mut dst)?;
91    debug_assert_eq!(n, cap);
92    dst.truncate(n);
93    Ok(dst)
94}
95
96/// Decompress one frame into a caller-owned buffer (no allocation here).
97/// Returns the number of bytes written; `dst` must be at least the frame's
98/// decompressed size.
99pub fn decompress_into(src: &[u8], dst: &mut [u8]) -> Result<usize> {
100    // SAFETY: dst/src are valid for their lengths; libzstd writes ≤ dst.len().
101    check(unsafe {
102        ZSTD_decompress(
103            dst.as_mut_ptr() as *mut c_void,
104            dst.len(),
105            src.as_ptr() as *const c_void,
106            src.len(),
107        )
108    })
109}
110
111/// Reusable decompression context. Creating a `ZSTD_DCtx` allocates and warms
112/// internal tables; holding one across many frames (with [`Self::decompress_into`]
113/// into a reused buffer) makes a decode loop allocation-free after warm-up.
114pub struct Decompressor {
115    dctx: *mut ZSTD_DCtx,
116}
117
118impl Decompressor {
119    /// Allocate a decompression context.
120    #[must_use]
121    pub fn new() -> Self {
122        // SAFETY: ZSTD_createDCtx returns an owned ctx or null on OOM.
123        let dctx = unsafe { ZSTD_createDCtx() };
124        assert!(!dctx.is_null(), "ZSTD_createDCtx returned null (OOM)");
125        Self { dctx }
126    }
127
128    /// Decompress one frame into `dst` reusing this context. No allocation.
129    pub fn decompress_into(&mut self, src: &[u8], dst: &mut [u8]) -> Result<usize> {
130        // SAFETY: self.dctx is a live ctx; dst/src valid for their lengths.
131        check(unsafe {
132            ZSTD_decompressDCtx(
133                self.dctx,
134                dst.as_mut_ptr() as *mut c_void,
135                dst.len(),
136                src.as_ptr() as *const c_void,
137                src.len(),
138            )
139        })
140    }
141}
142
143impl Default for Decompressor {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149impl Drop for Decompressor {
150    fn drop(&mut self) {
151        // SAFETY: dctx came from ZSTD_createDCtx and is freed exactly once.
152        unsafe { ZSTD_freeDCtx(self.dctx) };
153    }
154}
155
156// The context is owned exclusively (methods take &mut self); it is safe to move
157// to another thread, but not to share concurrently.
158unsafe impl Send for Decompressor {}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn roundtrip_oneshot() {
166        let data = b"the quick brown fox jumps over the lazy dog".repeat(1000);
167        let comp = compress(&data, 3).unwrap();
168        assert!(comp.len() < data.len());
169        assert_eq!(frame_content_size(&comp), Some(data.len()));
170        assert_eq!(decompress(&comp).unwrap(), data);
171    }
172
173    #[test]
174    fn roundtrip_zero_copy_reused_ctx() {
175        let frames: Vec<Vec<u8>> = (0..16)
176            .map(|i| compress(format!("frame {i} ").repeat(500).as_bytes(), 3).unwrap())
177            .collect();
178        let mut dctx = Decompressor::new();
179        let mut buf = vec![0u8; 64 * 1024];
180        for (i, f) in frames.iter().enumerate() {
181            let n = dctx.decompress_into(f, &mut buf).unwrap();
182            assert_eq!(&buf[..n], format!("frame {i} ").repeat(500).as_bytes());
183        }
184    }
185
186    #[test]
187    fn error_displays() {
188        // A bogus frame should error, not panic.
189        assert!(decompress_into(b"not a zstd frame", &mut [0u8; 16]).is_err());
190    }
191}