structured-zstd 0.2.6

Pure-Rust Zstandard (zstd) compression and decompression: all levels, streaming, dictionaries, no_std and WebAssembly ready — no FFI, no cmake
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
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
//! Integration tests for the fine-grained compression parameter API (#27):
//! builder → [`compress_with_parameters`] → decode round-trip, the LDM
//! activation surface, the `Greedy` strategy override, and the
//! default-equivalence invariant (an empty override reproduces plain
//! level-based output byte-for-byte).

extern crate std;

use alloc::vec::Vec;

use crate::decoding::StreamingDecoder;
use crate::encoding::{
    CompressionLevel, CompressionParameters, FrameCompressor, Strategy, compress_slice_to_vec,
    compress_with_parameters,
};
use crate::io::Read;

/// Deterministic LCG byte stream (mirrors `roundtrip_integrity`).
fn generate_data(seed: u64, len: usize) -> Vec<u8> {
    let mut state = seed;
    let mut data = Vec::with_capacity(len);
    for _ in 0..len {
        state = state
            .wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407);
        data.push((state >> 33) as u8);
    }
    data
}

/// A genuinely compressible long-range-repetitive fixture: a
/// low-entropy base block (a short motif tiled to 4 KiB, so the cheap
/// incompressibility sampler classifies it compressible) repeated many
/// times with a few bytes of per-copy noise. Back-references at the
/// 4 KiB base distance dominate, so the encoder compresses it well below
/// input size (unlike a random base, which the sampler raw-stores).
fn long_range_repetitive(total_len: usize) -> Vec<u8> {
    // 16-byte motif → tiled to a 4 KiB base. Low entropy: the sampler
    // sees the repetition and the block is compressed, not raw-stored.
    let motif = generate_data(0xABCD_1234, 16);
    let mut base = Vec::with_capacity(4096);
    while base.len() < 4096 {
        base.extend_from_slice(&motif);
    }
    base.truncate(4096);

    let mut data = Vec::with_capacity(total_len);
    let mut counter = 0u64;
    while data.len() < total_len {
        data.extend_from_slice(&base);
        // A few bytes of per-copy noise so each repeat is a distinct
        // long-range back-reference rather than one giant run.
        counter = counter.wrapping_add(1);
        data.extend_from_slice(&counter.to_le_bytes());
    }
    data.truncate(total_len);
    data
}

fn decode(compressed: &[u8]) -> Vec<u8> {
    let mut decoder = StreamingDecoder::new(compressed);
    let mut out = Vec::new();
    decoder.read_to_end(&mut out).unwrap();
    out
}

/// A parameter set that overrides nothing must produce byte-identical
/// output to plain level-based compression — the default path is
/// untouched.
#[test]
fn empty_override_is_byte_identical_to_level() {
    let data = generate_data(7, 64 * 1024);
    for level in [
        CompressionLevel::Fastest,
        CompressionLevel::Default,
        CompressionLevel::Better,
        CompressionLevel::Best,
        CompressionLevel::Level(5),
        CompressionLevel::Level(19),
    ] {
        let params = CompressionParameters::builder(level).build().unwrap();
        let via_params = compress_with_parameters(&data, &params);
        let via_level = compress_slice_to_vec(&data, level);
        assert_eq!(
            via_params, via_level,
            "empty override diverged from level {level:?}",
        );
    }
}

/// Once every match-finder knob is set, the base level has nothing left to
/// decide: the frame is the same whichever level the parameters start from, as
/// it is upstream, where the block splitter follows the effective cParams
/// (`ZSTD_resolveBlockSplitterMode`: `strategy >= btopt && windowLog >= 17`).
/// A dfast base level carrying btultra2 knobs used to skip the post-split pass
/// a btultra2 level runs, because that pass was keyed on the level number.
#[test]
fn a_fully_specified_parameter_set_ignores_the_base_level() {
    let data = &include_bytes!("../../decodecorpus_files/z000033")[..512 * 1024];
    let frame_from = |level: i32| {
        let params = CompressionParameters::builder(CompressionLevel::Level(level))
            .window_log(20)
            .chain_log(21)
            .hash_log(21)
            .search_log(9)
            .min_match(3)
            .target_length(999)
            .strategy(Strategy::Btultra2)
            .build()
            .unwrap();
        compress_with_parameters(data, &params)
    };
    let native = frame_from(22);
    for level in [3, 13] {
        let frame = frame_from(level);
        assert_eq!(
            frame.len(),
            native.len(),
            "level {level} base with btultra2 knobs diverged from level 22",
        );
        assert_eq!(frame, native);
    }
    assert_eq!(decode(&native), data);
}

