1#![recursion_limit = "512"]
12#![allow(clippy::future_not_send)]
19#![allow(clippy::large_futures)]
25#![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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum OverflowPolicy {
83 DropBusy,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum ParallelismProfile {
90 Balanced,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct BulkheadConfig {
97 pub max_concurrent: usize,
99 pub queue_depth: usize,
101 pub overflow_policy: OverflowPolicy,
103}
104
105impl BulkheadConfig {
106 #[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 #[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 #[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#[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#[must_use]
170pub fn available_parallelism_or_one() -> usize {
171 std::thread::available_parallelism().map_or(1, NonZeroUsize::get)
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub struct WideChunkLayout {
177 pub u128_chunks: usize,
179 pub u64_chunks: usize,
181 pub tail_bytes: usize,
183}
184
185impl WideChunkLayout {
186 #[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#[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
239pub 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#[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
304pub 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
317pub 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
365pub 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
407pub 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#[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 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 pub fn run<T>(&self, work: impl FnOnce() -> T) -> Result<T> {
507 let _permit = self.try_acquire()?;
508 Ok(work())
509 }
510}
511
512#[derive(Debug)]
514pub struct BulkheadPermit<'a> {
515 bulkhead: &'a Bulkhead,
516 released: bool,
517}
518
519impl BulkheadPermit<'_> {
520 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#[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 pub fn begin_close(&self) {
575 self.closing.store(true, Ordering::Release);
576 }
577
578 #[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, ¶ms)
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 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 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 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 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 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 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 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}