lz4r 1.10.0

LZ4 CLI programs — Rust port of lz4-1.10.0/programs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
//! Compression strategy selection for the benchmark subsystem.
//!
//! Defines the [`CompressionStrategy`] trait and four concrete implementations
//! covering every combination of dictionary / no-dictionary and fast / HC modes:
//!
//! | Type              | Dict | Algorithm |
//! |-------------------|------|-----------|
//! | [`NoStreamFast`]  | no   | fast      |
//! | [`NoStreamHC`]    | no   | HC        |
//! | [`StreamFast`]    | yes  | fast      |
//! | [`StreamHC`]      | yes  | HC        |
//!
//! All compression is performed through `crate::block` and `crate::hc` — no
//! FFI or third-party crate dependencies.  A zero return from any block
//! compression function is treated as an error.
//!
//! Use [`build_compression_parameters`] (no dict) or
//! [`build_compression_parameters_with_dict`] (with dict) to obtain a boxed
//! strategy.  The threshold [`LZ4HC_CLEVEL_MIN`]` = 2` determines whether the
//! fast or HC path is selected.

use std::io;

use crate::block::{compress_bound, compress_fast, Lz4Stream};
use crate::hc::{
    attach_hc_dictionary, compress_hc, compress_hc_continue, load_dict_hc, reset_stream_hc_fast,
    Lz4StreamHc,
};

/// Minimum compression level that activates HC (high-compression) mode.
/// Levels strictly below this threshold use the fast compression path.
const LZ4HC_CLEVEL_MIN: i32 = 2;

// ── CompressionStrategy trait ─────────────────────────────────────────────────

/// A single compression strategy used by the benchmark runner.
///
/// Each implementation owns its stream state, initialised in `new` and
/// released on `Drop`.  The per-block context reset is performed inside
/// [`compress_block`](CompressionStrategy::compress_block), so callers
/// require no per-block setup beyond passing source and destination buffers.
pub trait CompressionStrategy: Send + Sync {
    /// Compress `src` into `dst`.
    ///
    /// Before writing, `dst` is resized to hold at least
    /// `compress_bound(src.len())` bytes.  Returns the number of compressed
    /// bytes written into `dst`.
    fn compress_block(&mut self, src: &[u8], dst: &mut Vec<u8>) -> io::Result<usize>;
}

// ── Helper ────────────────────────────────────────────────────────────────────

/// Ensure `dst` is large enough to hold the LZ4 worst-case output for `src_len` bytes.
#[inline]
fn ensure_dst_capacity(src_len: usize, dst: &mut Vec<u8>) {
    let bound = compress_bound(src_len as i32) as usize;
    if dst.len() < bound {
        dst.resize(bound, 0u8);
    }
}

// ── Strategy 1: NoStreamFast ──────────────────────────────────────────────────

/// Stateless fast compression with no dictionary.
///
/// Each [`compress_block`](CompressionStrategy::compress_block) call is
/// fully independent — there is no cross-block history.
///
/// The acceleration factor is derived from the compression level:
/// `acceleration = if c_level < 0 { -c_level + 1 } else { 1 }`.
/// Negative levels trade compression ratio for higher throughput.
pub struct NoStreamFast {
    acceleration: i32,
}

impl NoStreamFast {
    pub fn new(c_level: i32) -> Self {
        let acceleration = if c_level < 0 { -c_level + 1 } else { 1 };
        NoStreamFast { acceleration }
    }
}

unsafe impl Send for NoStreamFast {}
unsafe impl Sync for NoStreamFast {}

impl CompressionStrategy for NoStreamFast {
    fn compress_block(&mut self, src: &[u8], dst: &mut Vec<u8>) -> io::Result<usize> {
        ensure_dst_capacity(src.len(), dst);
        // Block API: compress_fast returns Result<usize, Lz4Error>.
        compress_fast(src, dst, self.acceleration)
            .map_err(|e| io::Error::other(format!("compress_fast failed: {e:?}")))
    }
}

// ── Strategy 2: NoStreamHC ────────────────────────────────────────────────────

/// Stateless high-compression (HC) compression with no dictionary.
///
/// Each block is compressed independently using the HC search algorithm.
/// Intended for levels ≥ [`LZ4HC_CLEVEL_MIN`].
pub struct NoStreamHC {
    c_level: i32,
}

impl NoStreamHC {
    pub fn new(c_level: i32) -> Self {
        NoStreamHC { c_level }
    }
}

