lbzip2 0.5.8

Pure Rust parallel bzip2 decompressor — fast block scanning, multi-core Burrows-Wheeler decode
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
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
//! bzip2 block decoder.
//!
//! Decodes a single bzip2 compressed block given a `BitReader` positioned
//! right after the 48-bit block magic (0x314159265359).
//!
//! Designed for parallel use: each worker gets a byte slice + bit offset,
//! decodes independently, returns decompressed `Vec<u8>`.

use crate::bitreader::BitReader;
use crate::bwt;
use crate::huffman::HuffmanTree;
use crate::mtf::MtfDecoder;

use std::cell::RefCell;

/// Maximum block size: 9 × 100,000 bytes.
const MAX_BLOCKSIZE: usize = 900_000;
/// Maximum number of selectors (15-bit field, practical limit ~18001).
const MAX_SELECTORS: usize = 18002;

thread_local! {
    // A STACK of reusable `tt` buffers, not a single slot. The single-thread MLP
    // decode ([`decode_blocks_interleaved`]) keeps several blocks' `tt` arrays alive
    // at once (N chains interleaved), so the pool must serve that many buffers
    // concurrently. Balanced take/return keeps the stack depth at ~N; each buffer
    // retains its capacity across reuse. The multi-worker (serial) path pushes/pops
    // a single buffer, byte-identical to the old single-slot behaviour.
    static TT_POOL: RefCell<Vec<Vec<u32>>> = const { RefCell::new(Vec::new()) };
    // Reusable per-block RLE1 output scratch buffers, used ONLY by the interleaved
    // single-thread path (each chain writes into its own buffer, then the buffers are
    // copied into the segment output in stream order).
    static OUT_POOL: RefCell<Vec<Vec<u8>>> = const { RefCell::new(Vec::new()) };
}

/// Take a `tt` buffer from the thread-local pool, avoiding repeated heap allocation.
fn take_tt_buffer(capacity: usize) -> Vec<u32> {
    let mut buf = TT_POOL.with(|cell| cell.borrow_mut().pop()).unwrap_or_default();
    buf.clear();
    if buf.capacity() < capacity {
        buf.reserve(capacity - buf.len());
    }
    buf
}

/// Return a `tt` buffer to the thread-local pool for reuse.
fn return_tt_buffer(buf: Vec<u32>) {
    TT_POOL.with(|cell| cell.borrow_mut().push(buf));
}

/// Take a per-block RLE1 output scratch buffer from the thread-local pool
/// (interleaved single-thread path only).
fn take_out_buffer() -> Vec<u8> {
    OUT_POOL.with(|cell| cell.borrow_mut().pop()).unwrap_or_default()
}

/// Return a per-block RLE1 output scratch buffer to the thread-local pool.
fn return_out_buffer(buf: Vec<u8>) {
    OUT_POOL.with(|cell| cell.borrow_mut().push(buf));
}

/// Error type for block decoding.
#[derive(Debug)]
pub struct BlockError(pub &'static str);

impl std::fmt::Display for BlockError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "bzip2 block error: {}", self.0)
    }
}

impl std::error::Error for BlockError {}

/// bzip2 CRC-32 table (polynomial 0x04C11DB7, MSB-first — *not* the reflected
/// gzip/zlib variant). Built once at compile time.
const fn make_crc_table() -> [u32; 256] {
    let mut table = [0u32; 256];
    let mut i = 0usize;
    while i < 256 {
        let mut c = (i as u32) << 24;
        let mut k = 0;
        while k < 8 {
            c = if c & 0x8000_0000 != 0 { (c << 1) ^ 0x04C1_1DB7 } else { c << 1 };
            k += 1;
        }
        table[i] = c;
        i += 1;
    }
    table
}

static CRC_TABLE: [u32; 256] = make_crc_table();

/// Compute the bzip2 block CRC over decompressed bytes (MSB-first CRC-32,
/// init/final XOR 0xFFFFFFFF).
///
/// Exposed to the crate so the stream decoder can fold each block's CRC into the
/// combined stream CRC and verify it against the stored value after FINAL_MAGIC.
pub(crate) fn block_crc(data: &[u8]) -> u32 {
    let mut crc: u32 = 0xFFFF_FFFF;
    for &b in data {
        crc = crc_update(crc, b);
    }
    !crc
}

