oodle_safe/
lib.rs

1//! Minimal safe wrapper around oodle-sys.
2//!
3//! This crate provides a minimal translation of the
4//! [oodle-sys](https://crates.io/crates/oodle-sys) methods to Rust.
5//!
6//! Check Oodle's [website](http://www.radgametools.com/oodle.htm) for more
7//! information.
8
9#[cfg(test)]
10mod tests;
11
12use oodle_sys;
13
14include!("constants.rs");
15
16/// Set of compression algorithms.
17///
18/// Each compressor has its own trade-offs between compression ratio and speed.
19pub enum Compressor {
20    /// No compression, just a copy
21    None,
22
23    /// Fast decompression, high compression ratio
24    Kraken,
25
26    /// Slighly slower decompression but higher compression ratio than Kraken
27    Leviathan,
28
29    /// Between Kraken and Selkie in speed with decent compression ratio
30    Mermaid,
31
32    /// "Super fast" relative to Mermaid. Used for maximum decompression speed
33    Selkie,
34
35    /// Automatically selects between Kraken, Leviathan, Mermaid, and Selkie
36    Hydra,
37}
38
39impl Into<oodle_sys::OodleLZ_Compressor> for Compressor {
40    fn into(self) -> oodle_sys::OodleLZ_Compressor {
41        match self {
42            Compressor::None => oodle_sys::OodleLZ_Compressor_OodleLZ_Compressor_None,
43            Compressor::Kraken => oodle_sys::OodleLZ_Compressor_OodleLZ_Compressor_Kraken,
44            Compressor::Leviathan => oodle_sys::OodleLZ_Compressor_OodleLZ_Compressor_Leviathan,
45            Compressor::Mermaid => oodle_sys::OodleLZ_Compressor_OodleLZ_Compressor_Mermaid,
46            Compressor::Selkie => oodle_sys::OodleLZ_Compressor_OodleLZ_Compressor_Selkie,
47            Compressor::Hydra => oodle_sys::OodleLZ_Compressor_OodleLZ_Compressor_Hydra,
48        }
49    }
50}
51
52/// Set of compression levels.
53///
54/// A compressed data stream can be decompressed with any level, but the
55/// compression level used to compress the data must be known.
56///
57/// The compression level controls the amount of work done by the compressor to
58/// find the best compressed bitstream. It does not directly impact
59/// decompression speed, it trades off encode speed for compression bitstream
60/// quality.
61pub enum CompressionLevel {
62    /// Don't compress, just copy the data
63    None,
64
65    /// Lowest compression ratio, super fast
66    SuperFast,
67
68    /// Fastest with still decent compression ratio
69    VeryFast,
70
71    /// Good for daily use
72    Fast,
73
74    /// Standard medium speed
75    Normal,
76
77    /// Optimal parse level 1 (fastest)
78    Optimal1,
79
80    /// Optimal parse level 2 (recommended baseline)
81    Optimal2,
82
83    /// Optimal parse level 3 (slower)
84    Optimal3,
85
86    /// Optimal parse level 4 (very slow)
87    Optimal4,
88
89    /// Optimal parse level 5 (don't care about speed, just want best ratio)
90    Optimal5,
91
92    /// Faster than SuperFast, but lower compression ratio
93    HyperFast1,
94
95    /// Faster than HyperFast1, but lower compression ratio
96    HyperFast2,
97
98    /// Faster than HyperFast2, but lower compression ratio
99    HyperFast3,
100
101    /// Faster than HyperFast3, but lower compression ratio
102    HyperFast4,
103
104    /// Alias optimal standard level
105    Optimal,
106
107    /// Alias hyperfast base level
108    HyperFast,
109
110    /// Alias for the maximum compression level
111    Max,
112
113    /// Alias for the minimum compression level
114    Min,
115}
116
117impl Into<oodle_sys::OodleLZ_CompressionLevel> for CompressionLevel {
118    #[rustfmt::skip]
119    fn into(self) -> oodle_sys::OodleLZ_CompressionLevel {
120        match self {
121            CompressionLevel::None => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_None,
122            CompressionLevel::SuperFast => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_SuperFast,
123            CompressionLevel::VeryFast => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_VeryFast,
124            CompressionLevel::Fast => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_Fast,
125            CompressionLevel::Normal => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_Normal,
126            CompressionLevel::Optimal1 => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_Optimal1,
127            CompressionLevel::Optimal2 => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_Optimal2,
128            CompressionLevel::Optimal3 => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_Optimal3,
129            CompressionLevel::Optimal4 => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_Optimal4,
130            CompressionLevel::Optimal5 => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_Optimal5,
131            CompressionLevel::HyperFast1 => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_HyperFast1,
132            CompressionLevel::HyperFast2 => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_HyperFast2,
133            CompressionLevel::HyperFast3 => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_HyperFast3,
134            CompressionLevel::HyperFast4 => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_HyperFast4,
135            CompressionLevel::Optimal => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_Optimal,
136            CompressionLevel::HyperFast => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_HyperFast,
137            CompressionLevel::Max => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_Max,
138            CompressionLevel::Min => oodle_sys::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_Min,
139        }
140    }
141}
142
143impl Default for CompressionLevel {
144    fn default() -> Self {
145        CompressionLevel::Normal
146    }
147}
148
149/// Decoder profile to target.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum Profile {
152    /// Main profile, full feature set
153    Main,
154
155    /// Kraken only, limited feature set
156    Reduced,
157}
158
159impl Into<oodle_sys::OodleLZ_Profile> for Profile {
160    fn into(self) -> oodle_sys::OodleLZ_Profile {
161        match self {
162            Profile::Main => oodle_sys::OodleLZ_Profile_OodleLZ_Profile_Main,
163            Profile::Reduced => oodle_sys::OodleLZ_Profile_OodleLZ_Profile_Reduced,
164        }
165    }
166}
167
168impl From<oodle_sys::OodleLZ_Profile> for Profile {
169    fn from(profile: oodle_sys::OodleLZ_Profile) -> Self {
170        match profile {
171            oodle_sys::OodleLZ_Profile_OodleLZ_Profile_Main => Profile::Main,
172            oodle_sys::OodleLZ_Profile_OodleLZ_Profile_Reduced => Profile::Reduced,
173            _ => panic!("Invalid profile"),
174        }
175    }
176}
177
178/// Controls the amount of internal threading used by the compressor.
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum Jobify {
181    /// Use compressor default for level of internal job usage
182    Default,
183
184    /// Do not use jobs at all
185    Disable,
186
187    /// Try to balance parallelism with increased memory use
188    Normal,
189
190    /// Maximize parallelism at the cost of increased memory use
191    Aggressive,
192}
193
194impl Into<oodle_sys::OodleLZ_Jobify> for Jobify {
195    fn into(self) -> oodle_sys::OodleLZ_Jobify {
196        match self {
197            Jobify::Default => oodle_sys::OodleLZ_Jobify_OodleLZ_Jobify_Default,
198            Jobify::Disable => oodle_sys::OodleLZ_Jobify_OodleLZ_Jobify_Disable,
199            Jobify::Normal => oodle_sys::OodleLZ_Jobify_OodleLZ_Jobify_Normal,
200            Jobify::Aggressive => oodle_sys::OodleLZ_Jobify_OodleLZ_Jobify_Aggressive,
201        }
202    }
203}
204
205impl From<oodle_sys::OodleLZ_Jobify> for Jobify {
206    fn from(jobify: oodle_sys::OodleLZ_Jobify) -> Self {
207        match jobify {
208            oodle_sys::OodleLZ_Jobify_OodleLZ_Jobify_Default => Jobify::Default,
209            oodle_sys::OodleLZ_Jobify_OodleLZ_Jobify_Disable => Jobify::Disable,
210            oodle_sys::OodleLZ_Jobify_OodleLZ_Jobify_Normal => Jobify::Normal,
211            oodle_sys::OodleLZ_Jobify_OodleLZ_Jobify_Aggressive => Jobify::Aggressive,
212            _ => panic!("Invalid jobify"),
213        }
214    }
215}
216
217/// Options to use for compression.
218///
219/// Typically, you would use the default options and only change the fields you
220/// need to modify.
221///
222/// To ensure that the options are valid, call [CompressOptions::validate]
223/// after modifying the fields.
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub struct CompressOptions {
226    /// Was previously named `verbosity`, set to 0
227    unused: u32,
228
229    /// Cannot be used to reduce a compressor's default MML, but can be higher.
230    /// On some types of data, a large MML (6 or 8) is a space-speed win.
231    min_match_len: i32,
232
233    /// Whether chunks should be independent, for seeking and parallelism
234    seek_chunk_reset: bool,
235
236    /// Length of independent seek chunks if [seek_chunk_reset] is true.
237    /// This must be a power of 2 and >= [BLOCK_LEN]
238    seek_chunk_len: u32,
239
240    /// Decoder profile to target (set to 0)
241    profile: Profile,
242
243    /// Sets a maximum offset for matches, if lower than the maximum the format supports.
244    /// <= 0 means infinite (use whole buffer).
245    /// Often power of 2 but doesn't have to be.
246    dictionary_size: i32,
247
248    /// Number of bytes; It must gain at least this many bytes of compressed
249    /// size to accept a speed-decreasing decision
250    space_speed_tradeoff_bytes: i32,
251
252    /// Was previously named `max_huffmans_per_chunk`, set to 0
253    unused2: i32,
254
255    /// Whether the encoder should send CRCs for each compressed quantum for
256    /// integrity checking. This is necessary for using `CheckCRC::Yes` in
257    /// decompression.
258    send_quantum_crcs: bool,
259
260    /// Size of local dictionary before needing a long range matcher.
261    /// This does not set a window size for the decoder;
262    /// it's useful to limit memory use and time taken in the encoder.
263    /// This must be a power of 2 and < [LOCALDICTIONARYSIZE_MAX].
264    max_local_dictionary_size: u32,
265
266    /// Whether the encoder should should find matches beyond [max_local_dictionary_size]
267    /// when using a long range matcher.
268    make_long_range_matcher: bool,
269
270    /// Default is 0. If non-zero, this sets the size of the match finder structure
271    /// (often a hash table).
272    match_table_size_log2: i32,
273
274    /// Controls internal job usage for the compressor.
275    jobify: Jobify,
276
277    /// User pointer passed through to RunJob and WaitJob callbacks.
278    jobify_user_ptr: *mut std::ffi::c_void,
279
280    /// Far match must be at least this long.
281    far_match_min_len: i32,
282
283    /// If not 0, the log2 of the offset that must meet [far_match_min_len].
284    far_match_offset_log2: i32,
285
286    /// Reserved for future use, set to 0
287    reserved: [u32; 4],
288}
289
290impl CompressOptions {
291    pub fn validate(&mut self) {
292        let options: *mut oodle_sys::OodleLZ_CompressOptions = &mut (*self).into();
293        unsafe { oodle_sys::OodleLZ_CompressOptions_Validate(options) };
294        *self = CompressOptions::from(unsafe { *options });
295    }
296}
297
298impl Into<oodle_sys::OodleLZ_CompressOptions> for CompressOptions {
299    fn into(self) -> oodle_sys::OodleLZ_CompressOptions {
300        oodle_sys::OodleLZ_CompressOptions {
301            unused_was_verbosity: self.unused,
302            minMatchLen: self.min_match_len,
303            seekChunkReset: if self.seek_chunk_reset { 1 } else { 0 },
304            seekChunkLen: self.seek_chunk_len as i32,
305            profile: self.profile.into(),
306            dictionarySize: self.dictionary_size,
307            spaceSpeedTradeoffBytes: self.space_speed_tradeoff_bytes,
308            unused_was_maxHuffmansPerChunk: self.unused2,
309            sendQuantumCRCs: if self.send_quantum_crcs { 1 } else { 0 },
310            maxLocalDictionarySize: self.max_local_dictionary_size as i32,
311            makeLongRangeMatcher: if self.make_long_range_matcher { 1 } else { 0 },
312            matchTableSizeLog2: self.match_table_size_log2,
313            jobify: self.jobify.into(),
314            jobifyUserPtr: self.jobify_user_ptr,
315            farMatchMinLen: self.far_match_min_len,
316            farMatchOffsetLog2: self.far_match_offset_log2,
317            reserved: self.reserved,
318        }
319    }
320}
321
322impl From<oodle_sys::OodleLZ_CompressOptions> for CompressOptions {
323    fn from(options: oodle_sys::OodleLZ_CompressOptions) -> Self {
324        Self {
325            unused: options.unused_was_verbosity,
326            min_match_len: options.minMatchLen,
327            seek_chunk_reset: options.seekChunkReset != 0,
328            seek_chunk_len: options.seekChunkLen as u32,
329            profile: options.profile.into(),
330            dictionary_size: options.dictionarySize,
331            space_speed_tradeoff_bytes: options.spaceSpeedTradeoffBytes,
332            unused2: options.unused_was_maxHuffmansPerChunk,
333            send_quantum_crcs: options.sendQuantumCRCs != 0,
334            max_local_dictionary_size: options.maxLocalDictionarySize as u32,
335            make_long_range_matcher: options.makeLongRangeMatcher != 0,
336            match_table_size_log2: options.matchTableSizeLog2,
337            jobify: options.jobify.into(),
338            jobify_user_ptr: options.jobifyUserPtr,
339            far_match_min_len: options.farMatchMinLen,
340            far_match_offset_log2: options.farMatchOffsetLog2,
341            reserved: options.reserved,
342        }
343    }
344}
345
346impl Default for CompressOptions {
347    fn default() -> Self {
348        let options = unsafe {
349            *oodle_sys::OodleLZ_CompressOptions_GetDefault(
350                Compressor::None.into(),
351                CompressionLevel::None.into(),
352            )
353        };
354
355        options.into()
356    }
357}
358
359/// Compress some data from memory to memory synchronously.
360///
361/// # Arguments
362///
363/// * `compressor` - The compressor to use.
364/// * `decompressed` - The buffer containing the data to compress.
365/// * `compressed` - The buffer to write the compressed data to.
366/// * `level` - The compression level to use.
367/// * `options` - Additional options to use for compression.
368/// * `dictionary_base` - Preconditioned dictionary to use for compression.
369/// * `scratch_memory` - Scratch memory to use for compression.
370///
371/// When setting optionnal parameters to `None`, the default value will be used.
372///
373/// # Returns
374///
375/// The size of the compressed data.
376///
377/// # Example
378///
379/// ```rust
380/// // Load decompressed data from a file (or any other source).
381/// let decompressed = include_bytes!("../test_data/decompressed");
382///
383/// // Allocate compressed buffer with some extra space in case the compression
384/// // adds some overhead like the OodleLZ block header.
385/// let mut compressed = vec![0u8; decompressed.len() + 8];
386///
387/// // Compress the data.
388/// let compressed_size = oodle_safe::compress(
389///     oodle_safe::Compressor::Kraken,
390///     decompressed,
391///     &mut compressed,
392///     oodle_safe::CompressionLevel::Normal, // same as default
393///     Some(oodle_safe::CompressOptions::default()), // same as default
394///     None,
395///     None,
396/// )
397/// .unwrap_or_else(|_| panic!("compression failed"));
398///
399/// // Trim the output buffer to the actual size of the compressed data.
400/// let compressed = &compressed[..compressed_size];
401/// ```
402pub fn compress(
403    compressor: Compressor,
404    decompressed: &[u8],
405    compressed: &mut [u8],
406    level: CompressionLevel,
407    options: Option<CompressOptions>,
408    dictionary_base: Option<&[u8]>,
409    scratch_memory: Option<&mut [u8]>,
410) -> Result<usize, u32> {
411    let options = match options {
412        Some(x) => &x.into(),
413        None => std::ptr::null() as *const _,
414    };
415
416    let dictionary_base = match dictionary_base {
417        Some(x) => x.as_ptr(),
418        None => std::ptr::null(),
419    };
420
421    let (scratch_memory, scratch_memory_len) = match scratch_memory {
422        Some(x) => (x.as_mut_ptr(), x.len() as isize),
423        None => (std::ptr::null_mut(), 0),
424    };
425
426    let result = unsafe {
427        oodle_sys::OodleLZ_Compress(
428            compressor.into(),
429            decompressed.as_ptr() as *const _,
430            decompressed.len() as isize,
431            compressed.as_mut_ptr() as *mut _,
432            level.into(),
433            options,
434            dictionary_base as *const _,
435            std::ptr::null(), // TODO: add long_range_matcher
436            scratch_memory as *mut _,
437            scratch_memory_len,
438        ) as usize
439    };
440
441    if result == FAILED as usize {
442        Err(FAILED)
443    } else {
444        // This is necessary to avoid double free-ing the buffer when slicing
445        // the compressed buffer after the compression.
446        let compressed_data = compressed[..result].to_vec();
447        compressed[..result].copy_from_slice(&compressed_data);
448        Ok(result)
449    }
450}
451
452/// Bool enum for the LZ decoder to check the CRC of the compressed data.
453///
454/// To use [CheckCRC::Yes], the compressed data must have been compressed with
455/// the CRC option enabled.
456pub enum CheckCRC {
457    No,
458    Yes,
459}
460
461impl Default for CheckCRC {
462    fn default() -> Self {
463        CheckCRC::No
464    }
465}
466
467impl Into<oodle_sys::OodleLZ_CheckCRC> for CheckCRC {
468    fn into(self) -> oodle_sys::OodleLZ_CheckCRC {
469        match self {
470            CheckCRC::No => oodle_sys::OodleLZ_CheckCRC_OodleLZ_CheckCRC_No,
471            CheckCRC::Yes => oodle_sys::OodleLZ_CheckCRC_OodleLZ_CheckCRC_Yes,
472        }
473    }
474}
475
476/// Verbosity level for LZ decompression.
477pub enum Verbosity {
478    /// Will not log anything, even when the decoder sees corrupted data.
479    None,
480    Minimal,
481    Some,
482    Lots,
483}
484
485impl Default for Verbosity {
486    fn default() -> Self {
487        Verbosity::None
488    }
489}
490
491impl Into<oodle_sys::OodleLZ_Verbosity> for Verbosity {
492    fn into(self) -> oodle_sys::OodleLZ_Verbosity {
493        match self {
494            Verbosity::None => oodle_sys::OodleLZ_Verbosity_OodleLZ_Verbosity_None,
495            Verbosity::Minimal => oodle_sys::OodleLZ_Verbosity_OodleLZ_Verbosity_Minimal,
496            Verbosity::Some => oodle_sys::OodleLZ_Verbosity_OodleLZ_Verbosity_Some,
497            Verbosity::Lots => oodle_sys::OodleLZ_Verbosity_OodleLZ_Verbosity_Lots,
498        }
499    }
500}
501
502/// Thread phase for threaded decompression.
503///
504/// Note that threaded decompression is only available for the Kraken compressor.
505pub enum DecodeThreadPhase {
506    One,
507    Two,
508    All,
509    Unthreaded,
510}
511
512impl Default for DecodeThreadPhase {
513    fn default() -> Self {
514        DecodeThreadPhase::Unthreaded
515    }
516}
517
518impl Into<oodle_sys::OodleLZ_Decode_ThreadPhase> for DecodeThreadPhase {
519    #[rustfmt::skip]
520    fn into(self) -> oodle_sys::OodleLZ_Decode_ThreadPhase {
521        match self {
522            DecodeThreadPhase::One => oodle_sys::OodleLZ_Decode_ThreadPhase_OodleLZ_Decode_ThreadPhase1,
523            DecodeThreadPhase::Two => oodle_sys::OodleLZ_Decode_ThreadPhase_OodleLZ_Decode_ThreadPhase2,
524            DecodeThreadPhase::All => oodle_sys::OodleLZ_Decode_ThreadPhase_OodleLZ_Decode_ThreadPhaseAll,
525            DecodeThreadPhase::Unthreaded => oodle_sys::OodleLZ_Decode_ThreadPhase_OodleLZ_Decode_Unthreaded,
526        }
527    }
528}
529
530/// Decompress some data from memory to memory synchronously.
531///
532/// # Arguments
533///
534/// * `compressed` - The buffer containing the compressed data.
535/// * `decompressed` - The buffer to write the decompressed data to.
536/// * `dictionary_base` - Preconditioned dictionary to use for decompression.
537/// The dictionary must be the same as the one used for compression.
538/// * `check_crc` - Whether to check the validity of the compressed data.
539/// * `verbosity` - The verbosity of the decompression.
540/// * `thread_phase` - The thread phase for threaded decompression.
541///
542/// When setting optionnal parameters to `None`, the default value will be used.
543///
544/// # Returns
545///
546/// The size of the decompressed data.
547///
548/// # Example
549/// ```rust
550/// // Load compressed data from a file (or any other source).
551/// let compressed = include_bytes!("../test_data/compressed");
552///
553/// # let decompressed_size = u32::from_le_bytes(compressed[..4].try_into().unwrap()) as usize;
554/// // Allocate decompressed buffer with the size of the decompressed data.
555/// let mut decompressed = vec![0u8; decompressed_size];
556///
557/// let result = oodle_safe::decompress(
558///     &compressed[4..],
559///     &mut decompressed,
560///     None,
561///     Some(oodle_safe::CheckCRC::No), // same as default
562///     Some(oodle_safe::Verbosity::None), // same as default
563///     Some(oodle_safe::DecodeThreadPhase::Unthreaded), // same as default
564/// )
565/// .unwrap_or_else(|_| panic!("decompression failed"));
566/// ```
567pub fn decompress(
568    compressed: &[u8],
569    decompressed: &mut [u8],
570    dictionary_base: Option<&mut [u8]>,
571    check_crc: Option<CheckCRC>,
572    verbosity: Option<Verbosity>,
573    thread_phase: Option<DecodeThreadPhase>,
574) -> Result<usize, u32> {
575    let (dictionary_base, dictionary_base_len) = match dictionary_base {
576        Some(x) => (x.as_mut_ptr(), x.len() as isize),
577        None => (std::ptr::null_mut(), 0),
578    };
579
580    let result = unsafe {
581        oodle_sys::OodleLZ_Decompress(
582            compressed.as_ptr() as *const _,
583            compressed.len() as isize,
584            decompressed.as_mut_ptr() as *mut _,
585            decompressed.len() as isize,
586            oodle_sys::OodleLZ_FuzzSafe_OodleLZ_FuzzSafe_Yes,
587            check_crc.unwrap_or_default().into(),
588            verbosity.unwrap_or_default().into(),
589            dictionary_base as *mut _,
590            dictionary_base_len,
591            None, // TODO: add callback
592            std::ptr::null_mut(),
593            std::ptr::null_mut(),
594            0,
595            thread_phase.unwrap_or_default().into(),
596        ) as usize
597    };
598
599    if result == FAILED as usize {
600        Err(FAILED)
601    } else {
602        Ok(result)
603    }
604}