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
//! Unit coverage for the LDM helper cluster that batch 14 lifted
//! onto `impl BtMatcher`. The helpers walk the raw-LDM seq store
//! and translate its (literals, match) sequences into the optimal
//! parser's `MatchCandidate` shape. They are pure logic on
//! [`HcRawSeqStore`] / [`HcOptLdmState`] — no SIMD, no `MatchTable`
//! contact — so they exercise on every CI target.
use alloc::vec;
use alloc::vec::Vec;

use super::*;
use crate::encoding::opt::ldm::{HcOptLdmState, HcRawSeq, HcRawSeqStore};

fn raw(lit: usize, mat: usize, off: usize) -> HcRawSeq {
    HcRawSeq {
        lit_length: lit,
        offset: off,
        match_length: mat,
    }
}

fn bt_with_seqs(seqs: Vec<HcRawSeq>) -> BtMatcher {
    let mut bt = BtMatcher::new();
    bt.ldm_sequences = seqs;
    bt
}

fn store(size: usize) -> HcRawSeqStore {
    HcRawSeqStore {
        pos: 0,
        pos_in_sequence: 0,
        size,
    }
}

#[test]
fn ldm_skip_raw_seq_store_bytes_walks_whole_sequences() {
    let bt = bt_with_seqs(vec![raw(2, 3, 100), raw(4, 1, 200), raw(0, 5, 50)]);
    let mut s = store(3);
    // 5 = first seq (lit+match=5) → advance to seq 1 with pos_in_sequence=0
    bt.ldm_skip_raw_seq_store_bytes(&mut s, 5);
    assert_eq!(s.pos, 1);
    assert_eq!(s.pos_in_sequence, 0);
}

#[test]
fn ldm_skip_raw_seq_store_bytes_leaves_partial_offset() {
    let bt = bt_with_seqs(vec![raw(2, 3, 100), raw(4, 1, 200)]);
    let mut s = store(2);
    // 4 < first seq's 5 bytes → stay on seq 0, pos_in_sequence=4
    bt.ldm_skip_raw_seq_store_bytes(&mut s, 4);
    assert_eq!(s.pos, 0);
    assert_eq!(s.pos_in_sequence, 4);
}

#[test]
fn ldm_skip_raw_seq_store_bytes_exhausts_store_when_distance_too_large() {
    let bt = bt_with_seqs(vec![raw(1, 1, 1), raw(1, 1, 1)]);
    let mut s = store(2);
    // 10 bytes total but store only holds 4 (2 seqs × 2 bytes each)
    bt.ldm_skip_raw_seq_store_bytes(&mut s, 10);
    assert_eq!(s.pos, 2);
    assert_eq!(s.pos_in_sequence, 0);
}

#[test]
fn ldm_get_next_match_empty_store_clears_window() {
    let bt = bt_with_seqs(vec![]);
    let mut opt_ldm = HcOptLdmState::default();
    opt_ldm.seq_store.size = 0;
    bt.ldm_get_next_match_and_update_seq_store(&mut opt_ldm, 10, 100);
    assert_eq!(opt_ldm.start_pos_in_block, usize::MAX);
    assert_eq!(opt_ldm.end_pos_in_block, usize::MAX);
}

#[test]
fn ldm_get_next_match_marks_window_inside_block() {
    let bt = bt_with_seqs(vec![raw(2, 5, 17)]);
    let mut opt_ldm = HcOptLdmState::default();
    opt_ldm.seq_store.size = 1;
    // 100 bytes remain in block, current pos 10 → window starts at
    // 10 + lit_length (2) = 12 and ends 12 + match_length (5) = 17.
    bt.ldm_get_next_match_and_update_seq_store(&mut opt_ldm, 10, 100);
    assert_eq!(opt_ldm.start_pos_in_block, 12);
    assert_eq!(opt_ldm.end_pos_in_block, 17);
    assert_eq!(opt_ldm.offset, 17);
}

#[test]
fn ldm_get_next_match_skips_when_literals_exceed_remaining_block() {
    let bt = bt_with_seqs(vec![raw(50, 5, 17)]);
    let mut opt_ldm = HcOptLdmState::default();
    opt_ldm.seq_store.size = 1;
    // Only 30 bytes left in block but seq starts with 50 literals →
    // window stays "no LDM in flight" and cursor advances past the
    // remaining bytes.
    bt.ldm_get_next_match_and_update_seq_store(&mut opt_ldm, 0, 30);
    assert_eq!(opt_ldm.start_pos_in_block, usize::MAX);
    assert_eq!(opt_ldm.end_pos_in_block, usize::MAX);
    assert_eq!(opt_ldm.seq_store.pos_in_sequence, 30);
}

#[test]
fn ldm_maybe_add_match_returns_none_before_window() {
    let bt = bt_with_seqs(vec![]);
    let opt_ldm = HcOptLdmState {
        seq_store: store(0),
        start_pos_in_block: 20,
        end_pos_in_block: 30,
        offset: 7,
    };
    assert!(bt.ldm_maybe_add_match(&opt_ldm, 10, 4).is_none());
}

#[test]
fn ldm_maybe_add_match_returns_none_past_window() {
    let bt = bt_with_seqs(vec![]);
    let opt_ldm = HcOptLdmState {
        seq_store: store(0),
        start_pos_in_block: 20,
        end_pos_in_block: 30,
        offset: 7,
    };
    assert!(bt.ldm_maybe_add_match(&opt_ldm, 30, 4).is_none());
    assert!(bt.ldm_maybe_add_match(&opt_ldm, 35, 4).is_none());
}