/// Single-byte bzip2 CRC-32 step (MSB-first). Folded directly into the RLE1 emit
/// loops (`rle2_decode_append`, `chain_process`) so each output byte is CRC'd while
/// still hot in a register — replacing a second full byte-at-a-time pass over the
/// (up to ~1 GB) decompressed output that re-read long-evicted data from L3/DRAM.
/// The running CRC is seeded with `0xFFFF_FFFF` and finalized with a bitwise NOT,
/// exactly matching [`block_crc`].
#[inline(always)]
fn crc_update(crc: u32, b: u8) -> u32 {
    (crc << 8) ^ CRC_TABLE[(((crc >> 24) ^ (b as u32)) & 0xFF) as usize]
}

/// Decode header + Huffman + MTF + RLE1 → inverse BWT, returning (tt, t_pos, crc).
/// `crc` is the block's stored CRC-32, to be checked against the decompressed bytes.
/// Shared logic for both `decode_block` and `decode_block_into`.
fn decode_block_common(
    reader: &mut BitReader<'_>,
    max_blocksize: u32,
) -> Result<(Vec<u32>, u32, u32), BlockError> {
    // ── Block header ──────────────────────────────────────────────────────
    let expected_crc = reader.read_u32(32)
        .ok_or(BlockError("block CRC truncated"))?;

    let randomised = reader.read_bit()
        .ok_or(BlockError("randomised flag truncated"))?;
    if randomised {
        return Err(BlockError("randomised blocks not supported"));
    }

    let orig_ptr = reader.read_u32(24)
        .ok_or(BlockError("orig_ptr truncated"))? as usize;

    // ── Symbol bitmap (which bytes appear in this block) ──────────────────
    let mut used_bytes = [0u8; 256];
    let mut n_used: usize = 0;

    // 16 range flags: each covers 16 byte values.
    let mut ranges_present = [false; 16];
    for range in &mut ranges_present {
        *range = reader.read_bit()
            .ok_or(BlockError("symbol range truncated"))?;
    }

    for (range_idx, &present) in ranges_present.iter().enumerate() {
        if !present { continue; }
        for sub in 0..16u8 {
            if reader.read_bit().ok_or(BlockError("symbol bitmap truncated"))? {
                used_bytes[n_used] = range_idx as u8 * 16 + sub;
                n_used += 1;
            }
        }
    }

    if n_used == 0 {
        return Err(BlockError("no symbols in block"));
    }

    let n_symbols = n_used + 2; // +2 for RUNA, RUNB; EOB = n_symbols - 1

    // ── Huffman table selectors ───────────────────────────────────────────
    let n_groups = reader.read_u8(3)
        .ok_or(BlockError("huffman groups truncated"))?;
    if n_groups < 2 || n_groups > 6 {
        return Err(BlockError("invalid number of huffman groups"));
    }

    let n_selectors = reader.read_u16(15)
        .ok_or(BlockError("selectors_used truncated"))? as usize;
    if n_selectors > MAX_SELECTORS {
        return Err(BlockError("too many selectors"));
    }

    let mut selectors = [0u8; MAX_SELECTORS];
    let mut sel_mtf = MtfDecoder::new();
    for i in 0..n_selectors {
        let mut trees = 0u8;
        while reader.read_bit().ok_or(BlockError("selector bit truncated"))? {
            trees += 1;
            if trees >= n_groups {
                return Err(BlockError("selector tree index too large"));
            }
        }
        selectors[i] = sel_mtf.decode(trees);
    }

    // ── Huffman code lengths → trees ──────────────────────────────────────
    let mut trees = [const { HuffmanTree::empty() }; 6];
    let mut n_trees: usize = 0;
    for _ in 0..n_groups {
        let mut length = reader.read_u8(5)
            .ok_or(BlockError("huffman start length truncated"))? as i32;
        let mut lengths = [0u8; 258];

        for j in 0..n_symbols {
            loop {
                if length < 1 || length > 20 {
                    return Err(BlockError("huffman code length out of range"));
                }
                if !reader.read_bit().ok_or(BlockError("length adjust bit1 truncated"))? {
                    break;
                }
                if reader.read_bit().ok_or(BlockError("length adjust bit2 truncated"))? {
                    length -= 1;
                } else {
                    length += 1;
                }
            }
            lengths[j] = length as u8;
        }

        trees[n_trees] = HuffmanTree::from_lengths(&lengths[..n_symbols])
            .map_err(|_| BlockError("invalid huffman tree"))?;
        n_trees += 1;
    }

    // ── Huffman decode → MTF + RLE1 decode → tt array ─────────────────────
    // Perf: `take_tt_buffer(max_blocksize)` guarantees capacity >= max_blocksize
    // (it reserves up front and never shrinks below it). We fill the buffer via
    // raw pointer writes into that reserved spare capacity and track the length
    // in `tt_len`, calling `set_len` once at the very end. This removes the
    // per-symbol `Vec::push`/`resize` capacity/realloc branch from the hot loop
    // (mirroring the RLE2 stage in `rle2_decode_*`). The existing per-symbol /
    // per-run `<= max_blocksize` guards keep every write in-bounds.
    let mut tt: Vec<u32> = take_tt_buffer(max_blocksize as usize);
    debug_assert!(tt.capacity() >= max_blocksize as usize);
    let tt_ptr = tt.as_mut_ptr();
    let mut tt_len: usize = 0; // logical length; tt.set_len is called once below
    let mut c = [0u32; 256]; // byte frequency counts for BWT

    // Build MTF decoder with the actual used-byte alphabet.
    let mut byte_symbols = [0u8; 256];
    byte_symbols[..n_used].copy_from_slice(&used_bytes[..n_used]);
    let mut mtf = MtfDecoder::with_symbols(byte_symbols);

    let mut sel_idx: usize = 0;
    let mut current_tree = &trees[
        selectors[0] as usize
    ];

    let mut repeat: u32 = 0;
    let mut repeat_power: u32 = 0;

    let eob_symbol = (n_symbols - 1) as u16;

    'outer: loop {
        for _ in 0..50 {
            let sym = current_tree.decode(reader)
                .ok_or(BlockError("huffman bitstream truncated"))?;

            // RUNA (0) or RUNB (1): run-length encoding.
            if sym < 2 {
                if repeat == 0 {
                    repeat_power = 1;
                }
                repeat += repeat_power << sym;
                repeat_power <<= 1;

                if repeat as usize > MAX_BLOCKSIZE {
                    return Err(BlockError("repeat count too large"));
                }
                continue;
            }

            // Flush pending run.
            if repeat > 0 {
                let b = mtf.first();
                if tt_len + repeat as usize > max_blocksize as usize {
                    return Err(BlockError("data exceeds block size"));
                }
                // Raw fill: write `repeat` copies of b into the reserved buffer.
                // In-bounds by the guard above (tt_len + repeat <= max_blocksize
                // <= capacity).
                let val = u32::from(b);
                unsafe {
                    let mut p = tt_ptr.add(tt_len);
                    for _ in 0..repeat {
                        p.write(val);
                        p = p.add(1);
                    }
                }
                tt_len += repeat as usize;
                c[b as usize] += repeat;
                repeat = 0;
            }

            // EOB: end of block.
            if sym == eob_symbol {
                break 'outer;
            }

            // Regular symbol: MTF decode.
            let b = mtf.decode((sym - 1) as u8);
            if tt_len >= max_blocksize as usize {
                return Err(BlockError("data exceeds block size"));
            }
            // Raw single write into reserved capacity (in-bounds by the guard).
            unsafe { tt_ptr.add(tt_len).write(u32::from(b)); }
            tt_len += 1;
            c[b as usize] += 1;
        }

        // Switch Huffman table for next group of 50 symbols.
        sel_idx += 1;
        if sel_idx >= n_selectors {
            return Err(BlockError("ran out of selectors"));
        }
        let sel = selectors[sel_idx] as usize;
        if sel >= n_trees {
            return Err(BlockError("selector out of range"));
        }
        current_tree = &trees[sel];
    }

    // Commit the raw-written entries as the Vec's length. Safe: we wrote
    // tt_len contiguous u32 values starting at offset 0, all within the
    // reserved capacity, and never reallocated (no push/reserve in the loop).
    unsafe { tt.set_len(tt_len); }

    if orig_ptr >= tt_len {
        return Err(BlockError("orig_ptr out of bounds"));
    }

    // ── Inverse BWT ───────────────────────────────────────────────────────
    // A consistency-checked chase: a mis-decoded block whose byte histogram
    // does not match `tt` is rejected here rather than scattering/reading out
    // of bounds (which would SIGSEGV the process).
    let t_pos = bwt::inverse_bwt(&mut tt, orig_ptr, c)
        .ok_or(BlockError("inverse-BWT: inconsistent byte counts (corrupt or mis-decoded block)"))?;

    Ok((tt, t_pos, expected_crc))
}