/// Custom parameters must produce valid (decodable) frames that
/// reproduce the input.
#[test]
fn custom_parameters_round_trip() {
    let data = generate_data(99, 96 * 1024);

    let cases = [
        CompressionParameters::builder(CompressionLevel::Level(3))
            .window_log(18)
            .strategy(Strategy::Dfast)
            .build()
            .unwrap(),
        CompressionParameters::builder(CompressionLevel::Level(9))
            .strategy(Strategy::Lazy2)
            .search_log(6)
            .target_length(64)
            .build()
            .unwrap(),
        CompressionParameters::builder(CompressionLevel::Level(19))
            .window_log(22)
            .hash_log(23)
            .chain_log(24)
            .strategy(Strategy::Btultra2)
            .build()
            .unwrap(),
    ];

    for params in cases {
        let compressed = compress_with_parameters(&data, &params);
        assert_eq!(
            decode(&compressed),
            data,
            "round-trip failed for {params:?}"
        );
    }
}

/// `set_parameter(Strategy, Greedy)` produces a greedy-parsed frame at any
/// level, and it decodes back to the input.
#[test]
fn greedy_strategy_override_round_trips() {
    let data = generate_data(0x5151, 80 * 1024);
    // Force greedy onto a level the table would resolve to lazy (7).
    let params = CompressionParameters::builder(CompressionLevel::Level(7))
        .strategy(Strategy::Greedy)
        .build()
        .unwrap();
    let compressed = compress_with_parameters(&data, &params);
    assert_eq!(decode(&compressed), data);
    // Greedy on a highly-repetitive fixture compresses well below input
    // size (the back-references dominate) and still round-trips.
    let compressible = long_range_repetitive(80 * 1024);
    let greedy = compress_with_parameters(
        &compressible,
        &CompressionParameters::builder(CompressionLevel::Level(5))
            .strategy(Strategy::Greedy)
            .build()
            .unwrap(),
    );
    assert!(
        greedy.len() < compressible.len() / 2,
        "greedy did not compress repetitive fixture: {} vs {}",
        greedy.len(),
        compressible.len(),
    );
    assert_eq!(decode(&greedy), compressible);
}

/// LDM-on activation: `enable_long_distance_matching(true)` on an optimal
/// strategy round-trips correctly through our decoder.
#[cfg(feature = "hash")]
#[test]
fn ldm_on_round_trips() {
    let data = long_range_repetitive(512 * 1024);
    let params = CompressionParameters::builder(CompressionLevel::Level(19))
        .enable_long_distance_matching(true)
        .build()
        .unwrap();
    let compressed = compress_with_parameters(&data, &params);
    assert_eq!(decode(&compressed), data);
}

/// LDM ratio: on a long-range-repetitive fixture, the LDM-on variant must
/// be no larger than LDM-off. The LDM producer only adds candidates to the
/// optimal parser, never removes valid regular matches, so enabling it can
/// only keep or improve the ratio (within the regular window the two are
/// often equal, hence `<=` rather than strict `<`).
#[cfg(feature = "hash")]
#[test]
fn ldm_on_is_not_larger_than_ldm_off() {
    let data = long_range_repetitive(1024 * 1024);

    let off = CompressionParameters::builder(CompressionLevel::Level(19))
        .build()
        .unwrap();
    let on = CompressionParameters::builder(CompressionLevel::Level(19))
        .enable_long_distance_matching(true)
        .build()
        .unwrap();

    let off_sz = compress_with_parameters(&data, &off).len();
    let on_compressed = compress_with_parameters(&data, &on);
    let on_sz = on_compressed.len();

    // Correctness first: LDM-on must still decode to the input.
    assert_eq!(decode(&on_compressed), data);
    assert!(
        on_sz <= off_sz,
        "LDM-on ({on_sz}) larger than LDM-off ({off_sz})",
    );
}