#[test]
fn ldm_maybe_add_match_returns_none_below_min_match() {
    let bt = bt_with_seqs(vec![]);
    let opt_ldm = HcOptLdmState {
        seq_store: store(0),
        start_pos_in_block: 20,
        end_pos_in_block: 23,
        offset: 7,
    };
    // window length is 3 < min_match (4) → reject
    assert!(bt.ldm_maybe_add_match(&opt_ldm, 20, 4).is_none());
}

#[test]
fn ldm_maybe_add_match_emits_candidate_inside_window() {
    let bt = bt_with_seqs(vec![]);
    let opt_ldm = HcOptLdmState {
        seq_store: store(0),
        start_pos_in_block: 20,
        end_pos_in_block: 30,
        offset: 7,
    };
    let cand = bt
        .ldm_maybe_add_match(&opt_ldm, 22, 4)
        .expect("should emit");
    assert_eq!(cand.start, 22);
    assert_eq!(cand.offset, 7);
    // candidate length = window - (pos - start) = 10 - 2 = 8
    assert_eq!(cand.match_len, 8);
}

#[test]
fn ldm_process_match_candidate_returns_none_when_store_exhausted() {
    let mut bt = bt_with_seqs(vec![raw(2, 5, 17)]);
    bt.ldm_sequences.clear();
    let mut opt_ldm = HcOptLdmState::default();
    opt_ldm.seq_store.size = 0;
    assert!(
        bt.ldm_process_match_candidate(&mut opt_ldm, 0, 100, 4)
            .is_none()
    );
}

#[test]
fn push_candidate_ladder_rejects_below_min_match() {
    let mut out: Vec<MatchCandidate> = vec![];
    let mut best = 0usize;
    let accepted = BtMatcher::push_candidate_ladder(
        &mut out,
        &mut best,
        MatchCandidate {
            start: 10,
            offset: 5,
            match_len: 2,
        },
        4,
    );
    assert!(!accepted);
    assert!(out.is_empty());
    assert_eq!(best, 0);
}

#[test]
fn push_candidate_ladder_rejects_when_not_strictly_better() {
    let mut out: Vec<MatchCandidate> = vec![];
    let mut best = 8usize;
    let accepted = BtMatcher::push_candidate_ladder(
        &mut out,
        &mut best,
        MatchCandidate {
            start: 10,
            offset: 5,
            match_len: 8,
        },
        4,
    );
    // match_len == best_len_for_skip → not strictly greater → rejected
    assert!(!accepted);
    assert!(out.is_empty());
    assert_eq!(best, 8);
}

#[test]
fn ldm_get_next_match_clamps_window_to_block_end() {
    // Seq match window extends past the block boundary — the
    // function clamps `end_pos_in_block` to `curr_block_end_pos`
    // and skips through the seq store accordingly.
    let bt = bt_with_seqs(vec![raw(0, 100, 99)]);
    let mut opt_ldm = HcOptLdmState::default();
    opt_ldm.seq_store.size = 1;
    bt.ldm_get_next_match_and_update_seq_store(&mut opt_ldm, 10, 30);
    // Window would naturally be [10, 110); clamped to [10, 40)
    // because remaining block bytes only allow that span.
    assert_eq!(opt_ldm.start_pos_in_block, 10);
    assert_eq!(opt_ldm.end_pos_in_block, 40);
    assert_eq!(opt_ldm.offset, 99);
}

#[test]
fn ldm_process_match_candidate_handles_overshoot_into_next_seq() {
    // Parser jumped past the active LDM window — `pos_overshoot`
    // forces the seq-store cursor forward before re-seeding.
    let bt = bt_with_seqs(vec![raw(0, 5, 17), raw(0, 5, 33)]);
    let mut opt_ldm = HcOptLdmState {
        seq_store: HcRawSeqStore {
            pos: 1,
            pos_in_sequence: 0,
            size: 2,
        },
        start_pos_in_block: 0,
        end_pos_in_block: 5,
        offset: 17,
    };
    // curr_pos = 12 (overshot the previous window's end by 7) → must
    // consume the overshoot from the next seq, then re-seed.
    let _ = bt.ldm_process_match_candidate(&mut opt_ldm, 12, 100, 4);
    // After overshoot consumes 7 bytes from seq 1 (which only had
    // 5), cursor advances to pos=2 (store exhausted).
    assert_eq!(opt_ldm.seq_store.pos, 2);
}

#[test]
fn ldm_process_match_candidate_reseeds_after_overshoot() {
    // Two sequences. Seq 0 was just emitted (cursor advanced past it),
    // so the active window points at seq 1's offset (33). The parser
    // arrives at position 5 — past the seq 0 window — and must
    // re-seed onto seq 1's window without overshoot logic re-reading
    // seq 0.
    let bt = bt_with_seqs(vec![raw(0, 5, 17), raw(0, 5, 33)]);
    let mut opt_ldm = HcOptLdmState {
        seq_store: HcRawSeqStore {
            pos: 1,
            pos_in_sequence: 0,
            size: 2,
        },
        start_pos_in_block: 0,
        end_pos_in_block: 5,
        offset: 17,
    };
    let cand = bt
        .ldm_process_match_candidate(&mut opt_ldm, 5, 100, 4)
        .expect("should emit reseeded candidate");
    assert_eq!(cand.start, 5);
    assert_eq!(cand.offset, 33);
    assert_eq!(cand.match_len, 5);
}