/// RLE1-reverse (the final bzip2 stage) from tt[], **appending** onto `out` and
/// growing it as needed, and returning the block's finalized CRC-32.
///
/// Uses raw pointer writes; the inner dependent-load pointer-chase
/// `entry = tt[t_pos]; t_pos = entry >> 8` is irreducible — DO NOT CHANGE the reads.
///
/// CRC fold: every emitted byte is CRC'd inline via [`crc_update`] while still hot in
/// a register (like libbzip2's `BZ_UPDATE_CRC` inside `unRLE_obuf_to_output_FAST`), so
/// there is NO separate byte-at-a-time pass over the (up to ~1 GB) decompressed output
/// afterward — that pass re-read long-evicted data from L3/DRAM and was the code
/// review's #1 target. The returned value is the finalized block CRC (init
/// `0xFFFF_FFFF`, final NOT), identical to `block_crc(emitted bytes)`.
///
/// Growth is mandatory, not a micro-opt: bzip2's block-size limit bounds the
/// RLE1-*encoded* stream (the ≤ `max_blocksize` bytes fed into the BWT), but this
/// stage *reverses* RLE1, and a 5-byte run marker `b b b b <count>` expands to up
/// to 259 output bytes (~52×). A highly-repetitive-but-perfectly-valid block —
/// e.g. the long whitespace / tag runs in planet OSM-XML — therefore decodes to
/// many times `max_blocksize`. A fixed `max_blocksize + 25%` buffer is NOT a valid
/// bound; assuming it overflowed the segment buffer on real planet input (an
/// assert-panic on the checked path, a raw heap overrun / SIGSEGV on the lenient
/// convert path). Appending into a growable `Vec` is the only correct sink.
fn rle2_decode_append(tt: &[u32], mut t_pos: u32, out: &mut Vec<u8>) -> u32 {
    let n = tt.len();
    let start = out.len();
    // First-guess capacity: the overwhelmingly common block is ≤ 1.25×. The
    // count-run branch below grows on demand for the pathological case.
    out.reserve(n + n / 4 + 1);
    let mut out_len: usize = start;
    let tt_ptr = tt.as_ptr();
    // Hoist the write base and capacity out of the per-byte path — `out.as_mut_ptr()`
    // and `out.capacity()` were reloaded from the Vec header every byte. Refresh both
    // ONLY on a reserve (the sole realloc site), which is rare.
    let mut base = out.as_mut_ptr();
    let mut cap = out.capacity();
    let mut crc: u32 = 0xFFFF_FFFF;
    // `last_byte` as a u16 with a 0x100 sentinel = "no previous byte" (initially, and
    // reset after every run). Folds the old `has_last` bool + `u8` into one compare, so
    // the per-byte run-detect is a single `last_byte == b` with no extra `has_last &&`.
    let mut last_byte: u16 = 0x100;
    let mut byte_repeats: u8 = 0;

    for _ in 0..n {
        let entry = unsafe { *tt_ptr.add(t_pos as usize) };
        let b = entry as u8;
        t_pos = entry >> 8;

        if byte_repeats == 3 {
            let count = b as usize;
            // Ensure `count` for this run PLUS `n` trailing bytes of slack — the max
            // number of single-byte literals that can follow before the next run (there
            // are only `n` iterations total). That invariant lets the literal path below
            // write UNCHECKED: no per-byte capacity branch.
            if out_len + count + n > cap {
                unsafe { out.set_len(out_len); }
                out.reserve(count + n + 1);
                base = out.as_mut_ptr();
                cap = out.capacity();
            }
            let ch = last_byte as u8;
            unsafe {
                std::ptr::write_bytes(base.add(out_len), ch, count);
            }
            // CRC the run's `count` copies (still cheaper than re-reading the whole
            // decompressed output in a separate pass).
            let mut i = 0;
            while i < count {
                crc = crc_update(crc, ch);
                i += 1;
            }
            out_len += count;
            byte_repeats = 0;
            last_byte = 0x100; // run consumed → no previous byte
            continue;
        }

        if last_byte == b as u16 {
            byte_repeats += 1;
        } else {
            byte_repeats = 0;
        }
        last_byte = b as u16;
        crc = crc_update(crc, b);
        // Unchecked: the initial `reserve(n + n/4)` and the run path both guarantee
        // ≥ `n` bytes of trailing slack, and ≤ `n` literals ever follow, so this write
        // is always in bounds.
        unsafe { *base.add(out_len) = b; }
        out_len += 1;
    }

    unsafe { out.set_len(out_len); }
    !crc
}