/// Custom LDM knobs override the strategy-derived defaults and still
/// produce a decodable frame.
#[cfg(feature = "hash")]
#[test]
fn ldm_custom_knobs_round_trip() {
    let data = long_range_repetitive(512 * 1024);
    let params = CompressionParameters::builder(CompressionLevel::Level(19))
        .ldm_hash_log(20)
        .ldm_min_match(48)
        .ldm_bucket_size_log(4)
        .ldm_hash_rate_log(6)
        .build()
        .unwrap();
    assert_eq!(decode(&compress_with_parameters(&data, &params)), data);
}

/// Long-distance matching runs under every strategy, not only the optimal
/// ones: a random half repeated at a distance the strategy's own tables
/// cannot reach is found by LDM alone, so the second copy costs next to
/// nothing. Upstream applies LDM below btopt through `ZSTD_ldm_blockCompress`.
/// Six-bit symbols keep every block searched (a full-entropy block would be
/// written off by the raw skip, whose repeat grid could find the copy instead)
/// while leaving short local matches rare. The copy is laid out in shuffled
/// 4 KiB pieces, so each piece sits at an offset of its own and one lucky hit
/// cannot carry the rest through the repeat code.
#[cfg(feature = "ldm")]
#[test]
fn ldm_finds_far_repeat_under_every_strategy() {
    const PIECE: usize = 4096;
    let half: Vec<u8> = generate_data(0x5EED_0569, 512 * 1024)
        .into_iter()
        .map(|byte| byte & 63)
        .collect();
    let pieces = half.len() / PIECE;
    let mut data = half.clone();
    for i in 0..pieces {
        // 37 is coprime with the piece count, so this visits every piece once.
        let piece = (i * 37) % pieces;
        data.extend_from_slice(&half[piece * PIECE..(piece + 1) * PIECE]);
    }
    for strategy in [
        Strategy::Fast,
        Strategy::Dfast,
        Strategy::Greedy,
        Strategy::Lazy,
        Strategy::Lazy2,
        Strategy::Btlazy2,
        Strategy::Btopt,
    ] {
        // Tables far smaller than the distance, so nothing but LDM reaches the
        // first copy.
        let builder = || {
            CompressionParameters::builder(CompressionLevel::Level(3))
                .strategy(strategy)
                .window_log(22)
                .hash_log(12)
                .chain_log(12)
        };
        let off = compress_with_parameters(&data, &builder().build().unwrap());
        let on = compress_with_parameters(
            &data,
            &builder()
                .enable_long_distance_matching(true)
                .build()
                .unwrap(),
        );
        // Each copy costs about three quarters of its size in literals; the
        // second one costs nearly that again without LDM (a few pieces are
        // still found by chance) and almost nothing with it.
        assert!(
            off.len() > data.len() * 13 / 20,
            "{strategy:?}: the regular finder already reached the repeat ({} of {})",
            off.len(),
            data.len(),
        );
        assert!(
            on.len() < half.len() * 8 / 10,
            "{strategy:?}: LDM did not find the repeat ({} of {}, {} without LDM)",
            on.len(),
            data.len(),
            off.len(),
        );
        assert_eq!(decode(&on), data, "{strategy:?}: LDM frame round-trip");
    }
}

/// The block splitter does not cut inside a long-distance match: a repeat of
/// random data is one match per block, and a cut through it would only buy a
/// block whose single sequence carries the same match on. Both frame loops
/// ask, the one-shot one and the stream's.
#[cfg(feature = "ldm")]
#[test]
fn ldm_matches_are_not_cut_by_the_block_splitter() {
    use crate::encoding::StreamingEncoder;
    use crate::io::Write;

    let half = generate_data(0xC07_0569, 1 << 20);
    let mut data = half.clone();
    data.extend_from_slice(&half);
    let params = CompressionParameters::builder(CompressionLevel::Level(3))
        .window_log(22)
        .enable_long_distance_matching(true)
        .build()
        .unwrap();
    let one_shot = compress_with_parameters(&data, &params);
    let mut encoder = StreamingEncoder::new(Vec::new(), CompressionLevel::Level(3));
    encoder.set_parameters(&params).unwrap();
    encoder.write_all(&data).unwrap();
    let streamed = encoder.finish().unwrap();
    for (path, frame) in [("one-shot", &one_shot), ("stream", &streamed)] {
        // The first copy goes out raw; the second is eight blocks of one
        // sequence each, where a cut at every splitter boundary would make it
        // about a hundred.
        assert!(
            frame.len() < half.len() + 512,
            "{path}: the repeat cost {} bytes",
            frame.len() - half.len()
        );
        assert_eq!(decode(frame), data, "{path}: round-trip");
    }
}

