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