/// RLE1-reverse into a freshly allocated `Vec<u8>` (sequential/whole-block path).
/// Returns the decoded bytes and the block's finalized CRC-32.
fn rle2_decode_alloc(tt: &[u32], t_pos: u32) -> (Vec<u8>, u32) {
    let mut output = Vec::new();
    let crc = rle2_decode_append(tt, t_pos, &mut output);
    (output, crc)
}

// ─────────────────────────────────────────────────────────────────────────────
// Memory-level parallelism (MLP) — interleaved multi-block inverse-BWT chase.
// SINGLE-THREAD ONLY (gated by the caller): at 12 cores the decode is memory-
// bandwidth-bound so this is neutral-to-harmful, but on ONE core it hides the
// dependent-load latency and nearly doubles throughput.
//
// The bottleneck of `rle2_decode_append` is the serial dependent-load chain
// `entry = tt[t_pos]; t_pos = entry >> 8`: each load's ADDRESS depends on the
// previous load's RESULT, so they cannot overlap and the loop is pinned at DRAM/
// L3 latency per step. Each bzip2 BLOCK, however, has its OWN independent chain.
// By stepping several blocks' chains in lockstep within one worker — issue all N
// loads, then process all N results — the N loads are mutually independent and
// their memory latencies OVERLAP in the out-of-order window (memory-level
// parallelism), hiding the per-step stall behind the others.
//
// Output bytes of each block must still land in stream order, so each chain writes
// into its OWN scratch buffer and the buffers are copied into the segment output in
// stream order afterward (a cheap sequential streaming copy, not latency-bound).
// ─────────────────────────────────────────────────────────────────────────────