unsafe impl Send for NoStreamHC {}
unsafe impl Sync for NoStreamHC {}

impl CompressionStrategy for NoStreamHC {
    fn compress_block(&mut self, src: &[u8], dst: &mut Vec<u8>) -> io::Result<usize> {
        ensure_dst_capacity(src.len(), dst);
        let written = unsafe {
            compress_hc(
                src.as_ptr(),
                dst.as_mut_ptr(),
                src.len() as i32,
                dst.len() as i32,
                self.c_level,
            )
        };
        if written == 0 {
            Err(io::Error::other("compress_hc returned 0"))
        } else {
            Ok(written as usize)
        }
    }
}

// ── Strategy 3: StreamFast ────────────────────────────────────────────────────

/// Fast stream compression with an optional pre-loaded dictionary.
///
/// The dictionary is loaded into a dedicated `dict_stream` via
/// `Lz4Stream::load_dict_slow` at construction time.  On each block,
/// `attach_dictionary` links the dict stream before calling
/// `compress_fast_continue`, providing dictionary context without
/// accumulating cross-block history in the primary stream.
pub struct StreamFast {
    c_level: i32,
    stream: Box<Lz4Stream>,
    dict_stream: Box<Lz4Stream>,
    /// Owns the dict bytes so the pointer passed to lz4 remains valid.
    _dict: Vec<u8>,
}

impl StreamFast {
    /// Create a `StreamFast` strategy, pre-loading `dict` into a dedicated stream.
    ///
    /// Pass an empty slice for `dict` to compress without a dictionary.
    pub fn new(c_level: i32, dict: &[u8]) -> io::Result<Self> {
        let stream = Lz4Stream::new();
        let mut dict_stream = Lz4Stream::new();
        // Keep a private copy so the slice pointer passed to load_dict_slow
        // remains valid for the entire lifetime of this struct.
        let dict_copy: Vec<u8> = dict.to_vec();
        if !dict_copy.is_empty() {
            dict_stream.load_dict_slow(&dict_copy);
        }
        Ok(StreamFast {
            c_level,
            stream,
            dict_stream,
            _dict: dict_copy,
        })
    }
}

unsafe impl Send for StreamFast {}
unsafe impl Sync for StreamFast {}

impl CompressionStrategy for StreamFast {
    fn compress_block(&mut self, src: &[u8], dst: &mut Vec<u8>) -> io::Result<usize> {
        ensure_dst_capacity(src.len(), dst);
        // Negative levels trade compression ratio for speed via a higher acceleration value.
        let acceleration = if self.c_level < 0 {
            -self.c_level + 1
        } else {
            1
        };
        // Reset the compression context before each block to prevent unintended
        // cross-block history from a previous compress_block call.
        self.stream.reset_fast();
        // When no dictionary was provided, pass None to clear any prior attachment.
        let dict_ptr = if self._dict.is_empty() {
            None
        } else {
            Some(&*self.dict_stream as *const Lz4Stream)
        };
        unsafe {
            self.stream.attach_dictionary(dict_ptr);
        }

        let written = self.stream.compress_fast_continue(src, dst, acceleration);
        if written == 0 {
            Err(io::Error::other("compress_fast_continue returned 0"))
        } else {
            Ok(written as usize)
        }
    }
}

// ── Strategy 4: StreamHC ──────────────────────────────────────────────────────

/// HC stream compression with an optional pre-loaded dictionary.
///
/// Uses a dedicated `dict_stream_hc` to hold the dictionary context.
/// Before each block, `reset_stream_hc_fast` reinitialises the primary HC
/// stream and `attach_hc_dictionary` links the dict context, providing
/// dictionary-aware compression without persisting cross-block history.
pub struct StreamHC {
    c_level: i32,
    stream_hc: Box<Lz4StreamHc>,
    dict_stream_hc: Box<Lz4StreamHc>,
    /// Owns the dict bytes so the pointer passed to lz4 remains valid.
    _dict: Vec<u8>,
}