/// Long-distance matching reaches into a dictionary, under every strategy:
/// the dictionary is indexed into its table when it is loaded (upstream
/// `ZSTD_ldm_fillHashTable` from `ZSTD_loadDictionaryContent`), so a source
/// made of shuffled pieces of a large dictionary costs next to nothing. A
/// second frame on the same compressor restores the primed dictionary and must
/// write what a freshly primed compressor writes.
#[cfg(feature = "ldm")]
#[test]
fn ldm_reaches_into_the_dictionary_under_every_strategy() {
    use crate::decoding::{Dictionary, FrameDecoder};

    const PIECE: usize = 4096;
    const DICT_ID: u32 = 0x0569_D1C7;
    let dict: Vec<u8> = generate_data(0xD1C7_0569, 256 * 1024)
        .into_iter()
        .map(|byte| byte & 63)
        .collect();
    let pieces = dict.len() / PIECE;
    let mut source = Vec::new();
    for i in 0..pieces {
        let piece = (i * 37) % pieces;
        source.extend_from_slice(&dict[piece * PIECE..(piece + 1) * PIECE]);
    }
    for strategy in [
        Strategy::Fast,
        Strategy::Dfast,
        Strategy::Greedy,
        Strategy::Lazy2,
        Strategy::Btlazy2,
        Strategy::Btopt,
    ] {
        let params = CompressionParameters::builder(CompressionLevel::Level(3))
            .strategy(strategy)
            .window_log(22)
            .hash_log(12)
            .chain_log(12)
            .enable_long_distance_matching(true)
            .build()
            .unwrap();
        let compressor = || {
            let mut compressor: FrameCompressor = FrameCompressor::new(CompressionLevel::Default);
            compressor.set_parameters(&params);
            compressor
                .set_dictionary(Dictionary::from_raw_content(DICT_ID, dict.clone()).unwrap())
                .unwrap();
            compressor
        };
        let mut warm = compressor();
        let first = warm.compress_independent_frame(source.as_slice());
        let restored = warm.compress_independent_frame(source.as_slice());
        let fresh = compressor().compress_independent_frame(source.as_slice());
        assert!(
            first.len() < source.len() / 10,
            "{strategy:?}: LDM did not reach into the dictionary ({} of {})",
            first.len(),
            source.len(),
        );
        assert_eq!(
            first, fresh,
            "{strategy:?}: a fresh compressor wrote another frame"
        );
        assert_eq!(
            restored, fresh,
            "{strategy:?}: the restored dictionary wrote another frame"
        );
        let mut decoder = FrameDecoder::new();
        decoder
            .add_dict(Dictionary::from_raw_content(DICT_ID, dict.clone()).unwrap())
            .unwrap();
        let mut decoded = Vec::with_capacity(source.len());
        decoder.decode_all_to_vec(&restored, &mut decoded).unwrap();
        assert_eq!(decoded, source, "{strategy:?}: round-trip");
    }
}

/// The primed dictionary snapshot carries no long-distance state, so it is
/// restored whether the next frame turns long-distance matching on or off,
/// and the frame matches what a freshly primed compressor writes either way.
#[cfg(feature = "ldm")]
#[test]
fn a_primed_dictionary_restores_across_a_long_distance_switch() {
    use crate::decoding::Dictionary;

    const PIECE: usize = 4096;
    const DICT_ID: u32 = 0x0569_D1C8;
    let dict: Vec<u8> = generate_data(0xD1C7_0571, 256 * 1024)
        .into_iter()
        .map(|byte| byte & 63)
        .collect();
    let pieces = dict.len() / PIECE;
    let mut source = Vec::new();
    for i in 0..pieces {
        let piece = (i * 37) % pieces;
        source.extend_from_slice(&dict[piece * PIECE..(piece + 1) * PIECE]);
    }
    for strategy in [Strategy::Fast, Strategy::Lazy2, Strategy::Btopt] {
        let params = |long: bool| {
            CompressionParameters::builder(CompressionLevel::Level(3))
                .strategy(strategy)
                .window_log(22)
                .hash_log(12)
                .chain_log(12)
                .enable_long_distance_matching(long)
                .build()
                .unwrap()
        };
        let compressor = |long: bool| {
            let mut compressor: FrameCompressor = FrameCompressor::new(CompressionLevel::Default);
            compressor.set_parameters(&params(long));
            compressor
                .set_dictionary(Dictionary::from_raw_content(DICT_ID, dict.clone()).unwrap())
                .unwrap();
            compressor
        };
        for first in [false, true] {
            let mut warm = compressor(first);
            warm.compress_independent_frame(source.as_slice());
            warm.set_parameters(&params(!first));
            let restored = warm.compress_independent_frame(source.as_slice());
            let fresh = compressor(!first).compress_independent_frame(source.as_slice());
            assert!(
                restored == fresh,
                "{strategy:?}: long-distance matching {} after {}: {} bytes, fresh {}",
                !first,
                first,
                restored.len(),
                fresh.len(),
            );
        }
    }
}

