#![allow(clippy::many_single_char_names)]
use crate::raptorq::gf256::{Gf256, gf256_add_slice, gf256_addmul_slice};
use crate::raptorq::rfc6330::{next_prime_ge, repair_indices_for_esi, try_tuple};
#[cfg(test)]
use crate::util::DetRng;
#[derive(Debug, Clone)]
pub struct SystematicParams {
pub k: usize,
pub k_prime: usize,
pub j: usize,
pub s: usize,
pub h: usize,
pub l: usize,
pub w: usize,
pub p: usize,
pub b: usize,
pub symbol_size: usize,
}
const SYSTEMATIC_INDEX_TABLE: &[(u32, u16, u16, u8, u32)] =
&include!("rfc6330_systematic_index_table.inc");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SystematicError {
EsiOverflow {
esi: u32,
padding_delta: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SystematicParamError {
UnsupportedSourceBlockSize {
requested: usize,
max_supported: usize,
},
KPrimeExceedsU32 {
k_prime: usize,
max_u32: usize,
},
RfcTableInvariantViolation {
invariant: &'static str,
details: String,
},
}
impl SystematicParams {
#[must_use]
pub fn for_source_block(k: usize, symbol_size: usize) -> Self {
assert!(k > 0, "source block must have at least one symbol");
Self::try_for_source_block(k, symbol_size).unwrap_or_else(|err| match err {
SystematicParamError::UnsupportedSourceBlockSize {
requested,
max_supported,
} => {
panic!(
"unsupported source block size K={requested}; supported range is 1..={max_supported}"
)
}
SystematicParamError::KPrimeExceedsU32 {
k_prime,
max_u32,
} => {
panic!(
"K' value {k_prime} exceeds u32::MAX ({max_u32}); ESI calculations would overflow in decoder"
)
}
SystematicParamError::RfcTableInvariantViolation {
invariant,
details,
} => {
panic!(
"RFC 6330 table invariant violation: {invariant} - {details}"
)
}
})
}
pub fn try_for_source_block(
k: usize,
symbol_size: usize,
) -> Result<Self, SystematicParamError> {
let max_supported = SYSTEMATIC_INDEX_TABLE
.last()
.map_or(0usize, |row| row.0 as usize);
if k == 0 || k > max_supported {
return Err(SystematicParamError::UnsupportedSourceBlockSize {
requested: k,
max_supported,
});
}
let idx = SYSTEMATIC_INDEX_TABLE.partition_point(|row| row.0 < k as u32);
debug_assert!(idx < SYSTEMATIC_INDEX_TABLE.len());
let (k_prime, j, s, h, w) = SYSTEMATIC_INDEX_TABLE[idx];
let k_prime = k_prime as usize;
if k_prime > u32::MAX as usize {
return Err(SystematicParamError::KPrimeExceedsU32 {
k_prime,
max_u32: u32::MAX as usize,
});
}
let j = j as usize;
let s = s as usize;
let h = h as usize;
let w = w as usize;
let l = k_prime + s + h;
let b = w
.checked_sub(s)
.ok_or(SystematicParamError::RfcTableInvariantViolation {
invariant: "W >= S",
details: format!("W={w} < S={s} in RFC table entry for K={k}"),
})?;
let p = l
.checked_sub(w)
.ok_or(SystematicParamError::RfcTableInvariantViolation {
invariant: "L >= W",
details: format!("L={l} < W={w} in RFC table entry for K={k}"),
})?;
Ok(Self {
k,
k_prime,
j,
s,
h,
l,
w,
p,
b,
symbol_size,
})
}
pub fn rfc_repair_equation(
&self,
esi: u32,
) -> Result<(Vec<usize>, Vec<Gf256>), SystematicError> {
let columns = self.rfc_repair_indices(esi)?;
let coefficients = vec![Gf256::ONE; columns.len()];
Ok((columns, coefficients))
}
fn rfc_repair_indices(&self, esi: u32) -> Result<Vec<usize>, SystematicError> {
let padding_delta = u32::try_from(self.k_prime - self.k)
.expect("RFC systematic padding delta must fit in u32");
let repair_isi = esi
.checked_add(padding_delta)
.ok_or(SystematicError::EsiOverflow { esi, padding_delta })?;
Ok(repair_indices_for_esi(self.j, self.w, self.p, repair_isi))
}
}
#[cfg(test)]
#[derive(Debug, Clone)]
pub struct RobustSoliton {
cdf: Vec<u32>,
k: usize,
}
#[cfg(test)]
impl RobustSoliton {
#[must_use]
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
pub fn new(k: usize, c: f64, delta: f64) -> Self {
assert!(k > 0);
let k_f = k as f64;
let r = c * (k_f / delta).ln() * k_f.sqrt();
let mut rho = vec![0.0f64; k + 1];
rho[1] = 1.0 / k_f;
for (d, value) in rho.iter_mut().enumerate().skip(2) {
let d_f = d as f64;
*value = 1.0 / (d_f * (d_f - 1.0));
}
let mut tau = vec![0.0f64; k + 1];
let threshold = (k_f / r).floor() as usize;
let max_d = k.min(threshold.max(1));
for (d, value) in tau.iter_mut().enumerate().skip(1).take(max_d) {
if d < threshold {
*value = r / (d as f64 * k_f);
} else {
*value = r * (r / delta).ln() / k_f;
}
}
let mut mu: Vec<f64> = rho.iter().zip(tau.iter()).map(|(r, t)| r + t).collect();
let sum: f64 = mu.iter().sum();
if sum > 0.0 {
for m in &mut mu {
*m /= sum;
}
}
let mut cdf = Vec::with_capacity(k + 1);
let mut cumulative = 0.0f64;
let scale = f64::from(u32::MAX);
for &p in &mu {
cumulative += p;
cdf.push((cumulative * scale).min(scale) as u32);
}
if let Some(last) = cdf.last_mut() {
*last = u32::MAX;
}
Self { cdf, k }
}
#[must_use]
pub fn sample(&self, rand_val: u32) -> usize {
match self.cdf.binary_search(&rand_val) {
Ok(idx) | Err(idx) => idx.max(1).min(self.k),
}
}
#[must_use]
#[inline]
pub fn k(&self) -> usize {
self.k
}
#[must_use]
#[inline]
pub fn max_degree(&self) -> usize {
self.k
}
#[must_use]
pub fn validate_params(k: usize, c: f64, delta: f64) -> Option<&'static str> {
if k == 0 {
return Some("k must be positive");
}
if c <= 0.0 || !c.is_finite() {
return Some("c must be a positive finite number");
}
if delta <= 0.0 || delta >= 1.0 || !delta.is_finite() {
return Some("delta must be in (0, 1)");
}
None
}
}
#[derive(Debug, Clone)]
pub struct ConstraintMatrix {
data: Vec<Gf256>,
pub rows: usize,
pub cols: usize,
}
impl ConstraintMatrix {
#[must_use]
pub fn zeros(rows: usize, cols: usize) -> Self {
const MAX_MATRIX_SIZE: usize = 1024 * 1024 * 1024; let n = rows.saturating_mul(cols).min(MAX_MATRIX_SIZE);
Self {
data: vec![Gf256::ZERO; n],
rows,
cols,
}
}
#[inline]
#[must_use]
pub fn get(&self, row: usize, col: usize) -> Gf256 {
self.data[row * self.cols + col]
}
#[inline]
pub fn set(&mut self, row: usize, col: usize, val: Gf256) {
self.data[row * self.cols + col] = val;
}
#[inline]
pub fn add_assign(&mut self, row: usize, col: usize, val: Gf256) {
self.data[row * self.cols + col] += val;
}
#[must_use]
pub fn build(params: &SystematicParams, seed: u64) -> Self {
let l = params.l;
let total_rows = params.s + params.h + params.k_prime;
let mut matrix = Self::zeros(total_rows, l);
build_ldpc_rows(&mut matrix, params, seed);
build_hdpc_rows(&mut matrix, params, seed);
build_lt_rows(&mut matrix, params, seed);
matrix
}
#[must_use]
pub fn solve(&self, rhs: &[Vec<u8>]) -> Option<Vec<Vec<u8>>> {
assert_eq!(rhs.len(), self.rows);
let symbol_size = if rhs.is_empty() { 0 } else { rhs[0].len() };
let n = self.cols;
let mut a = self.data.clone();
let cols = self.cols;
let mut b: Vec<Vec<u8>> = rhs.to_vec();
let mut pivot_col = vec![usize::MAX; self.rows];
let mut used_col = vec![false; cols];
let mut used_row = vec![false; self.rows];
for step in 0..self.rows.min(n) {
let mut best: Option<(usize, usize, usize)> = None; for r in 0..self.rows {
if used_row[r] {
continue;
}
let mut deg = 0usize;
let mut first_col = None;
for c in 0..cols {
if !used_col[c] && !a[r * cols + c].is_zero() {
if first_col.is_none() {
first_col = Some(c);
}
deg += 1;
}
}
if let Some(fc) = first_col {
if best.is_none() || deg < best.expect("best solution must exist").2 {
best = Some((r, fc, deg));
if deg == 1 {
break; }
}
}
}
let Some((pivot_row, col, _)) = best else {
continue; };
used_row[pivot_row] = true;
if pivot_row != step {
for c in 0..cols {
let idx_a = step * cols + c;
let idx_b = pivot_row * cols + c;
a.swap(idx_a, idx_b);
}
b.swap(step, pivot_row);
pivot_col.swap(step, pivot_row);
used_row.swap(step, pivot_row);
}
let row = step;
used_col[col] = true;
pivot_col[row] = col;
let inv = a[row * cols + col].inv();
for c in 0..cols {
a[row * cols + c] *= inv;
}
gf256_mul_slice_inplace(&mut b[row], inv);
for other in 0..self.rows {
if other == row {
continue;
}
let factor = a[other * cols + col];
if factor.is_zero() {
continue;
}
for c in 0..cols {
let val = a[row * cols + c];
a[other * cols + c] += factor * val;
}
let row_rhs = std::mem::take(&mut b[row]);
gf256_addmul_slice(&mut b[other], &row_rhs, factor);
b[row] = row_rhs;
}
}
let mut col_has_pivot = vec![false; n];
for &col in &pivot_col {
if col < n {
col_has_pivot[col] = true;
}
}
if col_has_pivot.iter().any(|&has| !has) {
return None; }
let mut result = vec![vec![0u8; symbol_size]; n];
for (row, &col) in pivot_col.iter().enumerate() {
if col < n {
result[col].clone_from(&b[row]);
}
}
Some(result)
}
}
fn gf256_mul_slice_inplace(data: &mut [u8], c: Gf256) {
crate::raptorq::gf256::gf256_mul_slice(data, c);
}
fn build_ldpc_rows(matrix: &mut ConstraintMatrix, params: &SystematicParams, _seed: u64) {
let s = params.s;
let b = params.b;
let w = params.w;
let p = params.p;
for i in 0..b {
let a = 1 + i / s.max(1);
let mut row = i % s;
matrix.add_assign(row, i, Gf256::ONE);
row = (row + a) % s;
matrix.add_assign(row, i, Gf256::ONE);
row = (row + a) % s;
matrix.add_assign(row, i, Gf256::ONE);
}
for i in 0..s {
matrix.set(i, b + i, Gf256::ONE);
}
for i in 0..s {
matrix.set(i, (i % p) + w, Gf256::ONE);
matrix.set(i, ((i + 1) % p) + w, Gf256::ONE);
}
}
fn build_hdpc_rows(matrix: &mut ConstraintMatrix, params: &SystematicParams, _seed: u64) {
use crate::raptorq::rfc6330::rand;
let s = params.s;
let h = params.h;
let k_prime = params.k_prime;
let ks = k_prime + s;
if h == 0 {
return;
}
let mut hdpc = vec![vec![Gf256::ZERO; ks]; h];
for (row_idx, row) in hdpc.iter_mut().enumerate() {
row[ks - 1] = Gf256::ALPHA.pow((row_idx % 255) as u8);
}
for col in (0..ks.saturating_sub(1)).rev() {
for row in &mut hdpc {
row[col] = Gf256::ALPHA * row[col + 1];
}
let rand6 = rand((col + 1) as u32, 6, h as u32) as usize;
let rand7 = if h > 1 {
rand((col + 1) as u32, 7, (h - 1) as u32) as usize
} else {
0
};
let row1 = rand6;
let row2 = (rand6 + rand7 + 1) % h;
hdpc[row1][col] += Gf256::ONE;
hdpc[row2][col] += Gf256::ONE;
}
for (row, values) in hdpc.iter().enumerate() {
for (col, &val) in values.iter().enumerate() {
if !val.is_zero() {
matrix.set(s + row, col, val);
}
}
}
for r in 0..h {
matrix.set(s + r, ks + r, Gf256::ONE);
}
}
fn build_lt_rows(matrix: &mut ConstraintMatrix, params: &SystematicParams, _seed: u64) {
let s = params.s;
let h = params.h;
let k_prime = params.k_prime;
for i in 0..k_prime {
let row = s + h + i;
let esi = u32::try_from(i).expect("K' row index must fit in u32");
for col in repair_indices_for_esi(params.j, params.w, params.p, esi) {
matrix.set(row, col, Gf256::ONE);
}
}
}
#[derive(Debug, Clone, Default)]
pub struct EncodingStats {
pub source_symbol_count: usize,
pub ldpc_symbol_count: usize,
pub hdpc_symbol_count: usize,
pub intermediate_symbol_count: usize,
pub symbol_size: usize,
pub repair_symbols_generated: usize,
pub seed: u64,
pub degree_min: usize,
pub degree_max: usize,
pub degree_sum: usize,
pub degree_count: usize,
pub systematic_bytes_emitted: usize,
pub repair_bytes_emitted: usize,
}
impl EncodingStats {
#[must_use]
pub fn average_degree(&self) -> f64 {
if self.degree_count == 0 {
0.0
} else {
#[allow(clippy::cast_precision_loss)]
let sum = self.degree_sum as f64;
#[allow(clippy::cast_precision_loss)]
let count = self.degree_count as f64;
sum / count
}
}
#[must_use]
pub fn overhead_ratio(&self) -> f64 {
if self.source_symbol_count == 0 {
0.0
} else {
#[allow(clippy::cast_precision_loss)]
let intermediate = self.intermediate_symbol_count as f64;
#[allow(clippy::cast_precision_loss)]
let source = self.source_symbol_count as f64;
intermediate / source
}
}
#[must_use]
pub const fn total_bytes_emitted(&self) -> usize {
self.systematic_bytes_emitted + self.repair_bytes_emitted
}
#[must_use]
pub fn encoding_efficiency(&self) -> f64 {
let total = self.total_bytes_emitted();
if total == 0 {
0.0
} else {
#[allow(clippy::cast_precision_loss)]
let sys = self.systematic_bytes_emitted as f64;
#[allow(clippy::cast_precision_loss)]
let tot = total as f64;
sys / tot
}
}
#[must_use]
pub fn repair_overhead(&self) -> f64 {
if self.systematic_bytes_emitted == 0 {
0.0
} else {
#[allow(clippy::cast_precision_loss)]
let repair = self.repair_bytes_emitted as f64;
#[allow(clippy::cast_precision_loss)]
let sys = self.systematic_bytes_emitted as f64;
repair / sys
}
}
}
impl std::fmt::Display for EncodingStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"EncodingStats(K={}, S={}, H={}, L={}, sym={}B, repairs={}, \
bytes={}sys+{}rep, avg_deg={:.1}, overhead={:.2})",
self.source_symbol_count,
self.ldpc_symbol_count,
self.hdpc_symbol_count,
self.intermediate_symbol_count,
self.symbol_size,
self.repair_symbols_generated,
self.systematic_bytes_emitted,
self.repair_bytes_emitted,
self.average_degree(),
self.overhead_ratio(),
)
}
}
#[derive(Debug, Clone)]
pub struct EmittedSymbol {
pub esi: u32,
pub data: Vec<u8>,
pub is_source: bool,
pub degree: usize,
}
#[derive(Debug)]
pub struct SystematicEncoder {
params: SystematicParams,
intermediate: Vec<Vec<u8>>,
source_symbols: Vec<Vec<u8>>,
seed: u64,
stats: EncodingStats,
systematic_emitted: bool,
next_repair_esi: u32,
}
impl SystematicEncoder {
#[must_use]
pub fn new(source_symbols: &[Vec<u8>], symbol_size: usize, seed: u64) -> Option<Self> {
let k = source_symbols.len();
assert!(k > 0, "need at least one source symbol");
assert!(
source_symbols.iter().all(|s| s.len() == symbol_size),
"all source symbols must be symbol_size bytes"
);
let params = SystematicParams::for_source_block(k, symbol_size);
let matrix = ConstraintMatrix::build(¶ms, seed);
let mut rhs = Vec::with_capacity(matrix.rows);
for _ in 0..params.s + params.h {
rhs.push(vec![0u8; symbol_size]);
}
for sym in source_symbols {
rhs.push(sym.clone());
}
for _ in k..params.k_prime {
rhs.push(vec![0u8; symbol_size]);
}
let intermediate = matrix.solve(&rhs)?;
let stats = EncodingStats {
source_symbol_count: k,
ldpc_symbol_count: params.s,
hdpc_symbol_count: params.h,
intermediate_symbol_count: params.l,
symbol_size,
seed,
repair_symbols_generated: 0,
degree_min: 0,
degree_max: 0,
degree_sum: 0,
degree_count: 0,
systematic_bytes_emitted: 0,
repair_bytes_emitted: 0,
};
Some(Self {
params,
intermediate,
source_symbols: source_symbols.to_vec(),
seed,
stats,
systematic_emitted: false,
next_repair_esi: k as u32,
})
}
#[must_use]
pub const fn params(&self) -> &SystematicParams {
&self.params
}
#[must_use]
pub fn repair_symbol(&self, esi: u32) -> Vec<u8> {
self.repair_symbol_with_degree(esi).0
}
pub fn repair_symbol_into(&self, esi: u32, buf: &mut [u8]) {
assert!(
buf.len() >= self.params.symbol_size,
"buf too small: {} < {}",
buf.len(),
self.params.symbol_size
);
self.repair_symbol_into_with_degree(esi, buf);
}
#[must_use]
#[inline]
pub fn intermediate_symbol(&self, i: usize) -> &[u8] {
&self.intermediate[i]
}
#[must_use]
#[inline]
pub fn stats(&self) -> &EncodingStats {
&self.stats
}
#[must_use]
#[inline]
pub const fn seed(&self) -> u64 {
self.seed
}
pub fn emit_systematic(&mut self) -> Vec<EmittedSymbol> {
if self.systematic_emitted {
return Vec::new();
}
let symbols: Vec<EmittedSymbol> = self
.source_symbols
.iter()
.enumerate()
.map(|(i, data)| EmittedSymbol {
esi: i as u32,
data: data.clone(),
is_source: true,
degree: 1,
})
.collect();
debug_assert!(
symbols.iter().enumerate().all(|(i, s)| s.esi == i as u32),
"systematic emission ESIs must be 0..K in order"
);
let bytes: usize = symbols.iter().map(|s| s.data.len()).sum();
self.stats.systematic_bytes_emitted += bytes;
self.systematic_emitted = true;
symbols
}
pub fn emit_repair(&mut self, count: usize) -> Vec<EmittedSymbol> {
let start_esi = self.next_repair_esi;
let symbol_size = self.params.symbol_size;
let mut result = Vec::with_capacity(count);
let mut buf = vec![0u8; symbol_size];
for i in 0..count {
let i_u32 = match u32::try_from(i) {
Ok(i_u32) => i_u32,
Err(_) => {
#[cfg(feature = "tracing-integration")]
tracing::warn!(
"repair count index {i} exceeds u32, stopping symbol generation"
);
break;
}
};
let esi = match start_esi.checked_add(i_u32) {
Some(esi) => esi,
None => {
#[cfg(feature = "tracing-integration")]
tracing::warn!(
"repair ESI overflow at start_esi={start_esi} + i={i_u32}, stopping symbol generation"
);
break;
}
};
let degree = self.repair_symbol_into_with_degree(esi, &mut buf);
let data = buf[..symbol_size].to_vec();
self.stats.repair_symbols_generated += 1;
if self.stats.degree_count == 0 {
self.stats.degree_min = degree;
} else {
self.stats.degree_min = self.stats.degree_min.min(degree);
}
self.stats.degree_max = self.stats.degree_max.max(degree);
self.stats.degree_sum += degree;
self.stats.degree_count += 1;
self.stats.repair_bytes_emitted += data.len();
result.push(EmittedSymbol {
esi,
data,
is_source: false,
degree,
});
}
let actual_count = result.len(); let count_u32 = u32::try_from(actual_count).unwrap_or(u32::MAX);
self.next_repair_esi = start_esi.saturating_add(count_u32);
if count != 0 {
self.systematic_emitted = true;
}
debug_assert!(
result
.iter()
.enumerate()
.all(|(i, s)| s.esi == start_esi + i as u32),
"repair emission ESIs must be monotonically ascending"
);
debug_assert!(
result.iter().all(|s| s.esi >= self.params.k as u32),
"repair ESIs must be >= K"
);
result
}
pub fn emit_all(&mut self, repair_count: usize) -> Vec<EmittedSymbol> {
let mut result: Vec<EmittedSymbol> = self.emit_systematic();
result.extend(self.emit_repair(repair_count));
debug_assert!(
result.windows(2).all(|w| w[0].esi < w[1].esi),
"combined emission must have strictly ascending ESIs"
);
result
}
#[must_use]
pub const fn next_repair_esi(&self) -> u32 {
self.next_repair_esi
}
#[must_use]
pub const fn systematic_emitted(&self) -> bool {
self.systematic_emitted
}
fn repair_symbol_into_with_degree(&self, esi: u32, buf: &mut [u8]) -> usize {
let symbol_size = self.params.symbol_size;
buf[..symbol_size].fill(0);
let padding_delta = u32::try_from(self.params.k_prime - self.params.k)
.expect("RFC systematic padding delta must fit in u32");
let Some(repair_isi) = esi.checked_add(padding_delta) else {
debug_assert!(
false,
"repair ESI overflowed while mapping to RFC 6330 repair ISI"
);
return 0;
};
let Some(pi_modulus) = next_prime_ge(self.params.p) else {
debug_assert!(false, "RFC repair PI modulus must fit for encoder params");
return 0;
};
let Some(lt_tuple) = try_tuple(
self.params.j,
self.params.w,
self.params.p,
pi_modulus,
repair_isi,
) else {
debug_assert!(false, "RFC repair tuple must be valid for encoder params");
return 0;
};
let mut degree = 0usize;
let mut lt_index = lt_tuple.b % self.params.w;
gf256_add_slice(&mut buf[..symbol_size], &self.intermediate[lt_index]);
degree += 1;
for _ in 1..lt_tuple.d {
lt_index = (lt_index + lt_tuple.a) % self.params.w;
gf256_add_slice(&mut buf[..symbol_size], &self.intermediate[lt_index]);
degree += 1;
}
let mut pi_index = lt_tuple.b1 % pi_modulus;
while pi_index >= self.params.p {
pi_index = (pi_index + lt_tuple.a1) % pi_modulus;
}
gf256_add_slice(
&mut buf[..symbol_size],
&self.intermediate[self.params.w + pi_index],
);
degree += 1;
for _ in 1..lt_tuple.d1 {
pi_index = (pi_index + lt_tuple.a1) % pi_modulus;
while pi_index >= self.params.p {
pi_index = (pi_index + lt_tuple.a1) % pi_modulus;
}
gf256_add_slice(
&mut buf[..symbol_size],
&self.intermediate[self.params.w + pi_index],
);
degree += 1;
}
debug_assert_eq!(degree, lt_tuple.d + lt_tuple.d1);
degree
}
fn repair_symbol_with_degree(&self, esi: u32) -> (Vec<u8>, usize) {
let mut result = vec![0u8; self.params.symbol_size];
let degree = self.repair_symbol_into_with_degree(esi, &mut result);
(result, degree)
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::pedantic,
clippy::nursery,
clippy::expect_fun_call,
clippy::map_unwrap_or,
clippy::cast_possible_wrap,
clippy::future_not_send
)]
use super::*;
use crate::raptorq::decoder::{InactivationDecoder, ReceivedSymbol};
use proptest::prelude::*;
use raptorq::{
ObjectTransmissionInformation as RaptorqRsObjectTransmissionInformation,
SourceBlockEncoder as RaptorqRsSourceBlockEncoder,
};
fn make_source_symbols(k: usize, symbol_size: usize) -> Vec<Vec<u8>> {
(0..k)
.map(|i| {
(0..symbol_size)
.map(|j| ((i * 37 + j * 13 + 7) % 256) as u8)
.collect()
})
.collect()
}
fn make_encoder(k: usize, symbol_size: usize, seed: u64) -> SystematicEncoder {
let source = make_source_symbols(k, symbol_size);
SystematicEncoder::new(&source, symbol_size, seed).unwrap_or_else(|| {
panic!(
"expected encoder construction to succeed for supported parameters: \
k={k}, symbol_size={symbol_size}, seed={seed}"
)
})
}
fn failure_context(
scenario_id: &str,
seed: u64,
k: usize,
symbol_size: usize,
parameter_set: &str,
replay_ref: &str,
) -> String {
format!(
"scenario_id={scenario_id} seed={seed} parameter_set={parameter_set},k={k},symbol_size={symbol_size} replay_ref={replay_ref}"
)
}
fn decode_emitted_symbols(
symbols: &[EmittedSymbol],
k: usize,
symbol_size: usize,
seed: u64,
) -> Result<Vec<Vec<u8>>, crate::raptorq::decoder::DecodeError> {
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let mut received = decoder.constraint_symbols();
for symbol in symbols {
let row = if symbol.is_source {
ReceivedSymbol::source(symbol.esi, symbol.data.clone())
} else {
let (columns, coefficients) = decoder.repair_equation(symbol.esi).unwrap();
ReceivedSymbol::repair(symbol.esi, columns, coefficients, symbol.data.clone())
};
received.push(row);
}
decoder.decode(&received).map(|decoded| decoded.source)
}
#[test]
fn params_small() {
let p = SystematicParams::for_source_block(4, 64);
assert_eq!(p.k, 4);
assert_eq!(p.k_prime, 10);
assert_eq!(p.j, 254);
assert_eq!(p.s, 7);
assert_eq!(p.h, 10);
assert_eq!(p.w, 17);
assert_eq!(p.b, p.w - p.s);
assert_eq!(p.p, p.l - p.w);
assert_eq!(p.l, p.k_prime + p.s + p.h);
}
#[test]
fn params_medium() {
let p = SystematicParams::for_source_block(100, 256);
assert_eq!(p.k, 100);
assert_eq!(p.k_prime, 101);
assert_eq!(p.j, 562);
assert_eq!(p.s, 17);
assert_eq!(p.h, 10);
assert_eq!(p.w, 113);
assert_eq!(p.b, p.w - p.s);
assert_eq!(p.p, p.l - p.w);
assert_eq!(p.l, p.k_prime + p.s + p.h);
}
#[test]
fn params_lookup_uses_smallest_k_prime_ge_k() {
let p = SystematicParams::for_source_block(11, 64);
assert_eq!(p.k, 11);
assert_eq!(p.k_prime, 12);
assert_eq!(p.j, 630);
assert_eq!(p.s, 7);
assert_eq!(p.h, 10);
assert_eq!(p.w, 19);
}
#[test]
fn params_k_prime_ladder_lower_edge_boundary() {
const FIRST_ROW_K_PRIME: usize = 10;
const FIRST_ROW_J: usize = 254;
const FIRST_ROW_S: usize = 7;
const FIRST_ROW_H: usize = 10;
const FIRST_ROW_W: usize = 17;
const SYMBOL_SIZE: usize = 64;
for k in 1..=10 {
let p = SystematicParams::for_source_block(k, SYMBOL_SIZE);
assert_eq!(p.k, k, "k field must echo input for K={k}");
assert_eq!(
p.k_prime, FIRST_ROW_K_PRIME,
"K={k}: K' must round up to {FIRST_ROW_K_PRIME} (first table row)"
);
assert_eq!(p.j, FIRST_ROW_J, "K={k}: J(K')");
assert_eq!(p.s, FIRST_ROW_S, "K={k}: S(K')");
assert_eq!(p.h, FIRST_ROW_H, "K={k}: H(K')");
assert_eq!(p.w, FIRST_ROW_W, "K={k}: W(K')");
assert_eq!(
p.l,
FIRST_ROW_K_PRIME + FIRST_ROW_S + FIRST_ROW_H,
"K={k}: L = K' + S + H invariant"
);
assert_eq!(p.b, p.w - p.s, "K={k}: B = W - S invariant");
assert_eq!(p.p, p.l - p.w, "K={k}: P = L - W invariant");
assert_eq!(p.symbol_size, SYMBOL_SIZE);
}
}
#[test]
fn k_prime_validation_prevents_esi_overflow() {
let max_table_k_prime = SYSTEMATIC_INDEX_TABLE
.iter()
.map(|(k_prime, _, _, _, _)| *k_prime as usize)
.max()
.expect("systematic index table is non-empty");
assert!(max_table_k_prime <= u32::MAX as usize);
assert!(SystematicParams::try_for_source_block(1, 64).is_ok());
}
#[test]
fn rfc6330_systematic_index_table_stratified_k_coverage_decodes() {
let sample_k_values: &[usize] = &[
4, 5, 8, 10, 11, 12, 13, 18, 50, 100, 250, 500, 1000, 2500, 5000,
56_000, 56_403,
];
const SYMBOL_SIZE: usize = 4;
const SEED: u64 = 0xCAFE_BABE_DEAD_BEEFu64;
for &k in sample_k_values {
let params = SystematicParams::try_for_source_block(k, SYMBOL_SIZE)
.unwrap_or_else(|err| panic!("K={k} table lookup must succeed; got {err:?}"));
assert!(
params.k_prime >= k,
"K={k}: K' = {} must satisfy K' >= K (RFC 6330 §5.3.1)",
params.k_prime
);
assert!(
params.l == params.k_prime + params.s + params.h,
"K={k}: L = {} must equal K' + S + H = {} + {} + {}",
params.l,
params.k_prime,
params.s,
params.h
);
if k <= 1000 {
let source = make_source_symbols(k, SYMBOL_SIZE);
let encoder = SystematicEncoder::new(&source, SYMBOL_SIZE, SEED);
assert!(
encoder.is_some(),
"K={k}: SystematicEncoder::new must succeed for table-defined params"
);
let encoder = encoder.unwrap();
assert_eq!(encoder.params().k, k);
assert_eq!(encoder.params().k_prime, params.k_prime);
}
}
}
#[test]
fn rfc6330_constraint_matrix_structural_dimensions_per_band() {
for &k in &[10usize, 50, 100, 250, 1000] {
let params = SystematicParams::for_source_block(k, 4);
let matrix = ConstraintMatrix::build(¶ms, 0xC0FFEE);
let expected_rows = params.s + params.h + params.k_prime;
let expected_cols = params.l;
assert_eq!(
matrix.rows, expected_rows,
"K={k}: rows = S+H+K' = {}+{}+{} = {expected_rows}; got {}",
params.s, params.h, params.k_prime, matrix.rows
);
assert_eq!(
matrix.cols, expected_cols,
"K={k}: cols = L = K'+S+H = {}+{}+{} = {expected_cols}; got {}",
params.k_prime, params.s, params.h, matrix.cols
);
assert!(
params.w < params.l,
"K={k}: W ({}) must be < L ({}) so P > 0",
params.w,
params.l
);
assert_eq!(
params.p,
params.l - params.w,
"K={k}: P = L - W invariant violated; P={}, L-W={}",
params.p,
params.l - params.w
);
assert_eq!(
params.b,
params.w - params.s,
"K={k}: B = W - S invariant violated; B={}, W-S={}",
params.b,
params.w - params.s
);
}
}
#[test]
fn rfc6330_constraint_matrix_band_byte_exact() {
use crate::util::det_hash::DetHasher;
use std::hash::Hasher;
fn hash_band(
label: &'static str,
k: usize,
matrix: &ConstraintMatrix,
row_start: usize,
row_end: usize,
) -> u64 {
let mut h = DetHasher::default();
h.write(label.as_bytes());
h.write_usize(k);
h.write_usize(row_end - row_start);
h.write_usize(matrix.cols);
for r in row_start..row_end {
for c in 0..matrix.cols {
h.write_u8(matrix.get(r, c).0);
}
}
h.finish()
}
const SEED: u64 = 0xC0FFEE;
struct BandPin {
k: usize,
ldpc: u64,
hdpc: u64,
lt: u64,
}
let pins = [
BandPin {
k: 10,
ldpc: 0x1263_4aac_9f21_dc02,
hdpc: 0xfc6b_1d86_17e7_6cc5,
lt: 0xa6d4_e465_9ffa_6c86,
},
BandPin {
k: 100,
ldpc: 0x1c11_0812_e259_8b70,
hdpc: 0x106c_bc39_aaf5_7e17,
lt: 0x66b0_16b5_1f10_b5b9,
},
];
let mut first_run_lines: Vec<String> = Vec::new();
for pin in &pins {
let params = SystematicParams::for_source_block(pin.k, 4);
let matrix = ConstraintMatrix::build(¶ms, SEED);
let s = params.s;
let h = params.h;
let k_prime = params.k_prime;
let ldpc_hash = hash_band("LDPC", pin.k, &matrix, 0, s);
let hdpc_hash = hash_band("HDPC", pin.k, &matrix, s, s + h);
let lt_hash = hash_band("LT", pin.k, &matrix, s + h, s + h + k_prime);
if pin.ldpc == 0 || pin.hdpc == 0 || pin.lt == 0 {
first_run_lines.push(format!(
" K={}: ldpc=0x{ldpc_hash:016x}u64, hdpc=0x{hdpc_hash:016x}u64, lt=0x{lt_hash:016x}u64",
pin.k
));
continue;
}
assert_eq!(
ldpc_hash, pin.ldpc,
"K={}: LDPC band byte-exact regression — \
build_ldpc_rows changed without updating the pin",
pin.k
);
assert_eq!(
hdpc_hash, pin.hdpc,
"K={}: HDPC band byte-exact regression — \
build_hdpc_rows changed without updating the pin",
pin.k
);
assert_eq!(
lt_hash, pin.lt,
"K={}: LT band byte-exact regression — \
build_lt_rows changed without updating the pin",
pin.k
);
}
if !first_run_lines.is_empty() {
panic!(
"br-asupersync-bqhtau: ConstraintMatrix band pins not \
yet captured. Update the BandPin entries with these \
observed values:\n{}",
first_run_lines.join("\n")
);
}
}
#[test]
fn rfc6330_systematic_index_table_mid_table_boundary_jumps() {
const BOUNDARIES: &[(usize, usize, usize)] = &[
(12, 12, 18),
(18, 18, 20),
(20, 20, 26),
(26, 26, 30),
(30, 30, 32),
(32, 32, 36),
(36, 36, 42),
(42, 42, 46),
(46, 46, 48),
(48, 48, 49),
(49, 49, 55),
(55, 55, 60),
(60, 60, 62),
(62, 62, 69),
(69, 69, 75),
(75, 75, 84),
(84, 84, 88),
(88, 88, 91),
(91, 91, 95),
(95, 95, 97),
];
const SYMBOL_SIZE: usize = 4;
for &(k_boundary, k_prime_at_boundary, k_prime_above) in BOUNDARIES {
let p_at = SystematicParams::for_source_block(k_boundary, SYMBOL_SIZE);
assert_eq!(
p_at.k, k_boundary,
"boundary K={k_boundary}: k field must echo input"
);
assert_eq!(
p_at.k_prime, k_prime_at_boundary,
"boundary K={k_boundary}: must land EXACTLY on K'={k_prime_at_boundary}"
);
let p_above = SystematicParams::for_source_block(k_boundary + 1, SYMBOL_SIZE);
assert_eq!(
p_above.k_prime,
k_prime_above,
"boundary K={k_boundary}+1={}: must round up to next row's K'={k_prime_above}, got K'={}",
k_boundary + 1,
p_above.k_prime
);
}
let p_max = SystematicParams::try_for_source_block(56_403, SYMBOL_SIZE)
.expect("K=K'_max=56403 must be accepted");
assert_eq!(
p_max.k_prime, 56_403,
"K=56403 must land exactly on the last table row K'=56403"
);
}
#[test]
fn rfc6330_systematic_index_table_rejects_out_of_range_k() {
let err = SystematicParams::try_for_source_block(0, 64).unwrap_err();
assert!(
matches!(
err,
SystematicParamError::UnsupportedSourceBlockSize { requested: 0, .. }
),
"K=0 must return UnsupportedSourceBlockSize, got {err:?}"
);
let err = SystematicParams::try_for_source_block(56_404, 64).unwrap_err();
assert!(
matches!(
err,
SystematicParamError::UnsupportedSourceBlockSize {
requested: 56_404,
..
}
),
"K=56404 must return UnsupportedSourceBlockSize, got {err:?}"
);
let err = SystematicParams::try_for_source_block(usize::MAX, 64).unwrap_err();
assert!(
matches!(err, SystematicParamError::UnsupportedSourceBlockSize { .. }),
"K=usize::MAX must fail closed, got {err:?}"
);
}
#[test]
fn rfc_repair_equation_rejects_esi_overflow() {
let params = SystematicParams::for_source_block(11, 64);
let padding_delta = u32::try_from(params.k_prime - params.k).unwrap();
assert_eq!(padding_delta, 1, "test requires a non-zero K' - K delta");
let result = params.rfc_repair_equation(u32::MAX);
assert_eq!(
result,
Err(SystematicError::EsiOverflow {
esi: u32::MAX,
padding_delta,
}),
"u32::MAX ESI should trigger overflow error instead of wrapping to avoid silent corruption"
);
}
#[test]
fn params_lookup_reports_unsupported_k() {
let err = SystematicParams::try_for_source_block(56404, 64).unwrap_err();
assert_eq!(
err,
SystematicParamError::UnsupportedSourceBlockSize {
requested: 56404,
max_supported: 56403
}
);
}
#[test]
fn params_lookup_rejects_zero_k() {
let err = SystematicParams::try_for_source_block(0, 64).unwrap_err();
assert_eq!(
err,
SystematicParamError::UnsupportedSourceBlockSize {
requested: 0,
max_supported: 56403
}
);
}
#[test]
fn encoder_construction_succeeds_for_supported_small_k_across_seed_sweep() {
let symbol_size = 16;
let seeds = [0u64, 1, 42, 99, 7777, 2024];
for k in 1..=16 {
let source = make_source_symbols(k, symbol_size);
for &seed in &seeds {
assert!(
SystematicEncoder::new(&source, symbol_size, seed).is_some(),
"supported small-K encoder construction should succeed for k={k}, symbol_size={symbol_size}, seed={seed}"
);
}
}
}
#[test]
fn params_lookup_rejects_wrapped_large_k() {
let huge_k = (u32::MAX as usize) + 1;
let err = SystematicParams::try_for_source_block(huge_k, 64).unwrap_err();
assert_eq!(
err,
SystematicParamError::UnsupportedSourceBlockSize {
requested: huge_k,
max_supported: 56403
}
);
}
#[test]
fn soliton_samples_valid_degrees() {
let sol = RobustSoliton::new(50, 0.2, 0.05);
let mut rng = DetRng::new(42);
for _ in 0..1000 {
let d = sol.sample(rng.next_u64() as u32);
assert!((1..=50).contains(&d), "degree {d} out of range");
}
}
#[test]
fn soliton_degree_distribution_not_degenerate() {
let sol = RobustSoliton::new(20, 0.2, 0.05);
let mut rng = DetRng::new(123);
let mut degrees = [0u32; 21];
for _ in 0..10_000 {
let d = sol.sample(rng.next_u64() as u32);
degrees[d] += 1;
}
let nonzero = degrees.iter().filter(|&&c| c > 0).count();
assert!(
nonzero >= 3,
"distribution too concentrated: {nonzero} nonzero"
);
let low: u32 = degrees[1..=3].iter().sum();
assert!(
low > 1000,
"low degrees should appear frequently: {low}/10000"
);
}
#[test]
fn soliton_deterministic_same_seed() {
let sol = RobustSoliton::new(30, 0.2, 0.05);
let run = |seed: u64| -> Vec<usize> {
let mut rng = DetRng::new(seed);
(0..100)
.map(|_| sol.sample(rng.next_u64() as u32))
.collect()
};
let a = run(42);
let b = run(42);
assert_eq!(a, b, "same seed must produce identical degree sequence");
}
#[test]
fn soliton_different_seeds_differ() {
let sol = RobustSoliton::new(30, 0.2, 0.05);
let run = |seed: u64| -> Vec<usize> {
let mut rng = DetRng::new(seed);
(0..100)
.map(|_| sol.sample(rng.next_u64() as u32))
.collect()
};
let a = run(42);
let b = run(12345);
assert_ne!(a, b, "different seeds should produce different sequences");
}
#[test]
fn soliton_k_accessor() {
let sol = RobustSoliton::new(42, 0.2, 0.05);
assert_eq!(sol.k(), 42);
assert_eq!(sol.max_degree(), 42);
}
#[test]
fn soliton_validate_params() {
assert!(RobustSoliton::validate_params(50, 0.2, 0.05).is_none());
assert!(RobustSoliton::validate_params(0, 0.2, 0.05).is_some());
assert!(RobustSoliton::validate_params(50, -0.1, 0.05).is_some());
assert!(RobustSoliton::validate_params(50, 0.2, 0.0).is_some());
assert!(RobustSoliton::validate_params(50, 0.2, 1.0).is_some());
assert!(RobustSoliton::validate_params(50, f64::NAN, 0.05).is_some());
assert!(RobustSoliton::validate_params(50, 0.2, f64::INFINITY).is_some());
}
#[test]
fn soliton_k_1_produces_degree_1() {
let sol = RobustSoliton::new(1, 0.2, 0.05);
let mut rng = DetRng::new(0);
for _ in 0..100 {
let d = sol.sample(rng.next_u64() as u32);
assert_eq!(d, 1, "k=1 should always produce degree 1");
}
}
#[test]
fn soliton_large_k_low_degrees_dominate() {
let sol = RobustSoliton::new(1000, 0.2, 0.05);
let mut rng = DetRng::new(99);
let mut low_count = 0;
let n = 10_000;
for _ in 0..n {
let d = sol.sample(rng.next_u64() as u32);
if d <= 10 {
low_count += 1;
}
}
assert!(
low_count > n / 2,
"low degrees should dominate: {low_count}/{n}"
);
}
#[test]
fn soliton_configurable_parameters() {
let sol_a = RobustSoliton::new(50, 0.1, 0.01);
let sol_b = RobustSoliton::new(50, 0.5, 0.1);
let mut rng_a = DetRng::new(42);
let mut rng_b = DetRng::new(42);
let a: Vec<usize> = (0..100)
.map(|_| sol_a.sample(rng_a.next_u64() as u32))
.collect();
let b: Vec<usize> = (0..100)
.map(|_| sol_b.sample(rng_b.next_u64() as u32))
.collect();
assert_ne!(
a, b,
"different parameters should produce different samples"
);
}
#[test]
fn constraint_matrix_dimensions() {
let params = SystematicParams::for_source_block(10, 32);
let matrix = ConstraintMatrix::build(¶ms, 42);
assert_eq!(matrix.rows, params.s + params.h + params.k_prime);
assert_eq!(matrix.cols, params.l);
}
#[test]
fn encoder_creates_successfully() {
let k = 4;
let symbol_size = 32;
let source = make_source_symbols(k, symbol_size);
let enc = SystematicEncoder::new(&source, symbol_size, 42);
assert!(enc.is_some(), "encoder should be constructible for k={k}");
}
#[test]
fn mr_emit_repair_substitutes_for_withheld_systematic_across_arbitrary_k() {
proptest!(|(
k in 2usize..48,
symbol_size in prop_oneof![Just(8usize), Just(16usize), Just(24usize)],
seed in any::<u64>(),
withheld_raw in 1usize..5,
)| {
let source: Vec<Vec<u8>> = (0..k)
.map(|i| {
(0..symbol_size)
.map(|j| {
seed.wrapping_add((i as u64).wrapping_mul(37))
.wrapping_add((j as u64).wrapping_mul(13))
.wrapping_add(7) as u8
})
.collect()
})
.collect();
let mut baseline = SystematicEncoder::new(&source, symbol_size, seed)
.expect("baseline encoder construction should succeed");
let systematic = baseline.emit_systematic();
let baseline_decoded = decode_emitted_symbols(&systematic, k, symbol_size, seed)
.expect("full systematic emission must decode");
prop_assert_eq!(
&baseline_decoded,
&source,
"MR baseline violated: full systematic emission must preserve identity"
);
let withheld = withheld_raw.min(k - 1);
let mut transformed = SystematicEncoder::new(&source, symbol_size, seed)
.expect("transformed encoder construction should succeed");
let systematic_prefix = transformed.emit_systematic();
let repair_count = transformed.params().l + withheld;
let repair_symbols = transformed.emit_repair(repair_count);
let mut substituted = systematic_prefix[..k - withheld].to_vec();
substituted.extend(repair_symbols);
let substituted_decoded = decode_emitted_symbols(&substituted, k, symbol_size, seed)
.expect("repair-backed transformed emission must decode");
prop_assert_eq!(
&substituted_decoded,
&source,
"MR violated: repair-backed transformed emission must preserve identity \
for k={}, symbol_size={}, withheld={}, seed={:#x}",
k,
symbol_size,
withheld,
seed
);
prop_assert_eq!(
&substituted_decoded,
&baseline_decoded,
"MR violated: withholding systematic symbols and substituting repairs \
changed the decoded source for k={}, symbol_size={}, \
withheld={}, seed={:#x}",
k,
symbol_size,
withheld,
seed
);
});
}
#[test]
fn encoder_deterministic() {
let k = 8;
let symbol_size = 64;
let source = make_source_symbols(k, symbol_size);
let enc1 = SystematicEncoder::new(&source, symbol_size, 42).unwrap();
let enc2 = SystematicEncoder::new(&source, symbol_size, 42).unwrap();
for esi in 0..10u32 {
assert_eq!(
enc1.repair_symbol(esi),
enc2.repair_symbol(esi),
"repair symbol {esi} differs between runs"
);
}
}
#[test]
fn repair_symbols_differ_across_esi() {
let k = 8;
let symbol_size = 64;
let source = make_source_symbols(k, symbol_size);
let enc = SystematicEncoder::new(&source, symbol_size, 42).unwrap();
let r0 = enc.repair_symbol(0);
let r1 = enc.repair_symbol(1);
let r2 = enc.repair_symbol(2);
assert!(
r0 != r1 && r1 != r2,
"repair symbols should generally differ"
);
}
#[test]
fn repair_symbol_matches_rfc_equation_terms() {
let k = 12;
let symbol_size = 32;
let seed = 42u64;
let replay_ref = "replay:rq-u-systematic-rfc-equation-v1";
let context = failure_context(
"RQ-U-SYSTEMATIC-RFC-EQUATION",
seed,
k,
symbol_size,
"rfc_repair_equation",
replay_ref,
);
let source = make_source_symbols(k, symbol_size);
let enc = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
for esi in (k as u32)..(k as u32 + 8) {
let repair = enc.repair_symbol(esi);
let (columns, coefficients) = enc.params().rfc_repair_equation(esi).unwrap();
let mut expected = vec![0u8; symbol_size];
for (&column, &coefficient) in columns.iter().zip(coefficients.iter()) {
gf256_addmul_slice(&mut expected, enc.intermediate_symbol(column), coefficient);
}
assert_eq!(
repair, expected,
"repair symbol must equal RFC tuple-derived equation expansion for esi={esi}; {context}"
);
}
}
#[test]
fn emitted_repair_degree_matches_rfc_equation_width() {
let k = 10;
let symbol_size = 24;
let seed = 7u64;
let replay_ref = "replay:rq-u-systematic-degree-metadata-v1";
let context = failure_context(
"RQ-U-SYSTEMATIC-DEGREE-METADATA",
seed,
k,
symbol_size,
"emit_repair_degree",
replay_ref,
);
let source = make_source_symbols(k, symbol_size);
let mut enc = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let emitted = enc.emit_repair(6);
for symbol in emitted {
let (columns, _) = enc.params().rfc_repair_equation(symbol.esi).unwrap();
assert_eq!(
symbol.degree,
columns.len(),
"degree metadata must match RFC tuple term count for esi={}; {context}",
symbol.esi,
);
}
}
#[test]
fn same_source_same_repair_across_seeds() {
let k = 4;
let symbol_size = 32;
let seed = 1u64;
let replay_ref = "replay:rq-u-systematic-seed-determinism-v1";
let context = failure_context(
"RQ-U-DETERMINISM-SEED",
seed,
k,
symbol_size,
"seed_independent_repair",
replay_ref,
);
let source = make_source_symbols(k, symbol_size);
let enc1 = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let enc2 = SystematicEncoder::new(&source, symbol_size, 2).unwrap();
let esi = k as u32; assert_eq!(
enc1.repair_symbol(esi),
enc2.repair_symbol(esi),
"same source data should produce identical repair symbols; {context}"
);
}
#[test]
fn k200_repair_symbols_match_raptorq_rs_reference_encoder() {
let k = 200usize;
let symbol_size = 64usize;
let repair_count = 30u32;
let seed = 0x6330_0200_u64;
let source = make_source_symbols(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let source_bytes = source.concat();
let transfer_length = u64::try_from(source_bytes.len()).expect("transfer length fits u64");
let symbol_size_u16 =
u16::try_from(symbol_size).expect("symbol size must fit in u16 for raptorq-rs");
let reference_config =
RaptorqRsObjectTransmissionInformation::new(transfer_length, symbol_size_u16, 1, 1, 1);
let reference_encoder =
RaptorqRsSourceBlockEncoder::new2(0, &reference_config, &source_bytes);
let expected_reference_esi_start =
u32::try_from(encoder.params().k_prime).expect("K' must fit in u32");
let public_repair_esi_start = u32::try_from(k).expect("K must fit in u32");
let reference_repairs = reference_encoder.repair_packets(0, repair_count);
assert_eq!(
reference_repairs.len(),
usize::try_from(repair_count).expect("repair count fits usize"),
"raptorq-rs must emit the requested number of repair packets"
);
for (offset, packet) in reference_repairs.iter().enumerate() {
let offset_u32 = u32::try_from(offset).expect("repair offset must fit in u32");
let public_esi = public_repair_esi_start + offset_u32;
let reference_esi = packet.payload_id().encoding_symbol_id();
assert_eq!(
reference_esi,
expected_reference_esi_start + offset_u32,
"raptorq-rs repair packet ESI should start at K' for offset {offset}"
);
assert_eq!(
encoder.repair_symbol(public_esi),
packet.data(),
"repair payload mismatch at K={k}, public_esi={public_esi}, \
reference_esi={reference_esi}, offset={offset}"
);
}
}
#[test]
fn intermediate_symbol_count_equals_l() {
let k = 10;
let symbol_size = 16;
let source = make_source_symbols(k, symbol_size);
let enc = SystematicEncoder::new(&source, symbol_size, 99).unwrap();
let l = enc.params().l;
for i in 0..l {
assert_eq!(enc.intermediate_symbol(i).len(), symbol_size);
}
}
#[test]
fn repair_symbol_correct_size() {
let k = 6;
let symbol_size = 48;
let source = make_source_symbols(k, symbol_size);
let enc = SystematicEncoder::new(&source, symbol_size, 77).unwrap();
for esi in 0..20u32 {
assert_eq!(enc.repair_symbol(esi).len(), symbol_size);
}
}
#[test]
fn emit_systematic_order() {
let k = 5;
let symbol_size = 16;
let source = make_source_symbols(k, symbol_size);
let mut enc = SystematicEncoder::new(&source, symbol_size, 42).unwrap();
let emitted = enc.emit_systematic();
assert_eq!(emitted.len(), k, "should emit exactly K source symbols");
for (i, sym) in emitted.iter().enumerate() {
assert_eq!(sym.esi, i as u32, "ESI should be in order");
assert!(sym.is_source, "should be marked as source");
assert_eq!(sym.degree, 1, "source symbols have degree 1");
assert_eq!(sym.data, source[i], "data should match input");
}
}
#[test]
fn emit_repair_order() {
let k = 4;
let symbol_size = 32;
let source = make_source_symbols(k, symbol_size);
let mut enc = SystematicEncoder::new(&source, symbol_size, 42).unwrap();
let repair_count = 10;
let emitted = enc.emit_repair(repair_count);
assert_eq!(emitted.len(), repair_count, "should emit requested count");
for (i, sym) in emitted.iter().enumerate() {
let expected_esi = k as u32 + i as u32;
assert_eq!(sym.esi, expected_esi, "ESI should start at K");
assert!(!sym.is_source, "should be marked as repair");
assert!(sym.degree >= 1, "degree should be at least 1");
assert_eq!(sym.data.len(), symbol_size, "correct symbol size");
}
}
#[test]
fn emit_all_order() {
let k = 3;
let symbol_size = 24;
let source = make_source_symbols(k, symbol_size);
let mut enc = SystematicEncoder::new(&source, symbol_size, 99).unwrap();
let repair_count = 5;
let emitted = enc.emit_all(repair_count);
assert_eq!(emitted.len(), k + repair_count, "total count");
for (i, sym) in emitted.iter().take(k).enumerate() {
assert_eq!(sym.esi, i as u32);
assert!(sym.is_source);
}
for (i, sym) in emitted.iter().skip(k).enumerate() {
assert_eq!(sym.esi, k as u32 + i as u32);
assert!(!sym.is_source);
}
}
#[test]
fn emit_repair_deterministic() {
let k = 6;
let symbol_size = 32;
let source = make_source_symbols(k, symbol_size);
let mut enc1 = SystematicEncoder::new(&source, symbol_size, 42).unwrap();
let mut enc2 = SystematicEncoder::new(&source, symbol_size, 42).unwrap();
let r1 = enc1.emit_repair(10);
let r2 = enc2.emit_repair(10);
for (s1, s2) in r1.iter().zip(r2.iter()) {
assert_eq!(s1.esi, s2.esi);
assert_eq!(s1.data, s2.data);
assert_eq!(s1.degree, s2.degree);
}
}
#[test]
fn emit_repair_batching_preserves_sequence_and_stats() {
let k = 7;
let symbol_size = 24;
let seed = 0xC0BA_17BA_7C48_5EED;
let source = make_source_symbols(k, symbol_size);
let mut single = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let mut batched = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let single_symbols = single.emit_repair(12);
let mut batched_symbols = Vec::new();
batched_symbols.extend(batched.emit_repair(3));
batched_symbols.extend(batched.emit_repair(0));
batched_symbols.extend(batched.emit_repair(4));
batched_symbols.extend(batched.emit_repair(5));
assert_eq!(
batched_symbols.len(),
single_symbols.len(),
"batched repair emission must produce the same total count"
);
for (single_symbol, batched_symbol) in single_symbols.iter().zip(&batched_symbols) {
assert_eq!(batched_symbol.esi, single_symbol.esi);
assert_eq!(batched_symbol.data, single_symbol.data);
assert_eq!(batched_symbol.is_source, single_symbol.is_source);
assert_eq!(batched_symbol.degree, single_symbol.degree);
}
assert_eq!(batched.next_repair_esi(), single.next_repair_esi());
assert_eq!(batched.systematic_emitted(), single.systematic_emitted());
let single_stats = single.stats();
let batched_stats = batched.stats();
assert_eq!(
batched_stats.repair_symbols_generated,
single_stats.repair_symbols_generated
);
assert_eq!(batched_stats.degree_min, single_stats.degree_min);
assert_eq!(batched_stats.degree_max, single_stats.degree_max);
assert_eq!(batched_stats.degree_sum, single_stats.degree_sum);
assert_eq!(batched_stats.degree_count, single_stats.degree_count);
assert_eq!(
batched_stats.repair_bytes_emitted,
single_stats.repair_bytes_emitted
);
assert_eq!(
batched_stats.systematic_bytes_emitted,
single_stats.systematic_bytes_emitted
);
}
#[test]
fn stats_initialized() {
let k = 8;
let symbol_size = 64;
let seed = 12345u64;
let source = make_source_symbols(k, symbol_size);
let enc = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let stats = enc.stats();
assert_eq!(stats.source_symbol_count, k);
assert_eq!(stats.symbol_size, symbol_size);
assert_eq!(stats.seed, seed);
assert_eq!(stats.intermediate_symbol_count, enc.params().l);
assert_eq!(stats.ldpc_symbol_count, enc.params().s);
assert_eq!(stats.hdpc_symbol_count, enc.params().h);
assert_eq!(stats.repair_symbols_generated, 0);
assert_eq!(stats.degree_min, 0);
assert_eq!(stats.degree_max, 0);
assert_eq!(stats.degree_sum, 0);
assert_eq!(stats.degree_count, 0);
}
#[test]
fn stats_updated_on_emit_repair() {
let k = 4;
let symbol_size = 16;
let source = make_source_symbols(k, symbol_size);
let mut enc = SystematicEncoder::new(&source, symbol_size, 42).unwrap();
assert_eq!(enc.stats().repair_symbols_generated, 0);
assert_eq!(enc.stats().degree_count, 0);
enc.emit_repair(5);
let stats = enc.stats();
assert_eq!(stats.repair_symbols_generated, 5);
assert_eq!(stats.degree_count, 5);
assert!(stats.degree_min >= 1);
assert!(stats.degree_max >= stats.degree_min);
assert!(stats.degree_sum >= 5); }
#[test]
fn stats_average_degree() {
let k = 10;
let symbol_size = 32;
let source = make_source_symbols(k, symbol_size);
let mut enc = SystematicEncoder::new(&source, symbol_size, 42).unwrap();
let baseline = enc.stats().average_degree();
assert!(baseline.abs() < f64::EPSILON);
enc.emit_repair(100);
let avg = enc.stats().average_degree();
assert!(avg >= 1.0, "average degree should be at least 1");
#[allow(clippy::cast_precision_loss)]
let max_degree = enc.params().l as f64;
assert!(avg <= max_degree, "average should not exceed L");
}
#[test]
fn stats_overhead_ratio() {
let k = 20;
let symbol_size = 32;
let seed = 42u64;
let replay_ref = "replay:rq-u-systematic-overhead-ratio-v1";
let context = failure_context(
"RQ-U-SYSTEMATIC-OVERHEAD-RATIO",
seed,
k,
symbol_size,
"stats_overhead_ratio",
replay_ref,
);
let source = make_source_symbols(k, symbol_size);
let enc = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let ratio = enc.stats().overhead_ratio();
assert!(ratio > 1.0, "overhead ratio should be > 1; {context}");
assert!(
ratio < 3.0,
"overhead ratio should be reasonable; {context}"
);
}
#[test]
fn seed_accessor() {
let seed = 0xDEAD_BEEF_u64;
let source = make_source_symbols(4, 16);
let enc = SystematicEncoder::new(&source, 16, seed).unwrap();
assert_eq!(enc.seed(), seed);
}
#[test]
fn repair_cursor_advances_across_calls() {
let symbol_size = 16;
let mut enc = make_encoder(16, symbol_size, 42);
let k = enc.params().k;
assert_eq!(enc.next_repair_esi(), k as u32, "cursor starts at K");
let batch1 = enc.emit_repair(3);
assert_eq!(enc.next_repair_esi(), k as u32 + 3);
assert_eq!(batch1[0].esi, k as u32);
assert_eq!(batch1[2].esi, k as u32 + 2);
let batch2 = enc.emit_repair(5);
assert_eq!(enc.next_repair_esi(), k as u32 + 8);
assert_eq!(
batch2[0].esi,
k as u32 + 3,
"second batch continues from cursor"
);
assert_eq!(batch2[4].esi, k as u32 + 7);
}
#[test]
fn repair_cursor_no_overlap() {
let symbol_size = 32;
let mut enc = make_encoder(16, symbol_size, 99);
let a = enc.emit_repair(4);
let b = enc.emit_repair(4);
let a_esis: Vec<u32> = a.iter().map(|s| s.esi).collect();
let b_esis: Vec<u32> = b.iter().map(|s| s.esi).collect();
for esi in &a_esis {
assert!(!b_esis.contains(esi), "ESI {esi} appears in both batches");
}
}
#[test]
fn systematic_emitted_flag() {
let symbol_size = 16;
let mut enc = make_encoder(16, symbol_size, 42);
assert!(!enc.systematic_emitted(), "not emitted initially");
enc.emit_systematic();
assert!(enc.systematic_emitted(), "flag set after emission");
}
#[test]
fn systematic_lane_closes_after_repair_emission() {
let symbol_size = 16;
let mut enc = make_encoder(16, symbol_size, 42);
assert!(!enc.systematic_emitted(), "not emitted initially");
let repairs = enc.emit_repair(1);
assert_eq!(repairs.len(), 1, "repair batch should emit one symbol");
assert!(
enc.systematic_emitted(),
"repair-first usage must close the systematic lane to preserve wire order"
);
let source_after_repair = enc.emit_systematic();
assert!(
source_after_repair.is_empty(),
"source symbols must not be emitted after repairs have started"
);
assert_eq!(
enc.stats().systematic_bytes_emitted,
0,
"closing the systematic lane via repair-first emission must not count source bytes"
);
}
#[test]
fn stats_bytes_tracking() {
let symbol_size = 32;
let mut enc = make_encoder(16, symbol_size, 42);
let k = enc.params().k;
assert_eq!(enc.stats().systematic_bytes_emitted, 0);
assert_eq!(enc.stats().repair_bytes_emitted, 0);
assert_eq!(enc.stats().total_bytes_emitted(), 0);
enc.emit_systematic();
assert_eq!(
enc.stats().systematic_bytes_emitted,
k * symbol_size,
"systematic bytes = K * symbol_size"
);
assert_eq!(enc.stats().repair_bytes_emitted, 0);
let repair_count = 6;
enc.emit_repair(repair_count);
assert_eq!(
enc.stats().repair_bytes_emitted,
repair_count * symbol_size,
"repair bytes = count * symbol_size"
);
assert_eq!(
enc.stats().total_bytes_emitted(),
(k + repair_count) * symbol_size
);
}
#[test]
fn repeated_emit_systematic_is_idempotent() {
let symbol_size = 32;
let mut enc = make_encoder(16, symbol_size, 42);
let k = enc.params().k;
let first = enc.emit_systematic();
assert_eq!(first.len(), k, "first call should emit all source symbols");
assert_eq!(
enc.stats().systematic_bytes_emitted,
k * symbol_size,
"first source pass should count emitted bytes"
);
let second = enc.emit_systematic();
assert!(
second.is_empty(),
"second source pass should not re-emit systematic symbols"
);
assert_eq!(
enc.stats().systematic_bytes_emitted,
k * symbol_size,
"repeated source pass must not double-count emitted bytes"
);
}
#[test]
fn stats_encoding_efficiency() {
let symbol_size = 64;
let mut enc = make_encoder(16, symbol_size, 42);
let k = enc.params().k;
assert!(enc.stats().encoding_efficiency().abs() < f64::EPSILON);
enc.emit_systematic();
assert!(
(enc.stats().encoding_efficiency() - 1.0).abs() < f64::EPSILON,
"systematic-only emission has efficiency 1.0"
);
enc.emit_repair(k);
let eff = enc.stats().encoding_efficiency();
assert!(eff > 0.0 && eff < 1.0, "efficiency with repairs: {eff}");
assert!(
(eff - 0.5).abs() < f64::EPSILON,
"equal source/repair count should give 0.5 efficiency"
);
}
#[test]
fn stats_repair_overhead() {
let symbol_size = 16;
let mut enc = make_encoder(16, symbol_size, 42);
let k = enc.params().k;
assert!(enc.stats().repair_overhead().abs() < f64::EPSILON);
enc.emit_systematic();
assert!(
enc.stats().repair_overhead().abs() < f64::EPSILON,
"no repairs yet, overhead is 0"
);
enc.emit_repair(k); let overhead = enc.stats().repair_overhead();
assert!(
(overhead - 1.0).abs() < f64::EPSILON,
"equal repair/source should give overhead 1.0, got {overhead}"
);
}
#[test]
fn stats_display_stable() {
let symbol_size = 16;
let mut enc = make_encoder(16, symbol_size, 42);
let k = enc.params().k;
enc.emit_systematic();
enc.emit_repair(3);
let display = format!("{}", enc.stats());
assert!(
display.contains(&format!("K={k}")),
"should contain K value"
);
assert!(display.contains("sym=16B"), "should contain symbol size");
assert!(display.contains("repairs=3"), "should contain repair count");
let display2 = format!("{}", enc.stats());
assert_eq!(display, display2, "Display must be stable");
insta::assert_snapshot!(enc.stats().to_string());
}
#[test]
fn stats_cumulative_across_batches() {
let symbol_size = 32;
let mut enc = make_encoder(16, symbol_size, 42);
enc.emit_repair(5);
let after_first = enc.stats().clone();
enc.emit_repair(3);
let after_second = enc.stats().clone();
assert_eq!(after_second.repair_symbols_generated, 8);
assert_eq!(after_second.degree_count, 8);
assert_eq!(after_second.repair_bytes_emitted, 8 * symbol_size);
assert!(after_second.degree_sum >= after_first.degree_sum);
assert!(after_second.degree_min <= after_first.degree_min);
assert!(after_second.degree_max >= after_first.degree_max);
}
#[test]
fn emit_all_esi_strictly_ascending() {
let symbol_size = 24;
let mut enc = make_encoder(16, symbol_size, 42);
let k = enc.params().k;
let all = enc.emit_all(10);
for w in all.windows(2) {
assert!(
w[0].esi < w[1].esi,
"ESIs must be strictly ascending: {} vs {}",
w[0].esi,
w[1].esi
);
}
let source_esis: Vec<u32> = all.iter().filter(|s| s.is_source).map(|s| s.esi).collect();
let repair_esis: Vec<u32> = all.iter().filter(|s| !s.is_source).map(|s| s.esi).collect();
assert_eq!(source_esis.len(), k, "should have K source symbols");
if let (Some(&max_src), Some(&min_rep)) = (source_esis.last(), repair_esis.first()) {
assert!(
max_src < min_rep,
"all source ESIs must precede repair ESIs"
);
}
}
#[test]
fn emit_all_after_systematic_only_emits_repairs() {
let symbol_size = 24;
let repair_count = 5;
let mut enc = make_encoder(16, symbol_size, 42);
let k = enc.params().k;
let systematic = enc.emit_systematic();
assert_eq!(
systematic.len(),
k,
"initial source pass should emit every source symbol"
);
let combined = enc.emit_all(repair_count);
assert_eq!(
combined.len(),
repair_count,
"emit_all after a source pass should only emit repairs"
);
assert!(
combined.iter().all(|symbol| !symbol.is_source),
"combined batch should contain only repair symbols after systematic emission"
);
assert_eq!(
combined.first().map(|symbol| symbol.esi),
Some(k as u32),
"repair emission should resume at K (the first repair ESI per RFC 6330)"
);
assert_eq!(
enc.stats().systematic_bytes_emitted,
k * symbol_size,
"emit_all must not double-count systematic bytes after a source pass"
);
}
#[test]
fn emit_all_after_repair_only_emits_new_repairs() {
let symbol_size = 24;
let first_repair_count = 3;
let second_repair_count = 5;
let mut enc = make_encoder(16, symbol_size, 42);
let k = enc.params().k;
let first_batch = enc.emit_repair(first_repair_count);
assert_eq!(
first_batch.first().map(|symbol| symbol.esi),
Some(k as u32),
"repair-first usage still starts at the first repair ESI"
);
let combined = enc.emit_all(second_repair_count);
assert_eq!(
combined.len(),
second_repair_count,
"emit_all after repair-first usage should only emit fresh repairs"
);
assert!(
combined.iter().all(|symbol| !symbol.is_source),
"emit_all must not retroactively emit source symbols after repair emission"
);
assert_eq!(
combined.first().map(|symbol| symbol.esi),
Some(k as u32 + first_repair_count as u32),
"emit_all should resume from the advanced repair cursor"
);
assert_eq!(
enc.stats().systematic_bytes_emitted,
0,
"repair-first flows must not backfill systematic byte accounting"
);
}
#[test]
fn systematic_params_debug_clone() {
let p = SystematicParams::for_source_block(10, 64);
let dbg = format!("{p:?}");
assert!(dbg.contains("SystematicParams"), "{dbg}");
let cloned = p;
assert_eq!(format!("{cloned:?}"), dbg);
}
#[test]
fn systematic_param_error_debug_clone_copy_eq() {
let e = SystematicParamError::UnsupportedSourceBlockSize {
requested: 60000,
max_supported: 56403,
};
let dbg = format!("{e:?}");
assert!(dbg.contains("UnsupportedSourceBlockSize"), "{dbg}");
let copied: SystematicParamError = e.clone();
let cloned = e;
assert_eq!(copied, cloned);
}
}