/// The parsed core of one bzip2 block: its inverse-BWT `tt` array, the initial
/// traversal position, and the stored CRC-32. Produced by [`decode_block_core`]
/// (header + Huffman + MTF + RLE2 + inverse-BWT), consumed by
/// [`decode_blocks_interleaved`] (the RLE1 pointer-chase, done N-at-a-time).
pub(crate) struct BlockCore {
    tt: Vec<u32>,
    t_pos: u32,
    crc: u32,
}

/// Decode a block up to (and including) the inverse BWT, returning its [`BlockCore`]
/// WITHOUT running the final RLE1 pointer-chase. This is the "gather" half of the
/// single-thread MLP decode: a segment gathers up to `N` cores (advancing the
/// bitstream sequentially), then chases them interleaved via
/// [`decode_blocks_interleaved`].
pub(crate) fn decode_block_core(
    reader: &mut BitReader<'_>,
    max_blocksize: u32,
) -> Result<BlockCore, BlockError> {
    let (tt, t_pos, crc) = decode_block_common(reader, max_blocksize)?;
    Ok(BlockCore { tt, t_pos, crc })
}

/// Live state of one block's RLE1 pointer-chase. Mirrors the locals of
/// [`rle2_decode_append`] exactly (same capacity/slack invariants); the only
/// difference is that several `Chain`s are advanced in lockstep to overlap their
/// dependent loads. `tt` is retained so its buffer can be returned to the pool.
struct Chain {
    tt_ptr: *const u32,
    t_pos: u32,
    /// tt entries still to consume (starts at tt.len()).
    remaining: u32,
    /// The entry loaded this lockstep round, consumed by `chain_process`.
    pending: u32,
    out: Vec<u8>,
    out_len: usize,
    base: *mut u8,
    cap: usize,
    last_byte: u16,
    byte_repeats: u8,
    n: usize,
    /// Original stream order, to restore after the chase (which reorders by length).
    order: u32,
    /// The block's stored (expected) CRC-32.
    crc: u32,
    /// Running CRC-32 folded over emitted bytes (Phase 1); finalized with NOT and
    /// compared against `crc` after the chase — no separate output re-read pass.
    crc_acc: u32,
    tt: Vec<u32>,
}

