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
456
457
458
// Unit tests for task-020: src/file.rs — LZ4 file-level streaming I/O
//
// Verifies behavioural parity with lz4file.c / lz4file.h v1.10.0:
//   - Lz4ReadFile::open       → LZ4F_readOpen (lines 73–138)
//   - Lz4ReadFile::read       → LZ4F_read     (lines 140–181)
//   - Lz4WriteFile::open      → LZ4F_writeOpen (lines 217–279)
//   - Lz4WriteFile::write     → LZ4F_write    (lines 281–315)
//   - Lz4WriteFile::finish    → LZ4F_writeClose (lines 317–341)
//   - lz4_write_frame         → LZ4_writeFile convenience
//   - lz4_read_frame          → LZ4_readFile  convenience
//   - Sticky errored flag (C: errCode), Drop finalisation

use lz4::file::{lz4_read_frame, lz4_write_frame, Lz4ReadFile, Lz4WriteFile};
use lz4::frame::types::{BlockSizeId, ContentChecksum, FrameInfo, Preferences};
use std::io::{Cursor, Read, Write};

// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────

/// Compress `src` into a valid LZ4 frame using the convenience helper.
fn compress_to_frame(src: &[u8]) -> Vec<u8> {
    lz4_write_frame(src, Vec::new()).expect("lz4_write_frame failed")
}

/// Decompress a full LZ4 frame using the convenience helper.
fn decompress_frame(frame: &[u8]) -> Vec<u8> {
    let mut out = Vec::new();
    lz4_read_frame(Cursor::new(frame), &mut out).expect("lz4_read_frame failed");
    out
}

/// Build cycling bytes 0..=255 repeated as needed.
fn cycling_bytes(len: usize) -> Vec<u8> {
    (0u8..=255).cycle().take(len).collect()
}

// ─────────────────────────────────────────────────────────────────────────────
// lz4_write_frame / lz4_read_frame — convenience round-trips
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn convenience_round_trip_small() {
    // Basic round-trip: verify the convenience wrappers compress + decompress correctly.
    let original = b"Hello, LZ4 world! This is a test.";
    let compressed = compress_to_frame(original);
    let recovered = decompress_frame(&compressed);
    assert_eq!(recovered, original);
}

#[test]
fn convenience_round_trip_empty() {
    // Empty payload: lz4_write_frame should emit a valid frame (header + end-mark).
    // lz4_read_frame should recover an empty byte slice.
    let original: &[u8] = b"";
    let compressed = compress_to_frame(original);
    assert!(
        !compressed.is_empty(),
        "compressed frame must not be empty even for empty input"
    );
    let recovered = decompress_frame(&compressed);
    assert_eq!(recovered.as_slice(), original);
}

#[test]
fn convenience_round_trip_single_byte() {
    // Single-byte payload: smallest meaningful input.
    let original = b"x";
    let compressed = compress_to_frame(original);
    let recovered = decompress_frame(&compressed);
    assert_eq!(recovered.as_slice(), original.as_ref());
}

#[test]
fn convenience_round_trip_multi_block() {
    // Input > 64 KiB forces multiple LZ4 blocks within the frame (LZ4F default max64KB).
    let original: Vec<u8> = cycling_bytes(200 * 1024);
    let compressed = compress_to_frame(&original);
    let recovered = decompress_frame(&compressed);
    assert_eq!(recovered, original);
}

#[test]
fn convenience_round_trip_large_repetitive() {
    // Repetitive data compresses well — verify decompressed length equality.
    let original: Vec<u8> = b"AAAA".iter().cycle().take(512 * 1024).cloned().collect();
    let compressed = compress_to_frame(&original);
    // Compressed size should be significantly smaller than original for repetitive data.
    assert!(compressed.len() < original.len() / 2);
    let recovered = decompress_frame(&compressed);
    assert_eq!(recovered, original);
}

#[test]
fn convenience_round_trip_incompressible() {
    // Incompressible (random-ish) data: compressed may be slightly larger than input.
    let original: Vec<u8> = cycling_bytes(32 * 1024);
    let compressed = compress_to_frame(&original);
    let recovered = decompress_frame(&compressed);
    assert_eq!(recovered, original);
}

