Skip to main content

cu29_logstream/
rlc.rs

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/// Identity shared by one sender's continuous stream.
17#[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/// Receiver-side bounds for one continuous stream.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct ReceiverLimits {
26    /// Largest accepted framed semantic record.
27    pub max_record_bytes: usize,
28    /// Maximum number of incomplete and completed-but-ordered records retained.
29    /// This must be at least the active RLC window size.
30    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/// Why a range of CopperLists can no longer be reconstructed exactly.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum GapReason {
45    /// One or more required source symbols aged out of the configured RLC window.
46    RlcWindowExpired,
47    /// History unavailable before a verified late-join boundary.
48    LateJoin,
49    /// Recovery advanced to a verified recovery point after an outage.
50    RecoveryPoint,
51    /// The caller finalized the session before the range could be recovered.
52    SessionEnded,
53}
54
55/// An inclusive range of unrecoverable CopperList identifiers.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub struct CopperListGap {
58    /// First missing CopperList identifier, inclusive.
59    pub first_id: u64,
60    /// Last missing CopperList identifier, inclusive.
61    pub last_id: u64,
62    /// Condition that made this range definitive.
63    pub reason: GapReason,
64}
65
66/// Ordered output from a continuous receiver.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum ContinuousReceiveEvent<'a> {
69    /// One complete, digest-verified semantic record.
70    Record(&'a RecoveredRecord),
71    /// An inclusive CopperList range that can no longer be recovered.
72    Gap(CopperListGap),
73}
74
75/// Separates malformed stream input from a downstream consumer failure.
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub enum ReceiveError<E> {
78    /// Packet, FEC, assembly, or semantic-record failure.
79    Stream(Error),
80    /// Failure returned by the supplied event consumer.
81    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/// Current bounded receiver occupancy, exposed for monitoring and tests.
103#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
104pub struct ReceiverOccupancy {
105    /// Source symbols marked as processed inside the active RLC window.
106    pub processed_symbols: usize,
107    /// Records awaiting one or more source symbols.
108    pub incomplete_records: usize,
109    /// Complete records waiting for an earlier record or definitive gap.
110    pub ready_records: usize,
111}
112
113/// Observed delivery and recovery behavior for one continuous stream.
114#[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    /// Percentage of expected systematic symbols that did not arrive intact.
137    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    /// Percentage of missing systematic symbols reconstructed by FEC.
146    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    /// Percentage of expected semantic records reconstructed and digest-verified.
160    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/// One fully assembled and digest-verified semantic record.
174#[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    /// Borrows metadata verified on assembly; immutable records are never re-hashed.
201    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    /// CopperList ID decoded without copying the serialized task state.
216    pub fn keyframe_id(&self) -> Option<u64> {
217        self.keyframe_id
218    }
219}
220
221/// Stateful RFC 8681 encoder for a continuous CopperList lane.
222///
223/// Record serialization, fragmentation, hashing, FEC, and packet allocation run
224/// on the semantic-output worker, never on Copper's real-time task path.
225pub 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    /// Fragments and emits one already-framed CopperList record as systematic symbols.
256    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    /// Fragments a record and emits each systematic packet immediately from
265    /// reusable caller-owned storage.
266    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    /// Emits one repair symbol over the encoder's current sliding window.
347    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    /// Emits one repair packet immediately from reusable caller-owned storage.
360    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
428/// Stateful, bounded RFC 8681 decoder and ordered CopperList assembler.
429///
430/// Recovered records are delivered to the caller immediately and are never
431/// retained in an ever-growing history. Exact gap reporting assumes the sender
432/// serializes CopperLists in increasing identifier order, as generated Copper
433/// runtimes do. Assembly buffers grow on demand up to receiver limits and are
434/// recycled after delivery or expiry. Once buffer sizes and concurrency have
435/// warmed up, record assembly and borrowed delivery need no heap allocations.
436pub 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    /// Enable bounded finalized-loss accounting on the receiver worker.
493    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    /// Advances ordering at a verified semantic boundary, retaining the FEC
508    /// window and already assembled records at or beyond that boundary.
509    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    /// Retries delivery of ordered records or gaps already ready in receiver state.
529    ///
530    /// This is useful after a consumer error and when shutting down a transport
531    /// after its final datagram. A consumer failure leaves the current event
532    /// pending so a later call can retry it.
533    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    /// Finalizes ordered delivery through an inclusive CopperList identifier.
546    ///
547    /// Any unresolved record or wholly absent identifier through `last_object_id`
548    /// is emitted as a terminal gap. Calling this method is an explicit promise
549    /// that no more useful repair symbols for that range will arrive.
550    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    /// Validates and submits one carrier-verified datagram. Malformed or foreign packets are counted
569    /// and ignored before they can contaminate the FEC decoder. Recovered records
570    /// and definitive gaps are emitted in CopperList-id order.
571    /// Callers must uphold the packet integrity contract of [`crate::CuStreamRx`].
572    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    // The index bound guarantees that the offset is smaller than record_len.
1108    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        // Exercise the largest index and count without allocating a large record.
1177        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}