/// Process ONE already-loaded tt `entry` for a chain: the RLE1-reverse state
/// machine + output write. Byte-for-byte identical to the body of
/// [`rle2_decode_append`] (minus the load, which the caller issued). Kept separate
/// from the load so the caller can issue all N chains' loads back-to-back first.
#[inline(always)]
fn chain_process(c: &mut Chain, entry: u32) {
    let b = entry as u8;

    if c.byte_repeats == 3 {
        let count = b as usize;
        if c.out_len + count + c.n > c.cap {
            unsafe { c.out.set_len(c.out_len); }
            c.out.reserve(count + c.n + 1);
            c.base = c.out.as_mut_ptr();
            c.cap = c.out.capacity();
        }
        let ch = c.last_byte as u8;
        unsafe {
            std::ptr::write_bytes(c.base.add(c.out_len), ch, count);
        }
        let mut crc = c.crc_acc;
        let mut i = 0;
        while i < count {
            crc = crc_update(crc, ch);
            i += 1;
        }
        c.crc_acc = crc;
        c.out_len += count;
        c.byte_repeats = 0;
        c.last_byte = 0x100;
        return;
    }

    if c.last_byte == b as u16 {
        c.byte_repeats += 1;
    } else {
        c.byte_repeats = 0;
    }
    c.last_byte = b as u16;
    c.crc_acc = crc_update(c.crc_acc, b);
    unsafe { *c.base.add(c.out_len) = b; }
    c.out_len += 1;
}

/// Drive a slice of ACTIVE chains (each with `remaining >= steps`) for `steps`
/// lockstep iterations. Each iteration issues every chain's dependent load FIRST
/// (all mutually independent → their latencies overlap), then processes the loaded
/// entries. This is the memory-level-parallelism hot loop.
#[inline]
fn lockstep(active: &mut [Chain], steps: u32) {
    for _ in 0..steps {
        for c in active.iter_mut() {
            let e = unsafe { *c.tt_ptr.add(c.t_pos as usize) };
            c.t_pos = e >> 8;
            c.pending = e;
        }
        for c in active.iter_mut() {
            let e = c.pending;
            chain_process(c, e);
        }
    }
    for c in active.iter_mut() {
        c.remaining -= steps;
    }
}

/// Run the interleaved chase over all chains to completion. Chains are sorted by
/// remaining length so the active set is a contiguous suffix; each lockstep round
/// runs `min(remaining)` iterations (retiring at least the shortest chain) over the
/// whole active set, preserving overlap for as long as ≥2 chains remain.
fn chase_all(chains: &mut [Chain]) {
    chains.sort_by_key(|c| c.remaining);
    let k = chains.len();
    let mut done = 0usize;
    while done < k {
        if chains[done].remaining == 0 {
            done += 1;
            continue;
        }
        let step = chains[done].remaining;
        lockstep(&mut chains[done..k], step);
        while done < k && chains[done].remaining == 0 {
            done += 1;
        }
    }
}

