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