// ─────────────────────────────────────────────────────────────────────────────
// lz4_write_frame — returns inner writer on success
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn write_frame_returns_inner_writer() {
    // lz4_write_frame should return the inner writer (the Vec) on success.
    let data = b"test data";
    let result: Vec<u8> = lz4_write_frame(data, Vec::new()).expect("should succeed");
    assert!(
        !result.is_empty(),
        "returned Vec must contain the LZ4 frame bytes"
    );
}

// ─────────────────────────────────────────────────────────────────────────────
// Lz4WriteFile — streaming Write impl
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn write_file_open_default_prefs() {
    // Lz4WriteFile::open with None preferences should succeed and write the frame header.
    let mut buf = Vec::new();
    {
        let writer = Lz4WriteFile::open(&mut buf, None).expect("open should succeed");
        // finish() explicitly to ensure end-mark is written.
        writer.finish().expect("finish should succeed");
    }
    // The resulting frame should be a valid LZ4 frame (starts with magic 0x184D2204).
    assert!(
        buf.len() >= 7,
        "frame must contain at least header + end-mark"
    );
    let magic = u32::from_le_bytes(buf[0..4].try_into().unwrap());
    assert_eq!(magic, 0x184D2204, "frame must start with LZ4 magic number");
}

#[test]
fn write_file_open_with_preferences() {
    // Lz4WriteFile::open with explicit preferences (Max256Kb block size).
    let prefs = Preferences {
        frame_info: FrameInfo {
            block_size_id: BlockSizeId::Max256Kb,
            content_checksum_flag: ContentChecksum::Enabled,
            ..FrameInfo::default()
        },
        ..Preferences::default()
    };
    let compressed = {
        let mut lz4w = Lz4WriteFile::open(Vec::new(), Some(&prefs)).expect("open");
        lz4w.write_all(b"hello preferences").unwrap();
        lz4w.finish().expect("finish")
    };
    // Should decompress cleanly.
    let recovered = decompress_frame(&compressed);
    assert_eq!(recovered, b"hello preferences");
}

#[test]
fn write_file_chunked_writes() {
    // The C LZ4F_write loops over maxWriteSize-sized chunks.
    // Verify that writing in small pieces produces the same output as one large write.
    let original: Vec<u8> = cycling_bytes(8 * 1024);

    // Write in 256-byte chunks.
    let chunked = {
        let mut lz4w = Lz4WriteFile::open(Vec::new(), None).expect("open");
        for chunk in original.chunks(256) {
            lz4w.write_all(chunk).unwrap();
        }
        lz4w.finish().expect("finish")
    };

    // Write all at once.
    let one_shot = {
        let mut lz4w = Lz4WriteFile::open(Vec::new(), None).expect("open");
        lz4w.write_all(&original).unwrap();
        lz4w.finish().expect("finish")
    };

    // Both should decompress to the original.
    assert_eq!(decompress_frame(&chunked), original);
    assert_eq!(decompress_frame(&one_shot), original);
}

#[test]
fn write_file_write_returns_buf_len() {
    // The Write::write impl must return Ok(buf.len()) on success (all bytes consumed),
    // mirroring the C LZ4F_write return convention.
    let mut lz4w = Lz4WriteFile::open(Vec::new(), None).expect("open");
    let data = b"some data here";
    let written = lz4w.write(data).expect("write");
    assert_eq!(written, data.len());
    lz4w.finish().expect("finish");
}

#[test]
fn write_file_empty_write() {
    // Writing zero bytes should be a no-op (loop doesn't execute).
    let mut lz4w = Lz4WriteFile::open(Vec::new(), None).expect("open");
    let written = lz4w.write(b"").expect("write empty");
    assert_eq!(written, 0);
    lz4w.finish().expect("finish");
}

#[test]
fn write_file_multiple_finish_not_called_drop_finalizes() {
    // When finish() is NOT called, Drop should still write the end-mark.
    // The resulting frame should be decompressible.
    let mut buf = Vec::new();
    {
        let mut lz4w = Lz4WriteFile::open(&mut buf, None).expect("open");
        lz4w.write_all(b"dropped without finish").unwrap();
        // Drop is called here — it must write end-mark.
    }
    let recovered = decompress_frame(&buf);
    assert_eq!(recovered, b"dropped without finish");
}