/// MLP decode of a batch of gathered block cores (single-thread path): chase all of
/// them interleaved, then CRC-verify and append their decompressed bytes into
/// `output` in STREAM ORDER (the order of `cores`), pushing each verified CRC into
/// `block_crcs`.
///
/// On a CRC mismatch the batch's earlier (in-order) blocks are already appended;
/// the failing block's bytes are NOT appended and the error is returned — matching
/// the per-block truncate-and-error behaviour of [`decode_block_into`]. Every `tt`
/// and output scratch buffer is returned to the thread-local pools regardless.
pub(crate) fn decode_blocks_interleaved(
    cores: &mut Vec<BlockCore>,
    output: &mut Vec<u8>,
    block_crcs: &mut Vec<u32>,
) -> Result<(), BlockError> {
    let m = cores.len();
    let mut chains: Vec<Chain> = Vec::with_capacity(m);
    for (i, core) in cores.drain(..).enumerate() {
        let tt = core.tt;
        let n = tt.len();
        let tt_ptr = tt.as_ptr();
        let mut out = take_out_buffer();
        out.clear();
        // Same first-guess capacity + trailing-slack invariant as rle2_decode_append.
        out.reserve(n + n / 4 + 1);
        let base = out.as_mut_ptr();
        let cap = out.capacity();
        chains.push(Chain {
            tt_ptr,
            t_pos: core.t_pos,
            remaining: n as u32,
            pending: 0,
            out,
            out_len: 0,
            base,
            cap,
            last_byte: 0x100,
            byte_repeats: 0,
            n,
            order: i as u32,
            crc: core.crc,
            crc_acc: 0xFFFF_FFFF,
            tt,
        });
    }

    // The interleaved memory-level-parallel chase.
    chase_all(&mut chains);

    // Restore stream order for in-order CRC-check + append.
    chains.sort_by_key(|c| c.order);
    for c in chains.iter_mut() {
        // Commit the raw-written bytes as the scratch Vec's length.
        unsafe { c.out.set_len(c.out_len); }
    }

    let mut err: Option<BlockError> = None;
    for c in chains.into_iter() {
        if err.is_none() {
            if !c.crc_acc != c.crc {
                err = Some(BlockError("block CRC mismatch"));
            } else {
                output.extend_from_slice(&c.out);
                block_crcs.push(c.crc);
            }
        }
        return_tt_buffer(c.tt);
        return_out_buffer(c.out);
    }

    match err {
        Some(e) => Err(e),
        None => Ok(()),
    }
}

/// Decode one bzip2 block.
///
/// `reader` must be positioned right after the 48-bit block magic.
/// `max_blocksize` comes from the stream header (100_000 × blocksize_level).
///
/// Returns the fully decompressed block data.
pub fn decode_block(reader: &mut BitReader<'_>, max_blocksize: u32) -> Result<Vec<u8>, BlockError> {
    let (tt, t_pos, expected_crc) = decode_block_common(reader, max_blocksize)?;
    let (output, crc) = rle2_decode_alloc(&tt, t_pos);
    return_tt_buffer(tt);
    if crc != expected_crc {
        return Err(BlockError("block CRC mismatch"));
    }
    Ok(output)
}

/// Decode one bzip2 block, **appending** its bytes to `out` (which grows as
/// needed). Zero per-block temporary `Vec`: the segment buffer is extended in
/// place. The decoded size is data-dependent and can exceed `max_blocksize` by a
/// large factor (see [`rle2_decode_append`]), so `out` MUST be a growable `Vec`,
/// never a fixed slice.
///
/// Returns `(bytes_written, block_crc)` where `block_crc` is the block's verified
/// CRC-32 (equal to the stored value — a mismatch is returned as an error, never
/// silently accepted, and `out` is rolled back to its pre-block length). Callers
/// fold the returned CRC into the running combined stream CRC so the whole-stream
/// checksum can be verified on the parallel path.
pub fn decode_block_into(reader: &mut BitReader<'_>, max_blocksize: u32, out: &mut Vec<u8>) -> Result<(usize, u32), BlockError> {
    let (tt, t_pos, expected_crc) = decode_block_common(reader, max_blocksize)?;
    let start = out.len();
    let crc = rle2_decode_append(&tt, t_pos, out);
    return_tt_buffer(tt);
    let written = out.len() - start;
    if crc != expected_crc {
        out.truncate(start);
        return Err(BlockError("block CRC mismatch"));
    }
    Ok((written, expected_crc))
}

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

    #[test]
    fn decode_known_block() {
        // Use a real bzip2 file: compress "Hello, World!\n" and decode.
        // This is a minimal integration test.
        let compressed = include_bytes!("../test_data/hello.bz2");
        let expected = b"Hello, World!\n";

        // Parse header (4 bytes: "BZh9").
        assert_eq!(&compressed[..3], b"BZh");
        let level = compressed[3] - b'0';
        let max_blocksize = 100_000 * level as u32;

        // Skip header, read block magic.
        let mut reader = BitReader::from_bit_offset(compressed, 4 * 8);
        let magic = reader.read_u64(48).unwrap();
        assert_eq!(magic, crate::BLOCK_MAGIC, "expected block magic");

        let output = decode_block(&mut reader, max_blocksize).unwrap();
        assert_eq!(&output, expected);
    }
}