Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy-zoomies

lbzip2/src/block.rs

//! 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);
    }
}