#[test]
fn write_file_finish_takes_inner_writer() {
    // finish() should return the inner writer with the complete frame.
    let data = b"finish returns inner";
    let inner: Vec<u8> = {
        let mut lz4w = Lz4WriteFile::open(Vec::new(), None).expect("open");
        lz4w.write_all(data).unwrap();
        lz4w.finish().expect("finish")
    };
    let recovered = decompress_frame(&inner);
    assert_eq!(recovered, data);
}

// ─────────────────────────────────────────────────────────────────────────────
// Lz4ReadFile — streaming Read impl
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn read_file_open_valid_frame() {
    // Lz4ReadFile::open should succeed on a valid LZ4 frame.
    let compressed = compress_to_frame(b"valid frame");
    let _reader = Lz4ReadFile::open(Cursor::new(compressed)).expect("open should succeed");
}

#[test]
fn read_file_open_empty_input_fails() {
    // Lz4ReadFile::open on an empty reader should return an error
    // (the C LZ4F_readOpen fails if fread returns 0 bytes — no header to parse).
    let result = Lz4ReadFile::open(Cursor::new(b""));
    assert!(result.is_err(), "open on empty input must fail");
}

#[test]
fn read_file_open_corrupt_magic_fails() {
    // A stream with an invalid magic number should be rejected during open.
    let corrupt = vec![
        0xDEu8, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
    ];
    let result = Lz4ReadFile::open(Cursor::new(corrupt));
    assert!(result.is_err(), "open on corrupt magic must fail");
}

#[test]
fn read_file_read_full_small() {
    // Read all decompressed bytes in a single large buffer.
    let original = b"small read test";
    let compressed = compress_to_frame(original);
    let mut lz4r = Lz4ReadFile::open(Cursor::new(compressed)).expect("open");
    let mut out = vec![0u8; 1024];
    let n = lz4r.read(&mut out).unwrap();
    assert_eq!(&out[..n], original);
}

#[test]
fn read_file_read_chunks_reassemble_correctly() {
    // Read decompressed data in small pieces and verify the concatenation equals original.
    let original: Vec<u8> = cycling_bytes(16 * 1024);
    let compressed = compress_to_frame(&original);
    let mut lz4r = Lz4ReadFile::open(Cursor::new(compressed)).expect("open");

    let mut recovered = Vec::new();
    let mut tmp = [0u8; 512];
    loop {
        let n = lz4r.read(&mut tmp).unwrap();
        if n == 0 {
            break;
        }
        recovered.extend_from_slice(&tmp[..n]);
    }
    assert_eq!(recovered, original);
}

#[test]
fn read_file_returns_zero_at_eof() {
    // After the frame is fully decompressed, subsequent reads must return 0.
    let original = b"eof test";
    let compressed = compress_to_frame(original);
    let mut lz4r = Lz4ReadFile::open(Cursor::new(compressed)).expect("open");

    // Drain the stream.
    let mut out = vec![0u8; 4096];
    loop {
        let n = lz4r.read(&mut out).unwrap();
        if n == 0 {
            break;
        }
    }
    // Another read should return 0 (EOF).
    let n = lz4r.read(&mut out).unwrap();
    assert_eq!(n, 0);
}

#[test]
fn read_file_multi_block_frame() {
    // Data exceeding 64 KiB (default block size) creates multiple blocks;
    // verify all blocks are decompressed correctly.
    let original: Vec<u8> = cycling_bytes(150 * 1024);
    let compressed = compress_to_frame(&original);
    let mut lz4r = Lz4ReadFile::open(Cursor::new(compressed)).expect("open");
    let mut recovered = Vec::new();
    let mut tmp = [0u8; 65536];
    loop {
        let n = lz4r.read(&mut tmp).unwrap();
        if n == 0 {
            break;
        }
        recovered.extend_from_slice(&tmp[..n]);
    }
    assert_eq!(recovered, original);
}

