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