Skip to main content

fsqlite_core/
lib.rs

1//! Core bounded-parallelism primitives (§1.5, bd-22n.4).
2//!
3//! This module provides a small bulkhead framework for internal background work.
4//! It is intentionally non-blocking: overflow is rejected with `SQLITE_BUSY`
5//! (`FrankenError::Busy`) instead of queue-and-wait semantics.
6
7// `connection.rs` composes deeply nested `async fn` futures (statement dispatch
8// → DML → triggers → nested statement execution), and each layer widens the
9// generated future type. The default limit overflows while type-checking that
10// chain.
11#![recursion_limit = "512"]
12// bd-h9o9r: the engine's futures are deliberately not `Send` — a Connection
13// executes strictly sequentially on a current-thread runtime with RefCell
14// state throughout (see docs/concurrency-contract.md: single-Connection
15// sharing across threads is unsupported). Requiring `Send` futures
16// contradicts that design, so the lint is noise here (same rationale as
17// fsqlite-pager and fsqlite-vdbe).
18#![allow(clippy::future_not_send)]
19// bd-h9o9r: the nested statement futures are inherently large (the same
20// nesting that needs `recursion_limit = 512` above). Boxing them to satisfy
21// `large_futures` is precisely the change the perf ledger rejected without
22// allocation/CPU profile evidence (2026-07-26 "boxed WAL futures are not yet
23// a measured root cause"); do not "fix" this lint blind.
24#![allow(clippy::large_futures)]
25// bd-h9o9r: connection.rs holds RefCell borrows across awaits at ~76 sites.
26// Execution is strictly sequential per connection, so re-entrant borrows are
27// structurally confined, but the audit (and any borrow-scope repair) belongs
28// to the Phase-C reconstruction — tracked in bd-h9o9r rather than tagged
29// per-site in the FK-slice's active file.
30#![allow(clippy::await_holding_refcell_ref)]
31
32pub mod attach;
33#[cfg(not(target_arch = "wasm32"))]
34mod bounded_validation;
35pub mod commit_marker;
36#[cfg(not(target_arch = "wasm32"))]
37pub mod commit_repair;
38pub mod compat_persist;
39pub mod connection;
40#[cfg(not(target_arch = "wasm32"))]
41pub mod db_fec;
42pub mod decode_proofs;
43pub mod ecs_replication;
44pub mod epoch;
45pub mod explain;
46pub mod inter_object_coding;
47pub mod lrc;
48pub mod native_index;
49pub mod permeation_map;
50pub mod por;
51pub mod quiescence;
52#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
53pub mod raptorq_codec;
54pub mod raptorq_integration;
55pub mod region;
56pub mod remote_effects;
57#[cfg(not(target_arch = "wasm32"))]
58pub mod repair_engine;
59pub mod repair_symbols;
60pub mod replication_receiver;
61pub mod replication_sender;
62pub mod snapshot_shipping;
63pub mod source_block_partition;
64pub mod symbol_log;
65pub mod symbol_size_policy;
66pub mod tiered_storage;
67pub mod transaction;
68pub mod vacuum;
69pub mod wal_adapter;
70#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
71pub mod wal_fec_adapter;
72
73use std::num::NonZeroUsize;
74use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
75
76use fsqlite_error::{FrankenError, Result};
77use fsqlite_types::{ObjectId, Oti, PayloadHash, Region, SymbolRecord, SymbolRecordFlags};
78use tracing::{debug, error};
79
80const MAX_BALANCED_BG_CPU: usize = 16;
81
82/// Policy used when the bulkhead admission budget is exhausted.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum OverflowPolicy {
85    /// Reject overflow immediately with `SQLITE_BUSY`.
86    DropBusy,
87}
88
89/// Runtime profile for conservative parallelism defaults.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum ParallelismProfile {
92    /// Conservative profile used by default.
93    Balanced,
94}
95
96/// Bounded parallelism configuration for a work class.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct BulkheadConfig {
99    /// Number of tasks allowed to execute concurrently.
100    pub max_concurrent: usize,
101    /// Additional bounded admission slots (not unbounded queueing).
102    pub queue_depth: usize,
103    /// Overflow behavior when capacity is exhausted.
104    pub overflow_policy: OverflowPolicy,
105}
106
107impl BulkheadConfig {
108    /// Create an explicit configuration.
109    ///
110    /// Returns `None` when `max_concurrent` is zero.
111    #[must_use]
112    pub const fn new(
113        max_concurrent: usize,
114        queue_depth: usize,
115        overflow_policy: OverflowPolicy,
116    ) -> Option<Self> {
117        if max_concurrent == 0 {
118            None
119        } else {
120            Some(Self {
121                max_concurrent,
122                queue_depth,
123                overflow_policy,
124            })
125        }
126    }
127
128    /// Conservative default derived from available CPU parallelism.
129    ///
130    /// Uses the "balanced profile" formula from bd-22n.4:
131    /// `clamp(P / 8, 1, 16)` where `P = available_parallelism`.
132    #[must_use]
133    pub fn for_profile(profile: ParallelismProfile) -> Self {
134        let p = available_parallelism_or_one();
135        match profile {
136            ParallelismProfile::Balanced => Self {
137                max_concurrent: conservative_bg_cpu_max(p),
138                queue_depth: 0,
139                overflow_policy: OverflowPolicy::DropBusy,
140            },
141        }
142    }
143
144    /// Maximum admitted work units at once.
145    #[must_use]
146    pub const fn admission_limit(self) -> usize {
147        self.max_concurrent.saturating_add(self.queue_depth)
148    }
149}
150
151impl Default for BulkheadConfig {
152    fn default() -> Self {
153        Self::for_profile(ParallelismProfile::Balanced)
154    }
155}
156
157/// Compute conservative default background CPU parallelism from `P`.
158#[must_use]
159pub const fn conservative_bg_cpu_max(p: usize) -> usize {
160    let base = p / 8;
161    if base == 0 {
162        1
163    } else if base > MAX_BALANCED_BG_CPU {
164        MAX_BALANCED_BG_CPU
165    } else {
166        base
167    }
168}
169
170/// Return `std::thread::available_parallelism()` with a safe floor of 1.
171#[must_use]
172pub fn available_parallelism_or_one() -> usize {
173    std::thread::available_parallelism().map_or(1, NonZeroUsize::get)
174}
175
176/// Chunking plan for SIMD-friendly wide-word loops.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct WideChunkLayout {
179    /// Number of `u128` chunks processed.
180    pub u128_chunks: usize,
181    /// Number of `u64` chunks processed after `u128` chunks.
182    pub u64_chunks: usize,
183    /// Remaining tail bytes processed scalar.
184    pub tail_bytes: usize,
185}
186
187impl WideChunkLayout {
188    /// Compute the wide-chunk layout for a byte length.
189    #[must_use]
190    pub const fn for_len(len: usize) -> Self {
191        let u128_chunks = len / 16;
192        let rem_after_u128 = len % 16;
193        let u64_chunks = rem_after_u128 / 8;
194        let tail_bytes = rem_after_u128 % 8;
195        Self {
196            u128_chunks,
197            u64_chunks,
198            tail_bytes,
199        }
200    }
201}
202
203/// XOR patch application using `u128` + `u64` + tail loops.
204///
205/// This is the SIMD-friendly primitive for hot patch paths. LLVM can
206/// auto-vectorize the wide integer loops.
207#[allow(clippy::incompatible_msrv)]
208pub fn xor_patch_wide_chunks(dst: &mut [u8], patch: &[u8]) -> Result<WideChunkLayout> {
209    if dst.len() != patch.len() {
210        return Err(FrankenError::TypeMismatch {
211            expected: format!("equal lengths (dst == patch), got {}", dst.len()),
212            actual: patch.len().to_string(),
213        });
214    }
215
216    let layout = WideChunkLayout::for_len(dst.len());
217
218    let (dst_128, dst_rem) = dst.as_chunks_mut::<16>();
219    let (patch_128, patch_rem) = patch.as_chunks::<16>();
220    for (d, p) in dst_128.iter_mut().zip(patch_128.iter()) {
221        let d_word = u128::from_ne_bytes(*d);
222        let p_word = u128::from_ne_bytes(*p);
223        *d = (d_word ^ p_word).to_ne_bytes();
224    }
225
226    let (dst_64, dst_tail) = dst_rem.as_chunks_mut::<8>();
227    let (patch_64, patch_tail) = patch_rem.as_chunks::<8>();
228    for (d, p) in dst_64.iter_mut().zip(patch_64.iter()) {
229        let d_word = u64::from_ne_bytes(*d);
230        let p_word = u64::from_ne_bytes(*p);
231        *d = (d_word ^ p_word).to_ne_bytes();
232    }
233
234    for (d, p) in dst_tail.iter_mut().zip(patch_tail.iter()) {
235        *d ^= *p;
236    }
237
238    Ok(layout)
239}
240
241/// GF(256) addition (`+`) using wide XOR chunk loops.
242///
243/// In GF(256), addition is XOR, so this uses the same SIMD-friendly chunking
244/// strategy as [`xor_patch_wide_chunks`].
245pub fn gf256_add_assign_chunked(dst: &mut [u8], src: &[u8]) -> Result<WideChunkLayout> {
246    xor_patch_wide_chunks(dst, src)
247}
248
249const GF256_FIELD_SIZE: usize = 256;
250
251const fn gf256_mul_byte_for_table(mut a: u8, mut b: u8) -> u8 {
252    let mut out = 0_u8;
253    while b != 0 {
254        if (b & 1) != 0 {
255            out ^= a;
256        }
257        let carry = (a & 0x80) != 0;
258        a <<= 1;
259        if carry {
260            a ^= 0x1D;
261        }
262        b >>= 1;
263    }
264    out
265}
266
267const fn gf256_mul_row(coeff: u8) -> [u8; GF256_FIELD_SIZE] {
268    let mut row = [0_u8; GF256_FIELD_SIZE];
269    let mut byte = 0_u8;
270
271    loop {
272        row[byte as usize] = gf256_mul_byte_for_table(coeff, byte);
273        if byte == u8::MAX {
274            break;
275        }
276        byte = byte.wrapping_add(1);
277    }
278
279    row
280}
281
282// This builds `GF256_MUL_TABLES` at compile time; it is not a runtime stack allocation.
283#[allow(clippy::large_stack_arrays)]
284const fn gf256_mul_tables() -> [[u8; GF256_FIELD_SIZE]; GF256_FIELD_SIZE] {
285    let mut tables = [[0_u8; GF256_FIELD_SIZE]; GF256_FIELD_SIZE];
286    let mut coeff = 0_u8;
287
288    loop {
289        tables[coeff as usize] = gf256_mul_row(coeff);
290        if coeff == u8::MAX {
291            break;
292        }
293        coeff = coeff.wrapping_add(1);
294    }
295
296    tables
297}
298
299static GF256_MUL_TABLES: [[u8; GF256_FIELD_SIZE]; GF256_FIELD_SIZE] = gf256_mul_tables();
300
301#[inline]
302fn gf256_mul_table(coeff: u8) -> &'static [u8; GF256_FIELD_SIZE] {
303    &GF256_MUL_TABLES[usize::from(coeff)]
304}
305
306/// RaptorQ symbol add (`dst ^= src`) using chunked XOR.
307///
308/// This is the core symbol-add primitive from §3.2.2.
309pub fn symbol_add_assign(dst: &mut [u8], src: &[u8]) -> Result<WideChunkLayout> {
310    debug!(
311        bead_id = "bd-1hi.2",
312        op = "symbol_add_assign",
313        symbol_len = dst.len(),
314        "applying in-place XOR over symbol bytes"
315    );
316    gf256_add_assign_chunked(dst, src)
317}
318
319/// RaptorQ symbol scalar multiply (`out = c * src`) in GF(256).
320///
321/// Special cases:
322/// - `c == 0`: zero output
323/// - `c == 1`: copy input
324pub fn symbol_mul_into(coeff: u8, src: &[u8], out: &mut [u8]) -> Result<()> {
325    if src.len() != out.len() {
326        error!(
327            bead_id = "bd-1hi.2",
328            op = "symbol_mul_into",
329            coeff,
330            src_len = src.len(),
331            out_len = out.len(),
332            "symbol length mismatch"
333        );
334        return Err(FrankenError::TypeMismatch {
335            expected: format!("equal lengths (src == out), got {}", src.len()),
336            actual: out.len().to_string(),
337        });
338    }
339
340    debug!(
341        bead_id = "bd-1hi.2",
342        op = "symbol_mul_into",
343        coeff,
344        symbol_len = src.len(),
345        "applying GF(256) scalar multiplication"
346    );
347
348    match coeff {
349        0 => {
350            out.fill(0);
351            Ok(())
352        }
353        1 => {
354            out.copy_from_slice(src);
355            Ok(())
356        }
357        _ => {
358            let table = gf256_mul_table(coeff);
359            for (dst_byte, src_byte) in out.iter_mut().zip(src.iter()) {
360                *dst_byte = table[usize::from(*src_byte)];
361            }
362            Ok(())
363        }
364    }
365}
366
367/// RaptorQ fused multiply-add (`dst ^= c * src`) in GF(256).
368///
369/// Special cases:
370/// - `c == 0`: no-op
371/// - `c == 1`: pure XOR path
372pub fn symbol_addmul_assign(dst: &mut [u8], coeff: u8, src: &[u8]) -> Result<WideChunkLayout> {
373    if dst.len() != src.len() {
374        error!(
375            bead_id = "bd-1hi.2",
376            op = "symbol_addmul_assign",
377            coeff,
378            dst_len = dst.len(),
379            src_len = src.len(),
380            "symbol length mismatch"
381        );
382        return Err(FrankenError::TypeMismatch {
383            expected: format!("equal lengths (dst == src), got {}", dst.len()),
384            actual: src.len().to_string(),
385        });
386    }
387
388    debug!(
389        bead_id = "bd-1hi.2",
390        op = "symbol_addmul_assign",
391        coeff,
392        symbol_len = dst.len(),
393        "applying fused multiply-and-add over symbol bytes"
394    );
395
396    match coeff {
397        0 => Ok(WideChunkLayout::for_len(dst.len())),
398        1 => symbol_add_assign(dst, src),
399        _ => {
400            let table = gf256_mul_table(coeff);
401            for (dst_byte, src_byte) in dst.iter_mut().zip(src.iter()) {
402                *dst_byte ^= table[usize::from(*src_byte)];
403            }
404            Ok(WideChunkLayout::for_len(dst.len()))
405        }
406    }
407}
408
409/// Compute xxhash3 + blake3 on a contiguous input buffer.
410///
411/// - `xxhash3` path comes from `SymbolRecord::new` (`frame_xxh3`).
412/// - `blake3` path comes from `PayloadHash::blake3`.
413pub fn simd_friendly_checksum_pair(buffer: &[u8]) -> Result<(u64, [u8; 32])> {
414    let symbol_size = u32::try_from(buffer.len()).map_err(|_| FrankenError::OutOfRange {
415        what: "symbol_size".to_owned(),
416        value: buffer.len().to_string(),
417    })?;
418
419    let symbol_record = SymbolRecord::new(
420        ObjectId::from_bytes([0_u8; ObjectId::LEN]),
421        Oti {
422            f: u64::from(symbol_size),
423            al: 1,
424            t: symbol_size,
425            z: 1,
426            n: 1,
427        },
428        0,
429        buffer.to_vec(),
430        SymbolRecordFlags::empty(),
431    );
432    let blake = PayloadHash::blake3(buffer);
433
434    Ok((symbol_record.frame_xxh3, *blake.as_bytes()))
435}
436
437/// Non-blocking bulkhead admission gate.
438#[derive(Debug)]
439pub struct Bulkhead {
440    config: BulkheadConfig,
441    in_flight: AtomicUsize,
442    peak_in_flight: AtomicUsize,
443    busy_rejections: AtomicUsize,
444}
445
446impl Bulkhead {
447    #[must_use]
448    pub fn new(config: BulkheadConfig) -> Self {
449        Self {
450            config,
451            in_flight: AtomicUsize::new(0),
452            peak_in_flight: AtomicUsize::new(0),
453            busy_rejections: AtomicUsize::new(0),
454        }
455    }
456
457    #[must_use]
458    pub const fn config(&self) -> BulkheadConfig {
459        self.config
460    }
461
462    #[must_use]
463    pub fn in_flight(&self) -> usize {
464        self.in_flight.load(Ordering::Acquire)
465    }
466
467    #[must_use]
468    pub fn peak_in_flight(&self) -> usize {
469        self.peak_in_flight.load(Ordering::Acquire)
470    }
471
472    #[must_use]
473    pub fn busy_rejections(&self) -> usize {
474        self.busy_rejections.load(Ordering::Acquire)
475    }
476
477    /// Try to admit one work item.
478    ///
479    /// Never blocks. If the admission budget is exhausted, this returns
480    /// `FrankenError::Busy`.
481    pub fn try_acquire(&self) -> Result<BulkheadPermit<'_>> {
482        let limit = self.config.admission_limit();
483        loop {
484            let current = self.in_flight.load(Ordering::Acquire);
485            if current >= limit {
486                self.busy_rejections.fetch_add(1, Ordering::AcqRel);
487                return Err(match self.config.overflow_policy {
488                    OverflowPolicy::DropBusy => FrankenError::Busy,
489                });
490            }
491
492            let next = current.saturating_add(1);
493            if self
494                .in_flight
495                .compare_exchange_weak(current, next, Ordering::AcqRel, Ordering::Acquire)
496                .is_ok()
497            {
498                self.peak_in_flight.fetch_max(next, Ordering::AcqRel);
499                return Ok(BulkheadPermit {
500                    bulkhead: self,
501                    released: false,
502                });
503            }
504        }
505    }
506
507    /// Run work within a bulkhead permit.
508    pub fn run<T>(&self, work: impl FnOnce() -> T) -> Result<T> {
509        let _permit = self.try_acquire()?;
510        Ok(work())
511    }
512}
513
514/// RAII permit for a single admitted work item.
515#[derive(Debug)]
516pub struct BulkheadPermit<'a> {
517    bulkhead: &'a Bulkhead,
518    released: bool,
519}
520
521impl BulkheadPermit<'_> {
522    /// Explicitly release the permit.
523    pub fn release(mut self) {
524        if !self.released {
525            self.bulkhead.in_flight.fetch_sub(1, Ordering::AcqRel);
526            self.released = true;
527        }
528    }
529}
530
531impl Drop for BulkheadPermit<'_> {
532    fn drop(&mut self) {
533        if !self.released {
534            self.bulkhead.in_flight.fetch_sub(1, Ordering::AcqRel);
535            self.released = true;
536        }
537    }
538}
539
540/// Region-owned wrapper used for structured-concurrency integration.
541#[derive(Debug)]
542pub struct RegionBulkhead {
543    region: Region,
544    bulkhead: Bulkhead,
545    closing: AtomicBool,
546}
547
548impl RegionBulkhead {
549    #[must_use]
550    pub fn new(region: Region, config: BulkheadConfig) -> Self {
551        Self {
552            region,
553            bulkhead: Bulkhead::new(config),
554            closing: AtomicBool::new(false),
555        }
556    }
557
558    #[must_use]
559    pub const fn region(&self) -> Region {
560        self.region
561    }
562
563    #[must_use]
564    pub fn bulkhead(&self) -> &Bulkhead {
565        &self.bulkhead
566    }
567
568    pub fn try_acquire(&self) -> Result<BulkheadPermit<'_>> {
569        if self.closing.load(Ordering::Acquire) {
570            return Err(FrankenError::Busy);
571        }
572        self.bulkhead.try_acquire()
573    }
574
575    /// Begin region close: no new admissions are allowed after this point.
576    pub fn begin_close(&self) {
577        self.closing.store(true, Ordering::Release);
578    }
579
580    /// Whether all region-owned work has quiesced.
581    #[must_use]
582    pub fn is_quiescent(&self) -> bool {
583        self.bulkhead.in_flight() == 0
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use std::collections::VecDeque;
590    use std::pin::Pin;
591    use std::sync::Arc;
592    use std::task::{Context, Poll};
593    use std::thread;
594    use std::time::{Duration, Instant};
595
596    use asupersync::raptorq::decoder::{InactivationDecoder, ReceivedSymbol};
597    use asupersync::raptorq::gf256::{Gf256, gf256_add_slice, gf256_addmul_slice, gf256_mul_slice};
598    use asupersync::raptorq::systematic::{ConstraintMatrix, SystematicEncoder};
599    use asupersync::raptorq::{RaptorQReceiverBuilder, RaptorQSenderBuilder};
600    use asupersync::security::AuthenticationTag;
601    use asupersync::security::authenticated::AuthenticatedSymbol;
602    use asupersync::transport::error::{SinkError, StreamError};
603    use asupersync::transport::sink::SymbolSink;
604    use asupersync::transport::stream::SymbolStream;
605    use asupersync::types::{ObjectId as AsObjectId, ObjectParams, Symbol};
606    use asupersync::{Cx, RaptorQConfig};
607    use fsqlite_btree::compare_key_bytes_contiguous;
608    use fsqlite_types::gf256_mul_byte;
609
610    use super::*;
611
612    const BEAD_ID: &str = "bd-22n.4";
613    const SIMD_BEAD_ID: &str = "bd-22n.6";
614    const RAPTORQ_BEAD_ID: &str = "bd-1hi.2";
615
616    #[derive(Debug)]
617    struct VecSink {
618        symbols: Vec<Symbol>,
619    }
620
621    impl VecSink {
622        fn new() -> Self {
623            Self {
624                symbols: Vec::new(),
625            }
626        }
627    }
628
629    impl SymbolSink for VecSink {
630        fn poll_send(
631            mut self: Pin<&mut Self>,
632            _cx: &mut Context<'_>,
633            symbol: AuthenticatedSymbol,
634        ) -> Poll<std::result::Result<(), SinkError>> {
635            self.symbols.push(symbol.into_symbol());
636            Poll::Ready(Ok(()))
637        }
638
639        fn poll_flush(
640            self: Pin<&mut Self>,
641            _cx: &mut Context<'_>,
642        ) -> Poll<std::result::Result<(), SinkError>> {
643            Poll::Ready(Ok(()))
644        }
645
646        fn poll_close(
647            self: Pin<&mut Self>,
648            _cx: &mut Context<'_>,
649        ) -> Poll<std::result::Result<(), SinkError>> {
650            Poll::Ready(Ok(()))
651        }
652
653        fn poll_ready(
654            self: Pin<&mut Self>,
655            _cx: &mut Context<'_>,
656        ) -> Poll<std::result::Result<(), SinkError>> {
657            Poll::Ready(Ok(()))
658        }
659    }
660
661    #[derive(Debug)]
662    struct VecStream {
663        q: VecDeque<AuthenticatedSymbol>,
664    }
665
666    impl VecStream {
667        fn new(symbols: Vec<Symbol>) -> Self {
668            let q = symbols
669                .into_iter()
670                .map(|symbol| AuthenticatedSymbol::from_parts(symbol, AuthenticationTag::zero()))
671                .collect();
672            Self { q }
673        }
674    }
675
676    impl SymbolStream for VecStream {
677        fn poll_next(
678            mut self: Pin<&mut Self>,
679            _cx: &mut Context<'_>,
680        ) -> Poll<Option<std::result::Result<AuthenticatedSymbol, StreamError>>> {
681            match self.q.pop_front() {
682                Some(symbol) => Poll::Ready(Some(Ok(symbol))),
683                None => Poll::Ready(None),
684            }
685        }
686
687        fn size_hint(&self) -> (usize, Option<usize>) {
688            (self.q.len(), Some(self.q.len()))
689        }
690
691        fn is_exhausted(&self) -> bool {
692            self.q.is_empty()
693        }
694    }
695
696    fn raptorq_config(symbol_size: u16, repair_overhead: f64) -> RaptorQConfig {
697        let mut config = RaptorQConfig::default();
698        config.encoding.symbol_size = symbol_size;
699        config.encoding.max_block_size = 64 * 1024;
700        config.encoding.repair_overhead = repair_overhead;
701        config
702    }
703
704    fn deterministic_payload(len: usize, seed: u64) -> Vec<u8> {
705        let mut state = seed ^ 0x9E37_79B9_7F4A_7C15;
706        let mut out = Vec::with_capacity(len);
707        for idx in 0..len {
708            state ^= state << 7;
709            state ^= state >> 9;
710            state = state.wrapping_mul(0xA24B_AED4_963E_E407);
711            let idx_byte = u8::try_from(idx % 251).expect("modulo fits in u8");
712            out.push(u8::try_from(state & 0xFF).expect("masked to u8") ^ idx_byte);
713        }
714        out
715    }
716
717    fn xor_patch_bytewise(dst: &mut [u8], patch: &[u8]) {
718        for (dst_byte, patch_byte) in dst.iter_mut().zip(patch.iter()) {
719            *dst_byte ^= *patch_byte;
720        }
721    }
722
723    fn gf256_mul_bytewise(coeff: u8, src: &[u8], out: &mut [u8]) {
724        for (dst_byte, src_byte) in out.iter_mut().zip(src.iter()) {
725            *dst_byte = gf256_mul_byte(coeff, *src_byte);
726        }
727    }
728
729    fn collect_rs_files(root: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
730        let entries = std::fs::read_dir(root).expect("read_dir should succeed");
731        for entry in entries {
732            let path = entry.expect("read_dir entry should be readable").path();
733            if path.is_dir() {
734                collect_rs_files(&path, out);
735            } else if path.extension().is_some_and(|ext| ext == "rs") {
736                out.push(path);
737            }
738        }
739    }
740
741    fn encode_symbols(
742        config: RaptorQConfig,
743        object_id: AsObjectId,
744        data: &[u8],
745    ) -> (Vec<Symbol>, usize) {
746        let cx = Cx::for_testing();
747        let mut sender = RaptorQSenderBuilder::new()
748            .config(config)
749            .transport(VecSink::new())
750            .build()
751            .expect("sender build");
752        let outcome = sender
753            .send_object(&cx, object_id, data)
754            .expect("send_object must succeed");
755        let symbols = std::mem::take(&mut sender.transport_mut().symbols);
756        tracing::debug!(
757            bead_id = RAPTORQ_BEAD_ID,
758            case = "encode_symbols",
759            source_symbols = outcome.source_symbols,
760            emitted_symbols = symbols.len(),
761            object_size = data.len(),
762            "encoded object into source+repair symbol stream"
763        );
764        (symbols, outcome.source_symbols)
765    }
766
767    #[allow(clippy::result_large_err)]
768    fn decode_symbols(
769        config: RaptorQConfig,
770        object_id: AsObjectId,
771        object_size: usize,
772        source_symbols: usize,
773        symbols: Vec<Symbol>,
774    ) -> std::result::Result<Vec<u8>, asupersync::Error> {
775        let cx = Cx::for_testing();
776        let params = ObjectParams::new(
777            object_id,
778            u64::try_from(object_size).expect("object size fits u64"),
779            config.encoding.symbol_size,
780            1,
781            u16::try_from(source_symbols).expect("source symbol count fits u16"),
782        );
783        let mut receiver = RaptorQReceiverBuilder::new()
784            .config(config)
785            .source(VecStream::new(symbols))
786            .build()
787            .expect("receiver build");
788
789        receiver
790            .receive_object(&cx, &params)
791            .map(|outcome| outcome.data)
792    }
793
794    fn split_source_and_repair(
795        symbols: &[Symbol],
796        source_symbols: usize,
797    ) -> (Vec<Symbol>, Vec<Symbol>) {
798        let source_symbols_u32 =
799            u32::try_from(source_symbols).expect("source symbol count fits u32");
800        let mut sources = Vec::new();
801        let mut repairs = Vec::new();
802        for symbol in symbols {
803            if symbol.esi() < source_symbols_u32 {
804                sources.push(symbol.clone());
805            } else {
806                repairs.push(symbol.clone());
807            }
808        }
809        (sources, repairs)
810    }
811
812    fn low_level_source_block(k: usize, symbol_size: usize, seed: u64) -> Vec<Vec<u8>> {
813        (0..k)
814            .map(|source_index| {
815                deterministic_payload(
816                    symbol_size,
817                    seed + u64::try_from(source_index).expect("source index fits u64"),
818                )
819            })
820            .collect()
821    }
822
823    fn append_source_received_symbols(
824        received: &mut Vec<ReceivedSymbol>,
825        constraints: &ConstraintMatrix,
826        base_rows: usize,
827        k_prime: usize,
828        symbol_size: usize,
829        source: &[Vec<u8>],
830        source_indexes: &[usize],
831    ) {
832        for &source_index in source_indexes {
833            received.push(ReceivedSymbol::source(
834                u32::try_from(source_index).expect("source index fits u32"),
835                source[source_index].clone(),
836            ));
837        }
838
839        // RFC 6330 decode domain uses K' source-domain rows, not just K.
840        // The K' - K PI rows are internal zero-padding equations, not public
841        // source symbols. Public source ESIs remain restricted to [0, K).
842        for source_index in source.len()..k_prime {
843            let row = base_rows + source_index;
844            let mut columns = Vec::new();
845            let mut coefficients = Vec::new();
846            for col in 0..constraints.cols {
847                let coeff = constraints.get(row, col);
848                if !coeff.is_zero() {
849                    columns.push(col);
850                    coefficients.push(coeff);
851                }
852            }
853
854            received.push(ReceivedSymbol {
855                esi: u32::try_from(source_index).expect("source index fits u32"),
856                is_source: false,
857                columns,
858                coefficients,
859                data: vec![0_u8; symbol_size],
860            });
861        }
862    }
863
864    #[test]
865    fn test_parallelism_defaults_conservative() {
866        assert_eq!(
867            conservative_bg_cpu_max(16),
868            2,
869            "bead_id={BEAD_ID} case=balanced_profile_formula_p16"
870        );
871        assert_eq!(
872            conservative_bg_cpu_max(1),
873            1,
874            "bead_id={BEAD_ID} case=balanced_profile_min_floor"
875        );
876        assert_eq!(
877            conservative_bg_cpu_max(512),
878            16,
879            "bead_id={BEAD_ID} case=balanced_profile_max_cap"
880        );
881    }
882
883    #[test]
884    fn test_parallelism_bounded_by_available() {
885        let cfg = BulkheadConfig::default();
886        let p = available_parallelism_or_one();
887        assert!(
888            cfg.max_concurrent <= p,
889            "bead_id={BEAD_ID} case=default_exceeds_available_parallelism cfg={cfg:?} p={p}"
890        );
891
892        let bulkhead = Bulkhead::new(cfg);
893        let mut permits = Vec::new();
894        for _ in 0..cfg.admission_limit() {
895            permits.push(
896                bulkhead
897                    .try_acquire()
898                    .expect("admission under configured limit should succeed"),
899            );
900        }
901
902        let overflow = bulkhead.try_acquire();
903        assert!(
904            matches!(overflow, Err(FrankenError::Busy)),
905            "bead_id={BEAD_ID} case=bounded_admission_overflow_must_be_busy overflow={overflow:?}"
906        );
907
908        drop(permits);
909        assert_eq!(
910            bulkhead.in_flight(),
911            0,
912            "bead_id={BEAD_ID} case=permits_drop_to_zero"
913        );
914    }
915
916    #[test]
917    fn test_bulkhead_config_max_concurrent() {
918        let cfg = BulkheadConfig::new(3, 0, OverflowPolicy::DropBusy)
919            .expect("non-zero max_concurrent must be valid");
920        let bulkhead = Bulkhead::new(cfg);
921
922        let p1 = bulkhead.try_acquire().expect("slot 1");
923        let p2 = bulkhead.try_acquire().expect("slot 2");
924        let p3 = bulkhead.try_acquire().expect("slot 3");
925        let overflow = bulkhead.try_acquire();
926
927        assert!(
928            matches!(overflow, Err(FrankenError::Busy)),
929            "bead_id={BEAD_ID} case=max_concurrent_enforced overflow={overflow:?}"
930        );
931        drop((p1, p2, p3));
932    }
933
934    #[test]
935    fn test_overflow_policy_drop_with_busy() {
936        let cfg = BulkheadConfig::new(1, 0, OverflowPolicy::DropBusy)
937            .expect("non-zero max_concurrent must be valid");
938        let bulkhead = Bulkhead::new(cfg);
939        let _permit = bulkhead.try_acquire().expect("first permit must succeed");
940
941        let overflow = bulkhead.try_acquire();
942        assert!(
943            matches!(overflow, Err(FrankenError::Busy)),
944            "bead_id={BEAD_ID} case=overflow_policy_drop_busy overflow={overflow:?}"
945        );
946    }
947
948    #[test]
949    fn test_background_work_degrades_gracefully() {
950        let cfg = BulkheadConfig::new(2, 0, OverflowPolicy::DropBusy)
951            .expect("non-zero max_concurrent must be valid");
952        let bulkhead = Bulkhead::new(cfg);
953
954        let _a = bulkhead.try_acquire().expect("permit a");
955        let _b = bulkhead.try_acquire().expect("permit b");
956
957        for _ in 0..8 {
958            let result = bulkhead.try_acquire();
959            assert!(
960                matches!(result, Err(FrankenError::Busy)),
961                "bead_id={BEAD_ID} case=overflow_must_reject_not_wait result={result:?}"
962            );
963        }
964
965        assert_eq!(
966            bulkhead.busy_rejections(),
967            8,
968            "bead_id={BEAD_ID} case=busy_rejection_counter"
969        );
970    }
971
972    #[test]
973    fn test_region_integration() {
974        let cfg = BulkheadConfig::new(1, 0, OverflowPolicy::DropBusy)
975            .expect("non-zero max_concurrent must be valid");
976        let region_bulkhead = RegionBulkhead::new(Region::new(7), cfg);
977        assert_eq!(
978            region_bulkhead.region().get(),
979            7,
980            "bead_id={BEAD_ID} case=region_id_plumbed"
981        );
982
983        let permit = region_bulkhead.try_acquire().expect("first permit");
984        assert!(
985            !region_bulkhead.is_quiescent(),
986            "bead_id={BEAD_ID} case=region_non_quiescent_with_active_work"
987        );
988
989        region_bulkhead.begin_close();
990        let after_close = region_bulkhead.try_acquire();
991        assert!(
992            matches!(after_close, Err(FrankenError::Busy)),
993            "bead_id={BEAD_ID} case=region_close_blocks_new_work result={after_close:?}"
994        );
995
996        drop(permit);
997        assert!(
998            region_bulkhead.is_quiescent(),
999            "bead_id={BEAD_ID} case=region_quiescent_after_permit_drop"
1000        );
1001    }
1002
1003    #[test]
1004    fn test_gf256_ops_chunked() {
1005        let mut dst = vec![0xAA_u8; 40];
1006        let src = vec![0x55_u8; 40];
1007        let expected: Vec<u8> = dst.iter().zip(src.iter()).map(|(d, s)| *d ^ *s).collect();
1008
1009        let layout = gf256_add_assign_chunked(&mut dst, &src)
1010            .expect("equal-length buffers should be accepted");
1011        assert!(
1012            layout.u128_chunks > 0 || layout.u64_chunks > 0,
1013            "bead_id={SIMD_BEAD_ID} case=wide_chunks_expected layout={layout:?}"
1014        );
1015        assert_eq!(
1016            dst, expected,
1017            "bead_id={SIMD_BEAD_ID} case=gf256_addition_xor_equivalence"
1018        );
1019    }
1020
1021    #[test]
1022    fn test_xor_patch_wide_chunks() {
1023        let mut dst = vec![0xF0_u8; 37];
1024        let patch = vec![0x0F_u8; 37];
1025        let expected: Vec<u8> = dst.iter().zip(patch.iter()).map(|(d, p)| *d ^ *p).collect();
1026
1027        let layout =
1028            xor_patch_wide_chunks(&mut dst, &patch).expect("equal-length buffers should be valid");
1029        assert_eq!(
1030            layout,
1031            WideChunkLayout {
1032                u128_chunks: 2,
1033                u64_chunks: 0,
1034                tail_bytes: 5,
1035            },
1036            "bead_id={SIMD_BEAD_ID} case=chunk_layout_expected"
1037        );
1038        assert_eq!(
1039            dst, expected,
1040            "bead_id={SIMD_BEAD_ID} case=xor_patch_matches_scalar_reference"
1041        );
1042    }
1043
1044    #[test]
1045    fn test_xor_symbols_u64_chunks() {
1046        // Length 24 exercises both the u128 and u64 lanes.
1047        let mut dst = vec![0xAB_u8; 24];
1048        let src = vec![0xCD_u8; 24];
1049        let mut expected = vec![0xAB_u8; 24];
1050        xor_patch_bytewise(&mut expected, &src);
1051
1052        let layout = xor_patch_wide_chunks(&mut dst, &src).expect("equal-length buffers");
1053        assert_eq!(
1054            layout,
1055            WideChunkLayout {
1056                u128_chunks: 1,
1057                u64_chunks: 1,
1058                tail_bytes: 0,
1059            },
1060            "bead_id=bd-2ddc case=u64_chunk_lane_exercised"
1061        );
1062        assert_eq!(
1063            dst, expected,
1064            "bead_id=bd-2ddc case=chunked_xor_matches_bytewise_reference"
1065        );
1066    }
1067
1068    #[test]
1069    fn test_gf256_multiply_chunks() {
1070        let coeff = 0xA7_u8;
1071        let src = deterministic_payload(4096, 0xDDCC_BBAA_1122_3344);
1072        let mut chunked = vec![0_u8; src.len()];
1073        let mut scalar = vec![0_u8; src.len()];
1074
1075        symbol_mul_into(coeff, &src, &mut chunked).expect("chunked symbol_mul_into");
1076        gf256_mul_bytewise(coeff, &src, &mut scalar);
1077
1078        assert_eq!(
1079            chunked, scalar,
1080            "bead_id=bd-2ddc case=chunked_mul_matches_scalar_reference"
1081        );
1082    }
1083
1084    #[test]
1085    fn test_u128_chunk_alignment() {
1086        // Non-multiple of 16 exercises the u128 path + tail handling.
1087        let mut via_wide = deterministic_payload(4099, 0x1234_5678_9ABC_DEF0);
1088        let mut via_u64_only = via_wide.clone();
1089        let patch = deterministic_payload(4099, 0x0F0E_0D0C_0B0A_0908);
1090
1091        xor_patch_wide_chunks(&mut via_wide, &patch).expect("wide chunk xor");
1092
1093        let (dst_u64_chunks, dst_remainder) = via_u64_only.as_chunks_mut::<8>();
1094        let (patch_u64_chunks, patch_remainder) = patch.as_chunks::<8>();
1095        for (dst_chunk, patch_chunk) in dst_u64_chunks.iter_mut().zip(patch_u64_chunks) {
1096            let dst_word = u64::from_ne_bytes(*dst_chunk);
1097            let patch_word = u64::from_ne_bytes(*patch_chunk);
1098            *dst_chunk = (dst_word ^ patch_word).to_ne_bytes();
1099        }
1100        for (dst_byte, patch_byte) in dst_remainder.iter_mut().zip(patch_remainder) {
1101            *dst_byte ^= *patch_byte;
1102        }
1103
1104        assert_eq!(
1105            via_wide, via_u64_only,
1106            "bead_id=bd-2ddc case=u128_lane_matches_u64_plus_tail"
1107        );
1108    }
1109
1110    #[test]
1111    fn test_benchmark_chunk_vs_byte() {
1112        // Meaningful performance checks require optimized codegen.
1113        if cfg!(debug_assertions) {
1114            return;
1115        }
1116
1117        let iterations = 32_000_usize;
1118        let src = deterministic_payload(4096, 0xDEAD_BEEF_F00D_CAFE);
1119        let base = deterministic_payload(4096, 0x0123_4567_89AB_CDEF);
1120
1121        let mut chunked = base.clone();
1122        let chunked_start = Instant::now();
1123        for _ in 0..iterations {
1124            xor_patch_wide_chunks(&mut chunked, &src).expect("chunked xor");
1125            std::hint::black_box(&chunked);
1126        }
1127        let chunked_elapsed = chunked_start.elapsed();
1128
1129        let mut bytewise = base;
1130        let bytewise_start = Instant::now();
1131        for _ in 0..iterations {
1132            xor_patch_bytewise(&mut bytewise, &src);
1133            std::hint::black_box(&bytewise);
1134        }
1135        let bytewise_elapsed = bytewise_start.elapsed();
1136
1137        let speedup = bytewise_elapsed.as_secs_f64() / chunked_elapsed.as_secs_f64();
1138        assert!(
1139            speedup >= 4.0,
1140            "bead_id=bd-2ddc case=chunk_vs_byte_speedup speedup={speedup:.2}x \
1141             chunked_ns={} bytewise_ns={} iterations={iterations}",
1142            chunked_elapsed.as_nanos(),
1143            bytewise_elapsed.as_nanos()
1144        );
1145    }
1146
1147    #[test]
1148    fn test_no_unsafe_simd() {
1149        let manifest = include_str!("../../../Cargo.toml");
1150        assert!(
1151            manifest.contains(r#"unsafe_code = "forbid""#),
1152            "bead_id=bd-2ddc case=workspace_forbids_unsafe"
1153        );
1154
1155        let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1156            .parent()
1157            .expect("crate dir has parent")
1158            .parent()
1159            .expect("workspace root exists")
1160            .to_path_buf();
1161        let crates_dir = workspace_root.join("crates");
1162
1163        let mut rs_files = Vec::new();
1164        collect_rs_files(&crates_dir, &mut rs_files);
1165
1166        let simd_needles = [
1167            "_mm_",
1168            "std::arch::",
1169            "core::arch::",
1170            "__m128",
1171            "__m256",
1172            "__m512",
1173            "simd_shuffle",
1174            "vpxor",
1175            "vxorq",
1176        ];
1177
1178        let mut offenders = Vec::new();
1179        for file in rs_files {
1180            let Ok(content) = std::fs::read_to_string(&file) else {
1181                continue;
1182            };
1183
1184            let lines = content.lines().collect::<Vec<_>>();
1185            for (idx, line) in lines.iter().enumerate() {
1186                let has_intrinsic = simd_needles.iter().any(|needle| line.contains(needle));
1187                if !has_intrinsic {
1188                    continue;
1189                }
1190
1191                let window_start = idx.saturating_sub(3);
1192                let window_end = (idx + 3).min(lines.len().saturating_sub(1));
1193                let mut found_unsafe_nearby = false;
1194                for nearby in &lines[window_start..=window_end] {
1195                    let trimmed = nearby.trim_start();
1196                    let is_comment = trimmed.starts_with("//");
1197                    if !is_comment && trimmed.contains("unsafe") {
1198                        found_unsafe_nearby = true;
1199                        break;
1200                    }
1201                }
1202
1203                if found_unsafe_nearby {
1204                    offenders.push(format!("{}:{}", file.display(), idx + 1));
1205                }
1206            }
1207        }
1208
1209        assert!(
1210            offenders.is_empty(),
1211            "bead_id=bd-2ddc case=no_unsafe_simd_intrinsics offenders={offenders:?}"
1212        );
1213    }
1214
1215    #[test]
1216    fn test_checksum_simd_friendly() {
1217        let buffer = vec![0x11_u8; 256];
1218        let (xx_a, blake_a) =
1219            simd_friendly_checksum_pair(&buffer).expect("checksum pair must succeed");
1220
1221        let mut modified = buffer;
1222        modified[255] ^= 0x01;
1223        let (xx_b, blake_b) =
1224            simd_friendly_checksum_pair(&modified).expect("checksum pair must succeed");
1225
1226        assert_ne!(
1227            xx_a, xx_b,
1228            "bead_id={SIMD_BEAD_ID} case=xxhash3_changes_on_byte_flip"
1229        );
1230        assert_ne!(
1231            blake_a, blake_b,
1232            "bead_id={SIMD_BEAD_ID} case=blake3_changes_on_byte_flip"
1233        );
1234    }
1235
1236    #[test]
1237    fn test_e2e_bounded_parallelism_under_background_load() {
1238        let cfg = BulkheadConfig::new(4, 0, OverflowPolicy::DropBusy)
1239            .expect("non-zero max_concurrent must be valid");
1240        let bulkhead = Arc::new(Bulkhead::new(cfg));
1241
1242        let handles: Vec<_> = (0..48)
1243            .map(|_| {
1244                let bulkhead = Arc::clone(&bulkhead);
1245                thread::spawn(move || {
1246                    bulkhead.run(|| {
1247                        thread::sleep(Duration::from_millis(10));
1248                    })
1249                })
1250            })
1251            .collect();
1252
1253        let mut busy = 0_usize;
1254        for handle in handles {
1255            match handle.join().expect("worker thread should not panic") {
1256                Ok(()) => {}
1257                Err(FrankenError::Busy) => busy = busy.saturating_add(1),
1258                Err(err) => {
1259                    assert_eq!(
1260                        err.error_code(),
1261                        fsqlite_error::ErrorCode::Busy,
1262                        "bead_id={BEAD_ID} case=e2e_unexpected_bulkhead_error err={err}"
1263                    );
1264                    busy = busy.saturating_add(1);
1265                }
1266            }
1267        }
1268
1269        assert!(
1270            busy > 0,
1271            "bead_id={BEAD_ID} case=e2e_should_observe_overflow_rejections"
1272        );
1273        assert!(
1274            bulkhead.peak_in_flight() <= cfg.admission_limit(),
1275            "bead_id={BEAD_ID} case=e2e_peak_parallelism_exceeded peak={} limit={}",
1276            bulkhead.peak_in_flight(),
1277            cfg.admission_limit()
1278        );
1279    }
1280
1281    #[test]
1282    fn test_e2e_simd_hot_path_correctness() {
1283        // 1) B-tree hot comparison over contiguous slices.
1284        let contiguous = b"key-0001key-0002".to_vec();
1285        let left = &contiguous[0..8];
1286        let right = &contiguous[8..16];
1287        let compare_start = Instant::now();
1288        assert_eq!(
1289            compare_key_bytes_contiguous(left, right),
1290            left.cmp(right),
1291            "bead_id={SIMD_BEAD_ID} case=btree_contiguous_compare_correct"
1292        );
1293        let compare_elapsed = compare_start.elapsed();
1294
1295        // 2) GF(256) add (XOR) and XOR patch helpers.
1296        let mut symbol_a = (0_u8..64).collect::<Vec<u8>>();
1297        let symbol_b = (64_u8..128).collect::<Vec<u8>>();
1298        let expected_add: Vec<u8> = symbol_a
1299            .iter()
1300            .zip(symbol_b.iter())
1301            .map(|(a, b)| *a ^ *b)
1302            .collect();
1303        let gf256_start = Instant::now();
1304        gf256_add_assign_chunked(&mut symbol_a, &symbol_b).expect("gf256 add should succeed");
1305        let gf256_elapsed = gf256_start.elapsed();
1306        assert_eq!(
1307            symbol_a, expected_add,
1308            "bead_id={SIMD_BEAD_ID} case=gf256_chunked_add_correct"
1309        );
1310
1311        let mut patch_target = vec![0x33_u8; 64];
1312        let patch = vec![0xCC_u8; 64];
1313        let xor_start = Instant::now();
1314        xor_patch_wide_chunks(&mut patch_target, &patch).expect("xor patch should succeed");
1315        let xor_elapsed = xor_start.elapsed();
1316        assert!(
1317            patch_target.iter().all(|&byte| byte == (0x33_u8 ^ 0xCC_u8)),
1318            "bead_id={SIMD_BEAD_ID} case=xor_patch_chunked_correct"
1319        );
1320
1321        // 3) SIMD-friendly checksum feed.
1322        let checksum_start = Instant::now();
1323        let (xx, blake) =
1324            simd_friendly_checksum_pair(&patch_target).expect("checksum pair should succeed");
1325        let checksum_elapsed = checksum_start.elapsed();
1326        assert_ne!(
1327            xx, 0,
1328            "bead_id={SIMD_BEAD_ID} case=xxhash3_nonzero_for_nonempty_payload"
1329        );
1330        assert!(
1331            blake.iter().any(|&b| b != 0),
1332            "bead_id={SIMD_BEAD_ID} case=blake3_digest_nonzero"
1333        );
1334
1335        eprintln!(
1336            "bead_id={SIMD_BEAD_ID} metric=simd_hot_path_ns compare={} gf256_add={} xor_patch={} checksum={}",
1337            compare_elapsed.as_nanos(),
1338            gf256_elapsed.as_nanos(),
1339            xor_elapsed.as_nanos(),
1340            checksum_elapsed.as_nanos()
1341        );
1342    }
1343
1344    #[test]
1345    fn test_symbol_add_self_inverse() {
1346        let src = (0_u16..512)
1347            .map(|idx| u8::try_from(idx % 251).expect("modulo fits in u8"))
1348            .collect::<Vec<_>>();
1349        let mut dst = src.clone();
1350        symbol_add_assign(&mut dst, &src).expect("symbol_add should succeed");
1351        assert!(
1352            dst.iter().all(|byte| *byte == 0),
1353            "bead_id={RAPTORQ_BEAD_ID} case=symbol_add_self_inverse"
1354        );
1355    }
1356
1357    #[test]
1358    fn test_symbol_add_commutative_and_associative() {
1359        let a = (0_u16..128)
1360            .map(|idx| u8::try_from((idx * 3) % 251).expect("modulo fits"))
1361            .collect::<Vec<_>>();
1362        let b = (0_u16..128)
1363            .map(|idx| u8::try_from((idx * 5 + 7) % 251).expect("modulo fits"))
1364            .collect::<Vec<_>>();
1365        let c = (0_u16..128)
1366            .map(|idx| u8::try_from((idx * 11 + 13) % 251).expect("modulo fits"))
1367            .collect::<Vec<_>>();
1368
1369        let mut ab = a.clone();
1370        symbol_add_assign(&mut ab, &b).expect("a+b");
1371        let mut ba = b.clone();
1372        symbol_add_assign(&mut ba, &a).expect("b+a");
1373        assert_eq!(
1374            ab, ba,
1375            "bead_id={RAPTORQ_BEAD_ID} case=symbol_add_commutative"
1376        );
1377
1378        let mut lhs = a.clone();
1379        symbol_add_assign(&mut lhs, &b).expect("(a+b)");
1380        symbol_add_assign(&mut lhs, &c).expect("(a+b)+c");
1381
1382        let mut rhs = b;
1383        symbol_add_assign(&mut rhs, &c).expect("(b+c)");
1384        let mut rhs2 = a;
1385        symbol_add_assign(&mut rhs2, &rhs).expect("a+(b+c)");
1386
1387        assert_eq!(
1388            lhs, rhs2,
1389            "bead_id={RAPTORQ_BEAD_ID} case=symbol_add_associative"
1390        );
1391    }
1392
1393    #[test]
1394    fn test_symbol_mul_special_cases() {
1395        let src = (0_u16..256)
1396            .map(|idx| u8::try_from(idx).expect("idx fits"))
1397            .collect::<Vec<_>>();
1398
1399        let mut out_zero = vec![0_u8; src.len()];
1400        symbol_mul_into(0, &src, &mut out_zero).expect("mul by zero");
1401        assert!(
1402            out_zero.iter().all(|byte| *byte == 0),
1403            "bead_id={RAPTORQ_BEAD_ID} case=symbol_mul_zero"
1404        );
1405
1406        let mut out_one = vec![0_u8; src.len()];
1407        symbol_mul_into(1, &src, &mut out_one).expect("mul by one");
1408        assert_eq!(
1409            out_one, src,
1410            "bead_id={RAPTORQ_BEAD_ID} case=symbol_mul_identity"
1411        );
1412    }
1413
1414    #[test]
1415    fn test_symbol_mul_matches_scalar_reference() {
1416        let src = (0_u16..512)
1417            .map(|idx| u8::try_from((idx * 7 + 17) % 251).expect("modulo fits"))
1418            .collect::<Vec<_>>();
1419        let coeff = 0xA7_u8;
1420
1421        let mut out = vec![0_u8; src.len()];
1422        symbol_mul_into(coeff, &src, &mut out).expect("symbol mul");
1423        for (actual, input) in out.iter().zip(src.iter()) {
1424            let expected = gf256_mul_byte(coeff, *input);
1425            assert_eq!(
1426                *actual, expected,
1427                "bead_id={RAPTORQ_BEAD_ID} case=symbol_mul_scalar_match"
1428            );
1429        }
1430    }
1431
1432    #[test]
1433    fn test_symbol_addmul_special_cases_and_equivalence() {
1434        let src = (0_u16..512)
1435            .map(|idx| u8::try_from((idx * 13 + 19) % 251).expect("modulo fits"))
1436            .collect::<Vec<_>>();
1437        let original = (0_u16..512)
1438            .map(|idx| u8::try_from((idx * 9 + 3) % 251).expect("modulo fits"))
1439            .collect::<Vec<_>>();
1440
1441        let mut no_op = original.clone();
1442        symbol_addmul_assign(&mut no_op, 0, &src).expect("c=0");
1443        assert_eq!(
1444            no_op, original,
1445            "bead_id={RAPTORQ_BEAD_ID} case=symbol_addmul_c0_noop"
1446        );
1447
1448        let mut xor_path = original.clone();
1449        symbol_addmul_assign(&mut xor_path, 1, &src).expect("c=1");
1450        let mut expected_xor = original.clone();
1451        symbol_add_assign(&mut expected_xor, &src).expect("xor reference");
1452        assert_eq!(
1453            xor_path, expected_xor,
1454            "bead_id={RAPTORQ_BEAD_ID} case=symbol_addmul_c1_equals_xor"
1455        );
1456
1457        let coeff = 0x53_u8;
1458        let mut fused = original.clone();
1459        symbol_addmul_assign(&mut fused, coeff, &src).expect("fused");
1460        let mut mul = vec![0_u8; src.len()];
1461        symbol_mul_into(coeff, &src, &mut mul).expect("mul");
1462        let mut separate = original;
1463        symbol_add_assign(&mut separate, &mul).expect("add");
1464        assert_eq!(
1465            fused, separate,
1466            "bead_id={RAPTORQ_BEAD_ID} case=symbol_addmul_fused_equals_mul_plus_add"
1467        );
1468    }
1469
1470    #[test]
1471    fn test_symbol_operations_4096_and_512() {
1472        for symbol_len in [4096_usize, 1024_usize, 512_usize] {
1473            let a = vec![0xAA_u8; symbol_len];
1474            let b = vec![0x55_u8; symbol_len];
1475            let mut sum = a.clone();
1476            let layout = symbol_add_assign(&mut sum, &b).expect("symbol add");
1477            assert_eq!(
1478                layout,
1479                WideChunkLayout::for_len(symbol_len),
1480                "bead_id={RAPTORQ_BEAD_ID} case=symbol_len_layout_consistency len={symbol_len}"
1481            );
1482            assert!(
1483                sum.iter().all(|byte| *byte == (0xAA_u8 ^ 0x55_u8)),
1484                "bead_id={RAPTORQ_BEAD_ID} case=symbol_add_expected_xor len={symbol_len}"
1485            );
1486        }
1487    }
1488
1489    #[test]
1490    fn test_gf256_arithmetic_matches_asupersync() {
1491        for a in 0_u8..=u8::MAX {
1492            for b in 0_u8..=u8::MAX {
1493                assert_eq!(
1494                    gf256_mul_byte(a, b),
1495                    (Gf256(a) * Gf256(b)).raw(),
1496                    "bead_id={RAPTORQ_BEAD_ID} case=gf256_mul_parity a=0x{a:02X} b=0x{b:02X}"
1497                );
1498            }
1499        }
1500    }
1501
1502    #[test]
1503    fn test_symbol_ops_match_asupersync_gf256_slices() {
1504        for symbol_len in [512_usize, 1024_usize, 4096_usize] {
1505            let src = deterministic_payload(symbol_len, 0xA5A5_0101);
1506            let dst = deterministic_payload(symbol_len, 0x5A5A_0202);
1507
1508            let mut ours_add = dst.clone();
1509            symbol_add_assign(&mut ours_add, &src).expect("symbol add");
1510            let mut as_add = dst.clone();
1511            gf256_add_slice(&mut as_add, &src);
1512            assert_eq!(
1513                ours_add, as_add,
1514                "bead_id={RAPTORQ_BEAD_ID} case=asupersync_parity_add len={symbol_len}"
1515            );
1516
1517            for coeff in [0_u8, 1_u8, 0x53_u8, 0xA7_u8] {
1518                let mut ours_mul = vec![0_u8; symbol_len];
1519                symbol_mul_into(coeff, &src, &mut ours_mul).expect("symbol mul");
1520                let mut as_mul = src.clone();
1521                gf256_mul_slice(&mut as_mul, Gf256(coeff));
1522                assert_eq!(
1523                    ours_mul, as_mul,
1524                    "bead_id={RAPTORQ_BEAD_ID} case=asupersync_parity_mul len={symbol_len} coeff=0x{coeff:02X}"
1525                );
1526
1527                let mut ours_addmul = dst.clone();
1528                symbol_addmul_assign(&mut ours_addmul, coeff, &src).expect("symbol addmul");
1529                let mut as_addmul = dst.clone();
1530                gf256_addmul_slice(&mut as_addmul, &src, Gf256(coeff));
1531                assert_eq!(
1532                    ours_addmul, as_addmul,
1533                    "bead_id={RAPTORQ_BEAD_ID} case=asupersync_parity_addmul len={symbol_len} coeff=0x{coeff:02X}"
1534                );
1535            }
1536        }
1537    }
1538
1539    #[test]
1540    fn test_encode_single_source_block() {
1541        let config = raptorq_config(512, 1.25);
1542        let symbol_size = usize::from(config.encoding.symbol_size);
1543        let k = 8_usize;
1544        let data = deterministic_payload(k * symbol_size, 0x0102_0304);
1545        let object_id = AsObjectId::new_for_test(1201);
1546        tracing::info!(
1547            bead_id = RAPTORQ_BEAD_ID,
1548            case = "test_encode_single_source_block",
1549            symbol_size,
1550            requested_k = k,
1551            "encoding source block"
1552        );
1553        let (symbols, source_symbols) = encode_symbols(config, object_id, &data);
1554        let (sources, repairs) = split_source_and_repair(&symbols, source_symbols);
1555
1556        assert_eq!(
1557            source_symbols, k,
1558            "bead_id={RAPTORQ_BEAD_ID} case=encode_single_block_source_count"
1559        );
1560        assert_eq!(
1561            sources.len(),
1562            source_symbols,
1563            "bead_id={RAPTORQ_BEAD_ID} case=encode_single_block_source_partition"
1564        );
1565        assert!(
1566            !repairs.is_empty(),
1567            "bead_id={RAPTORQ_BEAD_ID} case=encode_single_block_repair_present"
1568        );
1569        assert!(
1570            symbols.iter().all(|symbol| symbol.len() == symbol_size),
1571            "bead_id={RAPTORQ_BEAD_ID} case=encode_single_block_symbol_size_consistent"
1572        );
1573    }
1574
1575    #[test]
1576    fn test_decode_exact_k_symbols() {
1577        let symbol_size = 512_usize;
1578        let k = 16_usize;
1579        let seed = 0x0BAD_CAFE_u64;
1580        let source = low_level_source_block(k, symbol_size, seed);
1581        let encoder =
1582            SystematicEncoder::new(&source, symbol_size, seed).expect("systematic encoder");
1583        let decoder = InactivationDecoder::new(k, symbol_size, seed);
1584        let params = decoder.params();
1585        let base_rows = params.s + params.h;
1586        let constraints = ConstraintMatrix::build(params, seed);
1587        let mut received = decoder.constraint_symbols();
1588        let source_indexes = (0..k).collect::<Vec<_>>();
1589        append_source_received_symbols(
1590            &mut received,
1591            &constraints,
1592            base_rows,
1593            params.k_prime,
1594            symbol_size,
1595            &source,
1596            &source_indexes,
1597        );
1598
1599        tracing::warn!(
1600            bead_id = RAPTORQ_BEAD_ID,
1601            case = "test_decode_exact_k_symbols",
1602            source_symbols = k,
1603            "decoding with minimum symbol count (fragile recovery threshold)"
1604        );
1605        let decode_outcome = decoder
1606            .decode(&received)
1607            .expect("decode exact-k must succeed");
1608        assert_eq!(
1609            decode_outcome.source, source,
1610            "bead_id={RAPTORQ_BEAD_ID} case=decode_exact_k_symbols_roundtrip"
1611        );
1612        assert_eq!(
1613            decode_outcome.intermediate[0].len(),
1614            symbol_size,
1615            "bead_id={RAPTORQ_BEAD_ID} case=decode_exact_k_symbol_size"
1616        );
1617        assert_eq!(
1618            encoder.intermediate_symbol(0),
1619            decode_outcome.intermediate[0],
1620            "bead_id={RAPTORQ_BEAD_ID} case=decode_exact_k_intermediate_consistency"
1621        );
1622    }
1623
1624    #[test]
1625    fn test_decode_with_repair_symbols() {
1626        let symbol_size = 512_usize;
1627        let k = 16_usize;
1628        let seed = 0xABC0_FED1_u64;
1629        let source = low_level_source_block(k, symbol_size, seed);
1630        let encoder =
1631            SystematicEncoder::new(&source, symbol_size, seed).expect("systematic encoder");
1632        let decoder = InactivationDecoder::new(k, symbol_size, seed);
1633        let params = decoder.params();
1634        let base_rows = params.s + params.h;
1635        let constraints = ConstraintMatrix::build(params, seed);
1636
1637        let mut received = decoder.constraint_symbols();
1638        let source_indexes = (1..k).collect::<Vec<_>>();
1639        append_source_received_symbols(
1640            &mut received,
1641            &constraints,
1642            base_rows,
1643            params.k_prime,
1644            symbol_size,
1645            &source,
1646            &source_indexes,
1647        );
1648
1649        let repair_esi = u32::try_from(k).expect("k fits u32");
1650        let (columns, coefficients) = decoder
1651            .repair_equation_rfc6330(repair_esi)
1652            .expect("first repair symbol should have an RFC6330 repair equation");
1653        let repair_data = encoder.repair_symbol(repair_esi);
1654        received.push(ReceivedSymbol::repair(
1655            repair_esi,
1656            columns,
1657            coefficients,
1658            repair_data,
1659        ));
1660
1661        let decode_outcome = decoder
1662            .decode(&received)
1663            .expect("decode with one repair must succeed");
1664        assert_eq!(
1665            decode_outcome.source, source,
1666            "bead_id={RAPTORQ_BEAD_ID} case=decode_with_repair_roundtrip"
1667        );
1668    }
1669
1670    #[test]
1671    fn test_decode_insufficient_symbols() {
1672        let symbol_size = 512_usize;
1673        let k = 8_usize;
1674        let seed = 0xDEAD_BEEF_u64;
1675        let source = low_level_source_block(k, symbol_size, seed);
1676        let decoder = InactivationDecoder::new(k, symbol_size, seed);
1677        let params = decoder.params();
1678        let base_rows = params.s + params.h;
1679        let constraints = ConstraintMatrix::build(params, seed);
1680        let mut received = decoder.constraint_symbols();
1681        let source_indexes = (0..k.saturating_sub(1)).collect::<Vec<_>>();
1682        append_source_received_symbols(
1683            &mut received,
1684            &constraints,
1685            base_rows,
1686            params.k_prime,
1687            symbol_size,
1688            &source,
1689            &source_indexes,
1690        );
1691
1692        let decode = decoder.decode(&received);
1693        assert!(
1694            decode.is_err(),
1695            "bead_id={RAPTORQ_BEAD_ID} case=decode_insufficient_symbols unexpectedly succeeded"
1696        );
1697        if let Err(err) = decode {
1698            tracing::error!(
1699                bead_id = RAPTORQ_BEAD_ID,
1700                case = "test_decode_insufficient_symbols",
1701                error = ?err,
1702                "decode failed as expected due to insufficient symbols"
1703            );
1704        }
1705    }
1706
1707    #[test]
1708    fn test_symbol_size_alignment() {
1709        let config = raptorq_config(4096, 1.20);
1710        let symbol_size = usize::from(config.encoding.symbol_size);
1711        let data = deterministic_payload(symbol_size * 3, 0x600D_1111);
1712        let object_id = AsObjectId::new_for_test(1205);
1713        let (symbols, source_symbols) = encode_symbols(config, object_id, &data);
1714
1715        assert_eq!(
1716            source_symbols, 3,
1717            "bead_id={RAPTORQ_BEAD_ID} case=symbol_size_alignment_source_count"
1718        );
1719        assert!(
1720            symbol_size.is_power_of_two(),
1721            "bead_id={RAPTORQ_BEAD_ID} case=symbol_size_alignment_power_of_two"
1722        );
1723        assert_eq!(
1724            symbol_size % 512,
1725            0,
1726            "bead_id={RAPTORQ_BEAD_ID} case=symbol_size_alignment_sector_multiple"
1727        );
1728        assert!(
1729            symbols.iter().all(|symbol| symbol.len() == symbol_size),
1730            "bead_id={RAPTORQ_BEAD_ID} case=symbol_size_alignment_symbol_lengths"
1731        );
1732    }
1733
1734    #[test]
1735    fn prop_encode_decode_roundtrip() {
1736        for seed in [11_u64, 29_u64, 43_u64, 71_u64] {
1737            for k in [8_usize, 16_usize] {
1738                let config = raptorq_config(512, 1.30);
1739                let symbol_size = usize::from(config.encoding.symbol_size);
1740                let data = deterministic_payload(k * symbol_size - 17, seed);
1741                let object_id = AsObjectId::new_for_test(2000 + seed);
1742                let (symbols, source_symbols) = encode_symbols(config.clone(), object_id, &data);
1743                let (sources, _) = split_source_and_repair(&symbols, source_symbols);
1744                let subset = sources
1745                    .iter()
1746                    .take(source_symbols)
1747                    .cloned()
1748                    .collect::<Vec<_>>();
1749                let decoded = decode_symbols(config, object_id, data.len(), source_symbols, subset)
1750                    .expect("property roundtrip decode");
1751                assert_eq!(
1752                    decoded, data,
1753                    "bead_id={RAPTORQ_BEAD_ID} case=prop_encode_decode_roundtrip seed={seed} k={k}"
1754                );
1755            }
1756        }
1757    }
1758
1759    #[test]
1760    fn prop_any_k_of_n_suffices() {
1761        let symbol_size = 512_usize;
1762        let k = 16_usize;
1763        let sources = (0..k)
1764            .map(|symbol_idx| {
1765                deterministic_payload(
1766                    symbol_size,
1767                    0x5000_0000 + u64::try_from(symbol_idx).expect("index fits u64"),
1768                )
1769            })
1770            .collect::<Vec<_>>();
1771
1772        let mut parity = vec![0_u8; symbol_size];
1773        for source in &sources {
1774            symbol_add_assign(&mut parity, source).expect("parity construction");
1775        }
1776
1777        for omitted in 0..=k {
1778            let rebuilt = if omitted == k {
1779                sources.clone()
1780            } else {
1781                let mut recovered = parity.clone();
1782                for (index, source) in sources.iter().enumerate() {
1783                    if index != omitted {
1784                        symbol_add_assign(&mut recovered, source).expect("single-erasure recovery");
1785                    }
1786                }
1787                let mut rebuilt = sources.clone();
1788                rebuilt[omitted] = recovered;
1789                rebuilt
1790            };
1791
1792            assert_eq!(
1793                rebuilt.len(),
1794                k,
1795                "bead_id={RAPTORQ_BEAD_ID} case=prop_any_k_of_n_subset_size omitted_index={omitted}"
1796            );
1797            assert_eq!(
1798                rebuilt, sources,
1799                "bead_id={RAPTORQ_BEAD_ID} case=prop_any_k_of_n_suffices omitted_index={omitted}"
1800            );
1801        }
1802    }
1803
1804    #[test]
1805    fn prop_symbol_size_consistent() {
1806        for symbol_size in [512_u16, 1024_u16, 4096_u16] {
1807            for k in [4_usize, 8_usize] {
1808                let config = raptorq_config(symbol_size, 1.25);
1809                let size = usize::from(symbol_size);
1810                let object_id = AsObjectId::new_for_test(
1811                    u64::from(symbol_size) * 100 + u64::try_from(k).expect("k fits u64"),
1812                );
1813                let data = deterministic_payload(k * size - 3, u64::from(symbol_size));
1814                let (symbols, _) = encode_symbols(config, object_id, &data);
1815                assert!(
1816                    symbols.iter().all(|symbol| symbol.len() == size),
1817                    "bead_id={RAPTORQ_BEAD_ID} case=prop_symbol_size_consistent symbol_size={symbol_size} k={k}"
1818                );
1819            }
1820        }
1821    }
1822
1823    #[test]
1824    fn test_e2e_symbol_ops_in_encode_decode_roundtrip() {
1825        for (run, k) in [8_usize, 16_usize, 64_usize].iter().copied().enumerate() {
1826            let config = raptorq_config(512, 1.30);
1827            let symbol_size = usize::from(config.encoding.symbol_size);
1828            let data = deterministic_payload(
1829                k * symbol_size,
1830                0x4455_6677 + u64::try_from(run).expect("run fits u64"),
1831            );
1832            let object_id = AsObjectId::new_for_test(3000 + u64::try_from(k).expect("k fits u64"));
1833
1834            tracing::info!(
1835                bead_id = RAPTORQ_BEAD_ID,
1836                case = "test_e2e_symbol_ops_in_encode_decode_roundtrip",
1837                k,
1838                symbol_size,
1839                "starting encode/decode roundtrip"
1840            );
1841            let (symbols, source_symbols) = encode_symbols(config.clone(), object_id, &data);
1842            let (sources, _) = split_source_and_repair(&symbols, source_symbols);
1843            let subset = sources
1844                .iter()
1845                .take(source_symbols)
1846                .cloned()
1847                .collect::<Vec<_>>();
1848            let decoded = decode_symbols(config, object_id, data.len(), source_symbols, subset)
1849                .expect("e2e decode must succeed");
1850
1851            assert_eq!(
1852                decoded, data,
1853                "bead_id={RAPTORQ_BEAD_ID} case=e2e_roundtrip_bytes k={k}"
1854            );
1855
1856            let mut source_parity = vec![0_u8; symbol_size];
1857            for chunk in data.chunks_exact(symbol_size) {
1858                symbol_add_assign(&mut source_parity, chunk).expect("source parity xor");
1859            }
1860
1861            let mut decoded_parity = vec![0_u8; symbol_size];
1862            for chunk in decoded.chunks_exact(symbol_size) {
1863                symbol_add_assign(&mut decoded_parity, chunk).expect("decoded parity xor");
1864            }
1865
1866            assert_eq!(
1867                decoded_parity, source_parity,
1868                "bead_id={RAPTORQ_BEAD_ID} case=e2e_symbol_ops_parity k={k}"
1869            );
1870        }
1871    }
1872}