impl StreamHC {
    /// Create a `StreamHC` strategy, pre-loading `dict` into a dedicated HC stream.
    ///
    /// Pass an empty slice for `dict` to compress without a dictionary.
    pub fn new(c_level: i32, dict: &[u8]) -> io::Result<Self> {
        let stream_hc =
            Lz4StreamHc::create().ok_or_else(|| io::Error::other("Lz4StreamHc::create failed"))?;
        let mut dict_stream_hc = Lz4StreamHc::create()
            .ok_or_else(|| io::Error::other("Lz4StreamHc::create (dict) failed"))?;
        let dict_copy: Vec<u8> = dict.to_vec();
        // Initialise the dict stream at the target level before loading the dictionary.
        reset_stream_hc_fast(&mut dict_stream_hc, c_level);
        if !dict_copy.is_empty() {
            unsafe {
                load_dict_hc(
                    &mut dict_stream_hc,
                    dict_copy.as_ptr(),
                    dict_copy.len() as i32,
                );
            }
        }
        Ok(StreamHC {
            c_level,
            stream_hc,
            dict_stream_hc,
            _dict: dict_copy,
        })
    }
}

unsafe impl Send for StreamHC {}
unsafe impl Sync for StreamHC {}

impl CompressionStrategy for StreamHC {
    fn compress_block(&mut self, src: &[u8], dst: &mut Vec<u8>) -> io::Result<usize> {
        ensure_dst_capacity(src.len(), dst);
        // Reset the HC stream before each block, discarding cross-block history
        // and re-applying the compression level.
        reset_stream_hc_fast(&mut self.stream_hc, self.c_level);
        // Only attach a dict stream that was prepared by load_dict_hc;
        // an uninitialised dict stream must not be passed here.
        let dict_ptr = if self._dict.is_empty() {
            None
        } else {
            Some(&*self.dict_stream_hc as *const Lz4StreamHc)
        };
        unsafe {
            attach_hc_dictionary(&mut self.stream_hc, dict_ptr);
        }

        let written = unsafe {
            compress_hc_continue(
                &mut self.stream_hc,
                src.as_ptr(),
                dst.as_mut_ptr(),
                src.len() as i32,
                dst.len() as i32,
            )
        };
        if written == 0 {
            Err(io::Error::other("compress_hc_continue returned 0"))
        } else {
            Ok(written as usize)
        }
    }
}

// ── Factory functions ─────────────────────────────────────────────────────────

/// Build a no-dict compression strategy for the given compression level.
///
/// - `c_level < LZ4HC_CLEVEL_MIN (2)` → [`NoStreamFast`]
/// - `c_level ≥ LZ4HC_CLEVEL_MIN`     → [`NoStreamHC`]
///
/// `_src_size` and `_block_size` are accepted for API compatibility with the
/// benchmark loop; strategy selection does not depend on block geometry.
pub fn build_compression_parameters(
    c_level: i32,
    _src_size: usize,
    _block_size: usize,
) -> Box<dyn CompressionStrategy> {
    if c_level < LZ4HC_CLEVEL_MIN {
        Box::new(NoStreamFast::new(c_level))
    } else {
        Box::new(NoStreamHC::new(c_level))
    }
}

