use std::collections::{HashMap, VecDeque};
use std::fmt;
use crate::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
use crate::config::EncodingConfig;
use crate::cx::Cx;
use crate::decoding::{DecodingConfig, DecodingPipeline};
use crate::encoding::EncodingPipeline;
use crate::security::{AuthenticatedSymbol, SecurityContext};
use crate::types::resource::{PoolConfig, SymbolPool};
use crate::types::{ObjectId, ObjectParams, Symbol, SymbolId, SymbolKind};
use crate::util::DetRng;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EcConfig {
pub symbol_size: u16,
pub repair_overhead: u16,
pub max_message_size: usize,
}
impl Default for EcConfig {
fn default() -> Self {
Self {
symbol_size: 1024,
repair_overhead: 4,
max_message_size: 1 << 20, }
}
}
impl EcConfig {
pub fn validate(&self) -> Result<(), EcError> {
if self.symbol_size == 0 {
return Err(EcError::ZeroSymbolSize);
}
if self.max_message_size == 0 {
return Err(EcError::ZeroMaxMessage);
}
Ok(())
}
pub fn plan(&self, message_size: usize) -> Result<BlockLayout, EcError> {
self.validate()?;
if message_size > self.max_message_size {
return Err(EcError::MessageTooLarge {
size: message_size,
max: self.max_message_size,
});
}
let symbol_size = self.symbol_size as usize;
let source_usize = message_size.div_ceil(symbol_size).max(1);
let source_symbols =
u16::try_from(source_usize).map_err(|_| EcError::SymbolCountOverflow)?;
let total_symbols = source_symbols
.checked_add(self.repair_overhead)
.ok_or(EcError::SymbolCountOverflow)?;
let padding = source_usize * symbol_size - message_size;
Ok(BlockLayout {
message_size,
symbol_size: self.symbol_size,
source_symbols,
repair_symbols: self.repair_overhead,
total_symbols,
padding,
})
}
pub fn encode_message(
&self,
message_id: u64,
message: &[u8],
) -> Result<EncodedMessage, EcError> {
self.plan(message.len())?;
let enc_config = EncodingConfig {
symbol_size: self.symbol_size,
max_block_size: self.max_message_size,
..EncodingConfig::default()
};
let mut pipeline =
EncodingPipeline::new(enc_config, SymbolPool::new(PoolConfig::default()));
let object_id = ObjectId::new(message_id, 0);
let repair_count = self.repair_overhead as usize;
let mut frames = Vec::new();
let mut source_symbols: u16 = 0;
for result in pipeline.encode_with_repair(object_id, message, repair_count) {
let symbol = result.map_err(|e| EcError::Coding(e.to_string()))?;
if symbol.id().sbn() != 0 {
return Err(EcError::Coding(
"message spans more than one source block".to_string(),
));
}
let esi = u16::try_from(symbol.id().esi()).map_err(|_| EcError::SymbolCountOverflow)?;
if symbol.kind().is_source() {
source_symbols = source_symbols
.checked_add(1)
.ok_or(EcError::SymbolCountOverflow)?;
}
frames.push(SymbolFrame::new(
message_id,
esi,
symbol.symbol().data().to_vec(),
));
}
let total_symbols =
u16::try_from(frames.len()).map_err(|_| EcError::SymbolCountOverflow)?;
let message_size =
u32::try_from(message.len()).map_err(|_| EcError::SymbolCountOverflow)?;
let header = MessageHeader {
message_id,
message_size,
symbol_size: self.symbol_size,
source_symbols,
total_symbols,
};
Ok(EncodedMessage { header, frames })
}
pub fn encode_message_authenticated(
&self,
message_id: u64,
message: &[u8],
auth: &SecurityContext,
) -> Result<(MessageHeader, Vec<AuthenticatedSymbol>), EcError> {
self.plan(message.len())?;
let enc_config = EncodingConfig {
symbol_size: self.symbol_size,
max_block_size: self.max_message_size,
..EncodingConfig::default()
};
let mut pipeline =
EncodingPipeline::new(enc_config, SymbolPool::new(PoolConfig::default()));
let object_id = ObjectId::new(message_id, 0);
let repair_count = self.repair_overhead as usize;
let mut symbols = Vec::new();
let mut source_symbols: u16 = 0;
for result in pipeline.encode_with_repair(object_id, message, repair_count) {
let symbol = result.map_err(|e| EcError::Coding(e.to_string()))?;
if symbol.id().sbn() != 0 {
return Err(EcError::Coding(
"message spans more than one source block".to_string(),
));
}
if symbol.kind().is_source() {
source_symbols = source_symbols
.checked_add(1)
.ok_or(EcError::SymbolCountOverflow)?;
}
symbols.push(auth.sign_symbol(symbol.symbol()));
}
let total_symbols =
u16::try_from(symbols.len()).map_err(|_| EcError::SymbolCountOverflow)?;
let message_size =
u32::try_from(message.len()).map_err(|_| EcError::SymbolCountOverflow)?;
let header = MessageHeader {
message_id,
message_size,
symbol_size: self.symbol_size,
source_symbols,
total_symbols,
};
Ok((header, symbols))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncodedMessage {
pub header: MessageHeader,
pub frames: Vec<SymbolFrame>,
}
pub fn decode_message(header: &MessageHeader, frames: &[SymbolFrame]) -> Result<Vec<u8>, EcError> {
if header.message_size == 0 {
return Ok(Vec::new());
}
let object_id = ObjectId::new(header.message_id, 0);
let source_symbols = header.source_symbols;
let mut config = DecodingConfig::without_auth();
config.symbol_size = header.symbol_size;
config.max_block_size = usize::from(source_symbols) * usize::from(header.symbol_size);
let mut decoder = DecodingPipeline::new(config);
decoder
.set_object_params(ObjectParams {
object_id,
object_size: u64::from(header.message_size),
symbol_size: header.symbol_size,
source_blocks: 1,
symbols_per_block: source_symbols,
})
.map_err(|e| EcError::Coding(e.to_string()))?;
for frame in frames {
let kind = if frame.esi < source_symbols {
SymbolKind::Source
} else {
SymbolKind::Repair
};
let symbol = Symbol::new(
SymbolId::new(object_id, 0, u32::from(frame.esi)),
frame.payload.clone(),
kind,
);
decoder
.feed(AuthenticatedSymbol::new_unauthenticated(symbol))
.map_err(|e| EcError::Coding(e.to_string()))?;
}
if !decoder.is_complete() {
return Err(EcError::IncompleteDecode {
needed: source_symbols,
});
}
decoder
.into_data()
.map_err(|e| EcError::Coding(e.to_string()))
}
pub fn decode_message_authenticated(
header: &MessageHeader,
symbols: &[AuthenticatedSymbol],
auth: &SecurityContext,
) -> Result<Vec<u8>, EcError> {
if header.message_size == 0 {
return Ok(Vec::new());
}
let object_id = ObjectId::new(header.message_id, 0);
let source_symbols = header.source_symbols;
let config = DecodingConfig {
symbol_size: header.symbol_size,
max_block_size: usize::from(source_symbols) * usize::from(header.symbol_size),
max_buffered_symbols: 0,
..DecodingConfig::default()
};
let mut decoder = DecodingPipeline::with_auth(config, auth.clone());
decoder
.set_object_params(ObjectParams {
object_id,
object_size: u64::from(header.message_size),
symbol_size: header.symbol_size,
source_blocks: 1,
symbols_per_block: source_symbols,
})
.map_err(|e| EcError::Coding(e.to_string()))?;
for symbol in symbols {
decoder
.feed(symbol.clone())
.map_err(|e| EcError::Coding(e.to_string()))?;
}
if !decoder.is_complete() {
return Err(EcError::IncompleteDecode {
needed: source_symbols,
});
}
decoder
.into_data()
.map_err(|e| EcError::Coding(e.to_string()))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WireUnit {
Header(MessageHeader),
Symbol(SymbolFrame),
}
#[must_use]
pub fn channel(config: EcConfig) -> (EcSender, EcReceiver) {
let (tx, rx) = unbounded_channel();
(
EcSender {
config,
tx,
next_message_id: 0,
},
EcReceiver {
rx,
pending: HashMap::new(),
pending_order: VecDeque::new(),
max_pending: DEFAULT_MAX_PENDING_MESSAGES,
},
)
}
pub struct EcSender {
config: EcConfig,
tx: UnboundedSender<WireUnit>,
next_message_id: u64,
}
impl EcSender {
pub fn send(&mut self, cx: &Cx, message: &[u8]) -> Result<u64, EcError> {
cx.checkpoint().map_err(|_| EcError::Cancelled)?;
let message_id = self.next_message_id;
let encoded = self.config.encode_message(message_id, message)?;
self.tx
.send(WireUnit::Header(encoded.header))
.map_err(|_| EcError::TransportClosed)?;
for frame in encoded.frames {
self.tx
.send(WireUnit::Symbol(frame))
.map_err(|_| EcError::TransportClosed)?;
}
self.next_message_id = self.next_message_id.wrapping_add(1);
Ok(message_id)
}
pub fn send_value<T: serde::Serialize>(&mut self, cx: &Cx, value: &T) -> Result<u64, EcError> {
let bytes = serde_json::to_vec(value).map_err(|e| EcError::Serialization(e.to_string()))?;
self.send(cx, &bytes)
}
}
const DEFAULT_MAX_PENDING_MESSAGES: usize = 1024;
pub struct EcReceiver {
rx: UnboundedReceiver<WireUnit>,
pending: HashMap<u64, (MessageHeader, MessageReassembler)>,
pending_order: VecDeque<u64>,
max_pending: usize,
}
impl EcReceiver {
pub async fn recv(&mut self, cx: &Cx) -> Result<Vec<u8>, EcError> {
loop {
let unit = self
.rx
.recv(cx)
.await
.map_err(|_| EcError::TransportClosed)?;
let message_id = self.ingest_unit(unit);
if let Some(bytes) = self.try_complete(message_id)? {
return Ok(bytes);
}
}
}
fn ingest_unit(&mut self, unit: WireUnit) -> u64 {
match unit {
WireUnit::Header(header) => {
let id = header.message_id;
if !self.pending.contains_key(&id) {
while self.pending.len() >= self.max_pending {
match self.pending_order.pop_front() {
Some(oldest) => {
self.pending.remove(&oldest);
}
None => break,
}
}
let reassembler = MessageReassembler::new(&header);
self.pending.insert(id, (header, reassembler));
self.pending_order.push_back(id);
}
id
}
WireUnit::Symbol(frame) => {
if let Some((_, reassembler)) = self.pending.get_mut(&frame.message_id) {
let _ = reassembler.accept_frame(&frame);
}
frame.message_id
}
}
}
fn try_complete(&mut self, message_id: u64) -> Result<Option<Vec<u8>>, EcError> {
let Some((header, reassembler)) = self.pending.get(&message_id) else {
return Ok(None);
};
if !reassembler.is_ready() {
return Ok(None);
}
let header = *header;
let held: Vec<SymbolFrame> = reassembler
.symbols()
.map(|(esi, bytes)| SymbolFrame::new(header.message_id, esi, bytes.to_vec()))
.collect();
let bytes = decode_message(&header, &held)?;
self.pending.remove(&message_id);
self.pending_order.retain(|&id| id != message_id);
Ok(Some(bytes))
}
pub async fn recv_value<T: serde::de::DeserializeOwned>(
&mut self,
cx: &Cx,
) -> Result<T, EcError> {
let bytes = self.recv(cx).await?;
serde_json::from_slice(&bytes).map_err(|e| EcError::Serialization(e.to_string()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlockLayout {
pub message_size: usize,
pub symbol_size: u16,
pub source_symbols: u16,
pub repair_symbols: u16,
pub total_symbols: u16,
pub padding: usize,
}
impl BlockLayout {
#[must_use]
pub fn loss_margin(&self) -> f64 {
if self.total_symbols == 0 {
return 0.0;
}
f64::from(self.repair_symbols) / f64::from(self.total_symbols)
}
#[must_use]
pub const fn min_symbols_to_decode(&self) -> u16 {
self.source_symbols
}
#[must_use]
pub const fn is_decodable(&self, distinct_received: u16) -> bool {
distinct_received >= self.source_symbols
}
#[must_use]
pub const fn symbols_until_decodable(&self, distinct_received: u16) -> u16 {
self.source_symbols.saturating_sub(distinct_received)
}
#[must_use]
pub const fn is_unrecoverable(&self, distinct_lost: u16) -> bool {
distinct_lost > self.repair_symbols
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MessageHeader {
pub message_id: u64,
pub message_size: u32,
pub symbol_size: u16,
pub source_symbols: u16,
pub total_symbols: u16,
}
impl MessageHeader {
pub const ENCODED_LEN: usize = 8 + 4 + 2 + 2 + 2;
pub fn from_layout(message_id: u64, layout: &BlockLayout) -> Result<Self, EcError> {
let message_size =
u32::try_from(layout.message_size).map_err(|_| EcError::SymbolCountOverflow)?;
Ok(Self {
message_id,
message_size,
symbol_size: layout.symbol_size,
source_symbols: layout.source_symbols,
total_symbols: layout.total_symbols,
})
}
#[must_use]
pub fn block_layout(&self) -> BlockLayout {
let symbol_size = self.symbol_size as usize;
let source = self.source_symbols as usize;
let message_size = self.message_size as usize;
let padding = source
.saturating_mul(symbol_size)
.saturating_sub(message_size);
BlockLayout {
message_size,
symbol_size: self.symbol_size,
source_symbols: self.source_symbols,
repair_symbols: self.total_symbols.saturating_sub(self.source_symbols),
total_symbols: self.total_symbols,
padding,
}
}
#[must_use]
pub fn encode(&self) -> [u8; Self::ENCODED_LEN] {
let mut out = [0u8; Self::ENCODED_LEN];
out[0..8].copy_from_slice(&self.message_id.to_le_bytes());
out[8..12].copy_from_slice(&self.message_size.to_le_bytes());
out[12..14].copy_from_slice(&self.symbol_size.to_le_bytes());
out[14..16].copy_from_slice(&self.source_symbols.to_le_bytes());
out[16..18].copy_from_slice(&self.total_symbols.to_le_bytes());
out
}
pub fn decode(bytes: &[u8]) -> Result<Self, EcError> {
if bytes.len() < Self::ENCODED_LEN {
return Err(EcError::ShortHeader {
got: bytes.len(),
need: Self::ENCODED_LEN,
});
}
let message_id = u64::from_le_bytes(bytes[0..8].try_into().expect("8 bytes"));
let message_size = u32::from_le_bytes(bytes[8..12].try_into().expect("4 bytes"));
let symbol_size = u16::from_le_bytes(bytes[12..14].try_into().expect("2 bytes"));
let source_symbols = u16::from_le_bytes(bytes[14..16].try_into().expect("2 bytes"));
let total_symbols = u16::from_le_bytes(bytes[16..18].try_into().expect("2 bytes"));
Ok(Self {
message_id,
message_size,
symbol_size,
source_symbols,
total_symbols,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SymbolFrame {
pub message_id: u64,
pub esi: u16,
pub payload: Vec<u8>,
}
impl SymbolFrame {
pub const HEADER_LEN: usize = 8 + 2;
#[must_use]
pub const fn new(message_id: u64, esi: u16, payload: Vec<u8>) -> Self {
Self {
message_id,
esi,
payload,
}
}
#[must_use]
pub const fn encoded_len(&self) -> usize {
Self::HEADER_LEN + self.payload.len()
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.encoded_len());
out.extend_from_slice(&self.message_id.to_le_bytes());
out.extend_from_slice(&self.esi.to_le_bytes());
out.extend_from_slice(&self.payload);
out
}
pub fn decode(bytes: &[u8]) -> Result<Self, EcError> {
if bytes.len() < Self::HEADER_LEN {
return Err(EcError::ShortFrame {
got: bytes.len(),
need: Self::HEADER_LEN,
});
}
let message_id = u64::from_le_bytes(bytes[0..8].try_into().expect("8 bytes"));
let esi = u16::from_le_bytes(bytes[8..10].try_into().expect("2 bytes"));
Ok(Self {
message_id,
esi,
payload: bytes[Self::HEADER_LEN..].to_vec(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SymbolAccept {
Accepted,
Duplicate,
OutOfRange {
esi: u16,
total: u16,
},
WrongSize {
esi: u16,
got: usize,
expected: usize,
},
WrongMessage {
expected: u64,
got: u64,
},
}
#[derive(Debug, Clone)]
pub struct MessageReassembler {
message_id: u64,
layout: BlockLayout,
symbols: std::collections::BTreeMap<u16, Vec<u8>>,
}
impl MessageReassembler {
#[must_use]
pub fn new(header: &MessageHeader) -> Self {
Self {
message_id: header.message_id,
layout: header.block_layout(),
symbols: std::collections::BTreeMap::new(),
}
}
#[must_use]
pub const fn message_id(&self) -> u64 {
self.message_id
}
#[must_use]
pub const fn layout(&self) -> &BlockLayout {
&self.layout
}
#[must_use]
pub fn distinct_received(&self) -> u16 {
u16::try_from(self.symbols.len()).unwrap_or(u16::MAX)
}
#[must_use]
pub fn is_ready(&self) -> bool {
self.layout.is_decodable(self.distinct_received())
}
#[must_use]
pub fn symbols_until_ready(&self) -> u16 {
self.layout
.symbols_until_decodable(self.distinct_received())
}
#[must_use]
pub fn accept(&mut self, esi: u16, payload: &[u8]) -> SymbolAccept {
if esi >= self.layout.total_symbols {
return SymbolAccept::OutOfRange {
esi,
total: self.layout.total_symbols,
};
}
let expected = self.layout.symbol_size as usize;
if payload.len() != expected {
return SymbolAccept::WrongSize {
esi,
got: payload.len(),
expected,
};
}
if self.symbols.contains_key(&esi) {
return SymbolAccept::Duplicate;
}
self.symbols.insert(esi, payload.to_vec());
SymbolAccept::Accepted
}
#[must_use]
pub fn accept_frame(&mut self, frame: &SymbolFrame) -> SymbolAccept {
if frame.message_id != self.message_id {
return SymbolAccept::WrongMessage {
expected: self.message_id,
got: frame.message_id,
};
}
self.accept(frame.esi, &frame.payload)
}
pub fn symbols(&self) -> impl Iterator<Item = (u16, &[u8])> {
self.symbols
.iter()
.map(|(&esi, bytes)| (esi, bytes.as_slice()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LossModel {
pub drop_ppm: u32,
pub duplicate_ppm: u32,
pub seed: u64,
}
impl LossModel {
pub const PPM_SCALE: u32 = 1_000_000;
#[must_use]
pub const fn new(seed: u64) -> Self {
Self {
drop_ppm: 0,
duplicate_ppm: 0,
seed,
}
}
#[must_use]
pub const fn with_drop_ppm(mut self, drop_ppm: u32) -> Self {
self.drop_ppm = if drop_ppm > Self::PPM_SCALE {
Self::PPM_SCALE
} else {
drop_ppm
};
self
}
#[must_use]
pub const fn with_duplicate_ppm(mut self, duplicate_ppm: u32) -> Self {
self.duplicate_ppm = if duplicate_ppm > Self::PPM_SCALE {
Self::PPM_SCALE
} else {
duplicate_ppm
};
self
}
#[must_use]
pub fn apply(&self, frames: &[SymbolFrame]) -> Vec<SymbolFrame> {
let mut rng = DetRng::new(self.seed);
let mut delivered = Vec::with_capacity(frames.len());
for frame in frames {
let drop_roll = rng.next_u32() % Self::PPM_SCALE;
if drop_roll < self.drop_ppm {
continue;
}
delivered.push(frame.clone());
let dup_roll = rng.next_u32() % Self::PPM_SCALE;
if dup_roll < self.duplicate_ppm {
delivered.push(frame.clone());
}
}
delivered
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EcError {
ZeroSymbolSize,
ZeroMaxMessage,
MessageTooLarge {
size: usize,
max: usize,
},
SymbolCountOverflow,
ShortHeader {
got: usize,
need: usize,
},
ShortFrame {
got: usize,
need: usize,
},
Coding(String),
IncompleteDecode {
needed: u16,
},
Cancelled,
TransportClosed,
Serialization(String),
}
impl fmt::Display for EcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ZeroSymbolSize => write!(f, "symbol_size must be >= 1"),
Self::ZeroMaxMessage => write!(f, "max_message_size must be >= 1"),
Self::MessageTooLarge { size, max } => {
write!(
f,
"message of {size} bytes exceeds the {max}-byte block maximum"
)
}
Self::SymbolCountOverflow => {
write!(
f,
"erasure block layout exceeds the on-wire symbol-count space"
)
}
Self::ShortHeader { got, need } => {
write!(f, "message header needs {need} bytes, got {got}")
}
Self::ShortFrame { got, need } => {
write!(f, "symbol frame header needs {need} bytes, got {got}")
}
Self::Coding(detail) => write!(f, "erasure coder error: {detail}"),
Self::IncompleteDecode { needed } => {
write!(
f,
"insufficient symbols to decode (need {needed} source symbols)"
)
}
Self::Cancelled => write!(f, "erasure channel send cancelled before flush"),
Self::TransportClosed => write!(f, "erasure channel transport closed"),
Self::Serialization(detail) => {
write!(f, "erasure channel (de)serialization error: {detail}")
}
}
}
}
impl std::error::Error for EcError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_validation() {
assert!(EcConfig::default().validate().is_ok());
assert_eq!(
EcConfig {
symbol_size: 0,
..EcConfig::default()
}
.validate(),
Err(EcError::ZeroSymbolSize)
);
assert_eq!(
EcConfig {
max_message_size: 0,
..EcConfig::default()
}
.validate(),
Err(EcError::ZeroMaxMessage)
);
}
#[test]
fn small_message_is_one_source_symbol() {
let cfg = EcConfig {
symbol_size: 1024,
repair_overhead: 4,
max_message_size: 1 << 20,
};
let layout = cfg.plan(10).expect("plan");
assert_eq!(layout.source_symbols, 1);
assert_eq!(layout.repair_symbols, 4);
assert_eq!(layout.total_symbols, 5);
assert_eq!(layout.padding, 1014);
}
#[test]
fn exact_multiple_has_no_padding() {
let cfg = EcConfig {
symbol_size: 100,
repair_overhead: 2,
max_message_size: 1 << 20,
};
let layout = cfg.plan(300).expect("plan");
assert_eq!(layout.source_symbols, 3);
assert_eq!(layout.total_symbols, 5);
assert_eq!(layout.padding, 0);
}
#[test]
fn non_multiple_pads_final_symbol() {
let cfg = EcConfig {
symbol_size: 100,
repair_overhead: 1,
max_message_size: 1 << 20,
};
let layout = cfg.plan(250).expect("plan");
assert_eq!(layout.source_symbols, 3); assert_eq!(layout.padding, 50); }
#[test]
fn empty_message_still_gets_one_symbol() {
let layout = EcConfig::default().plan(0).expect("plan");
assert_eq!(layout.source_symbols, 1);
}
#[test]
fn message_too_large_rejected() {
let cfg = EcConfig {
symbol_size: 16,
repair_overhead: 2,
max_message_size: 100,
};
assert_eq!(
cfg.plan(101),
Err(EcError::MessageTooLarge {
size: 101,
max: 100
})
);
}
#[test]
fn loss_margin_matches_overhead_fraction() {
let cfg = EcConfig {
symbol_size: 100,
repair_overhead: 5,
max_message_size: 1 << 20,
};
let layout = cfg.plan(500).expect("plan"); assert_eq!(layout.total_symbols, 10);
assert!((layout.loss_margin() - 0.5).abs() < 1e-9);
assert_eq!(layout.min_symbols_to_decode(), 5);
}
#[test]
fn header_roundtrips() {
let cfg = EcConfig::default();
let layout = cfg.plan(5000).expect("plan");
let header = MessageHeader::from_layout(7, &layout).expect("header");
let bytes = header.encode();
assert_eq!(bytes.len(), MessageHeader::ENCODED_LEN);
let decoded = MessageHeader::decode(&bytes).expect("decode");
assert_eq!(decoded, header);
assert_eq!(decoded.message_id, 7);
assert_eq!(decoded.message_size, 5000);
}
#[test]
fn header_decode_rejects_short_buffer() {
let result = MessageHeader::decode(&[0u8; 4]);
assert_eq!(
result,
Err(EcError::ShortHeader {
got: 4,
need: MessageHeader::ENCODED_LEN
})
);
}
#[test]
fn decode_progress_predicates_track_the_repair_budget() {
let cfg = EcConfig {
symbol_size: 100,
repair_overhead: 5,
max_message_size: 1 << 20,
};
let layout = cfg.plan(500).expect("plan"); assert_eq!(layout.source_symbols, 5);
assert_eq!(layout.total_symbols, 10);
assert!(!layout.is_decodable(4));
assert!(layout.is_decodable(5));
assert!(layout.is_decodable(7));
assert_eq!(layout.symbols_until_decodable(0), 5);
assert_eq!(layout.symbols_until_decodable(3), 2);
assert_eq!(layout.symbols_until_decodable(5), 0);
assert_eq!(layout.symbols_until_decodable(9), 0);
assert!(!layout.is_unrecoverable(5));
assert!(layout.is_unrecoverable(6));
}
#[test]
fn decode_predicates_are_self_consistent_across_the_range() {
let cfg = EcConfig {
symbol_size: 64,
repair_overhead: 3,
max_message_size: 1 << 20,
};
let layout = cfg.plan(400).expect("plan"); let n = layout.total_symbols;
let mut last_remaining = u16::MAX;
for received in 0..=n {
let remaining = layout.symbols_until_decodable(received);
assert_eq!(layout.is_decodable(received), remaining == 0);
assert!(remaining <= last_remaining, "remaining must not grow");
last_remaining = remaining;
}
for lost in 0..=n {
assert_eq!(layout.is_unrecoverable(lost), lost > layout.repair_symbols);
}
assert!(!layout.is_unrecoverable(layout.repair_symbols));
assert!(layout.is_unrecoverable(layout.repair_symbols + 1));
}
#[test]
fn block_layout_inverts_from_layout() {
let cfg = EcConfig::default();
for size in [0usize, 1, 10, 1023, 1024, 1025, 5000, 65_535, 1 << 20] {
let layout = cfg.plan(size).expect("plan");
let header = MessageHeader::from_layout(42, &layout).expect("header");
assert_eq!(
header.block_layout(),
layout,
"block_layout must invert from_layout at size {size}"
);
}
}
#[test]
fn block_layout_recomputes_geometry_from_header_fields() {
let header = MessageHeader {
message_id: 9,
message_size: 250,
symbol_size: 100,
source_symbols: 3,
total_symbols: 7,
};
let layout = header.block_layout();
assert_eq!(layout.repair_symbols, 4); assert_eq!(layout.padding, 50); assert_eq!(layout.message_size, 250);
assert_eq!(layout.min_symbols_to_decode(), 3);
}
fn k3_n5_header(message_id: u64) -> MessageHeader {
let cfg = EcConfig {
symbol_size: 4,
repair_overhead: 2,
max_message_size: 1 << 20,
};
let layout = cfg.plan(9).expect("plan");
assert_eq!(layout.source_symbols, 3);
assert_eq!(layout.total_symbols, 5);
MessageHeader::from_layout(message_id, &layout).expect("header")
}
#[test]
fn symbol_frame_roundtrips() {
let frame = SymbolFrame::new(0x00A1_B2C3_D4E5_F601, 9, vec![1, 2, 3, 4, 5]);
assert_eq!(frame.encoded_len(), SymbolFrame::HEADER_LEN + 5);
let bytes = frame.encode();
assert_eq!(bytes.len(), frame.encoded_len());
let decoded = SymbolFrame::decode(&bytes).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(decoded.message_id, 0x00A1_B2C3_D4E5_F601);
assert_eq!(decoded.esi, 9);
assert_eq!(decoded.payload, vec![1, 2, 3, 4, 5]);
}
#[test]
fn symbol_frame_empty_payload_roundtrips() {
let frame = SymbolFrame::new(5, 0, Vec::new());
let bytes = frame.encode();
assert_eq!(bytes.len(), SymbolFrame::HEADER_LEN);
let decoded = SymbolFrame::decode(&bytes).expect("decode");
assert_eq!(decoded, frame);
assert!(decoded.payload.is_empty());
}
#[test]
fn symbol_frame_decode_rejects_short_buffer() {
let result = SymbolFrame::decode(&[0u8; 9]);
assert_eq!(
result,
Err(EcError::ShortFrame {
got: 9,
need: SymbolFrame::HEADER_LEN
})
);
}
#[test]
fn reassembler_dedups_reorders_and_tracks_readiness() {
let header = k3_n5_header(11);
let mut ra = MessageReassembler::new(&header);
assert_eq!(ra.message_id(), 11);
assert_eq!(ra.layout().source_symbols, 3);
assert_eq!(ra.distinct_received(), 0);
assert!(!ra.is_ready());
assert_eq!(ra.symbols_until_ready(), 3);
assert_eq!(ra.accept(3, &[3, 3, 3, 3]), SymbolAccept::Accepted);
assert_eq!(ra.accept(0, &[0, 0, 0, 0]), SymbolAccept::Accepted);
assert_eq!(ra.distinct_received(), 2);
assert!(!ra.is_ready());
assert_eq!(ra.symbols_until_ready(), 1);
assert_eq!(ra.accept(3, &[9, 9, 9, 9]), SymbolAccept::Duplicate);
assert_eq!(ra.distinct_received(), 2);
assert_eq!(ra.accept(4, &[4, 4, 4, 4]), SymbolAccept::Accepted);
assert_eq!(ra.distinct_received(), 3);
assert!(ra.is_ready());
assert_eq!(ra.symbols_until_ready(), 0);
assert_eq!(ra.accept(1, &[1, 1, 1, 1]), SymbolAccept::Accepted);
assert_eq!(ra.distinct_received(), 4);
assert!(ra.is_ready());
let held: Vec<(u16, Vec<u8>)> = ra.symbols().map(|(e, b)| (e, b.to_vec())).collect();
assert_eq!(
held,
vec![
(0, vec![0, 0, 0, 0]),
(1, vec![1, 1, 1, 1]),
(3, vec![3, 3, 3, 3]),
(4, vec![4, 4, 4, 4]),
]
);
}
#[test]
fn reassembler_rejects_out_of_range_and_wrong_size() {
let header = k3_n5_header(1);
let mut ra = MessageReassembler::new(&header);
assert_eq!(
ra.accept(5, &[0, 0, 0, 0]),
SymbolAccept::OutOfRange { esi: 5, total: 5 }
);
assert_eq!(
ra.accept(0, &[0, 0, 0]),
SymbolAccept::WrongSize {
esi: 0,
got: 3,
expected: 4
}
);
assert_eq!(ra.distinct_received(), 0);
}
#[test]
fn reassembler_routes_frames_by_message_id() {
let header = k3_n5_header(100);
let mut ra = MessageReassembler::new(&header);
let foreign = SymbolFrame::new(200, 0, vec![0, 0, 0, 0]);
assert_eq!(
ra.accept_frame(&foreign),
SymbolAccept::WrongMessage {
expected: 100,
got: 200
}
);
assert_eq!(ra.distinct_received(), 0);
let ours = SymbolFrame::new(100, 0, vec![7, 7, 7, 7]);
assert_eq!(ra.accept_frame(&ours), SymbolAccept::Accepted);
assert_eq!(ra.distinct_received(), 1);
assert_eq!(ra.accept_frame(&ours), SymbolAccept::Duplicate);
assert_eq!(ra.distinct_received(), 1);
}
#[test]
fn reassembler_intake_is_arrival_order_independent() {
let header = k3_n5_header(7);
let payloads: [(u16, [u8; 4]); 3] = [(0, [10; 4]), (1, [11; 4]), (2, [12; 4])];
let mut forward = MessageReassembler::new(&header);
for (esi, bytes) in payloads {
assert_eq!(forward.accept(esi, &bytes), SymbolAccept::Accepted);
}
let mut reversed = MessageReassembler::new(&header);
for (esi, bytes) in payloads.iter().rev() {
assert_eq!(reversed.accept(*esi, bytes), SymbolAccept::Accepted);
}
let fwd: Vec<(u16, Vec<u8>)> = forward.symbols().map(|(e, b)| (e, b.to_vec())).collect();
let rev: Vec<(u16, Vec<u8>)> = reversed.symbols().map(|(e, b)| (e, b.to_vec())).collect();
assert_eq!(fwd, rev);
assert_eq!(forward.is_ready(), reversed.is_ready());
assert!(forward.is_ready());
}
fn k5_n8_header(message_id: u64) -> MessageHeader {
let cfg = EcConfig {
symbol_size: 4,
repair_overhead: 3,
max_message_size: 1 << 20,
};
let layout = cfg.plan(20).expect("plan");
assert_eq!(layout.source_symbols, 5);
assert_eq!(layout.total_symbols, 8);
MessageHeader::from_layout(message_id, &layout).expect("header")
}
fn n_frames(message_id: u64, n: u16) -> Vec<SymbolFrame> {
(0..n)
.map(|esi| SymbolFrame::new(message_id, esi, vec![esi as u8; 4]))
.collect()
}
#[test]
fn loss_model_lossless_is_identity() {
let frames = n_frames(1, 8);
let delivered = LossModel::new(42).apply(&frames);
assert_eq!(
delivered, frames,
"a lossless model must deliver its input verbatim"
);
}
#[test]
fn loss_model_total_drop_delivers_nothing() {
let frames = n_frames(1, 8);
let model = LossModel::new(42).with_drop_ppm(LossModel::PPM_SCALE);
assert!(model.apply(&frames).is_empty());
}
#[test]
fn loss_model_total_duplicate_delivers_each_twice() {
let frames = n_frames(1, 4);
let model = LossModel::new(42).with_duplicate_ppm(LossModel::PPM_SCALE);
let delivered = model.apply(&frames);
let expected: Vec<SymbolFrame> =
frames.iter().flat_map(|f| [f.clone(), f.clone()]).collect();
assert_eq!(
delivered, expected,
"every frame must be delivered exactly twice"
);
}
#[test]
fn loss_model_is_deterministic_for_a_seed() {
let frames = n_frames(1, 8);
let model = LossModel::new(7)
.with_drop_ppm(300_000)
.with_duplicate_ppm(100_000);
assert_eq!(model.apply(&frames), model.apply(&frames));
let twin = LossModel::new(7)
.with_drop_ppm(300_000)
.with_duplicate_ppm(100_000);
assert_eq!(model, twin);
assert_eq!(model.apply(&frames), twin.apply(&frames));
}
#[test]
fn loss_model_saturates_out_of_range_rates() {
let model = LossModel::new(1)
.with_drop_ppm(2_000_000)
.with_duplicate_ppm(5_000_000);
assert_eq!(model.drop_ppm, LossModel::PPM_SCALE);
assert_eq!(model.duplicate_ppm, LossModel::PPM_SCALE);
}
#[test]
fn loss_model_feeds_reassembler_respecting_repair_budget() {
let header = k5_n8_header(55);
let frames = n_frames(55, header.total_symbols);
let layout = header.block_layout();
for seed in [1u64, 2, 7, 99, 12_345, 654_321] {
let model = LossModel::new(seed).with_drop_ppm(300_000);
let delivered = model.apply(&frames);
let mut ra = MessageReassembler::new(&header);
for frame in &delivered {
let _ = ra.accept_frame(frame);
}
let lost = header.total_symbols - ra.distinct_received();
assert_eq!(
ra.is_ready(),
!layout.is_unrecoverable(lost),
"seed {seed}: readiness must track the repair budget (lost={lost}, repair={})",
layout.repair_symbols
);
}
}
#[test]
fn loss_model_duplicates_never_inflate_distinct_count() {
let header = k5_n8_header(8);
let frames = n_frames(8, header.total_symbols);
let model = LossModel::new(3).with_duplicate_ppm(LossModel::PPM_SCALE);
let delivered = model.apply(&frames);
assert_eq!(delivered.len(), 2 * frames.len());
let mut ra = MessageReassembler::new(&header);
let mut duplicates = 0u32;
for frame in &delivered {
if ra.accept_frame(frame) == SymbolAccept::Duplicate {
duplicates += 1;
}
}
assert_eq!(ra.distinct_received(), header.total_symbols);
assert_eq!(duplicates, u32::from(header.total_symbols));
assert!(ra.is_ready());
}
#[test]
fn pending_reassembly_buffer_is_bounded_with_fifo_eviction() {
let (_tx, mut rx) = channel(EcConfig::default());
rx.max_pending = 4;
for id in 0..6u64 {
let _ = rx.ingest_unit(WireUnit::Header(k3_n5_header(id)));
}
assert_eq!(rx.pending.len(), 4);
assert_eq!(rx.pending_order.len(), rx.pending.len());
assert!(!rx.pending.contains_key(&0));
assert!(!rx.pending.contains_key(&1));
for id in 2..6u64 {
assert!(rx.pending.contains_key(&id), "id {id} should be retained");
}
}
}