/// A dictionary a reused context keeps resident between unsized frames stays
/// in reach of long-distance matching: every frame on the context writes what
/// a fresh encoder writes, the first one included. The pieces come from the
/// dictionary's start, which the regular tables leave unindexed.
#[cfg(feature = "ldm")]
#[test]
fn ldm_reaches_into_a_resident_dictionary_frame_after_frame() {
    use crate::encoding::{CompressionContext, EncoderDictionary, StreamingEncoder};
    use crate::io::Write;

    const PIECE: usize = 4096;
    let dict: Vec<u8> = generate_data(0xD1C7_0570, 256 * 1024)
        .into_iter()
        .map(|byte| byte & 63)
        .collect();
    let mut source = Vec::new();
    for piece in [3, 17, 9] {
        source.extend_from_slice(&dict[piece * PIECE..(piece + 1) * PIECE]);
    }
    let dictionary = EncoderDictionary::from_serialized_or_raw_content(&dict).unwrap();
    for strategy in [
        Strategy::Fast,
        Strategy::Dfast,
        Strategy::Greedy,
        Strategy::Lazy2,
        Strategy::Btlazy2,
        Strategy::Btopt,
    ] {
        let params = CompressionParameters::builder(CompressionLevel::Level(3))
            .strategy(strategy)
            .window_log(22)
            .hash_log(12)
            .chain_log(12)
            .enable_long_distance_matching(true)
            .build()
            .unwrap();
        let mut fresh = StreamingEncoder::new(Vec::new(), CompressionLevel::Level(3));
        fresh.set_parameters(&params).unwrap();
        fresh.set_encoder_dictionary(dictionary.clone()).unwrap();
        fresh.write_all(&source).unwrap();
        let expected = fresh.finish().unwrap();
        let mut without = StreamingEncoder::new(Vec::new(), CompressionLevel::Level(3));
        without
            .set_parameters(
                &CompressionParameters::builder(CompressionLevel::Level(3))
                    .strategy(strategy)
                    .window_log(22)
                    .hash_log(12)
                    .chain_log(12)
                    .build()
                    .unwrap(),
            )
            .unwrap();
        without.set_encoder_dictionary(dictionary.clone()).unwrap();
        without.write_all(&source).unwrap();
        let without = without.finish().unwrap();
        // The optimal parser weighs the long matches as candidates rather than
        // taking them outright, so it is held to a gain, not to a size.
        assert!(
            expected.len() < without.len() / 2,
            "{strategy:?}: LDM did not reach into the dictionary ({} bytes, {} without)",
            expected.len(),
            without.len(),
        );
        let mut context = CompressionContext::new(CompressionLevel::Level(3));
        context.set_parameters(&params).unwrap();
        context.set_encoder_dictionary(dictionary.clone()).unwrap();
        for frame_index in 0..3 {
            let mut frame = Vec::new();
            let mut rest = source.as_slice();
            while !rest.is_empty() {
                let taken = context.write(&mut frame, rest).unwrap();
                rest = &rest[taken..];
            }
            context.finish_frame(&mut frame).unwrap();
            assert!(
                frame == expected,
                "{strategy:?}: frame {frame_index} on the reused context wrote {} bytes, a fresh encoder {}",
                frame.len(),
                expected.len(),
            );
        }
    }
}

