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