/// Build a dict-aware compression strategy for the given level and dictionary.
///
/// - `c_level < LZ4HC_CLEVEL_MIN (2)` → [`StreamFast`]
/// - `c_level ≥ LZ4HC_CLEVEL_MIN`     → [`StreamHC`]
///
/// Pass an empty slice for `dict` to skip dictionary preloading while still
/// using the streaming code path.
pub fn build_compression_parameters_with_dict(
    c_level: i32,
    dict: &[u8],
) -> io::Result<Box<dyn CompressionStrategy>> {
    if c_level < LZ4HC_CLEVEL_MIN {
        Ok(Box::new(StreamFast::new(c_level, dict)?))
    } else {
        Ok(Box::new(StreamHC::new(c_level, dict)?))
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::block::decompress_safe;

    const SAMPLE: &[u8] = b"hello world hello world hello world hello world \
          this is a test of lz4 block compression round-trip!";

    fn lz4_decompress(compressed: &[u8], original_len: usize) -> Vec<u8> {
        let mut out = vec![0u8; original_len];
        let n = decompress_safe(compressed, &mut out).expect("decompress_safe failed");
        assert_eq!(n, original_len);
        out
    }

    #[test]
    fn no_stream_fast_roundtrip() {
        let mut strategy = NoStreamFast::new(1);
        let mut dst = Vec::new();
        let n = strategy.compress_block(SAMPLE, &mut dst).unwrap();
        let recovered = lz4_decompress(&dst[..n], SAMPLE.len());
        assert_eq!(recovered.as_slice(), SAMPLE);
    }

    #[test]
    fn no_stream_fast_negative_level_roundtrip() {
        // Negative c_level: acceleration = -c_level + 1
        let mut strategy = NoStreamFast::new(-5);
        let mut dst = Vec::new();
        let n = strategy.compress_block(SAMPLE, &mut dst).unwrap();
        let recovered = lz4_decompress(&dst[..n], SAMPLE.len());
        assert_eq!(recovered.as_slice(), SAMPLE);
    }

    #[test]
    fn no_stream_hc_roundtrip() {
        let mut strategy = NoStreamHC::new(9);
        let mut dst = Vec::new();
        let n = strategy.compress_block(SAMPLE, &mut dst).unwrap();
        let recovered = lz4_decompress(&dst[..n], SAMPLE.len());
        assert_eq!(recovered.as_slice(), SAMPLE);
    }

    #[test]
    fn no_stream_hc_min_level_roundtrip() {
        let mut strategy = NoStreamHC::new(LZ4HC_CLEVEL_MIN);
        let mut dst = Vec::new();
        let n = strategy.compress_block(SAMPLE, &mut dst).unwrap();
        let recovered = lz4_decompress(&dst[..n], SAMPLE.len());
        assert_eq!(recovered.as_slice(), SAMPLE);
    }

    #[test]
    fn stream_fast_no_dict_roundtrip() {
        let mut strategy = StreamFast::new(1, b"").unwrap();
        let mut dst = Vec::new();
        let n = strategy.compress_block(SAMPLE, &mut dst).unwrap();
        let recovered = lz4_decompress(&dst[..n], SAMPLE.len());
        assert_eq!(recovered.as_slice(), SAMPLE);
    }

    #[test]
    fn stream_fast_with_dict_roundtrip() {
        let dict = b"hello world ";
        let mut strategy = StreamFast::new(1, dict).unwrap();
        let mut dst = Vec::new();
        // compress_block should succeed even with a dict attached
        let n = strategy.compress_block(SAMPLE, &mut dst).unwrap();
        assert!(n > 0);
    }

    #[test]
    fn stream_hc_no_dict_roundtrip() {
        let mut strategy = StreamHC::new(9, b"").unwrap();
        let mut dst = Vec::new();
        let n = strategy.compress_block(SAMPLE, &mut dst).unwrap();
        let recovered = lz4_decompress(&dst[..n], SAMPLE.len());
        assert_eq!(recovered.as_slice(), SAMPLE);
    }

    #[test]
    fn stream_hc_with_dict_roundtrip() {
        let dict = b"hello world ";
        let mut strategy = StreamHC::new(9, dict).unwrap();
        let mut dst = Vec::new();
        let n = strategy.compress_block(SAMPLE, &mut dst).unwrap();
        assert!(n > 0);
    }

    #[test]
    fn build_compression_parameters_selects_fast() {
        // c_level 1 < LZ4HC_CLEVEL_MIN=2 → NoStreamFast
        let mut s = build_compression_parameters(1, 65536, 65536);
        let mut dst = Vec::new();
        let n = s.compress_block(SAMPLE, &mut dst).unwrap();
        let recovered = lz4_decompress(&dst[..n], SAMPLE.len());
        assert_eq!(recovered.as_slice(), SAMPLE);
    }

    #[test]
    fn build_compression_parameters_selects_hc() {
        // c_level 9 ≥ LZ4HC_CLEVEL_MIN=2 → NoStreamHC
        let mut s = build_compression_parameters(9, 65536, 65536);
        let mut dst = Vec::new();
        let n = s.compress_block(SAMPLE, &mut dst).unwrap();
        let recovered = lz4_decompress(&dst[..n], SAMPLE.len());
        assert_eq!(recovered.as_slice(), SAMPLE);
    }

    #[test]
    fn build_with_dict_selects_stream_fast() {
        let dict = b"hello world ";
        let mut s = build_compression_parameters_with_dict(1, dict).unwrap();
        let mut dst = Vec::new();
        s.compress_block(SAMPLE, &mut dst).unwrap();
    }

    #[test]
    fn build_with_dict_selects_stream_hc() {
        let dict = b"hello world ";
        let mut s = build_compression_parameters_with_dict(9, dict).unwrap();
        let mut dst = Vec::new();
        s.compress_block(SAMPLE, &mut dst).unwrap();
    }
}