/// `FrameCompressor::set_parameters` on the streaming path reuses one
/// compressor across frames and produces decodable output.
#[test]
fn set_parameters_streaming_round_trip() {
    let data = generate_data(0xC0FFEE, 70 * 1024);
    let params = CompressionParameters::builder(CompressionLevel::Level(11))
        .strategy(Strategy::Lazy2)
        .window_log(20)
        .build()
        .unwrap();

    let mut compressed = Vec::new();
    let mut compressor = FrameCompressor::new(CompressionLevel::Default);
    compressor.set_parameters(&params);
    compressor.set_source(data.as_slice());
    compressor.set_drain(&mut compressed);
    compressor.compress();

    assert_eq!(decode(&compressed), data);
}

/// A window_log override shrinks the advertised frame window: a frame
/// built with window_log 17 must decode with a smaller declared window
/// than one at the level default. We assert the round-trip and that the
/// override path is honoured by decoding successfully under our decoder
/// (which enforces the advertised window).
#[test]
fn window_log_override_round_trips() {
    let data = long_range_repetitive(256 * 1024);
    let params = CompressionParameters::builder(CompressionLevel::Level(9))
        .window_log(17)
        .build()
        .unwrap();
    let compressed = compress_with_parameters(&data, &params);
    assert_eq!(decode(&compressed), data);
}
/// LDM at a large window (`window_log >= 24`) over a multi-block,
/// multi-segment input. Regression for the LDM↔optimal-parser segment
/// misalignment: `ldm_sequences` are block-relative, but the optimal
/// parser runs per-segment with segment-relative positions, so every
/// segment after the first must fast-forward the raw LDM seq-store by its
/// block offset. Before the fix this emitted matches copying the wrong
/// bytes (frame rejected by both our decoder and upstream `zstd -d`).
#[cfg(feature = "hash")]
#[test]
fn ldm_large_window_multi_segment_round_trips() {
    let data = long_range_repetitive(512 * 1024);
    for window_log in [24, 25, 26] {
        let params = CompressionParameters::builder(CompressionLevel::Level(19))
            .window_log(window_log)
            .enable_long_distance_matching(true)
            .build()
            .unwrap();
        let compressed = compress_with_parameters(&data, &params);
        assert_eq!(
            decode(&compressed),
            data,
            "LDM round-trip failed at window_log {window_log}",
        );
    }
}

/// Streaming decode of a frame whose content far exceeds its window, so the
/// decoder's `RingBuffer` cycles many times. Exercises the bounded-ring drain
/// loop in `StreamingDecoder::read` (decode one block, drain into the caller's
/// buffer, repeat) instead of accumulating the whole `read_to_end` request in
/// the ring. A fast level keeps the decode dominated by match copies through
/// the wrapped window.
#[test]
fn streaming_decode_cycles_small_window_round_trips() {
    // 1 MiB content, 128 KiB window (window_log 17): the ring wraps ~8 times.
    let data = long_range_repetitive(1024 * 1024);
    let params = CompressionParameters::builder(CompressionLevel::Level(1))
        .window_log(17)
        .build()
        .unwrap();
    let compressed = compress_with_parameters(&data, &params);
    assert_eq!(
        decode(&compressed),
        data,
        "streaming decode over a cycling window must round-trip exactly",
    );
}

/// Reverting to a plain level via `set_compression_level` after a
/// customized frame must drop the parameter overrides. Otherwise the
/// strategy/LDM/log overrides stay sticky and the "plain" frame is still
/// encoded with the previous tuning. The second frame must be
/// byte-identical to a fresh plain-level compression of the same input.
#[test]
fn set_compression_level_clears_parameter_overrides() {
    // Compressible fixture so the strategy choice actually changes the
    // output (incompressible data raw-stores regardless of strategy).
    let data = long_range_repetitive(64 * 1024);

    let mut compressor: FrameCompressor = FrameCompressor::new(CompressionLevel::Default);
    // First frame: a custom strategy override (greedy) far from the
    // level-19 default (btultra2).
    let params = CompressionParameters::builder(CompressionLevel::Level(19))
        .strategy(Strategy::Greedy)
        .build()
        .unwrap();
    compressor.set_parameters(&params);
    let _first = compressor.compress_independent_frame(&data);

    // Revert to a plain level and compress again.
    compressor.set_compression_level(CompressionLevel::Level(19));
    let reverted = compressor.compress_independent_frame(&data);

    // Must match a brand-new compressor at plain level 19 (no overrides).
    let expected = compress_slice_to_vec(&data, CompressionLevel::Level(19));
    assert_eq!(
        reverted, expected,
        "set_compression_level did not clear sticky parameter overrides",
    );
    assert_eq!(decode(&reverted), data);
}