Skip to main content

oodle/
lib.rs

1use libloading::Library;
2use std::ffi::{OsStr, c_void};
3use std::fmt;
4
5// ---------------------------------------------------------------------------
6// Enums
7// ---------------------------------------------------------------------------
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10#[repr(i32)]
11pub enum OodleFuzzSafe {
12    No = 0,
13    Yes = 1,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17#[repr(i32)]
18pub enum OodleCheckCrc {
19    No = 0,
20    Yes = 1,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24#[repr(i32)]
25pub enum OodleVerbosity {
26    None = 0,
27    Minimal = 1,
28    Some = 2,
29    Lots = 3,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33#[repr(i32)]
34pub enum OodleDecodeThreadPhase {
35    Phase1 = 1,
36    Phase2 = 2,
37    All = 3,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41#[repr(i32)]
42pub enum OodleCompressor {
43    Invalid = -1,
44    None = 3,
45    Kraken = 8,
46    Mermaid = 9,
47    Selkie = 11,
48    Hydra = 12,
49    Leviathan = 13,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53#[repr(i32)]
54pub enum OodleCompressionLevel {
55    HyperFast4 = -4,
56    HyperFast3 = -3,
57    HyperFast2 = -2,
58    HyperFast1 = -1,
59    None = 0,
60    SuperFast = 1,
61    VeryFast = 2,
62    Fast = 3,
63    Normal = 4,
64    Optimal1 = 5,
65    Optimal2 = 6,
66    Optimal3 = 7,
67    Optimal4 = 8,
68    Optimal5 = 9,
69}
70
71// ---------------------------------------------------------------------------
72// FFI function types — matching oodle2.h 2.9.11
73// ---------------------------------------------------------------------------
74
75type CompressFn = unsafe extern "C" fn(
76    compressor: OodleCompressor,
77    raw_buf: *const c_void,
78    raw_len: isize,
79    comp_buf: *mut c_void,
80    level: OodleCompressionLevel,
81    p_options: *const c_void,
82    dictionary_base: *const c_void,
83    lrm: *const c_void,
84    scratch_mem: *mut c_void,
85    scratch_size: isize,
86) -> isize;
87
88type DecompressFn = unsafe extern "C" fn(
89    comp_buf: *const c_void,
90    comp_buf_size: isize,
91    raw_buf: *mut c_void,
92    raw_len: isize,
93    fuzz_safe: OodleFuzzSafe,
94    check_crc: OodleCheckCrc,
95    verbosity: OodleVerbosity,
96    dec_buf_base: *mut c_void,
97    dec_buf_size: isize,
98    fp_callback: *mut c_void,
99    callback_user_data: *mut c_void,
100    decoder_memory: *mut c_void,
101    decoder_memory_size: isize,
102    thread_phase: OodleDecodeThreadPhase,
103) -> isize;
104
105type GetCompressedBufferSizeNeededFn =
106    unsafe extern "C" fn(compressor: OodleCompressor, raw_size: isize) -> isize;
107
108type GetDecodeBufferSizeFn = unsafe extern "C" fn(
109    compressor: OodleCompressor,
110    raw_size: isize,
111    corruption_possible: i32,
112) -> isize;
113
114type GetCompressScratchMemBoundFn = unsafe extern "C" fn(
115    compressor: OodleCompressor,
116    level: OodleCompressionLevel,
117    raw_len: isize,
118    p_options: *const c_void,
119) -> isize;
120
121// ---------------------------------------------------------------------------
122// Error
123// ---------------------------------------------------------------------------
124
125#[derive(Debug)]
126pub enum Error {
127    LibLoadError(libloading::Error),
128    FunctionLoadError(libloading::Error),
129    CompressFailed,
130    DecompressFailed,
131}
132
133impl fmt::Display for Error {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        match self {
136            Error::LibLoadError(e) => write!(f, "failed to load library: {e}"),
137            Error::FunctionLoadError(e) => write!(f, "failed to load function: {e}"),
138            Error::CompressFailed => write!(f, "compression failed"),
139            Error::DecompressFailed => write!(f, "decompression failed"),
140        }
141    }
142}
143
144impl std::error::Error for Error {
145    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
146        match self {
147            Error::LibLoadError(e) | Error::FunctionLoadError(e) => Some(e),
148            Error::CompressFailed | Error::DecompressFailed => None,
149        }
150    }
151}
152
153// ---------------------------------------------------------------------------
154// Oodle
155// ---------------------------------------------------------------------------
156
157/// Oodle compression library loaded at runtime.
158pub struct Oodle {
159    compress_fn: CompressFn,
160    decompress_fn: DecompressFn,
161    get_compressed_buffer_size_needed_fn: GetCompressedBufferSizeNeededFn,
162    get_decode_buffer_size_fn: GetDecodeBufferSizeFn,
163    get_compress_scratch_mem_bound_fn: GetCompressScratchMemBoundFn,
164    _lib: Library,
165}
166
167impl fmt::Debug for Oodle {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        f.debug_struct("Oodle").finish_non_exhaustive()
170    }
171}
172
173// SAFETY: The raw `fn` pointers are plain function pointers into the loaded
174// shared library. They contain no interior mutability and are safe to share
175// across threads. The `Library` handle itself is `Send` and `Sync` in
176// libloading 0.8.
177const _: () = {
178    const fn assert_send_sync<T: Send + Sync>() {}
179    assert_send_sync::<Oodle>();
180};
181
182impl Oodle {
183    /// Load the Oodle shared library from the given path.
184    pub fn load(path: impl AsRef<OsStr>) -> Result<Self, Error> {
185        unsafe {
186            let lib = Library::new(path.as_ref()).map_err(Error::LibLoadError)?;
187
188            let compress_fn = *lib
189                .get::<CompressFn>(b"OodleLZ_Compress")
190                .map_err(Error::FunctionLoadError)?;
191            let decompress_fn = *lib
192                .get::<DecompressFn>(b"OodleLZ_Decompress")
193                .map_err(Error::FunctionLoadError)?;
194            let get_compressed_buffer_size_needed_fn = *lib
195                .get::<GetCompressedBufferSizeNeededFn>(b"OodleLZ_GetCompressedBufferSizeNeeded")
196                .map_err(Error::FunctionLoadError)?;
197            let get_decode_buffer_size_fn = *lib
198                .get::<GetDecodeBufferSizeFn>(b"OodleLZ_GetDecodeBufferSize")
199                .map_err(Error::FunctionLoadError)?;
200            let get_compress_scratch_mem_bound_fn = *lib
201                .get::<GetCompressScratchMemBoundFn>(b"OodleLZ_GetCompressScratchMemBound")
202                .map_err(Error::FunctionLoadError)?;
203
204            Ok(Self {
205                compress_fn,
206                decompress_fn,
207                get_compressed_buffer_size_needed_fn,
208                get_decode_buffer_size_fn,
209                get_compress_scratch_mem_bound_fn,
210                _lib: lib,
211            })
212        }
213    }
214
215    /// Compress `input` into `output` using the given compressor and level.
216    ///
217    /// Returns the number of bytes written to `output`.
218    pub fn compress(
219        &self,
220        compressor: OodleCompressor,
221        level: OodleCompressionLevel,
222        input: &[u8],
223        output: &mut [u8],
224    ) -> Result<usize, Error> {
225        let result = unsafe {
226            (self.compress_fn)(
227                compressor,
228                input.as_ptr() as *const c_void,
229                input.len() as isize,
230                output.as_mut_ptr() as *mut c_void,
231                level,
232                std::ptr::null(),
233                std::ptr::null(),
234                std::ptr::null(),
235                std::ptr::null_mut(),
236                0,
237            )
238        };
239        if result == 0 {
240            Err(Error::CompressFailed)
241        } else {
242            Ok(result as usize)
243        }
244    }
245
246    /// Decompress `source` into `dest` with default options.
247    ///
248    /// Returns the number of decompressed bytes written to `dest`.
249    pub fn decompress(&self, source: &[u8], dest: &mut [u8]) -> Result<usize, Error> {
250        self.decompress_with_options(
251            source,
252            dest,
253            OodleFuzzSafe::Yes,
254            OodleCheckCrc::No,
255            OodleVerbosity::None,
256            OodleDecodeThreadPhase::All,
257        )
258    }
259
260    /// Decompress `source` into `dest` with explicit options.
261    ///
262    /// Returns the number of decompressed bytes written to `dest`.
263    pub fn decompress_with_options(
264        &self,
265        source: &[u8],
266        dest: &mut [u8],
267        fuzz_safe: OodleFuzzSafe,
268        check_crc: OodleCheckCrc,
269        verbosity: OodleVerbosity,
270        thread_phase: OodleDecodeThreadPhase,
271    ) -> Result<usize, Error> {
272        let result = unsafe {
273            (self.decompress_fn)(
274                source.as_ptr() as *const c_void,
275                source.len() as isize,
276                dest.as_mut_ptr() as *mut c_void,
277                dest.len() as isize,
278                fuzz_safe,
279                check_crc,
280                verbosity,
281                std::ptr::null_mut(),
282                0,
283                std::ptr::null_mut(),
284                std::ptr::null_mut(),
285                std::ptr::null_mut(),
286                0,
287                thread_phase,
288            )
289        };
290        if result == 0 {
291            Err(Error::DecompressFailed)
292        } else {
293            Ok(result as usize)
294        }
295    }
296
297    /// Returns the buffer size needed to hold the compressed output for the
298    /// given compressor and raw data size.
299    pub fn get_compressed_buffer_size_needed(
300        &self,
301        compressor: OodleCompressor,
302        raw_size: usize,
303    ) -> usize {
304        unsafe {
305            (self.get_compressed_buffer_size_needed_fn)(compressor, raw_size as isize) as usize
306        }
307    }
308
309    /// Returns the buffer size needed for decompression, optionally accounting
310    /// for possible data corruption.
311    pub fn get_decode_buffer_size(
312        &self,
313        compressor: OodleCompressor,
314        raw_size: usize,
315        corruption_possible: bool,
316    ) -> usize {
317        unsafe {
318            (self.get_decode_buffer_size_fn)(
319                compressor,
320                raw_size as isize,
321                corruption_possible as i32,
322            ) as usize
323        }
324    }
325
326    /// Returns the scratch memory size bound needed for compression.
327    pub fn get_compress_scratch_mem_bound(
328        &self,
329        compressor: OodleCompressor,
330        level: OodleCompressionLevel,
331        raw_len: usize,
332    ) -> usize {
333        unsafe {
334            (self.get_compress_scratch_mem_bound_fn)(
335                compressor,
336                level,
337                raw_len as isize,
338                std::ptr::null(),
339            ) as usize
340        }
341    }
342}
343
344// ---------------------------------------------------------------------------
345// Tests
346// ---------------------------------------------------------------------------
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn enum_compressor_discriminants() {
354        assert_eq!(OodleCompressor::Invalid as i32, -1);
355        assert_eq!(OodleCompressor::None as i32, 3);
356        assert_eq!(OodleCompressor::Kraken as i32, 8);
357        assert_eq!(OodleCompressor::Mermaid as i32, 9);
358        assert_eq!(OodleCompressor::Selkie as i32, 11);
359        assert_eq!(OodleCompressor::Hydra as i32, 12);
360        assert_eq!(OodleCompressor::Leviathan as i32, 13);
361    }
362
363    #[test]
364    fn enum_compression_level_discriminants() {
365        assert_eq!(OodleCompressionLevel::HyperFast4 as i32, -4);
366        assert_eq!(OodleCompressionLevel::HyperFast3 as i32, -3);
367        assert_eq!(OodleCompressionLevel::HyperFast2 as i32, -2);
368        assert_eq!(OodleCompressionLevel::HyperFast1 as i32, -1);
369        assert_eq!(OodleCompressionLevel::None as i32, 0);
370        assert_eq!(OodleCompressionLevel::SuperFast as i32, 1);
371        assert_eq!(OodleCompressionLevel::VeryFast as i32, 2);
372        assert_eq!(OodleCompressionLevel::Fast as i32, 3);
373        assert_eq!(OodleCompressionLevel::Normal as i32, 4);
374        assert_eq!(OodleCompressionLevel::Optimal1 as i32, 5);
375        assert_eq!(OodleCompressionLevel::Optimal2 as i32, 6);
376        assert_eq!(OodleCompressionLevel::Optimal3 as i32, 7);
377        assert_eq!(OodleCompressionLevel::Optimal4 as i32, 8);
378        assert_eq!(OodleCompressionLevel::Optimal5 as i32, 9);
379    }
380
381    #[test]
382    fn enum_copy_and_eq() {
383        let a = OodleCompressor::Kraken;
384        let b = a;
385        assert_eq!(a, b);
386    }
387
388    #[test]
389    fn enum_hash() {
390        use std::collections::HashSet;
391        let mut set = HashSet::new();
392        set.insert(OodleCompressor::Kraken);
393        set.insert(OodleCompressor::Mermaid);
394        assert_eq!(set.len(), 2);
395        assert!(set.contains(&OodleCompressor::Kraken));
396    }
397
398    #[test]
399    fn error_display() {
400        assert_eq!(Error::CompressFailed.to_string(), "compression failed");
401        assert_eq!(Error::DecompressFailed.to_string(), "decompression failed");
402    }
403
404    #[test]
405    fn error_is_std_error() {
406        fn assert_error<T: std::error::Error>() {}
407        assert_error::<Error>();
408    }
409
410    #[test]
411    fn load_nonexistent_library() {
412        let result = Oodle::load("nonexistent_library.so");
413        assert!(result.is_err());
414        assert!(matches!(result.unwrap_err(), Error::LibLoadError(_)));
415    }
416
417    #[test]
418    fn oodle_is_send_and_sync() {
419        fn assert_send_sync<T: Send + Sync>() {}
420        assert_send_sync::<Oodle>();
421    }
422}