1use crate::{
2 Error, FecScheme, FecSymbolKind, Lane, PACKET_HEADER_LEN, RecordKind, Result, WireHeader,
3 WirePacketRef, decode_record, encode_packet_into,
4};
5use alloc::{boxed::Box, vec, vec::Vec};
6use bincode::{Decode, Encode};
7use core::fmt::{Display, Formatter};
8use cu_fec::{
9 DensityThreshold, EncodingSymbolId, Field, RepairParameters, RepairPayloadId, RlcConfig,
10 RlcDecoder, RlcEncoder, SourcePayloadId, SourceStatus,
11};
12
13const FRAGMENT_MAGIC: [u8; 4] = *b"CUFR";
14pub(crate) const FRAGMENT_HEADER_LEN: usize = 20;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Encode, Decode)]
18pub struct StreamIdentity {
19 pub session_id: [u8; 16],
20 pub sender_id: u32,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct ReceiverLimits {
26 pub max_record_bytes: usize,
28 pub max_buffered_records: usize,
31}
32
33impl ReceiverLimits {
34 pub const fn new(max_record_bytes: usize, max_buffered_records: usize) -> Self {
35 Self {
36 max_record_bytes,
37 max_buffered_records,
38 }
39 }
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum GapReason {
45 RlcWindowExpired,
47 LateJoin,
49 RecoveryPoint,
51 SessionEnded,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub struct CopperListGap {
58 pub first_id: u64,
60 pub last_id: u64,
62 pub reason: GapReason,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum ContinuousReceiveEvent<'a> {
69 Record(&'a RecoveredRecord),
71 Gap(CopperListGap),
73}
74
75#[derive(Clone, Debug, PartialEq, Eq)]
77pub enum ReceiveError<E> {
78 Stream(Error),
80 Consumer(E),
82}
83
84impl<E> From<Error> for ReceiveError<E> {
85 fn from(error: Error) -> Self {
86 Self::Stream(error)
87 }
88}
89
90impl<E: Display> Display for ReceiveError<E> {
91 fn fmt(&self, formatter: &mut Formatter<'_>) -> core::fmt::Result {
92 match self {
93 Self::Stream(error) => write!(formatter, "log stream receive failed: {error}"),
94 Self::Consumer(error) => write!(formatter, "log stream consumer failed: {error}"),
95 }
96 }
97}
98
99#[cfg(feature = "std")]
100impl<E: core::fmt::Debug + Display> std::error::Error for ReceiveError<E> {}
101
102#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
104pub struct ReceiverOccupancy {
105 pub processed_symbols: usize,
107 pub incomplete_records: usize,
109 pub ready_records: usize,
111}
112
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
115pub struct ContinuousRecoveryStats {
116 pub datagrams_seen: usize,
117 pub valid_datagrams: usize,
118 pub invalid_datagrams: usize,
119 pub inconsistent_datagrams: usize,
120 pub duplicate_symbols: usize,
121 pub expired_datagrams: usize,
122 pub source_symbols_received: usize,
123 pub source_symbols_recovered: usize,
124 pub repair_symbols_received: usize,
125 pub innovative_repairs: usize,
126 pub redundant_repairs: usize,
127 pub records_recovered: usize,
128 pub records_emitted: usize,
129 pub records_expired: usize,
130 pub gaps_emitted: usize,
131 pub missing_copperlists: u64,
132 pub peak_buffered_records: usize,
133}
134
135impl ContinuousRecoveryStats {
136 pub const fn destruction_basis_points(self, expected_source_symbols: usize) -> u16 {
138 if expected_source_symbols == 0 {
139 return 0;
140 }
141 let destroyed = expected_source_symbols.saturating_sub(self.source_symbols_received);
142 ((destroyed as u64 * 10_000) / expected_source_symbols as u64) as u16
143 }
144
145 pub const fn source_recovery_basis_points(self, expected_source_symbols: usize) -> u16 {
147 let missing = expected_source_symbols.saturating_sub(self.source_symbols_received);
148 if missing == 0 {
149 return 10_000;
150 }
151 let recovered = if self.source_symbols_recovered < missing {
152 self.source_symbols_recovered
153 } else {
154 missing
155 };
156 ((recovered as u64 * 10_000) / missing as u64) as u16
157 }
158
159 pub const fn semantic_recovery_basis_points(self, expected_records: usize) -> u16 {
161 if expected_records == 0 {
162 return 0;
163 }
164 let recovered = if self.records_recovered < expected_records {
165 self.records_recovered
166 } else {
167 expected_records
168 };
169 ((recovered as u64 * 10_000) / expected_records as u64) as u16
170 }
171}
172
173#[derive(Clone, Debug, PartialEq, Eq)]
175pub struct RecoveredRecord {
176 bytes: Vec<u8>,
177 kind: RecordKind,
178 object_id: u64,
179 digest: [u8; 32],
180 keyframe_id: Option<u64>,
181}
182
183impl RecoveredRecord {
184 pub(crate) fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
185 let decoded = decode_record(&bytes)?;
186 let keyframe_id = None;
187 Ok(Self {
188 kind: decoded.kind,
189 object_id: decoded.object_id,
190 digest: decoded.digest,
191 keyframe_id,
192 bytes,
193 })
194 }
195
196 pub fn bytes(&self) -> &[u8] {
197 &self.bytes
198 }
199
200 pub fn decoded(&self) -> crate::DecodedRecord<'_> {
202 crate::DecodedRecord {
203 kind: self.kind,
204 object_id: self.object_id,
205 digest: self.digest,
206 payload: &self.bytes[crate::record::RECORD_HEADER_LEN..],
207 }
208 }
209
210 pub(crate) fn validate_keyframe(&mut self) -> Result<()> {
211 self.keyframe_id = Some(crate::recovery::keyframe_id(self.decoded().payload)?);
212 Ok(())
213 }
214
215 pub fn keyframe_id(&self) -> Option<u64> {
217 self.keyframe_id
218 }
219}
220
221pub struct ContinuousEncoder<const MAX_SYMBOL_SIZE: usize, const MAX_WINDOW_SYMBOLS: usize> {
226 identity: StreamIdentity,
227 lane: Lane,
228 max_record_bytes: usize,
229 fec: RlcEncoder<MAX_SYMBOL_SIZE, MAX_WINDOW_SYMBOLS>,
230}
231
232impl<const MAX_SYMBOL_SIZE: usize, const MAX_WINDOW_SYMBOLS: usize>
233 ContinuousEncoder<MAX_SYMBOL_SIZE, MAX_WINDOW_SYMBOLS>
234{
235 pub fn new(
236 identity: StreamIdentity,
237 lane: Lane,
238 config: RlcConfig,
239 max_record_bytes: usize,
240 initial_esi: EncodingSymbolId,
241 ) -> Result<Self> {
242 validate_config(config, max_record_bytes)?;
243 Ok(Self {
244 identity,
245 lane,
246 max_record_bytes,
247 fec: RlcEncoder::new(config, initial_esi)?,
248 })
249 }
250
251 pub const fn config(&self) -> RlcConfig {
252 self.fec.config()
253 }
254
255 pub fn push_record(&mut self, record: &[u8], output: &mut Vec<Vec<u8>>) -> Result<usize> {
257 let mut datagram = vec![0; PACKET_HEADER_LEN + self.config().symbol_size()];
258 self.push_record_with(record, &mut datagram, |packet| {
259 output.push(packet.to_vec());
260 Ok(())
261 })
262 }
263
264 pub fn push_record_with(
267 &mut self,
268 record: &[u8],
269 datagram: &mut [u8],
270 mut emit: impl FnMut(&[u8]) -> Result<()>,
271 ) -> Result<usize> {
272 self.push_record_inner(record, datagram, &mut emit, None)
273 .map(|(source_symbols, _)| source_symbols)
274 }
275
276 pub(crate) fn push_record_with_repairs(
277 &mut self,
278 record: &[u8],
279 datagram: &mut [u8],
280 emit: &mut impl FnMut(&[u8]) -> Result<()>,
281 repair_schedule: &mut RepairSchedule,
282 ) -> Result<(usize, usize)> {
283 self.push_record_inner(record, datagram, emit, Some(repair_schedule))
284 }
285
286 fn push_record_inner(
287 &mut self,
288 record: &[u8],
289 datagram: &mut [u8],
290 emit: &mut impl FnMut(&[u8]) -> Result<()>,
291 mut repair_schedule: Option<&mut RepairSchedule>,
292 ) -> Result<(usize, usize)> {
293 let decoded = decode_record(record)?;
294 if decoded.kind != RecordKind::CopperList {
295 return Err(Error::InconsistentObject);
296 }
297 if record.len() > self.max_record_bytes {
298 return Err(Error::ObjectTooLarge {
299 actual: record.len() as u64,
300 maximum: self.max_record_bytes as u64,
301 });
302 }
303 let record_len = u32::try_from(record.len())
304 .map_err(|_| Error::InvalidConfig("record length exceeds u32"))?;
305 let fragment_capacity = fragment_capacity(self.config())?;
306 let fragment_count = record.len().div_ceil(fragment_capacity);
307 let fragment_count = u32::try_from(fragment_count)
308 .map_err(|_| Error::InvalidConfig("fragment count exceeds u32"))?;
309 let mut symbol = [0_u8; MAX_SYMBOL_SIZE];
310 let mut repair_symbols = 0usize;
311
312 for (fragment_index, chunk) in record.chunks(fragment_capacity).enumerate() {
313 let active_symbol = &mut symbol[..self.config().symbol_size()];
314 active_symbol.fill(0);
315 encode_fragment_header(
316 active_symbol,
317 decoded.object_id,
318 record_len,
319 fragment_index as u32,
320 );
321 active_symbol[FRAGMENT_HEADER_LEN..FRAGMENT_HEADER_LEN + chunk.len()]
322 .copy_from_slice(chunk);
323 let esi = self.fec.push_source(active_symbol)?;
324 let mut fec_metadata = [0_u8; 12];
325 fec_metadata[..4].copy_from_slice(&SourcePayloadId::new(esi).to_bytes());
326 let header = self.packet_header(FecSymbolKind::Source, fec_metadata);
327 self.emit_packet_with(header, active_symbol, datagram, emit)?;
328 if let Some(schedule) = repair_schedule.as_mut() {
329 schedule.source_symbols_since_repair =
330 schedule.source_symbols_since_repair.saturating_add(1);
331 if schedule.source_symbols_since_repair >= schedule.every {
332 schedule.source_symbols_since_repair -= schedule.every;
333 self.push_repair_with(
334 RepairParameters::new(schedule.next_repair_key, schedule.density),
335 datagram,
336 &mut *emit,
337 )?;
338 schedule.next_repair_key = schedule.next_repair_key.wrapping_add(1);
339 repair_symbols = repair_symbols.saturating_add(1);
340 }
341 }
342 }
343 Ok((fragment_count as usize, repair_symbols))
344 }
345
346 pub fn push_repair(
348 &mut self,
349 parameters: RepairParameters,
350 output: &mut Vec<Vec<u8>>,
351 ) -> Result<RepairPayloadId> {
352 let mut datagram = vec![0; PACKET_HEADER_LEN + self.config().symbol_size()];
353 self.push_repair_with(parameters, &mut datagram, |packet| {
354 output.push(packet.to_vec());
355 Ok(())
356 })
357 }
358
359 pub fn push_repair_with(
361 &mut self,
362 parameters: RepairParameters,
363 datagram: &mut [u8],
364 mut emit: impl FnMut(&[u8]) -> Result<()>,
365 ) -> Result<RepairPayloadId> {
366 let mut symbol = [0_u8; MAX_SYMBOL_SIZE];
367 let active_symbol = &mut symbol[..self.config().symbol_size()];
368 let id = self.fec.encode_repair(parameters, active_symbol)?;
369 let mut fec_metadata = [0_u8; 12];
370 fec_metadata[..8].copy_from_slice(&id.to_bytes());
371 let header = self.packet_header(FecSymbolKind::Repair, fec_metadata);
372 self.emit_packet_with(header, active_symbol, datagram, &mut emit)?;
373 Ok(id)
374 }
375
376 fn packet_header(&self, symbol_kind: FecSymbolKind, fec_metadata: [u8; 12]) -> WireHeader {
377 WireHeader {
378 lane: self.lane,
379 record_kind: RecordKind::CopperList,
380 fec_scheme: fec_scheme(self.config().field()),
381 symbol_kind,
382 session_id: self.identity.session_id,
383 sender_id: self.identity.sender_id,
384 object_id: 0,
385 fec_metadata,
386 fragment_count: 0,
387 }
388 }
389
390 fn emit_packet_with(
391 &mut self,
392 header: WireHeader,
393 payload: &[u8],
394 datagram: &mut [u8],
395 emit: &mut impl FnMut(&[u8]) -> Result<()>,
396 ) -> Result<()> {
397 let encoded = encode_packet_into(header, payload, datagram)?;
398 emit(&datagram[..encoded])?;
399 Ok(())
400 }
401}
402
403pub(crate) struct RepairSchedule {
404 every: usize,
405 source_symbols_since_repair: usize,
406 next_repair_key: u16,
407 density: DensityThreshold,
408}
409
410impl RepairSchedule {
411 pub(crate) fn set_interval(&mut self, every: usize) {
412 self.every = every;
413 self.source_symbols_since_repair = self
414 .source_symbols_since_repair
415 .min(every.saturating_sub(1));
416 }
417
418 pub(crate) const fn new(every: usize, next_repair_key: u16, density: DensityThreshold) -> Self {
419 Self {
420 every,
421 source_symbols_since_repair: 0,
422 next_repair_key,
423 density,
424 }
425 }
426}
427
428pub struct ContinuousDecoder<
437 const MAX_SYMBOL_SIZE: usize,
438 const MAX_WINDOW_SYMBOLS: usize,
439 const MAX_EQUATIONS: usize,
440> {
441 identity: StreamIdentity,
442 lane: Lane,
443 limits: ReceiverLimits,
444 fec: Box<RlcDecoder<MAX_SYMBOL_SIZE, MAX_WINDOW_SYMBOLS, MAX_EQUATIONS>>,
445 processed_symbols: Vec<EncodingSymbolId>,
446 symbols_pending: bool,
447 assemblies: Vec<RecordAssembly>,
448 ready: Vec<ReadyRecord>,
449 spares: Vec<RecordBuffers>,
450 next_object_id: u64,
451 observed_window_base: Option<EncodingSymbolId>,
452 retention_floor: Option<EncodingSymbolId>,
453 stats: ContinuousRecoveryStats,
454 source_observer: Option<crate::feedback::SourceObserver<MAX_WINDOW_SYMBOLS>>,
455}
456
457impl<const MAX_SYMBOL_SIZE: usize, const MAX_WINDOW_SYMBOLS: usize, const MAX_EQUATIONS: usize>
458 ContinuousDecoder<MAX_SYMBOL_SIZE, MAX_WINDOW_SYMBOLS, MAX_EQUATIONS>
459{
460 pub fn new(
461 identity: StreamIdentity,
462 lane: Lane,
463 config: RlcConfig,
464 equation_capacity: usize,
465 first_object_id: u64,
466 limits: ReceiverLimits,
467 ) -> Result<Self> {
468 validate_config(config, limits.max_record_bytes)?;
469 if limits.max_buffered_records < config.window_symbols() {
470 return Err(Error::InvalidConfig(
471 "buffered-record capacity must cover the RLC symbol window",
472 ));
473 }
474 Ok(Self {
475 identity,
476 lane,
477 limits,
478 fec: Box::new(RlcDecoder::new(config, equation_capacity)?),
479 processed_symbols: Vec::with_capacity(config.window_symbols()),
480 symbols_pending: false,
481 assemblies: Vec::with_capacity(limits.max_buffered_records),
482 ready: Vec::with_capacity(limits.max_buffered_records),
483 spares: Vec::with_capacity(limits.max_buffered_records),
484 next_object_id: first_object_id,
485 observed_window_base: None,
486 retention_floor: None,
487 stats: ContinuousRecoveryStats::default(),
488 source_observer: None,
489 })
490 }
491
492 pub fn enable_feedback(&mut self) {
494 self.source_observer.get_or_insert_with(Default::default);
495 }
496
497 pub fn source_outcomes(&self) -> crate::feedback::SourceOutcomes {
498 self.source_observer
499 .as_ref()
500 .map_or_else(Default::default, |observer| observer.outcomes())
501 }
502
503 pub const fn next_object_id(&self) -> u64 {
504 self.next_object_id
505 }
506
507 pub(crate) fn resume_at(&mut self, boundary: u64) {
510 self.next_object_id = self.next_object_id.max(boundary);
511 let next = self.next_object_id;
512 self.discard_assemblies(|id| id < next);
513 self.discard_stale_ready();
514 }
515
516 pub const fn stats(&self) -> ContinuousRecoveryStats {
517 self.stats
518 }
519
520 pub fn occupancy(&self) -> ReceiverOccupancy {
521 ReceiverOccupancy {
522 processed_symbols: self.processed_symbols.len(),
523 incomplete_records: self.assemblies.len(),
524 ready_records: self.ready.len(),
525 }
526 }
527
528 pub fn drain_events<E>(
534 &mut self,
535 mut emit: impl FnMut(ContinuousReceiveEvent<'_>) -> core::result::Result<(), E>,
536 ) -> core::result::Result<(), ReceiveError<E>> {
537 self.expire_and_emit(&mut emit)?;
538 if self.symbols_pending {
539 self.process_new_symbols()?;
540 self.expire_and_emit(&mut emit)?;
541 }
542 Ok(())
543 }
544
545 pub fn finish_through<E>(
551 &mut self,
552 last_object_id: u64,
553 mut emit: impl FnMut(ContinuousReceiveEvent<'_>) -> core::result::Result<(), E>,
554 ) -> core::result::Result<(), ReceiveError<E>> {
555 self.process_new_symbols()?;
556 self.emit_through(last_object_id, GapReason::SessionEnded, &mut emit)?;
557 let unresolved = self
558 .assemblies
559 .iter()
560 .filter(|assembly| assembly.object_id <= last_object_id)
561 .count();
562 self.discard_assemblies(|id| id <= last_object_id);
563 self.stats.records_expired = self.stats.records_expired.saturating_add(unresolved);
564 self.discard_stale_ready();
565 Ok(())
566 }
567
568 pub fn receive_datagram<E>(
573 &mut self,
574 datagram: &[u8],
575 emit: impl FnMut(ContinuousReceiveEvent<'_>) -> core::result::Result<(), E>,
576 ) -> core::result::Result<(), ReceiveError<E>> {
577 let packet = match WirePacketRef::decode(datagram) {
578 Ok(packet) => packet,
579 Err(_) => {
580 self.stats.datagrams_seen = self.stats.datagrams_seen.saturating_add(1);
581 self.stats.invalid_datagrams = self.stats.invalid_datagrams.saturating_add(1);
582 return Ok(());
583 }
584 };
585 self.receive_packet(packet, emit)
586 }
587
588 pub(crate) fn receive_packet<E>(
589 &mut self,
590 packet: WirePacketRef<'_>,
591 mut emit: impl FnMut(ContinuousReceiveEvent<'_>) -> core::result::Result<(), E>,
592 ) -> core::result::Result<(), ReceiveError<E>> {
593 self.stats.datagrams_seen = self.stats.datagrams_seen.saturating_add(1);
594 if packet.header.session_id != self.identity.session_id
595 || packet.header.sender_id != self.identity.sender_id
596 || packet.header.lane != self.lane
597 || packet.header.fec_scheme != fec_scheme(self.fec.config().field())
598 || packet.header.record_kind != RecordKind::CopperList
599 {
600 self.stats.inconsistent_datagrams = self.stats.inconsistent_datagrams.saturating_add(1);
601 return Ok(());
602 }
603 if packet.payload.len() != self.fec.config().symbol_size() {
604 self.stats.invalid_datagrams = self.stats.invalid_datagrams.saturating_add(1);
605 return Ok(());
606 }
607
608 let accepted = match packet.header.symbol_kind {
609 FecSymbolKind::Source => self.receive_source_packet(&packet)?,
610 FecSymbolKind::Repair => self.receive_repair_packet(&packet)?,
611 };
612 if !accepted {
613 return Ok(());
614 }
615 if let Some(observer) = &mut self.source_observer
616 && let Some((base, _)) = self.fec.window()
617 {
618 observer.advance(base.get());
619 for (esi, _) in self.fec.known_symbols() {
620 observer.observe(esi.get(), false);
621 }
622 }
623 self.stats.valid_datagrams = self.stats.valid_datagrams.saturating_add(1);
624 self.update_retention_floor();
625 self.symbols_pending = true;
626 self.drain_events(&mut emit)
627 }
628
629 fn receive_source_packet(&mut self, packet: &WirePacketRef<'_>) -> Result<bool> {
630 let id = SourcePayloadId::from_bytes(packet.header.fec_metadata[..4].try_into().unwrap());
631 if decode_fragment(packet.payload, fragment_capacity(self.fec.config())?).is_err() {
632 self.stats.invalid_datagrams = self.stats.invalid_datagrams.saturating_add(1);
633 return Ok(false);
634 }
635 let report = match self.fec.receive_source(id.esi(), packet.payload) {
636 Ok(report) => report,
637 Err(cu_fec::Error::WindowExpired) => {
638 self.stats.expired_datagrams = self.stats.expired_datagrams.saturating_add(1);
639 return Ok(false);
640 }
641 Err(error) => return Err(error.into()),
642 };
643 if let Some(observer) = &mut self.source_observer
644 && let Some((base, _)) = self.fec.window()
645 {
646 observer.advance(base.get());
647 observer.observe(id.esi().get(), true);
648 }
649 if report.status() == SourceStatus::Duplicate {
650 self.stats.duplicate_symbols = self.stats.duplicate_symbols.saturating_add(1);
651 return Ok(false);
652 }
653 self.stats.source_symbols_received = self.stats.source_symbols_received.saturating_add(1);
654 self.stats.source_symbols_recovered = self
655 .stats
656 .source_symbols_recovered
657 .saturating_add(report.recovered());
658 Ok(true)
659 }
660
661 fn receive_repair_packet(&mut self, packet: &WirePacketRef<'_>) -> Result<bool> {
662 let raw_id: [u8; 8] = packet.header.fec_metadata[..8].try_into().unwrap();
663 let id = RepairPayloadId::from_bytes(raw_id)?;
664 let report = match self.fec.receive_repair(id, packet.payload) {
665 Ok(report) => report,
666 Err(cu_fec::Error::WindowExpired) => {
667 self.stats.expired_datagrams = self.stats.expired_datagrams.saturating_add(1);
668 return Ok(false);
669 }
670 Err(error) => return Err(error.into()),
671 };
672 self.stats.repair_symbols_received = self.stats.repair_symbols_received.saturating_add(1);
673 if report.is_innovative() {
674 self.stats.innovative_repairs = self.stats.innovative_repairs.saturating_add(1);
675 } else {
676 self.stats.redundant_repairs = self.stats.redundant_repairs.saturating_add(1);
677 }
678 self.stats.source_symbols_recovered = self
679 .stats
680 .source_symbols_recovered
681 .saturating_add(report.recovered());
682 Ok(true)
683 }
684
685 fn process_new_symbols(&mut self) -> Result<()> {
686 if !self.symbols_pending {
687 return Ok(());
688 }
689 while let Some(esi) = self
690 .fec
691 .known_symbols()
692 .map(|(esi, _)| esi)
693 .find(|esi| !self.processed_symbols.contains(esi))
694 {
695 let symbol = self
696 .fec
697 .symbol(esi)
698 .expect("known symbol disappeared from retained RLC window");
699 Self::process_fragment(
700 self.limits,
701 &mut self.assemblies,
702 &mut self.ready,
703 &mut self.spares,
704 &mut self.stats,
705 esi,
706 decode_fragment(symbol, fragment_capacity(self.fec.config())?)?,
707 )?;
708 self.processed_symbols.push(esi);
709 }
710 self.symbols_pending = false;
711 Ok(())
712 }
713
714 fn process_fragment(
715 limits: ReceiverLimits,
716 assemblies: &mut Vec<RecordAssembly>,
717 ready: &mut Vec<ReadyRecord>,
718 spares: &mut Vec<RecordBuffers>,
719 stats: &mut ContinuousRecoveryStats,
720 esi: EncodingSymbolId,
721 fragment: Fragment<'_>,
722 ) -> Result<()> {
723 if fragment.record_len > limits.max_record_bytes {
724 return Err(Error::ObjectTooLarge {
725 actual: fragment.record_len as u64,
726 maximum: limits.max_record_bytes as u64,
727 });
728 }
729 let assembly_index = match assemblies.iter().position(|assembly| {
730 assembly.kind == fragment.kind && assembly.object_id == fragment.object_id
731 }) {
732 Some(index) => index,
733 None => {
734 let count = assemblies
735 .len()
736 .saturating_add(ready.len())
737 .saturating_add(1);
738 if count > limits.max_buffered_records {
739 return Err(Error::TooManyRecords {
740 actual: count,
741 maximum: limits.max_buffered_records,
742 });
743 }
744 assemblies.push(RecordAssembly::new(
745 esi,
746 &fragment,
747 spares.pop().unwrap_or_default(),
748 ));
749 stats.peak_buffered_records = stats
750 .peak_buffered_records
751 .max(assemblies.len() + ready.len());
752 assemblies.len() - 1
753 }
754 };
755 let complete = assemblies[assembly_index].insert(esi, &fragment)?;
756 if !complete {
757 return Ok(());
758 }
759
760 let assembly = assemblies.swap_remove(assembly_index);
761 let record = RecoveredRecord::from_bytes(assembly.bytes)?;
762 let decoded = record.decoded();
763 if decoded.kind != assembly.kind || decoded.object_id != assembly.object_id {
764 return Err(Error::InconsistentObject);
765 }
766 ready.push(ReadyRecord {
767 object_id: assembly.object_id,
768 first_esi: assembly.first_esi,
769 record,
770 received: assembly.received,
771 });
772 stats.records_recovered = stats.records_recovered.saturating_add(1);
773 stats.peak_buffered_records = stats
774 .peak_buffered_records
775 .max(assemblies.len() + ready.len());
776 Ok(())
777 }
778
779 fn expire_and_emit<E>(
780 &mut self,
781 emit: &mut impl FnMut(ContinuousReceiveEvent<'_>) -> core::result::Result<(), E>,
782 ) -> core::result::Result<(), ReceiveError<E>> {
783 let Some((base_esi, _)) = self.fec.window() else {
784 return Ok(());
785 };
786 self.processed_symbols
787 .retain(|esi| !esi_is_before(*esi, base_esi));
788
789 if let Some(retention_floor) = self.retention_floor {
790 loop {
791 let expired = self
792 .assemblies
793 .iter()
794 .enumerate()
795 .filter(|(_, assembly)| assembly.has_expired_fragment(retention_floor))
796 .min_by_key(|(_, assembly)| assembly.object_id)
797 .map(|(index, _)| index);
798 let Some(index) = expired else {
799 break;
800 };
801 let object_id = self.assemblies[index].object_id;
802 self.emit_through(object_id, GapReason::RlcWindowExpired, emit)?;
803 let assembly = self.assemblies.swap_remove(index);
804 self.spares.push(RecordBuffers {
805 bytes: assembly.bytes,
806 received: assembly.received,
807 });
808 self.stats.records_expired = self.stats.records_expired.saturating_add(1);
809 }
810 }
811
812 self.emit_ready(self.retention_floor, emit)
813 }
814
815 fn emit_ready<E>(
816 &mut self,
817 retention_floor: Option<EncodingSymbolId>,
818 emit: &mut impl FnMut(ContinuousReceiveEvent<'_>) -> core::result::Result<(), E>,
819 ) -> core::result::Result<(), ReceiveError<E>> {
820 loop {
821 self.discard_stale_ready();
822 if self.emit_next_ready(emit)? {
823 continue;
824 }
825
826 let Some(retention_floor) = retention_floor else {
827 return Ok(());
828 };
829 let expired_boundary = self
830 .ready
831 .iter()
832 .filter(|record| {
833 record.object_id > self.next_object_id
834 && !esi_is_after(record.first_esi, retention_floor)
835 })
836 .min_by_key(|record| record.object_id)
837 .map(|record| record.object_id);
838 let Some(boundary) = expired_boundary else {
839 return Ok(());
840 };
841 self.emit_gap(boundary - 1, GapReason::RlcWindowExpired, emit)?;
842 }
843 }
844
845 fn emit_through<E>(
846 &mut self,
847 last_id: u64,
848 reason: GapReason,
849 emit: &mut impl FnMut(ContinuousReceiveEvent<'_>) -> core::result::Result<(), E>,
850 ) -> core::result::Result<(), ReceiveError<E>> {
851 while self.next_object_id <= last_id {
852 self.discard_stale_ready();
853 if self.emit_next_ready(emit)? {
854 continue;
855 }
856 let next_ready = self
857 .ready
858 .iter()
859 .map(|record| record.object_id)
860 .filter(|object_id| *object_id <= last_id)
861 .min();
862 let gap_last = next_ready.map_or(last_id, |object_id| object_id - 1);
863 self.emit_gap(gap_last, reason, emit)?;
864 }
865 Ok(())
866 }
867
868 fn emit_next_ready<E>(
869 &mut self,
870 emit: &mut impl FnMut(ContinuousReceiveEvent<'_>) -> core::result::Result<(), E>,
871 ) -> core::result::Result<bool, ReceiveError<E>> {
872 let Some(index) = self
873 .ready
874 .iter()
875 .position(|record| record.object_id == self.next_object_id)
876 else {
877 return Ok(false);
878 };
879 emit(ContinuousReceiveEvent::Record(&self.ready[index].record))
880 .map_err(ReceiveError::Consumer)?;
881 let ready = self.ready.swap_remove(index);
882 self.spares.push(RecordBuffers {
883 bytes: ready.record.bytes,
884 received: ready.received,
885 });
886 self.stats.records_emitted = self.stats.records_emitted.saturating_add(1);
887 self.next_object_id = self.next_object_id.wrapping_add(1);
888 Ok(true)
889 }
890
891 fn emit_gap<E>(
892 &mut self,
893 last_id: u64,
894 reason: GapReason,
895 emit: &mut impl FnMut(ContinuousReceiveEvent<'_>) -> core::result::Result<(), E>,
896 ) -> core::result::Result<(), ReceiveError<E>> {
897 if last_id < self.next_object_id {
898 return Ok(());
899 }
900 let first_id = self.next_object_id;
901 emit(ContinuousReceiveEvent::Gap(CopperListGap {
902 first_id,
903 last_id,
904 reason,
905 }))
906 .map_err(ReceiveError::Consumer)?;
907 self.stats.gaps_emitted = self.stats.gaps_emitted.saturating_add(1);
908 self.stats.missing_copperlists = self
909 .stats
910 .missing_copperlists
911 .saturating_add(last_id - first_id + 1);
912 self.next_object_id = last_id.wrapping_add(1);
913 Ok(())
914 }
915
916 fn discard_stale_ready(&mut self) {
917 let mut index = 0;
918 while index < self.ready.len() {
919 if self.ready[index].object_id < self.next_object_id {
920 let ready = self.ready.swap_remove(index);
921 self.spares.push(RecordBuffers {
922 bytes: ready.record.bytes,
923 received: ready.received,
924 });
925 } else {
926 index += 1;
927 }
928 }
929 }
930
931 fn discard_assemblies(&mut self, discard: impl Fn(u64) -> bool) {
932 let mut index = 0;
933 while index < self.assemblies.len() {
934 if discard(self.assemblies[index].object_id) {
935 let assembly = self.assemblies.swap_remove(index);
936 self.spares.push(RecordBuffers {
937 bytes: assembly.bytes,
938 received: assembly.received,
939 });
940 } else {
941 index += 1;
942 }
943 }
944 }
945
946 fn update_retention_floor(&mut self) {
947 let Some((base_esi, _)) = self.fec.window() else {
948 return;
949 };
950 if self
951 .observed_window_base
952 .is_some_and(|previous| esi_is_after(base_esi, previous))
953 {
954 self.retention_floor = Some(base_esi);
955 }
956 self.observed_window_base = Some(base_esi);
957 }
958}
959
960#[derive(Clone, Debug)]
961struct ReadyRecord {
962 object_id: u64,
963 first_esi: EncodingSymbolId,
964 record: RecoveredRecord,
965 received: Vec<bool>,
966}
967
968#[derive(Debug)]
969struct Fragment<'a> {
970 kind: RecordKind,
971 object_id: u64,
972 record_len: usize,
973 fragment_index: usize,
974 fragment_count: usize,
975 fragment_capacity: usize,
976 payload: &'a [u8],
977}
978
979#[derive(Default)]
980struct RecordBuffers {
981 bytes: Vec<u8>,
982 received: Vec<bool>,
983}
984
985struct RecordAssembly {
986 kind: RecordKind,
987 object_id: u64,
988 first_esi: EncodingSymbolId,
989 bytes: Vec<u8>,
990 received: Vec<bool>,
991 received_count: usize,
992 fragment_capacity: usize,
993}
994
995impl RecordAssembly {
996 fn new(esi: EncodingSymbolId, fragment: &Fragment<'_>, mut buffers: RecordBuffers) -> Self {
997 buffers.bytes.resize(fragment.record_len, 0);
998 buffers.received.resize(fragment.fragment_count, false);
999 buffers.received.fill(false);
1000 Self {
1001 kind: fragment.kind,
1002 object_id: fragment.object_id,
1003 first_esi: esi.wrapping_sub(fragment.fragment_index as u32),
1004 bytes: buffers.bytes,
1005 received: buffers.received,
1006 received_count: 0,
1007 fragment_capacity: fragment.fragment_capacity,
1008 }
1009 }
1010
1011 fn insert(&mut self, esi: EncodingSymbolId, fragment: &Fragment<'_>) -> Result<bool> {
1012 if self.bytes.len() != fragment.record_len || self.received.len() != fragment.fragment_count
1013 {
1014 return Err(Error::InvalidFragment("record geometry changed"));
1015 }
1016 if self.first_esi.wrapping_add(fragment.fragment_index as u32) != esi {
1017 return Err(Error::InvalidFragment(
1018 "fragment ESI does not match record geometry",
1019 ));
1020 }
1021 let offset = fragment
1022 .fragment_index
1023 .checked_mul(self.fragment_capacity)
1024 .ok_or(Error::InvalidFragment("fragment offset overflow"))?;
1025 let end = offset
1026 .checked_add(fragment.payload.len())
1027 .ok_or(Error::InvalidFragment("fragment length overflow"))?;
1028 if end > self.bytes.len() {
1029 return Err(Error::InvalidFragment("fragment exceeds record"));
1030 }
1031 if self.received[fragment.fragment_index] {
1032 if self.bytes[offset..end] != *fragment.payload {
1033 return Err(Error::InvalidFragment("duplicate fragment differs"));
1034 }
1035 return Ok(self.received_count == self.received.len());
1036 }
1037 self.bytes[offset..end].copy_from_slice(fragment.payload);
1038 self.received[fragment.fragment_index] = true;
1039 self.received_count += 1;
1040 Ok(self.received_count == self.received.len())
1041 }
1042
1043 fn has_expired_fragment(&self, base_esi: EncodingSymbolId) -> bool {
1044 self.received.iter().enumerate().any(|(index, received)| {
1045 !received && esi_is_before(self.first_esi.wrapping_add(index as u32), base_esi)
1046 })
1047 }
1048}
1049
1050fn esi_is_before(candidate: EncodingSymbolId, reference: EncodingSymbolId) -> bool {
1051 (candidate.get().wrapping_sub(reference.get()) as i32) < 0
1052}
1053
1054fn esi_is_after(candidate: EncodingSymbolId, reference: EncodingSymbolId) -> bool {
1055 (candidate.get().wrapping_sub(reference.get()) as i32) > 0
1056}
1057
1058fn validate_config(config: RlcConfig, max_record_bytes: usize) -> Result<()> {
1059 let _ = fragment_capacity(config)?;
1060 if max_record_bytes == 0 {
1061 return Err(Error::InvalidConfig("maximum record size must be nonzero"));
1062 }
1063 if max_record_bytes > u32::MAX as usize {
1064 return Err(Error::InvalidConfig("maximum record size exceeds u32"));
1065 }
1066 Ok(())
1067}
1068
1069fn fragment_capacity(config: RlcConfig) -> Result<usize> {
1070 config
1071 .symbol_size()
1072 .checked_sub(FRAGMENT_HEADER_LEN)
1073 .filter(|capacity| *capacity > 0 && *capacity <= u16::MAX as usize)
1074 .ok_or(Error::InvalidConfig(
1075 "RLC symbol cannot hold the fragment header and payload",
1076 ))
1077}
1078
1079fn fec_scheme(field: Field) -> FecScheme {
1080 match field {
1081 Field::Gf2 => FecScheme::RlcGf2,
1082 Field::Gf256 => FecScheme::RlcGf256,
1083 }
1084}
1085
1086fn encode_fragment_header(symbol: &mut [u8], object_id: u64, record_len: u32, fragment_index: u32) {
1087 symbol[..4].copy_from_slice(&FRAGMENT_MAGIC);
1088 symbol[4..12].copy_from_slice(&object_id.to_be_bytes());
1089 symbol[12..16].copy_from_slice(&record_len.to_be_bytes());
1090 symbol[16..20].copy_from_slice(&fragment_index.to_be_bytes());
1091}
1092
1093fn decode_fragment(symbol: &[u8], fragment_capacity: usize) -> Result<Fragment<'_>> {
1094 if symbol.len() < FRAGMENT_HEADER_LEN || symbol[..4] != FRAGMENT_MAGIC {
1095 return Err(Error::InvalidFragment("missing fragment header"));
1096 }
1097 let object_id = u64::from_be_bytes(symbol[4..12].try_into().unwrap());
1098 let record_len = u32::from_be_bytes(symbol[12..16].try_into().unwrap()) as usize;
1099 let fragment_index = u32::from_be_bytes(symbol[16..20].try_into().unwrap()) as usize;
1100 if record_len == 0 || fragment_capacity == 0 {
1101 return Err(Error::InvalidFragment("invalid fragment geometry"));
1102 }
1103 let fragment_count = record_len.div_ceil(fragment_capacity);
1104 if fragment_index >= fragment_count {
1105 return Err(Error::InvalidFragment("invalid fragment geometry"));
1106 }
1107 let offset = fragment_index * fragment_capacity;
1109 let fragment_len = fragment_capacity.min(record_len - offset);
1110 let body = &symbol[FRAGMENT_HEADER_LEN..];
1111 let payload = body.get(..fragment_len).ok_or(Error::InvalidFragment(
1112 "fragment length does not match geometry",
1113 ))?;
1114 if body[fragment_len..].iter().any(|byte| *byte != 0) {
1115 return Err(Error::InvalidFragment("fragment padding is nonzero"));
1116 }
1117 Ok(Fragment {
1118 kind: RecordKind::CopperList,
1119 object_id,
1120 record_len,
1121 fragment_index,
1122 fragment_count,
1123 fragment_capacity,
1124 payload,
1125 })
1126}
1127
1128#[cfg(test)]
1129mod tests {
1130 use super::*;
1131
1132 #[test]
1133 fn fragment_header_round_trips_and_rejects_padding() {
1134 let mut symbol = [0_u8; 52];
1135 encode_fragment_header(&mut symbol, 42, 40, 1);
1136 symbol[FRAGMENT_HEADER_LEN..FRAGMENT_HEADER_LEN + 8].copy_from_slice(b"fragment");
1137 assert_eq!(FRAGMENT_HEADER_LEN, 20);
1138 assert_eq!(&symbol[..4], b"CUFR");
1139 assert_eq!(&symbol[4..12], &42_u64.to_be_bytes());
1140 assert_eq!(&symbol[12..16], &40_u32.to_be_bytes());
1141 assert_eq!(&symbol[16..20], &1_u32.to_be_bytes());
1142 let decoded = decode_fragment(&symbol, 32).unwrap();
1143 assert_eq!(decoded.kind, RecordKind::CopperList);
1144 assert_eq!(decoded.object_id, 42);
1145 assert_eq!(decoded.record_len, 40);
1146 assert_eq!(decoded.fragment_index, 1);
1147 assert_eq!(decoded.fragment_count, 2);
1148 assert_eq!(decoded.payload, b"fragment");
1149
1150 symbol[51] = 1;
1151 assert_eq!(
1152 decode_fragment(&symbol, 32).unwrap_err(),
1153 Error::InvalidFragment("fragment padding is nonzero")
1154 );
1155 }
1156
1157 #[test]
1158 fn fragment_geometry_handles_record_boundaries() {
1159 for (record_len, index, count, len) in [
1160 (1, 0, 1, 1),
1161 (31, 0, 1, 31),
1162 (32, 0, 1, 32),
1163 (33, 0, 2, 32),
1164 (33, 1, 2, 1),
1165 (64, 1, 2, 32),
1166 (65, 2, 3, 1),
1167 (u32::MAX, 134_217_727, 134_217_728, 31),
1168 ] {
1169 let mut symbol = [0_u8; 52];
1170 encode_fragment_header(&mut symbol, u64::MAX, record_len, index);
1171 symbol[FRAGMENT_HEADER_LEN..FRAGMENT_HEADER_LEN + len].fill(0xa5);
1172 let fragment = decode_fragment(&symbol, 32).unwrap();
1173 assert_eq!(fragment.fragment_count, count);
1174 assert_eq!(fragment.payload, &vec![0xa5; len]);
1175 }
1176 let mut symbol = [0_u8; 21];
1178 encode_fragment_header(&mut symbol, 42, u32::MAX, u32::MAX - 1);
1179 let fragment = decode_fragment(&symbol, 1).unwrap();
1180 assert_eq!(fragment.fragment_count, u32::MAX as usize);
1181 assert_eq!(fragment.payload.len(), 1);
1182 }
1183
1184 #[test]
1185 fn fragment_rejects_invalid_geometry_and_truncation() {
1186 let mut symbol = [0_u8; 52];
1187 for (record_len, index, capacity) in [
1188 (0, 0, 32),
1189 (32, 0, 0),
1190 (32, 1, 32),
1191 (33, 2, 32),
1192 (u32::MAX, u32::MAX, 1),
1193 ] {
1194 encode_fragment_header(&mut symbol, 42, record_len, index);
1195 assert_eq!(
1196 decode_fragment(&symbol, capacity).unwrap_err(),
1197 Error::InvalidFragment("invalid fragment geometry")
1198 );
1199 }
1200 for (record_len, index, len) in [(32, 0, 32), (40, 1, 8)] {
1201 encode_fragment_header(&mut symbol, 42, record_len, index);
1202 for end in 0..FRAGMENT_HEADER_LEN + len {
1203 assert!(decode_fragment(&symbol[..end], 32).is_err());
1204 }
1205 assert!(decode_fragment(&symbol[..FRAGMENT_HEADER_LEN + len], 32).is_ok());
1206 }
1207 symbol[0] ^= 1;
1208 assert_eq!(
1209 decode_fragment(&symbol, 32).unwrap_err(),
1210 Error::InvalidFragment("missing fragment header")
1211 );
1212 }
1213}