// ─────────────────────────────────────────────────────────────────────────────
// Streaming Write then Read — full parity scenarios
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn streaming_write_then_streaming_read() {
    // Validates the core parity scenario: Lz4WriteFile + Lz4ReadFile behave
    // like LZ4F_writeOpen/write/writeClose + LZ4F_readOpen/read/readClose.
    let original: Vec<u8> = b"streaming parity test"
        .iter()
        .cycle()
        .take(32 * 1024)
        .cloned()
        .collect();

    // Write in 1 KiB pieces (exercises the chunking loop in LZ4F_write).
    let compressed = {
        let mut lz4w = Lz4WriteFile::open(Vec::new(), None).expect("open writer");
        for chunk in original.chunks(1024) {
            lz4w.write_all(chunk).unwrap();
        }
        lz4w.finish().expect("finish")
    };

    // Read back in 3 KiB pieces (exercises multiple refill passes in LZ4F_read).
    let mut lz4r = Lz4ReadFile::open(Cursor::new(&compressed)).expect("open reader");
    let mut recovered = Vec::new();
    let mut tmp = [0u8; 3 * 1024];
    loop {
        let n = lz4r.read(&mut tmp).unwrap();
        if n == 0 {
            break;
        }
        recovered.extend_from_slice(&tmp[..n]);
    }

    assert_eq!(recovered, original);
}

#[test]
fn round_trip_with_content_checksum() {
    // Content checksum (FLG.C_Size bit) is validated during decompression.
    let prefs = Preferences {
        frame_info: FrameInfo {
            content_checksum_flag: ContentChecksum::Enabled,
            ..FrameInfo::default()
        },
        ..Preferences::default()
    };
    let original = b"checksum round trip";
    let compressed = {
        let mut lz4w = Lz4WriteFile::open(Vec::new(), Some(&prefs)).expect("open");
        lz4w.write_all(original).unwrap();
        lz4w.finish().expect("finish")
    };
    let recovered = decompress_frame(&compressed);
    assert_eq!(recovered.as_slice(), original.as_ref());
}

#[test]
fn round_trip_max1mb_block_size() {
    // Verify the block_size_from_id mapping for Max1Mb (1 MiB block size).
    let prefs = Preferences {
        frame_info: FrameInfo {
            block_size_id: BlockSizeId::Max1Mb,
            ..FrameInfo::default()
        },
        ..Preferences::default()
    };
    let original: Vec<u8> = cycling_bytes(2 * 1024 * 1024);
    let compressed = {
        let mut lz4w = Lz4WriteFile::open(Vec::new(), Some(&prefs)).expect("open");
        lz4w.write_all(&original).unwrap();
        lz4w.finish().expect("finish")
    };
    let recovered = decompress_frame(&compressed);
    assert_eq!(recovered, original);
}

#[test]
fn round_trip_max256kb_block_size() {
    // Verify the block_size_from_id mapping for Max256Kb.
    let prefs = Preferences {
        frame_info: FrameInfo {
            block_size_id: BlockSizeId::Max256Kb,
            ..FrameInfo::default()
        },
        ..Preferences::default()
    };
    let original: Vec<u8> = cycling_bytes(500 * 1024);
    let compressed = {
        let mut lz4w = Lz4WriteFile::open(Vec::new(), Some(&prefs)).expect("open");
        lz4w.write_all(&original).unwrap();
        lz4w.finish().expect("finish")
    };
    let recovered = decompress_frame(&compressed);
    assert_eq!(recovered, original);
}

// ─────────────────────────────────────────────────────────────────────────────
// Compressed output structure sanity checks
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn compressed_frame_starts_with_lz4_magic() {
    // Every LZ4 frame must begin with magic number 0x184D2204 (little-endian).
    let compressed = compress_to_frame(b"magic check");
    assert!(compressed.len() >= 4);
    let magic = u32::from_le_bytes(compressed[0..4].try_into().unwrap());
    assert_eq!(magic, 0x184D2204u32);
}

#[test]
fn compressed_output_smaller_than_repetitive_input() {
    // LZ4 compression of repetitive data must produce output smaller than input
    // for inputs above the minimum useful size.
    let original: Vec<u8> = vec![b'A'; 64 * 1024];
    let compressed = compress_to_frame(&original);
    assert!(
        compressed.len() < original.len(),
        "compressed ({} bytes) should be < original ({} bytes) for repetitive data",
        compressed.len(),
        original.len()
    );
}