1use crate::error::{Error, ErrorKind};
10use crate::raptorq::decoder::{
11 DecodeError as RaptorDecodeError, InactivationDecoder, RankStatus, ReceivedSymbol,
12};
13use crate::raptorq::systematic::{SystematicError, SystematicParams};
14use crate::security::{AuthenticatedSymbol, SecurityContext};
15use crate::types::symbol_set::{InsertResult, SymbolSet, ThresholdConfig};
16use crate::types::{ObjectId, ObjectParams, Symbol, SymbolId, SymbolKind};
17use std::collections::{HashMap, HashSet};
18use std::time::{Duration, Instant};
19
20const REPAIR_RETENTION_MIN_SLACK: usize = 128;
21const REPAIR_RETENTION_MAX_SLACK: usize = 2048;
22
23const AUTO_REPAIR_RETENTION_MIN_EXTRA_SYMBOLS: usize = 256;
24const AUTO_REPAIR_RETENTION_MAX_EXTRA_SYMBOLS: usize = 8192;
25
26#[derive(Debug, thiserror::Error)]
28pub enum DecodingError {
29 #[error("authentication failed for symbol {symbol_id}")]
31 AuthenticationFailed {
32 symbol_id: SymbolId,
34 },
35 #[error("insufficient symbols: have {received}, need {needed}")]
37 InsufficientSymbols {
38 received: usize,
40 needed: usize,
42 },
43 #[error("matrix inversion failed: {reason}")]
45 MatrixInversionFailed {
46 reason: String,
48 },
49 #[error("block timeout after {elapsed:?}")]
51 BlockTimeout {
52 sbn: u8,
54 elapsed: Duration,
56 },
57 #[error("inconsistent block metadata: {sbn} {details}")]
59 InconsistentMetadata {
60 sbn: u8,
62 details: String,
64 },
65 #[error("symbol size mismatch: expected {expected}, got {actual}")]
67 SymbolSizeMismatch {
68 expected: u16,
70 actual: usize,
72 },
73}
74
75impl From<DecodingError> for Error {
76 fn from(err: DecodingError) -> Self {
77 match &err {
78 DecodingError::AuthenticationFailed { .. } => Self::new(ErrorKind::CorruptedSymbol),
79 DecodingError::InsufficientSymbols { .. } => Self::new(ErrorKind::InsufficientSymbols),
80 DecodingError::MatrixInversionFailed { .. }
81 | DecodingError::InconsistentMetadata { .. }
82 | DecodingError::SymbolSizeMismatch { .. } => Self::new(ErrorKind::DecodingFailed),
83 DecodingError::BlockTimeout { .. } => Self::new(ErrorKind::ThresholdTimeout),
84 }
85 .with_message(err.to_string())
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum RejectReason {
92 WrongObjectId,
94 AuthenticationFailed,
96 SymbolSizeMismatch,
98 BlockAlreadyDecoded,
100 InsufficientRank,
102 InconsistentEquations,
104 InvalidMetadata,
106 MemoryLimitReached,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum SymbolAcceptResult {
113 Accepted {
115 received: usize,
117 needed: usize,
119 },
120 DecodingStarted {
122 block_sbn: u8,
124 },
125 BlockComplete {
127 block_sbn: u8,
129 data: Vec<u8>,
131 },
132 Duplicate,
134 Rejected(RejectReason),
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
140enum FeedDecodeMode {
141 Inline,
142 Deferred,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146enum FeedAuthPolicy {
147 VerifyInPipeline,
148 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
151 CallerVerified,
152}
153
154#[derive(Debug)]
157pub(crate) enum DeferredSymbolAcceptResult {
158 Immediate(SymbolAcceptResult),
160 Decode(BlockDecodeJob),
163}
164
165#[derive(Debug, Clone)]
167pub(crate) struct BlockDecodeJob {
168 sbn: u8,
169 plan: BlockPlan,
170 symbols: Vec<Symbol>,
171 source_symbols: usize,
172 symbol_size: usize,
173 retain_decoded_block: bool,
174}
175
176impl BlockDecodeJob {
177 #[must_use]
178 pub(crate) const fn sbn(&self) -> u8 {
179 self.sbn
180 }
181}
182
183#[derive(Debug)]
184enum BlockDecodeResolution {
185 Complete(Vec<u8>),
186 Retry {
187 reason: RejectReason,
188 symbols: Vec<Symbol>,
189 },
190 Failed {
191 reason: RejectReason,
192 symbols: Vec<Symbol>,
193 },
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub(crate) enum BlockDecodeKind {
199 SourceComplete,
201 RaptorQRepair,
203}
204
205#[derive(Debug)]
208pub(crate) struct BlockDecodeOutcome {
209 sbn: u8,
210 input_symbols: usize,
211 retain_decoded_block: bool,
212 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
215 kind: BlockDecodeKind,
216 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
217 elapsed: Duration,
218 resolution: BlockDecodeResolution,
219}
220
221#[must_use]
225pub(crate) fn audit_fnv1a64(bytes: &[u8]) -> u64 {
226 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
227 for byte in bytes {
228 hash ^= u64::from(*byte);
229 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
230 }
231 hash
232}
233
234fn audit_inconsistent_reject(sbn: u8, k: usize, symbols: &[Symbol], label: &str) {
240 if std::env::var_os("ATP_RQ_INCONSISTENT_AUDIT").is_none() {
241 return;
242 }
243 eprintln!(
244 "[RQ_AUDIT] decode_reject sbn={sbn} k={k} n_symbols={} label={label}",
245 symbols.len()
246 );
247 for symbol in symbols {
248 eprintln!(
249 "[RQ_AUDIT] sym sbn={} esi={} kind={:?} len={} h8={:016x}",
250 symbol.sbn(),
251 symbol.esi(),
252 symbol.kind(),
253 symbol.data().len(),
254 audit_fnv1a64(symbol.data()),
255 );
256 }
257}
258
259#[must_use]
261pub(crate) fn run_block_decode_job(job: BlockDecodeJob) -> BlockDecodeOutcome {
262 let BlockDecodeJob {
263 sbn,
264 plan,
265 symbols,
266 source_symbols,
267 symbol_size,
268 retain_decoded_block,
269 } = job;
270 let input_symbols = symbols.len();
271 let started = Instant::now();
272 let source_complete = (source_symbols >= plan.k)
273 .then(|| complete_block_data_from_source_symbols(&plan, &symbols))
274 .flatten();
275 let (kind, resolution) = match source_complete {
276 Some(data) => (
277 BlockDecodeKind::SourceComplete,
278 BlockDecodeResolution::Complete(data),
279 ),
280 None => {
281 let resolution = match decode_block_data(&plan, &symbols, symbol_size) {
282 Ok(data) => BlockDecodeResolution::Complete(data),
283 Err(DecodingError::InsufficientSymbols { .. }) => BlockDecodeResolution::Retry {
284 reason: RejectReason::InsufficientRank,
285 symbols,
286 },
287 Err(DecodingError::MatrixInversionFailed { .. }) => {
303 audit_inconsistent_reject(sbn, plan.k, &symbols, "rank_deficient_singular");
304 BlockDecodeResolution::Retry {
305 reason: RejectReason::InsufficientRank,
306 symbols,
307 }
308 }
309 Err(DecodingError::InconsistentMetadata { .. }) => BlockDecodeResolution::Failed {
310 reason: RejectReason::InvalidMetadata,
311 symbols,
312 },
313 Err(DecodingError::SymbolSizeMismatch { .. }) => BlockDecodeResolution::Failed {
314 reason: RejectReason::SymbolSizeMismatch,
315 symbols,
316 },
317 Err(_) => {
318 audit_inconsistent_reject(sbn, plan.k, &symbols, "decode_failed_other");
319 BlockDecodeResolution::Failed {
320 reason: RejectReason::InconsistentEquations,
321 symbols,
322 }
323 }
324 };
325 (BlockDecodeKind::RaptorQRepair, resolution)
326 }
327 };
328 BlockDecodeOutcome {
329 sbn,
330 input_symbols,
331 retain_decoded_block,
332 kind,
333 elapsed: started.elapsed().max(Duration::from_nanos(1)),
334 resolution,
335 }
336}
337
338impl BlockDecodeOutcome {
339 #[must_use]
343 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
344 pub(crate) fn elapsed(&self) -> Duration {
345 self.elapsed
346 }
347
348 #[must_use]
351 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
352 pub(crate) fn sbn(&self) -> u8 {
353 self.sbn
354 }
355
356 #[must_use]
357 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
358 pub(crate) fn kind(&self) -> BlockDecodeKind {
359 self.kind
360 }
361}
362
363#[derive(Debug, Clone)]
365pub struct DecodingConfig {
366 pub symbol_size: u16,
368 pub max_block_size: usize,
370 pub repair_overhead: f64,
372 pub min_overhead: usize,
374 pub max_buffered_symbols: usize,
376 pub block_timeout: Duration,
378 pub verify_auth: bool,
380}
381
382impl Default for DecodingConfig {
383 fn default() -> Self {
397 Self {
398 symbol_size: 256,
399 max_block_size: 1024 * 1024,
400 repair_overhead: 1.05,
401 min_overhead: 0,
402 max_buffered_symbols: 8192,
403 block_timeout: Duration::from_secs(30),
404 verify_auth: true,
405 }
406 }
407}
408
409impl DecodingConfig {
410 #[must_use]
423 pub fn without_auth() -> Self {
424 Self {
425 verify_auth: false,
426 ..Self::default()
427 }
428 }
429}
430
431#[derive(Debug, Clone, Copy)]
433pub struct DecodingProgress {
434 pub blocks_complete: usize,
436 pub blocks_total: Option<usize>,
438 pub symbols_received: usize,
440 pub symbols_needed_estimate: usize,
442}
443
444#[derive(Debug, Clone, Copy)]
446pub struct BlockStatus {
447 pub sbn: u8,
449 pub symbols_received: usize,
451 pub symbols_needed: usize,
453 pub rank: Option<usize>,
455 pub rank_deficit: Option<usize>,
457 pub state: BlockStateKind,
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq)]
463pub struct MissingSourceSymbol {
464 pub sbn: u8,
466 pub esi: u32,
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
472pub enum BlockStateKind {
473 Collecting,
475 Decoding,
477 Decoded,
479 Failed,
481}
482
483#[derive(Debug)]
484struct BlockDecoder {
485 state: BlockDecodingState,
486 decoded: Option<Vec<u8>>,
487}
488
489#[derive(Debug, Default, Clone, Copy)]
490struct PipelineBlockCounts {
491 source_symbols: usize,
492 repair_symbols: usize,
493}
494
495impl PipelineBlockCounts {
496 const fn total(self) -> usize {
497 self.source_symbols + self.repair_symbols
498 }
499}
500
501#[derive(Debug)]
502enum BlockDecodingState {
503 Collecting,
504 Decoding,
505 Decoded,
506 Failed,
507}
508
509#[derive(Debug)]
511pub struct DecodingPipeline {
512 config: DecodingConfig,
513 symbols: SymbolSet,
514 accepted_symbols_total: usize,
515 block_symbol_counts: HashMap<u8, PipelineBlockCounts>,
516 inflight_decode_symbols: HashSet<SymbolId>,
517 blocks: HashMap<u8, BlockDecoder>,
518 completed_blocks: HashSet<u8>,
519 object_id: Option<ObjectId>,
520 object_size: Option<u64>,
521 block_plans: Option<Vec<BlockPlan>>,
522 block_plan_by_sbn: [Option<usize>; 256],
523 auth_context: Option<SecurityContext>,
524 skipped_verifications: u64,
531 verify_auth_disabled_warned: bool,
537}
538
539impl DecodingPipeline {
540 #[must_use]
543 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
544 pub(crate) fn config_symbol_size(&self) -> u16 {
545 self.config.symbol_size
546 }
547
548 #[must_use]
550 pub fn new(config: DecodingConfig) -> Self {
551 let threshold = ThresholdConfig::new(
552 config.repair_overhead,
553 config.min_overhead,
554 config.max_buffered_symbols,
555 );
556 Self {
557 config,
558 symbols: SymbolSet::with_config(threshold),
559 accepted_symbols_total: 0,
560 block_symbol_counts: HashMap::new(),
561 inflight_decode_symbols: HashSet::new(),
562 blocks: HashMap::new(),
563 completed_blocks: HashSet::new(),
564 object_id: None,
565 object_size: None,
566 block_plans: None,
567 block_plan_by_sbn: [None; 256],
568 auth_context: None,
569 skipped_verifications: 0,
570 verify_auth_disabled_warned: false,
571 }
572 }
573
574 #[must_use]
584 #[inline]
585 pub const fn skipped_verifications(&self) -> u64 {
586 self.skipped_verifications
587 }
588
589 #[must_use]
591 pub fn with_auth(config: DecodingConfig, ctx: SecurityContext) -> Self {
592 let mut pipeline = Self::new(config);
593 pipeline.auth_context = Some(ctx);
594 pipeline
595 }
596
597 pub fn set_object_params(&mut self, params: ObjectParams) -> Result<(), DecodingError> {
599 if params.symbol_size != self.config.symbol_size {
600 return Err(DecodingError::SymbolSizeMismatch {
601 expected: self.config.symbol_size,
602 actual: params.symbol_size as usize,
603 });
604 }
605 if let Some(existing) = self.object_id {
606 if existing != params.object_id {
607 return Err(DecodingError::InconsistentMetadata {
608 sbn: 0,
609 details: format!(
610 "object id mismatch: expected {existing:?}, got {:?}",
611 params.object_id
612 ),
613 });
614 }
615 }
616 let plans = plan_blocks(
617 params.object_size as usize,
618 usize::from(params.symbol_size),
619 self.config.max_block_size,
620 )?;
621 validate_object_params_layout(params, &plans)?;
622 let block_plan_by_sbn = block_plan_index_by_sbn(&plans);
623 self.object_id = Some(params.object_id);
624 self.object_size = Some(params.object_size);
625 self.block_plans = Some(plans);
626 self.block_plan_by_sbn = block_plan_by_sbn;
627 self.configure_auto_buffer_limit();
628 self.configure_block_k();
629 Ok(())
630 }
631
632 #[must_use]
639 pub fn block_accept_cap(&self) -> usize {
640 self.symbols.max_per_block()
641 }
642
643 pub fn feed(
645 &mut self,
646 auth_symbol: AuthenticatedSymbol,
647 ) -> Result<SymbolAcceptResult, DecodingError> {
648 self.feed_with_retention(auth_symbol, true)
649 }
650
651 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
657 pub(crate) fn feed_deferred(
658 &mut self,
659 auth_symbol: AuthenticatedSymbol,
660 ) -> Result<DeferredSymbolAcceptResult, DecodingError> {
661 self.feed_with_retention_and_mode(
662 auth_symbol,
663 true,
664 FeedDecodeMode::Deferred,
665 FeedAuthPolicy::VerifyInPipeline,
666 )
667 }
668
669 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
673 pub(crate) fn feed_streaming_block_deferred(
674 &mut self,
675 auth_symbol: AuthenticatedSymbol,
676 ) -> Result<DeferredSymbolAcceptResult, DecodingError> {
677 self.feed_with_retention_and_mode(
678 auth_symbol,
679 false,
680 FeedDecodeMode::Deferred,
681 FeedAuthPolicy::VerifyInPipeline,
682 )
683 }
684
685 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
694 pub(crate) fn feed_preverified_streaming_block_deferred(
695 &mut self,
696 auth_symbol: AuthenticatedSymbol,
697 ) -> Result<DeferredSymbolAcceptResult, DecodingError> {
698 self.feed_with_retention_and_mode(
699 auth_symbol,
700 false,
701 FeedDecodeMode::Deferred,
702 FeedAuthPolicy::CallerVerified,
703 )
704 }
705
706 fn feed_with_retention(
707 &mut self,
708 auth_symbol: AuthenticatedSymbol,
709 retain_decoded_block: bool,
710 ) -> Result<SymbolAcceptResult, DecodingError> {
711 match self.feed_with_retention_and_mode(
712 auth_symbol,
713 retain_decoded_block,
714 FeedDecodeMode::Inline,
715 FeedAuthPolicy::VerifyInPipeline,
716 )? {
717 DeferredSymbolAcceptResult::Immediate(result) => Ok(result),
718 DeferredSymbolAcceptResult::Decode(job) => Ok(SymbolAcceptResult::DecodingStarted {
719 block_sbn: job.sbn(),
720 }),
721 }
722 }
723
724 fn feed_with_retention_and_mode(
725 &mut self,
726 mut auth_symbol: AuthenticatedSymbol,
727 retain_decoded_block: bool,
728 mode: FeedDecodeMode,
729 auth_policy: FeedAuthPolicy,
730 ) -> Result<DeferredSymbolAcceptResult, DecodingError> {
731 if matches!(auth_policy, FeedAuthPolicy::CallerVerified) {
732 if !auth_symbol.is_verified() {
733 return Ok(DeferredSymbolAcceptResult::Immediate(
734 SymbolAcceptResult::Rejected(RejectReason::AuthenticationFailed),
735 ));
736 }
737 } else if self.config.verify_auth {
738 match &self.auth_context {
739 Some(ctx) => {
740 if ctx.verify_authenticated_symbol(&mut auth_symbol).is_err()
741 || !auth_symbol.is_verified()
742 {
743 return Ok(DeferredSymbolAcceptResult::Immediate(
744 SymbolAcceptResult::Rejected(RejectReason::AuthenticationFailed),
745 ));
746 }
747 }
748 None => {
749 return Ok(DeferredSymbolAcceptResult::Immediate(
753 SymbolAcceptResult::Rejected(RejectReason::AuthenticationFailed),
754 ));
755 }
756 }
757 } else if !self.verify_auth_disabled_warned {
758 self.verify_auth_disabled_warned = true;
767 crate::tracing_compat::warn!(
768 target: "asupersync::decoding",
769 "br-asupersync-f4mdcr: DecodingPipeline configured \
770 with verify_auth=false; subsequent symbols are accepted \
771 without authentication. Skipped count is exposed via \
772 DecodingPipeline::skipped_verifications()."
773 );
774 }
775
776 let symbol = auth_symbol.into_symbol();
777
778 if symbol.len() != usize::from(self.config.symbol_size) {
779 return Ok(DeferredSymbolAcceptResult::Immediate(
780 SymbolAcceptResult::Rejected(RejectReason::SymbolSizeMismatch),
781 ));
782 }
783
784 if let Some(object_id) = self.object_id {
785 if object_id != symbol.object_id() {
786 return Ok(DeferredSymbolAcceptResult::Immediate(
787 SymbolAcceptResult::Rejected(RejectReason::WrongObjectId),
788 ));
789 }
790 } else {
791 self.object_id = Some(symbol.object_id());
792 }
793
794 let symbol_id = symbol.id();
795 let sbn = symbol.sbn();
796 let kind = symbol.kind();
797 if self.block_plans.is_some() && self.block_plan(sbn).is_none() {
798 return Ok(DeferredSymbolAcceptResult::Immediate(
799 SymbolAcceptResult::Rejected(RejectReason::InvalidMetadata),
800 ));
801 }
802 if self.completed_blocks.contains(&sbn) {
803 return Ok(DeferredSymbolAcceptResult::Immediate(
804 SymbolAcceptResult::Rejected(RejectReason::BlockAlreadyDecoded),
805 ));
806 }
807 if self.inflight_decode_symbols.contains(&symbol_id) {
808 return Ok(DeferredSymbolAcceptResult::Immediate(
809 SymbolAcceptResult::Duplicate,
810 ));
811 }
812 if kind.is_repair() && !self.symbols.contains(&symbol_id) {
813 self.configure_block_k();
814 if self.repair_retention_saturated(sbn) {
815 return Ok(DeferredSymbolAcceptResult::Immediate(
816 SymbolAcceptResult::Rejected(RejectReason::MemoryLimitReached),
817 ));
818 }
819 }
820
821 self.blocks.entry(sbn).or_insert_with(|| BlockDecoder {
823 state: BlockDecodingState::Collecting,
824 decoded: None,
825 });
826
827 let insert_result = self.symbols.insert(symbol);
828 match insert_result {
829 InsertResult::Duplicate => Ok(DeferredSymbolAcceptResult::Immediate(
830 SymbolAcceptResult::Duplicate,
831 )),
832 InsertResult::MemoryLimitReached | InsertResult::BlockLimitReached { .. } => {
833 Ok(DeferredSymbolAcceptResult::Immediate(
834 SymbolAcceptResult::Rejected(RejectReason::MemoryLimitReached),
835 ))
836 }
837 InsertResult::Inserted {
838 block_progress,
839 threshold_reached: _,
840 } => {
841 self.accepted_symbols_total = self.accepted_symbols_total.saturating_add(1);
842 if !self.config.verify_auth {
843 self.skipped_verifications = self.skipped_verifications.saturating_add(1);
844 }
845 let counts = self.block_symbol_counts.entry(sbn).or_default();
846 match kind {
847 SymbolKind::Source => counts.source_symbols += 1,
848 SymbolKind::Repair => counts.repair_symbols += 1,
849 }
850 let received = counts.total();
851 let source_received = counts.source_symbols;
852
853 if block_progress.k.is_none() {
854 self.configure_block_k();
855 }
856
857 let progress = self
858 .symbols
859 .block_progress(sbn)
860 .copied()
861 .unwrap_or(block_progress);
862 let k = self
863 .block_plan(sbn)
864 .map(|plan| plan.k)
865 .or_else(|| progress.k.map(usize::from));
866 let needed = k.map_or(0, |k| {
867 required_symbols(
868 u16::try_from(k).unwrap_or(u16::MAX),
869 self.config.repair_overhead,
870 self.config.min_overhead,
871 )
872 });
873 if k.is_some_and(|k| source_received >= k || received >= needed) {
874 if matches!(mode, FeedDecodeMode::Deferred) && self.block_is_decoding(sbn) {
875 return Ok(DeferredSymbolAcceptResult::Immediate(
876 SymbolAcceptResult::Accepted { received, needed },
877 ));
878 }
879
880 if let Some(block) = self.blocks.get_mut(&sbn) {
882 block.state = BlockDecodingState::Decoding;
883 }
884 if matches!(mode, FeedDecodeMode::Deferred) {
885 if let Some(job) = self.prepare_decode_job(sbn, retain_decoded_block) {
886 return Ok(DeferredSymbolAcceptResult::Decode(job));
887 }
888 } else if let Some(result) = self.try_decode_block(sbn, retain_decoded_block) {
889 return Ok(DeferredSymbolAcceptResult::Immediate(result));
890 }
891 }
892
893 if let Some(block) = self.blocks.get_mut(&sbn) {
895 if !matches!(
896 block.state,
897 BlockDecodingState::Decoded | BlockDecodingState::Failed
898 ) {
899 block.state = BlockDecodingState::Collecting;
900 }
901 }
902 Ok(DeferredSymbolAcceptResult::Immediate(
903 SymbolAcceptResult::Accepted { received, needed },
904 ))
905 }
906 }
907 }
908
909 pub fn feed_batch(
911 &mut self,
912 symbols: impl Iterator<Item = AuthenticatedSymbol>,
913 ) -> Vec<Result<SymbolAcceptResult, DecodingError>> {
914 symbols.map(|symbol| self.feed(symbol)).collect()
915 }
916
917 #[must_use]
919 pub fn is_complete(&self) -> bool {
920 let Some(plans) = &self.block_plans else {
921 return false;
922 };
923 self.completed_blocks.len() == plans.len()
924 }
925
926 #[must_use]
928 pub fn progress(&self) -> DecodingProgress {
929 let blocks_total = self.block_plans.as_ref().map(Vec::len);
930 let symbols_received = self.accepted_symbols_total;
931 let symbols_needed_estimate = self.block_plans.as_ref().map_or(0, |plans| {
932 sum_required_symbols(plans, self.config.repair_overhead, self.config.min_overhead)
933 });
934
935 DecodingProgress {
936 blocks_complete: self.completed_blocks.len(),
937 blocks_total,
938 symbols_received,
939 symbols_needed_estimate,
940 }
941 }
942
943 #[must_use]
945 pub fn block_status(&self, sbn: u8) -> Option<BlockStatus> {
946 let progress = self.symbols.block_progress(sbn)?;
947 let state = self
948 .blocks
949 .get(&sbn)
950 .map_or(BlockStateKind::Collecting, |block| match block.state {
951 BlockDecodingState::Collecting => BlockStateKind::Collecting,
952 BlockDecodingState::Decoding => BlockStateKind::Decoding,
953 BlockDecodingState::Decoded => BlockStateKind::Decoded,
954 BlockDecodingState::Failed => BlockStateKind::Failed,
955 });
956
957 let symbols_needed = progress.k.map_or(0, |k| {
958 required_symbols(k, self.config.repair_overhead, self.config.min_overhead)
959 });
960 let rank_status = self.block_rank_status(sbn);
961
962 Some(BlockStatus {
963 sbn,
964 symbols_received: progress.total(),
965 symbols_needed,
966 rank: rank_status.map(|status| status.rank),
967 rank_deficit: rank_status.map(|status| status.deficit),
968 state,
969 })
970 }
971
972 pub fn into_data(self) -> Result<Vec<u8>, DecodingError> {
974 let Some(plans) = &self.block_plans else {
975 return Err(DecodingError::InconsistentMetadata {
976 sbn: 0,
977 details: "object parameters not set".to_string(),
978 });
979 };
980 if !self.is_complete() {
981 let received = self.accepted_symbols_total;
982 let needed =
983 sum_required_symbols(plans, self.config.repair_overhead, self.config.min_overhead);
984 return Err(DecodingError::InsufficientSymbols { received, needed });
985 }
986
987 let mut output = Vec::with_capacity(self.object_size.unwrap_or(0) as usize);
988 for plan in plans {
989 let block = self
990 .blocks
991 .get(&plan.sbn)
992 .and_then(|b| b.decoded.as_ref())
993 .ok_or_else(|| DecodingError::InconsistentMetadata {
994 sbn: plan.sbn,
995 details: "missing decoded block".to_string(),
996 })?;
997 output.extend_from_slice(block);
998 }
999
1000 if let Some(size) = self.object_size {
1001 output.truncate(size as usize);
1002 }
1003
1004 Ok(output)
1005 }
1006
1007 fn configure_block_k(&mut self) {
1008 let Some(plans) = &self.block_plans else {
1009 return;
1010 };
1011 for plan in plans {
1012 let k = u16::try_from(plan.k).unwrap_or(u16::MAX);
1013 let _ = self.symbols.set_block_k(plan.sbn, k);
1014 }
1015 }
1016
1017 fn configure_auto_buffer_limit(&mut self) {
1018 if self.config.max_buffered_symbols != 0 {
1019 return;
1020 }
1021 let Some(plans) = &self.block_plans else {
1022 return;
1023 };
1024 let max_k = plans.iter().map(|plan| plan.k).max().unwrap_or(0);
1025 if max_k == 0 {
1026 return;
1027 }
1028 let extra = max_k.clamp(
1029 AUTO_REPAIR_RETENTION_MIN_EXTRA_SYMBOLS,
1030 AUTO_REPAIR_RETENTION_MAX_EXTRA_SYMBOLS,
1031 );
1032 self.symbols.set_max_per_block(max_k.saturating_add(extra));
1033 }
1034
1035 fn try_decode_block(
1036 &mut self,
1037 sbn: u8,
1038 retain_decoded_block: bool,
1039 ) -> Option<SymbolAcceptResult> {
1040 if let Some(result) = self.try_complete_source_block(sbn, retain_decoded_block) {
1041 return Some(result);
1042 }
1043
1044 let job = self.prepare_decode_job(sbn, retain_decoded_block)?;
1045 let outcome = run_block_decode_job(job);
1046 Some(self.finish_inline_decode_job(outcome))
1047 }
1048
1049 fn try_complete_source_block(
1050 &mut self,
1051 sbn: u8,
1052 retain_decoded_block: bool,
1053 ) -> Option<SymbolAcceptResult> {
1054 let block_plan = self.block_plan(sbn)?.clone();
1055 if self
1062 .block_symbol_counts
1063 .get(&sbn)
1064 .map_or(0, |counts| counts.source_symbols)
1065 < block_plan.k
1066 {
1067 return None;
1068 }
1069 let block_data = self.try_complete_from_source_symbols(&block_plan)?;
1070
1071 self.mark_block_complete(sbn, retain_decoded_block.then(|| block_data.clone()));
1072
1073 Some(SymbolAcceptResult::BlockComplete {
1074 block_sbn: sbn,
1075 data: block_data,
1076 })
1077 }
1078
1079 fn prepare_decode_job(
1080 &mut self,
1081 sbn: u8,
1082 retain_decoded_block: bool,
1083 ) -> Option<BlockDecodeJob> {
1084 let block_plan = self.block_plan(sbn)?.clone();
1085 if block_plan.k == 0 {
1086 return None;
1087 }
1088
1089 let counts = self
1096 .block_symbol_counts
1097 .get(&sbn)
1098 .copied()
1099 .unwrap_or_default();
1100 if counts.total() < block_plan.k {
1101 return None;
1102 }
1103
1104 let symbols = self.symbols.take_block_symbols(sbn);
1105 if symbols.len() < block_plan.k {
1106 self.restore_decode_symbols(sbn, symbols);
1107 return None;
1108 }
1109 self.remember_decode_job_symbols(&symbols);
1110
1111 Some(BlockDecodeJob {
1112 sbn,
1113 plan: block_plan,
1114 symbols,
1115 source_symbols: counts.source_symbols,
1116 symbol_size: usize::from(self.config.symbol_size),
1117 retain_decoded_block,
1118 })
1119 }
1120
1121 fn block_is_decoding(&self, sbn: u8) -> bool {
1122 self.blocks
1123 .get(&sbn)
1124 .is_some_and(|block| matches!(block.state, BlockDecodingState::Decoding))
1125 }
1126
1127 fn repair_retention_saturated(&self, sbn: u8) -> bool {
1128 let Some(cap) = self.repair_retention_cap(sbn) else {
1129 return false;
1130 };
1131 self.block_symbol_counts
1132 .get(&sbn)
1133 .is_some_and(|counts| counts.total() >= cap)
1134 }
1135
1136 fn repair_retention_cap(&self, sbn: u8) -> Option<usize> {
1137 let k = self.block_plan(sbn).map(|plan| plan.k).or_else(|| {
1138 self.symbols
1139 .block_progress(sbn)
1140 .and_then(|progress| progress.k.map(usize::from))
1141 })?;
1142 if k == 0 {
1143 return Some(0);
1144 }
1145 let needed = required_symbols(
1146 u16::try_from(k).unwrap_or(u16::MAX),
1147 self.config.repair_overhead,
1148 self.config.min_overhead,
1149 );
1150 let slack = k.clamp(REPAIR_RETENTION_MIN_SLACK, REPAIR_RETENTION_MAX_SLACK);
1151 let minimum_safe_cap = needed.max(k);
1152 let dynamic_cap = needed.saturating_add(slack).max(k);
1153 let configured_cap = self.config.max_buffered_symbols;
1154 Some(if configured_cap == 0 {
1155 dynamic_cap
1156 } else {
1157 configured_cap.max(minimum_safe_cap)
1158 })
1159 }
1160
1161 fn finish_inline_decode_job(&mut self, outcome: BlockDecodeOutcome) -> SymbolAcceptResult {
1162 let BlockDecodeOutcome {
1163 sbn,
1164 input_symbols: _,
1165 retain_decoded_block,
1166 kind: _,
1167 elapsed: _,
1168 resolution,
1169 } = outcome;
1170 match resolution {
1171 BlockDecodeResolution::Complete(block_data) => {
1172 self.mark_block_complete(sbn, retain_decoded_block.then(|| block_data.clone()));
1173 SymbolAcceptResult::BlockComplete {
1174 block_sbn: sbn,
1175 data: block_data,
1176 }
1177 }
1178 BlockDecodeResolution::Retry { reason, symbols } => {
1179 self.restore_decode_symbols(sbn, symbols);
1180 if let Some(block) = self.blocks.get_mut(&sbn) {
1181 block.state = BlockDecodingState::Collecting;
1182 }
1183 SymbolAcceptResult::Rejected(reason)
1184 }
1185 BlockDecodeResolution::Failed { reason, symbols } => {
1186 self.restore_decode_symbols(sbn, symbols);
1187 if let Some(block) = self.blocks.get_mut(&sbn) {
1188 block.state = BlockDecodingState::Failed;
1189 }
1190 SymbolAcceptResult::Rejected(reason)
1191 }
1192 }
1193 }
1194
1195 #[must_use]
1197 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
1198 pub(crate) fn finish_decode_job(&mut self, outcome: BlockDecodeOutcome) -> SymbolAcceptResult {
1199 match self.finish_decode_job_deferred(outcome) {
1200 DeferredSymbolAcceptResult::Immediate(result) => result,
1201 DeferredSymbolAcceptResult::Decode(job) => {
1202 let outcome = run_block_decode_job(job);
1203 self.finish_inline_decode_job(outcome)
1204 }
1205 }
1206 }
1207
1208 #[must_use]
1213 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
1214 pub(crate) fn finish_decode_job_deferred(
1215 &mut self,
1216 outcome: BlockDecodeOutcome,
1217 ) -> DeferredSymbolAcceptResult {
1218 let BlockDecodeOutcome {
1219 sbn,
1220 input_symbols,
1221 retain_decoded_block,
1222 kind: _,
1223 elapsed: _,
1224 resolution,
1225 } = outcome;
1226 if self.completed_blocks.contains(&sbn) {
1227 return DeferredSymbolAcceptResult::Immediate(SymbolAcceptResult::Rejected(
1228 RejectReason::BlockAlreadyDecoded,
1229 ));
1230 }
1231
1232 match resolution {
1233 BlockDecodeResolution::Complete(block_data) => {
1234 self.mark_block_complete(sbn, retain_decoded_block.then(|| block_data.clone()));
1235 DeferredSymbolAcceptResult::Immediate(SymbolAcceptResult::BlockComplete {
1236 block_sbn: sbn,
1237 data: block_data,
1238 })
1239 }
1240 BlockDecodeResolution::Retry { reason, symbols } => {
1241 if let Some(block) = self.blocks.get_mut(&sbn) {
1242 block.state = BlockDecodingState::Collecting;
1243 }
1244 self.restore_decode_symbols(sbn, symbols);
1245 let current_symbols = self.symbols.symbols_for_block(sbn).count();
1246 if current_symbols > input_symbols {
1247 if let Some(job) = self.prepare_decode_job(sbn, retain_decoded_block) {
1248 if let Some(block) = self.blocks.get_mut(&sbn) {
1249 block.state = BlockDecodingState::Decoding;
1250 }
1251 return DeferredSymbolAcceptResult::Decode(job);
1252 }
1253 }
1254 DeferredSymbolAcceptResult::Immediate(SymbolAcceptResult::Rejected(reason))
1255 }
1256 BlockDecodeResolution::Failed { reason, symbols } => {
1257 self.restore_decode_symbols(sbn, symbols);
1258 if let Some(block) = self.blocks.get_mut(&sbn) {
1259 block.state = BlockDecodingState::Failed;
1260 }
1261 DeferredSymbolAcceptResult::Immediate(SymbolAcceptResult::Rejected(reason))
1262 }
1263 }
1264 }
1265
1266 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
1267 pub(crate) fn cancel_decode_job(&mut self, sbn: u8) {
1268 if self.completed_blocks.contains(&sbn) {
1269 return;
1270 }
1271 if let Some(block) = self.blocks.get_mut(&sbn) {
1272 if matches!(block.state, BlockDecodingState::Decoding) {
1273 block.state = BlockDecodingState::Collecting;
1274 }
1275 }
1276 }
1277
1278 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
1279 pub(crate) fn restore_decode_job(&mut self, job: BlockDecodeJob) {
1280 let sbn = job.sbn;
1281 if self.completed_blocks.contains(&sbn) {
1282 for symbol in &job.symbols {
1283 self.inflight_decode_symbols.remove(&symbol.id());
1284 }
1285 return;
1286 }
1287 self.cancel_decode_job(sbn);
1288 self.restore_decode_symbols(sbn, job.symbols);
1289 }
1290
1291 fn remember_decode_job_symbols(&mut self, symbols: &[Symbol]) {
1292 for symbol in symbols {
1293 self.inflight_decode_symbols.insert(symbol.id());
1294 }
1295 }
1296
1297 fn restore_decode_symbols(&mut self, sbn: u8, symbols: Vec<Symbol>) {
1298 for symbol in symbols {
1299 self.inflight_decode_symbols.remove(&symbol.id());
1300 debug_assert_eq!(symbol.sbn(), sbn);
1301 let _ = self.symbols.restore_retained(symbol);
1302 }
1303 }
1304
1305 fn forget_inflight_decode_symbols_for_block(&mut self, sbn: u8) {
1306 self.inflight_decode_symbols
1307 .retain(|symbol_id| symbol_id.sbn() != sbn);
1308 }
1309
1310 fn try_complete_from_source_symbols(&self, block_plan: &BlockPlan) -> Option<Vec<u8>> {
1311 let object_id = self.object_id?;
1312 let mut block_data = Vec::with_capacity(block_plan.len);
1313 for esi in 0..block_plan.k {
1314 let esi = u32::try_from(esi).ok()?;
1315 let id = SymbolId::new(object_id, block_plan.sbn, esi);
1316 let symbol = self.symbols.get(&id)?;
1317 if symbol.kind() != SymbolKind::Source {
1318 return None;
1319 }
1320 let remaining = block_plan.len.saturating_sub(block_data.len());
1321 if remaining == 0 {
1322 break;
1323 }
1324 let take = remaining.min(symbol.data().len());
1325 block_data.extend_from_slice(&symbol.data()[..take]);
1326 }
1327 (block_data.len() == block_plan.len).then_some(block_data)
1328 }
1329
1330 fn mark_block_complete(&mut self, sbn: u8, retained_block: Option<Vec<u8>>) {
1331 if let Some(block) = self.blocks.get_mut(&sbn) {
1332 block.state = BlockDecodingState::Decoded;
1333 block.decoded = retained_block;
1334 }
1335 self.completed_blocks.insert(sbn);
1336 self.forget_inflight_decode_symbols_for_block(sbn);
1337 self.symbols.clear_block(sbn);
1338 self.block_symbol_counts.remove(&sbn);
1339 }
1340
1341 fn block_plan(&self, sbn: u8) -> Option<&BlockPlan> {
1342 let idx = self.block_plan_by_sbn[usize::from(sbn)]?;
1343 self.block_plans.as_ref()?.get(idx)
1344 }
1345
1346 fn block_rank_status(&self, sbn: u8) -> Option<RankStatus> {
1347 let block_plan = self.block_plan(sbn)?.clone();
1348 let symbols: Vec<Symbol> = self.symbols.symbols_for_block(sbn).cloned().collect();
1349 rank_status_for_block(&block_plan, &symbols, usize::from(self.config.symbol_size)).ok()
1350 }
1351
1352 #[must_use]
1359 pub fn missing_source_symbols(&self, limit: usize) -> Vec<MissingSourceSymbol> {
1360 let Some(object_id) = self.object_id else {
1361 return Vec::new();
1362 };
1363 let Some(plans) = &self.block_plans else {
1364 return Vec::new();
1365 };
1366
1367 let mut missing = Vec::new();
1368 for plan in plans {
1369 if self.completed_blocks.contains(&plan.sbn) {
1370 continue;
1371 }
1372 for esi in 0..plan.k {
1373 let Ok(esi) = u32::try_from(esi) else {
1374 break;
1375 };
1376 let id = SymbolId::new(object_id, plan.sbn, esi);
1377 if !self.symbols.contains(&id) && !self.inflight_decode_symbols.contains(&id) {
1378 missing.push(MissingSourceSymbol { sbn: plan.sbn, esi });
1379 if limit != 0 && missing.len() >= limit {
1380 return missing;
1381 }
1382 }
1383 }
1384 }
1385 missing
1386 }
1387}
1388
1389#[derive(Debug, Clone)]
1390struct BlockPlan {
1391 sbn: u8,
1392 len: usize,
1393 k: usize,
1394}
1395
1396fn plan_blocks(
1397 object_size: usize,
1398 symbol_size: usize,
1399 max_block_size: usize,
1400) -> Result<Vec<BlockPlan>, DecodingError> {
1401 if object_size == 0 {
1402 return Ok(Vec::new());
1403 }
1404
1405 if symbol_size == 0 {
1406 return Err(DecodingError::InconsistentMetadata {
1407 sbn: 0,
1408 details: "symbol_size must be > 0".to_string(),
1409 });
1410 }
1411
1412 let max_blocks = u8::MAX as usize + 1;
1413 let max_total = max_block_size.saturating_mul(max_blocks);
1414 if object_size > max_total {
1415 return Err(DecodingError::InconsistentMetadata {
1416 sbn: 0,
1417 details: format!("object size {object_size} exceeds limit {max_total}"),
1418 });
1419 }
1420
1421 let mut blocks = Vec::new();
1422 let mut offset = 0;
1423 let mut sbn: u8 = 0;
1424
1425 while offset < object_size {
1426 let len = usize::min(max_block_size, object_size - offset);
1427 let k = len.div_ceil(symbol_size);
1428 blocks.push(BlockPlan { sbn, len, k });
1429 offset += len;
1430 sbn = sbn.wrapping_add(1);
1431 }
1432
1433 Ok(blocks)
1434}
1435
1436fn block_plan_index_by_sbn(plans: &[BlockPlan]) -> [Option<usize>; 256] {
1437 let mut index = [None; 256];
1438 for (idx, plan) in plans.iter().enumerate() {
1439 index[usize::from(plan.sbn)] = Some(idx);
1440 }
1441 index
1442}
1443
1444fn validate_object_params_layout(
1445 params: ObjectParams,
1446 plans: &[BlockPlan],
1447) -> Result<(), DecodingError> {
1448 let declared_blocks = usize::from(params.source_blocks);
1449 let declared_k = usize::from(params.symbols_per_block);
1450
1451 if plans.is_empty() {
1452 if declared_blocks == 0 && declared_k == 0 {
1453 return Ok(());
1454 }
1455 if declared_blocks == 1 {
1456 return Ok(());
1457 }
1458 return Err(DecodingError::InconsistentMetadata {
1459 sbn: 0,
1460 details: format!(
1461 "object params layout mismatch: empty object expects either 0 blocks / 0 symbols-per-block or a single empty sentinel block, got {declared_blocks} block(s) with {declared_k} symbols/block"
1462 ),
1463 });
1464 }
1465
1466 let expected_blocks = plans.len();
1467 if declared_blocks != expected_blocks {
1468 return Err(DecodingError::InconsistentMetadata {
1469 sbn: 0,
1470 details: format!(
1471 "object params block count mismatch: expected {expected_blocks}, got {declared_blocks}"
1472 ),
1473 });
1474 }
1475
1476 let expected_k = plans.iter().map(|plan| plan.k).max().unwrap_or(0);
1477 if declared_k != expected_k {
1478 return Err(DecodingError::InconsistentMetadata {
1479 sbn: 0,
1480 details: format!(
1481 "object params symbols_per_block mismatch: expected {expected_k}, got {declared_k}"
1482 ),
1483 });
1484 }
1485
1486 let symbol_size = usize::from(params.symbol_size);
1494 if symbol_size > 0 {
1495 for plan in plans {
1496 if let Err(err) = SystematicParams::try_for_source_block(plan.k, symbol_size) {
1497 return Err(DecodingError::InconsistentMetadata {
1498 sbn: plan.sbn,
1499 details: format!(
1500 "block K={} exceeds RFC 6330 systematic-index table: {err:?}",
1501 plan.k
1502 ),
1503 });
1504 }
1505 }
1506 }
1507
1508 Ok(())
1509}
1510
1511fn required_symbols(k: u16, overhead: f64, min_overhead: usize) -> usize {
1512 if k == 0 {
1513 return 0;
1514 }
1515 let raw = (f64::from(k) * overhead).ceil();
1516 let minimum_threshold = usize::from(k).saturating_add(min_overhead);
1517 if raw.is_nan() {
1518 return minimum_threshold;
1519 }
1520 if raw.is_sign_positive() && !raw.is_finite() {
1521 return usize::MAX;
1522 }
1523 if raw.is_sign_negative() {
1524 return minimum_threshold;
1525 }
1526 #[allow(clippy::cast_sign_loss)]
1527 let factor_threshold = raw as usize;
1528 factor_threshold.max(minimum_threshold)
1531}
1532
1533fn sum_required_symbols(plans: &[BlockPlan], overhead: f64, min_overhead: usize) -> usize {
1534 plans.iter().fold(0usize, |acc, plan| {
1535 acc.saturating_add(required_symbols(
1536 u16::try_from(plan.k).unwrap_or(u16::MAX),
1537 overhead,
1538 min_overhead,
1539 ))
1540 })
1541}
1542
1543fn received_symbols_for_block(
1544 plan: &BlockPlan,
1545 symbols: &[Symbol],
1546 decoder: &InactivationDecoder,
1547) -> Result<Vec<ReceivedSymbol>, DecodingError> {
1548 let k = plan.k;
1549 let mut received = decoder.constraint_symbols();
1550 received.reserve(symbols.len());
1551
1552 for symbol in symbols {
1553 match symbol.kind() {
1554 SymbolKind::Source => {
1555 let esi = symbol.esi() as usize;
1556 if esi >= k {
1557 return Err(DecodingError::InconsistentMetadata {
1558 sbn: plan.sbn,
1559 details: format!("source esi {esi} >= k {k}"),
1560 });
1561 }
1562 received.push(ReceivedSymbol::source(symbol.esi(), symbol.data().to_vec()));
1563 }
1564 SymbolKind::Repair => {
1565 let (columns, coefficients) = match decoder.repair_equation(symbol.esi()) {
1566 Ok(equation) => equation,
1567 Err(SystematicError::RepairEsiBelowK { esi, k }) => {
1568 return Err(DecodingError::InconsistentMetadata {
1569 sbn: plan.sbn,
1570 details: format!("repair esi {esi} < first repair esi {k}"),
1571 });
1572 }
1573 Err(SystematicError::EsiOverflow { esi, padding_delta }) => {
1574 return Err(DecodingError::InconsistentMetadata {
1575 sbn: plan.sbn,
1576 details: format!(
1577 "repair esi {esi} overflows RFC repair-ISI padding delta {padding_delta}"
1578 ),
1579 });
1580 }
1581 };
1582 received.push(ReceivedSymbol {
1583 esi: symbol.esi(),
1584 is_source: false,
1585 columns,
1586 coefficients,
1587 data: symbol.data().to_vec(),
1588 });
1589 }
1590 }
1591 }
1592
1593 Ok(received)
1594}
1595
1596fn rank_status_for_block(
1597 plan: &BlockPlan,
1598 symbols: &[Symbol],
1599 symbol_size: usize,
1600) -> Result<RankStatus, DecodingError> {
1601 if plan.k == 0 {
1602 return Ok(RankStatus {
1603 rank: 0,
1604 columns: 0,
1605 deficit: 0,
1606 });
1607 }
1608
1609 let object_id = symbols.first().map_or(ObjectId::NIL, Symbol::object_id);
1610 let block_seed = seed_for_block(object_id, plan.sbn);
1611 let decoder = InactivationDecoder::new(plan.k, symbol_size, block_seed);
1612 let received = received_symbols_for_block(plan, symbols, &decoder)?;
1613 decoder.rank_status(&received).map_err(|err| match err {
1614 RaptorDecodeError::SymbolSizeMismatch { expected, actual } => {
1615 DecodingError::SymbolSizeMismatch {
1616 expected: u16::try_from(expected).unwrap_or(u16::MAX),
1617 actual,
1618 }
1619 }
1620 RaptorDecodeError::SymbolEquationArityMismatch {
1621 esi,
1622 columns,
1623 coefficients,
1624 } => DecodingError::InconsistentMetadata {
1625 sbn: plan.sbn,
1626 details: format!(
1627 "symbol {esi} has mismatched equation vectors: columns={columns}, coefficients={coefficients}"
1628 ),
1629 },
1630 RaptorDecodeError::ColumnIndexOutOfRange {
1631 esi,
1632 column,
1633 max_valid,
1634 } => DecodingError::InconsistentMetadata {
1635 sbn: plan.sbn,
1636 details: format!(
1637 "symbol {esi} references out-of-range column {column} (valid < {max_valid})"
1638 ),
1639 },
1640 RaptorDecodeError::SourceEsiOutOfRange { esi, max_valid } => {
1641 DecodingError::InconsistentMetadata {
1642 sbn: plan.sbn,
1643 details: format!(
1644 "source symbol {esi} falls outside the systematic domain (valid < {max_valid})"
1645 ),
1646 }
1647 }
1648 RaptorDecodeError::InvalidSourceSymbolEquation {
1649 esi,
1650 expected_column,
1651 } => DecodingError::InconsistentMetadata {
1652 sbn: plan.sbn,
1653 details: format!(
1654 "source symbol {esi} must use the identity equation for column {expected_column}"
1655 ),
1656 },
1657 other => DecodingError::MatrixInversionFailed {
1658 reason: format!("{other:?}"),
1659 },
1660 })
1661}
1662
1663#[allow(clippy::too_many_lines)]
1664fn decode_block(
1665 plan: &BlockPlan,
1666 symbols: &[Symbol],
1667 symbol_size: usize,
1668) -> Result<Vec<Symbol>, DecodingError> {
1669 let k = plan.k;
1670 if symbols.len() < k {
1671 return Err(DecodingError::InsufficientSymbols {
1672 received: symbols.len(),
1673 needed: k,
1674 });
1675 }
1676
1677 let object_id = symbols.first().map_or(ObjectId::NIL, Symbol::object_id);
1678 let block_seed = seed_for_block(object_id, plan.sbn);
1679 let decoder = InactivationDecoder::new(k, symbol_size, block_seed);
1680 let received = received_symbols_for_block(plan, symbols, &decoder)?;
1681
1682 let result = match decoder.decode(&received) {
1683 Ok(result) => result,
1684 Err(err) => {
1685 let mapped = match err {
1686 RaptorDecodeError::InsufficientSymbols { received, required } => {
1687 DecodingError::InsufficientSymbols {
1688 received,
1689 needed: required,
1690 }
1691 }
1692 RaptorDecodeError::SingularMatrix { row } => DecodingError::MatrixInversionFailed {
1693 reason: format!("singular matrix at row {row}"),
1694 },
1695 RaptorDecodeError::SymbolSizeMismatch { expected, actual } => {
1696 DecodingError::SymbolSizeMismatch {
1697 expected: u16::try_from(expected).unwrap_or(u16::MAX),
1698 actual,
1699 }
1700 }
1701 RaptorDecodeError::SymbolEquationArityMismatch {
1702 esi,
1703 columns,
1704 coefficients,
1705 } => DecodingError::InconsistentMetadata {
1706 sbn: plan.sbn,
1707 details: format!(
1708 "symbol {esi} has mismatched equation vectors: columns={columns}, coefficients={coefficients}"
1709 ),
1710 },
1711 RaptorDecodeError::ColumnIndexOutOfRange {
1712 esi,
1713 column,
1714 max_valid,
1715 } => DecodingError::InconsistentMetadata {
1716 sbn: plan.sbn,
1717 details: format!(
1718 "symbol {esi} references out-of-range column {column} (valid < {max_valid})"
1719 ),
1720 },
1721 RaptorDecodeError::SourceEsiOutOfRange { esi, max_valid } => {
1722 DecodingError::InconsistentMetadata {
1723 sbn: plan.sbn,
1724 details: format!(
1725 "source symbol {esi} falls outside the systematic domain (valid < {max_valid})"
1726 ),
1727 }
1728 }
1729 RaptorDecodeError::InvalidSourceSymbolEquation {
1730 esi,
1731 expected_column,
1732 } => DecodingError::InconsistentMetadata {
1733 sbn: plan.sbn,
1734 details: format!(
1735 "source symbol {esi} must use the identity equation for column {expected_column}"
1736 ),
1737 },
1738 RaptorDecodeError::CorruptDecodedOutput {
1739 esi,
1740 byte_index,
1741 expected,
1742 actual,
1743 } => DecodingError::MatrixInversionFailed {
1744 reason: format!(
1745 "decoded output verification failed at symbol {esi}, byte {byte_index}: expected 0x{expected:02x}, actual 0x{actual:02x}"
1746 ),
1747 },
1748 RaptorDecodeError::ComputeBudgetExhausted {
1749 used,
1750 requested,
1751 max,
1752 } => DecodingError::MatrixInversionFailed {
1753 reason: format!(
1754 "compute budget exhausted: used {used}, requested {requested}, max {max}"
1755 ),
1756 },
1757 RaptorDecodeError::EsiRateLimitExceeded {
1758 esi,
1759 column_count,
1760 max_columns,
1761 } => DecodingError::InconsistentMetadata {
1762 sbn: plan.sbn,
1763 details: format!(
1764 "ESI rate limit exceeded: symbol {esi} would generate {column_count} columns (max {max_columns})"
1765 ),
1766 },
1767 };
1768 return Err(mapped);
1769 }
1770 };
1771
1772 let mut decoded_symbols = Vec::with_capacity(k);
1776 for (esi, data) in result.source.into_iter().enumerate() {
1777 decoded_symbols.push(Symbol::new(
1778 SymbolId::new(object_id, plan.sbn, esi as u32),
1779 data,
1780 SymbolKind::Source,
1781 ));
1782 }
1783
1784 Ok(decoded_symbols)
1785}
1786
1787fn decode_block_data(
1788 plan: &BlockPlan,
1789 symbols: &[Symbol],
1790 symbol_size: usize,
1791) -> Result<Vec<u8>, DecodingError> {
1792 let decoded_symbols = decode_block(plan, symbols, symbol_size)?;
1793 let mut block_data = Vec::with_capacity(plan.len);
1794 for symbol in &decoded_symbols {
1795 block_data.extend_from_slice(symbol.data());
1796 }
1797 block_data.truncate(plan.len);
1798 Ok(block_data)
1799}
1800
1801fn complete_block_data_from_source_symbols(
1802 plan: &BlockPlan,
1803 symbols: &[Symbol],
1804) -> Option<Vec<u8>> {
1805 if plan.k == 0 {
1806 return Some(Vec::new());
1807 }
1808
1809 let mut source_payloads = vec![None; plan.k];
1810 for symbol in symbols {
1811 if symbol.kind() != SymbolKind::Source {
1812 continue;
1813 }
1814 let esi = usize::try_from(symbol.esi()).ok()?;
1815 if esi < plan.k {
1816 source_payloads[esi] = Some(symbol.data());
1817 }
1818 }
1819
1820 let mut block_data = Vec::with_capacity(plan.len);
1821 for payload in source_payloads {
1822 let payload = payload?;
1823 let remaining = plan.len.saturating_sub(block_data.len());
1824 if remaining == 0 {
1825 break;
1826 }
1827 let take = remaining.min(payload.len());
1828 block_data.extend_from_slice(&payload[..take]);
1829 }
1830
1831 (block_data.len() == plan.len).then_some(block_data)
1832}
1833
1834fn seed_for_block(object_id: ObjectId, sbn: u8) -> u64 {
1835 seed_for(object_id, sbn, 0)
1836}
1837
1838fn seed_for(object_id: ObjectId, sbn: u8, esi: u32) -> u64 {
1839 let obj = object_id.as_u128();
1840 let hi = (obj >> 64) as u64;
1841 let lo = obj as u64;
1842 let mut seed = hi ^ lo.rotate_left(13);
1843 seed ^= u64::from(sbn) << 56;
1844 seed ^= u64::from(esi);
1845 if seed == 0 { 1 } else { seed }
1846}
1847
1848#[cfg(test)]
1849mod tests {
1850 #![allow(
1851 clippy::pedantic,
1852 clippy::nursery,
1853 clippy::expect_fun_call,
1854 clippy::map_unwrap_or,
1855 clippy::cast_possible_wrap,
1856 clippy::future_not_send
1857 )]
1858 use super::*;
1859 use crate::encoding::EncodingPipeline;
1860 use crate::types::resource::{PoolConfig, SymbolPool};
1861
1862 fn init_test(name: &str) {
1863 crate::test_utils::init_test_logging();
1864 crate::test_phase!(name);
1865 }
1866
1867 fn pool() -> SymbolPool {
1868 SymbolPool::new(PoolConfig {
1869 symbol_size: 256,
1870 initial_size: 64,
1871 max_size: 64,
1872 allow_growth: false,
1873 growth_increment: 0,
1874 })
1875 }
1876
1877 fn encoding_config() -> crate::config::EncodingConfig {
1878 crate::config::EncodingConfig {
1879 symbol_size: 256,
1880 max_block_size: 1024,
1881 repair_overhead: 1.05,
1882 encoding_parallelism: 1,
1883 decoding_parallelism: 1,
1884 }
1885 }
1886
1887 fn decoder_with_params(
1888 config: &crate::config::EncodingConfig,
1889 object_id: ObjectId,
1890 data_len: usize,
1891 repair_overhead: f64,
1892 min_overhead: usize,
1893 ) -> DecodingPipeline {
1894 let mut decoder = DecodingPipeline::new(DecodingConfig {
1895 symbol_size: config.symbol_size,
1896 max_block_size: config.max_block_size,
1897 repair_overhead,
1898 min_overhead,
1899 max_buffered_symbols: 8192,
1900 block_timeout: Duration::from_secs(30),
1901 verify_auth: false,
1902 });
1903 let symbols_per_block = (data_len.div_ceil(usize::from(config.symbol_size))) as u16;
1904 decoder
1905 .set_object_params(ObjectParams::new(
1906 object_id,
1907 data_len as u64,
1908 config.symbol_size,
1909 1,
1910 symbols_per_block,
1911 ))
1912 .expect("params");
1913 decoder
1914 }
1915
1916 #[test]
1917 fn missing_source_symbols_reports_absent_source_esis() {
1918 init_test("missing_source_symbols_reports_absent_source_esis");
1919 let config = encoding_config();
1920 let mut encoder = EncodingPipeline::new(config.clone(), pool());
1921 let object_id = ObjectId::new_for_test(101);
1922 let data = vec![7u8; 768];
1923 let mut decoder = decoder_with_params(&config, object_id, data.len(), 1.0, 0);
1924
1925 for encoded in encoder.encode_with_repair(object_id, &data, 0) {
1926 let symbol = encoded.expect("encode").into_symbol();
1927 if symbol.esi() == 1 {
1928 continue;
1929 }
1930 decoder
1931 .feed(AuthenticatedSymbol::new_unauthenticated(symbol))
1932 .expect("feed");
1933 }
1934
1935 assert_eq!(
1936 decoder.missing_source_symbols(0),
1937 vec![MissingSourceSymbol { sbn: 0, esi: 1 }]
1938 );
1939 assert_eq!(
1940 decoder.missing_source_symbols(1),
1941 vec![MissingSourceSymbol { sbn: 0, esi: 1 }]
1942 );
1943 }
1944
1945 #[test]
1946 fn decode_roundtrip_sources_only() {
1947 init_test("decode_roundtrip_sources_only");
1948 let config = encoding_config();
1949 let mut encoder = EncodingPipeline::new(config.clone(), pool());
1950 let object_id = ObjectId::new_for_test(1);
1951 let data = vec![42u8; 512];
1952 let symbols: Vec<Symbol> = encoder
1953 .encode_with_repair(object_id, &data, 0)
1954 .map(|res| res.unwrap().into_symbol())
1955 .collect();
1956
1957 let mut decoder = decoder_with_params(&config, object_id, data.len(), 1.0, 0);
1958
1959 for symbol in symbols {
1960 let auth = AuthenticatedSymbol::from_parts(
1961 symbol,
1962 crate::security::tag::AuthenticationTag::zero(),
1963 );
1964 let _ = decoder.feed(auth).unwrap();
1965 }
1966
1967 let decoded_data = decoder.into_data().expect("decoded");
1968 let ok = decoded_data == data;
1969 crate::assert_with_log!(ok, "decoded data", data, decoded_data);
1970 crate::test_complete!("decode_roundtrip_sources_only");
1971 }
1972
1973 #[test]
1974 fn decode_roundtrip_out_of_order() {
1975 init_test("decode_roundtrip_out_of_order");
1976 let config = encoding_config();
1977 let mut encoder = EncodingPipeline::new(config.clone(), pool());
1978 let object_id = ObjectId::new_for_test(2);
1979 let data = vec![7u8; 768];
1980 let mut symbols: Vec<Symbol> = encoder
1981 .encode_with_repair(object_id, &data, 2)
1982 .map(|res| res.expect("encode").into_symbol())
1983 .collect();
1984
1985 symbols.reverse();
1986
1987 let mut decoder =
1988 decoder_with_params(&config, object_id, data.len(), config.repair_overhead, 0);
1989
1990 for symbol in symbols {
1991 let auth = AuthenticatedSymbol::from_parts(
1992 symbol,
1993 crate::security::tag::AuthenticationTag::zero(),
1994 );
1995 let _ = decoder.feed(auth).expect("feed");
1996 }
1997
1998 let decoded_data = decoder.into_data().expect("decoded");
1999 let ok = decoded_data == data;
2000 crate::assert_with_log!(ok, "decoded data", data, decoded_data);
2001 crate::test_complete!("decode_roundtrip_out_of_order");
2002 }
2003
2004 #[test]
2005 fn reject_wrong_object_id() {
2006 init_test("reject_wrong_object_id");
2007 let config = encoding_config();
2008 let mut encoder = EncodingPipeline::new(config.clone(), pool());
2009 let object_id_a = ObjectId::new_for_test(10);
2010 let object_id_b = ObjectId::new_for_test(11);
2011 let data = vec![1u8; 128];
2012
2013 let mut decoder =
2014 decoder_with_params(&config, object_id_a, data.len(), config.repair_overhead, 0);
2015
2016 let symbol_b = encoder
2017 .encode_with_repair(object_id_b, &data, 0)
2018 .next()
2019 .expect("symbol")
2020 .expect("encode")
2021 .into_symbol();
2022 let auth = AuthenticatedSymbol::from_parts(
2023 symbol_b,
2024 crate::security::tag::AuthenticationTag::zero(),
2025 );
2026
2027 let result = decoder.feed(auth).expect("feed");
2028 let expected = SymbolAcceptResult::Rejected(RejectReason::WrongObjectId);
2029 let ok = result == expected;
2030 crate::assert_with_log!(ok, "wrong object id", expected, result);
2031 crate::test_complete!("reject_wrong_object_id");
2032 }
2033
2034 #[test]
2035 fn reject_symbol_size_mismatch() {
2036 init_test("reject_symbol_size_mismatch");
2037 let config = encoding_config();
2038 let mut decoder = DecodingPipeline::new(DecodingConfig {
2039 symbol_size: config.symbol_size,
2040 max_block_size: config.max_block_size,
2041 repair_overhead: config.repair_overhead,
2042 min_overhead: 0,
2043 max_buffered_symbols: 8192,
2044 block_timeout: Duration::from_secs(30),
2045 verify_auth: false,
2046 });
2047
2048 let symbol = Symbol::new(
2049 SymbolId::new(ObjectId::new_for_test(20), 0, 0),
2050 vec![0u8; 8],
2051 SymbolKind::Source,
2052 );
2053 let auth = AuthenticatedSymbol::from_parts(
2054 symbol,
2055 crate::security::tag::AuthenticationTag::zero(),
2056 );
2057 let result = decoder.feed(auth).expect("feed");
2058 let expected = SymbolAcceptResult::Rejected(RejectReason::SymbolSizeMismatch);
2059 let ok = result == expected;
2060 crate::assert_with_log!(ok, "symbol size mismatch", expected, result);
2061 crate::test_complete!("reject_symbol_size_mismatch");
2062 }
2063
2064 #[test]
2065 fn reject_invalid_metadata_esi_out_of_range() {
2066 init_test("reject_invalid_metadata_esi_out_of_range");
2067 let mut decoder = DecodingPipeline::new(DecodingConfig {
2068 symbol_size: 8,
2069 max_block_size: 8,
2070 repair_overhead: 1.0,
2071 min_overhead: 0,
2072 max_buffered_symbols: 8192,
2073 block_timeout: Duration::from_secs(30),
2074 verify_auth: false,
2075 });
2076 let object_id = ObjectId::new_for_test(21);
2077 decoder
2078 .set_object_params(ObjectParams::new(object_id, 8, 8, 1, 1))
2079 .expect("params");
2080
2081 let symbol = Symbol::new(
2082 SymbolId::new(object_id, 0, 1),
2083 vec![0u8; 8],
2084 SymbolKind::Source,
2085 );
2086 let auth = AuthenticatedSymbol::from_parts(
2087 symbol,
2088 crate::security::tag::AuthenticationTag::zero(),
2089 );
2090
2091 let result = decoder.feed(auth).expect("feed");
2092 let expected = SymbolAcceptResult::Rejected(RejectReason::InvalidMetadata);
2093 let ok = result == expected;
2094 crate::assert_with_log!(ok, "invalid metadata", expected, result);
2095 crate::test_complete!("reject_invalid_metadata_esi_out_of_range");
2096 }
2097
2098 #[test]
2099 fn auto_buffer_limit_scales_per_block_cap_to_k() {
2100 init_test("auto_buffer_limit_scales_per_block_cap_to_k");
2101 let big_k: u16 = 16_384;
2108 let object_size = u64::from(big_k) * 8;
2109 let max_block_size = usize::try_from(object_size).expect("fits usize");
2110 let object_id = ObjectId::new_for_test(99);
2111
2112 let mut auto = DecodingPipeline::new(DecodingConfig {
2113 symbol_size: 8,
2114 max_block_size,
2115 repair_overhead: 1.05,
2116 min_overhead: 0,
2117 max_buffered_symbols: 0,
2118 block_timeout: Duration::from_secs(30),
2119 verify_auth: false,
2120 });
2121 auto.set_object_params(ObjectParams::new(object_id, object_size, 8, 1, big_k))
2122 .expect("params");
2123 let auto_cap = auto.block_accept_cap();
2124 crate::assert_with_log!(
2125 auto_cap >= usize::from(big_k),
2126 "auto-sized per-block cap covers K",
2127 usize::from(big_k),
2128 auto_cap
2129 );
2130
2131 let mut fixed = DecodingPipeline::new(DecodingConfig {
2132 symbol_size: 8,
2133 max_block_size,
2134 repair_overhead: 1.05,
2135 min_overhead: 0,
2136 max_buffered_symbols: 8192,
2137 block_timeout: Duration::from_secs(30),
2138 verify_auth: false,
2139 });
2140 fixed
2141 .set_object_params(ObjectParams::new(object_id, object_size, 8, 1, big_k))
2142 .expect("params");
2143 let fixed_cap = fixed.block_accept_cap();
2144 crate::assert_with_log!(
2145 fixed_cap == 8192,
2146 "fixed default does not scale with K (the bug)",
2147 8192,
2148 fixed_cap
2149 );
2150
2151 crate::test_complete!("auto_buffer_limit_scales_per_block_cap_to_k");
2152 }
2153
2154 #[test]
2155 fn reject_invalid_metadata_repair_esi_overflow_without_panicking() {
2156 init_test("reject_invalid_metadata_repair_esi_overflow_without_panicking");
2157 let mut decoder = DecodingPipeline::new(DecodingConfig {
2158 symbol_size: 8,
2159 max_block_size: 16,
2160 repair_overhead: 1.0,
2161 min_overhead: 0,
2162 max_buffered_symbols: 8192,
2163 block_timeout: Duration::from_secs(30),
2164 verify_auth: false,
2165 });
2166 let object_id = ObjectId::new_for_test(22);
2167 decoder
2168 .set_object_params(ObjectParams::new(object_id, 16, 8, 1, 2))
2169 .expect("params");
2170
2171 let source = Symbol::new(
2172 SymbolId::new(object_id, 0, 0),
2173 vec![0x11; 8],
2174 SymbolKind::Source,
2175 );
2176 let repair = Symbol::new(
2177 SymbolId::new(object_id, 0, u32::MAX),
2178 vec![0x22; 8],
2179 SymbolKind::Repair,
2180 );
2181
2182 let first = decoder
2183 .feed(AuthenticatedSymbol::from_parts(
2184 source,
2185 crate::security::tag::AuthenticationTag::zero(),
2186 ))
2187 .expect("feed source");
2188 let first_ok = matches!(first, SymbolAcceptResult::Accepted { .. });
2189 crate::assert_with_log!(first_ok, "source accepted before threshold", true, first_ok);
2190
2191 let result = decoder
2192 .feed(AuthenticatedSymbol::from_parts(
2193 repair,
2194 crate::security::tag::AuthenticationTag::zero(),
2195 ))
2196 .expect("feed repair overflow");
2197 let expected = SymbolAcceptResult::Rejected(RejectReason::InvalidMetadata);
2198 let ok = result == expected;
2199 crate::assert_with_log!(
2200 ok,
2201 "repair overflow rejected as invalid metadata",
2202 expected,
2203 result
2204 );
2205
2206 crate::test_complete!("reject_invalid_metadata_repair_esi_overflow_without_panicking");
2207 }
2208
2209 #[test]
2210 fn reject_invalid_metadata_out_of_layout_sbn_without_buffering() {
2211 init_test("reject_invalid_metadata_out_of_layout_sbn_without_buffering");
2212 let mut decoder = DecodingPipeline::new(DecodingConfig {
2213 symbol_size: 8,
2214 max_block_size: 8,
2215 repair_overhead: 1.0,
2216 min_overhead: 0,
2217 max_buffered_symbols: 8192,
2218 block_timeout: Duration::from_secs(30),
2219 verify_auth: false,
2220 });
2221 let object_id = ObjectId::new_for_test(23);
2222 decoder
2223 .set_object_params(ObjectParams::new(object_id, 8, 8, 1, 1))
2224 .expect("params");
2225
2226 let result = decoder
2227 .feed(AuthenticatedSymbol::from_parts(
2228 Symbol::new(
2229 SymbolId::new(object_id, 1, 0),
2230 vec![0x33; 8],
2231 SymbolKind::Source,
2232 ),
2233 crate::security::tag::AuthenticationTag::zero(),
2234 ))
2235 .expect("feed out-of-layout block");
2236 let expected = SymbolAcceptResult::Rejected(RejectReason::InvalidMetadata);
2237 let ok = result == expected;
2238 crate::assert_with_log!(ok, "out-of-layout sbn rejected", expected, result);
2239
2240 let progress = decoder.progress();
2241 crate::assert_with_log!(
2242 progress.symbols_received == 0,
2243 "rejected out-of-layout block must not advance buffered symbol count",
2244 0,
2245 progress.symbols_received
2246 );
2247 crate::assert_with_log!(
2248 decoder.block_status(1).is_none(),
2249 "rejected out-of-layout block must not create block state",
2250 true,
2251 decoder.block_status(1).is_some()
2252 );
2253
2254 crate::test_complete!("reject_invalid_metadata_out_of_layout_sbn_without_buffering");
2255 }
2256
2257 #[test]
2258 fn duplicate_symbol_before_decode() {
2259 init_test("duplicate_symbol_before_decode");
2260 let config = encoding_config();
2261 let mut encoder = EncodingPipeline::new(config.clone(), pool());
2262 let object_id = ObjectId::new_for_test(30);
2263 let data = vec![9u8; 512];
2265
2266 let symbol = encoder
2267 .encode_with_repair(object_id, &data, 0)
2268 .next()
2269 .expect("symbol")
2270 .expect("encode")
2271 .into_symbol();
2272
2273 let mut decoder = decoder_with_params(&config, object_id, data.len(), 1.5, 1);
2274
2275 let first = decoder
2276 .feed(AuthenticatedSymbol::from_parts(
2277 symbol.clone(),
2278 crate::security::tag::AuthenticationTag::zero(),
2279 ))
2280 .expect("feed");
2281 let accepted = matches!(
2282 first,
2283 SymbolAcceptResult::Accepted { .. } | SymbolAcceptResult::DecodingStarted { .. }
2284 );
2285 crate::assert_with_log!(accepted, "first accepted", true, accepted);
2286
2287 let second = decoder
2288 .feed(AuthenticatedSymbol::from_parts(
2289 symbol,
2290 crate::security::tag::AuthenticationTag::zero(),
2291 ))
2292 .expect("feed");
2293 let expected = SymbolAcceptResult::Duplicate;
2294 let ok = second == expected;
2295 crate::assert_with_log!(ok, "second duplicate", expected, second);
2296 crate::test_complete!("duplicate_symbol_before_decode");
2297 }
2298
2299 #[test]
2300 fn into_data_reports_insufficient_symbols() {
2301 init_test("into_data_reports_insufficient_symbols");
2302 let config = encoding_config();
2303 let mut encoder = EncodingPipeline::new(config.clone(), pool());
2304 let object_id = ObjectId::new_for_test(40);
2305 let data = vec![5u8; 512];
2306
2307 let mut decoder =
2308 decoder_with_params(&config, object_id, data.len(), config.repair_overhead, 0);
2309
2310 let symbol = encoder
2311 .encode_with_repair(object_id, &data, 0)
2312 .next()
2313 .expect("symbol")
2314 .expect("encode")
2315 .into_symbol();
2316 let auth = AuthenticatedSymbol::from_parts(
2317 symbol,
2318 crate::security::tag::AuthenticationTag::zero(),
2319 );
2320 let _ = decoder.feed(auth).expect("feed");
2321
2322 let err = decoder
2323 .into_data()
2324 .expect_err("expected insufficient symbols");
2325 let insufficient = matches!(err, DecodingError::InsufficientSymbols { .. });
2326 crate::assert_with_log!(insufficient, "insufficient symbols", true, insufficient);
2327 crate::test_complete!("into_data_reports_insufficient_symbols");
2328 }
2329
2330 #[test]
2333 fn decoding_error_display_authentication_failed() {
2334 let err = DecodingError::AuthenticationFailed {
2335 symbol_id: SymbolId::new(ObjectId::new_for_test(1), 0, 0),
2336 };
2337 let msg = err.to_string();
2338 assert!(msg.contains("authentication failed"), "{msg}");
2339 }
2340
2341 #[test]
2342 fn decoding_error_display_insufficient_symbols() {
2343 let err = DecodingError::InsufficientSymbols {
2344 received: 3,
2345 needed: 10,
2346 };
2347 assert_eq!(err.to_string(), "insufficient symbols: have 3, need 10");
2348 }
2349
2350 #[test]
2351 fn decoding_error_display_matrix_inversion() {
2352 let err = DecodingError::MatrixInversionFailed {
2353 reason: "rank deficient".into(),
2354 };
2355 assert_eq!(err.to_string(), "matrix inversion failed: rank deficient");
2356 }
2357
2358 #[test]
2359 fn decoding_error_display_block_timeout() {
2360 let err = DecodingError::BlockTimeout {
2361 sbn: 2,
2362 elapsed: Duration::from_millis(1500),
2363 };
2364 let msg = err.to_string();
2365 assert!(msg.contains("block timeout"), "{msg}");
2366 assert!(msg.contains("1.5"), "{msg}");
2367 }
2368
2369 #[test]
2370 fn decoding_error_display_inconsistent_metadata() {
2371 let err = DecodingError::InconsistentMetadata {
2372 sbn: 0,
2373 details: "mismatch".into(),
2374 };
2375 let msg = err.to_string();
2376 assert!(msg.contains("inconsistent block metadata"), "{msg}");
2377 assert!(msg.contains("mismatch"), "{msg}");
2378 }
2379
2380 #[test]
2381 fn decoding_error_display_symbol_size_mismatch() {
2382 let err = DecodingError::SymbolSizeMismatch {
2383 expected: 256,
2384 actual: 128,
2385 };
2386 assert_eq!(
2387 err.to_string(),
2388 "symbol size mismatch: expected 256, got 128"
2389 );
2390 }
2391
2392 #[test]
2395 fn decoding_error_into_error_auth() {
2396 let err = DecodingError::AuthenticationFailed {
2397 symbol_id: SymbolId::new(ObjectId::new_for_test(1), 0, 0),
2398 };
2399 let error: crate::error::Error = err.into();
2400 assert_eq!(error.kind(), crate::error::ErrorKind::CorruptedSymbol);
2401 }
2402
2403 #[test]
2404 fn decoding_error_into_error_insufficient() {
2405 let err = DecodingError::InsufficientSymbols {
2406 received: 1,
2407 needed: 5,
2408 };
2409 let error: crate::error::Error = err.into();
2410 assert_eq!(error.kind(), crate::error::ErrorKind::InsufficientSymbols);
2411 }
2412
2413 #[test]
2414 fn decoding_error_into_error_matrix() {
2415 let err = DecodingError::MatrixInversionFailed {
2416 reason: "singular".into(),
2417 };
2418 let error: crate::error::Error = err.into();
2419 assert_eq!(error.kind(), crate::error::ErrorKind::DecodingFailed);
2420 }
2421
2422 #[test]
2423 fn decoding_error_into_error_timeout() {
2424 let err = DecodingError::BlockTimeout {
2425 sbn: 0,
2426 elapsed: Duration::from_secs(30),
2427 };
2428 let error: crate::error::Error = err.into();
2429 assert_eq!(error.kind(), crate::error::ErrorKind::ThresholdTimeout);
2430 }
2431
2432 #[test]
2433 fn decoding_error_into_error_inconsistent() {
2434 let err = DecodingError::InconsistentMetadata {
2435 sbn: 1,
2436 details: "x".into(),
2437 };
2438 let error: crate::error::Error = err.into();
2439 assert_eq!(error.kind(), crate::error::ErrorKind::DecodingFailed);
2440 }
2441
2442 #[test]
2443 fn decoding_error_into_error_size_mismatch() {
2444 let err = DecodingError::SymbolSizeMismatch {
2445 expected: 256,
2446 actual: 64,
2447 };
2448 let error: crate::error::Error = err.into();
2449 assert_eq!(error.kind(), crate::error::ErrorKind::DecodingFailed);
2450 }
2451
2452 #[test]
2455 fn reject_reason_variants_are_eq() {
2456 assert_eq!(RejectReason::WrongObjectId, RejectReason::WrongObjectId);
2457 assert_ne!(
2458 RejectReason::AuthenticationFailed,
2459 RejectReason::SymbolSizeMismatch
2460 );
2461 }
2462
2463 #[test]
2464 fn reject_reason_debug() {
2465 let dbg = format!("{:?}", RejectReason::BlockAlreadyDecoded);
2466 assert_eq!(dbg, "BlockAlreadyDecoded");
2467 }
2468
2469 #[test]
2472 fn symbol_accept_result_accepted_eq() {
2473 let a = SymbolAcceptResult::Accepted {
2474 received: 3,
2475 needed: 5,
2476 };
2477 let b = SymbolAcceptResult::Accepted {
2478 received: 3,
2479 needed: 5,
2480 };
2481 assert_eq!(a, b);
2482 }
2483
2484 #[test]
2485 fn symbol_accept_result_duplicate_eq() {
2486 assert_eq!(SymbolAcceptResult::Duplicate, SymbolAcceptResult::Duplicate);
2487 }
2488
2489 #[test]
2490 fn symbol_accept_result_rejected_eq() {
2491 let a = SymbolAcceptResult::Rejected(RejectReason::MemoryLimitReached);
2492 let b = SymbolAcceptResult::Rejected(RejectReason::MemoryLimitReached);
2493 assert_eq!(a, b);
2494 }
2495
2496 #[test]
2497 fn symbol_accept_result_variants_ne() {
2498 assert_ne!(
2499 SymbolAcceptResult::Duplicate,
2500 SymbolAcceptResult::Rejected(RejectReason::WrongObjectId)
2501 );
2502 }
2503
2504 #[test]
2507 fn decoding_config_default_values() {
2508 let cfg = DecodingConfig::default();
2509 assert_eq!(cfg.symbol_size, 256);
2510 assert_eq!(cfg.max_block_size, 1024 * 1024);
2511 assert!((cfg.repair_overhead - 1.05).abs() < f64::EPSILON);
2512 assert_eq!(cfg.min_overhead, 0);
2513 assert_eq!(cfg.max_buffered_symbols, 8192);
2514 assert_eq!(cfg.block_timeout, Duration::from_secs(30));
2515 assert!(cfg.verify_auth);
2516 }
2517
2518 #[test]
2519 fn required_symbols_uses_total_factor_and_minimum_extra_floor() {
2520 assert_eq!(required_symbols(0, 1.05, 3), 0);
2521 assert_eq!(required_symbols(10, 1.05, 3), 13);
2522 assert_eq!(required_symbols(10, 1.5, 1), 15);
2523 assert_eq!(required_symbols(10, 0.5, 0), 10);
2524 assert_eq!(required_symbols(10, f64::NAN, 3), 13);
2525 assert_eq!(required_symbols(10, f64::INFINITY, 3), usize::MAX);
2526 }
2527
2528 #[test]
2531 fn block_state_kind_eq_and_debug() {
2532 assert_eq!(BlockStateKind::Collecting, BlockStateKind::Collecting);
2533 assert_ne!(BlockStateKind::Collecting, BlockStateKind::Decoded);
2534 assert_eq!(format!("{:?}", BlockStateKind::Failed), "Failed");
2535 assert_eq!(format!("{:?}", BlockStateKind::Decoding), "Decoding");
2536 }
2537
2538 #[test]
2541 fn pipeline_new_starts_empty() {
2542 let pipeline = DecodingPipeline::new(DecodingConfig::default());
2543 let progress = pipeline.progress();
2544 assert_eq!(progress.blocks_complete, 0);
2545 assert_eq!(progress.symbols_received, 0);
2546 }
2547
2548 #[test]
2549 fn pipeline_set_object_params_rejects_mismatched_symbol_size() {
2550 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2551 symbol_size: 256,
2552 ..DecodingConfig::without_auth()
2553 });
2554 let params = ObjectParams::new(ObjectId::new_for_test(1), 1024, 128, 1, 8);
2555 let err = pipeline.set_object_params(params).unwrap_err();
2556 assert!(matches!(err, DecodingError::SymbolSizeMismatch { .. }));
2557 }
2558
2559 #[test]
2560 fn pipeline_set_object_params_rejects_inconsistent_object_id() {
2561 let config = encoding_config();
2562 let oid1 = ObjectId::new_for_test(1);
2563 let oid2 = ObjectId::new_for_test(2);
2564
2565 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2566 symbol_size: config.symbol_size,
2567 ..DecodingConfig::without_auth()
2568 });
2569 pipeline
2570 .set_object_params(ObjectParams::new(oid1, 512, config.symbol_size, 1, 2))
2571 .expect("first set_object_params");
2572 let err = pipeline
2573 .set_object_params(ObjectParams::new(oid2, 512, config.symbol_size, 1, 2))
2574 .unwrap_err();
2575 assert!(matches!(err, DecodingError::InconsistentMetadata { .. }));
2576 }
2577
2578 #[test]
2579 fn pipeline_set_object_params_same_id_is_ok() {
2580 let config = encoding_config();
2581 let oid = ObjectId::new_for_test(1);
2582
2583 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2584 symbol_size: config.symbol_size,
2585 ..DecodingConfig::without_auth()
2586 });
2587 pipeline
2588 .set_object_params(ObjectParams::new(oid, 512, config.symbol_size, 1, 2))
2589 .expect("first");
2590 pipeline
2591 .set_object_params(ObjectParams::new(oid, 512, config.symbol_size, 1, 2))
2592 .expect("second with same id should succeed");
2593 }
2594
2595 #[test]
2596 fn pipeline_indexes_block_plans_by_sbn() {
2597 let object_id = ObjectId::new_for_test(3);
2598 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2599 symbol_size: 8,
2600 max_block_size: 16,
2601 ..DecodingConfig::without_auth()
2602 });
2603 pipeline
2604 .set_object_params(ObjectParams::new(object_id, 40, 8, 3, 2))
2605 .expect("multi-block params");
2606
2607 assert_eq!(pipeline.block_plan_by_sbn[0], Some(0));
2608 assert_eq!(pipeline.block_plan_by_sbn[1], Some(1));
2609 assert_eq!(pipeline.block_plan_by_sbn[2], Some(2));
2610 assert_eq!(pipeline.block_plan(2).map(|plan| plan.len), Some(8));
2611 assert!(pipeline.block_plan(3).is_none());
2612 }
2613
2614 #[test]
2615 fn pipeline_set_object_params_rejects_k_above_rfc_systematic_max() {
2616 let object_id = ObjectId::new_for_test(0xDE);
2625 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2626 symbol_size: 1,
2627 max_block_size: 1024 * 1024,
2628 ..DecodingConfig::without_auth()
2629 });
2630 let err = pipeline
2633 .set_object_params(ObjectParams::new(object_id, 65_000, 1, 1, 65_000))
2634 .unwrap_err();
2635 assert!(
2636 matches!(err, DecodingError::InconsistentMetadata { .. }),
2637 "expected InconsistentMetadata, got {err:?}"
2638 );
2639 assert!(
2640 err.to_string().contains("RFC 6330 systematic-index table"),
2641 "expected RFC bound message, got: {err}"
2642 );
2643 }
2644
2645 #[test]
2646 fn pipeline_set_object_params_rejects_declared_block_count_drift() {
2647 let config = encoding_config();
2648 let object_id = ObjectId::new_for_test(104);
2649
2650 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2651 symbol_size: config.symbol_size,
2652 max_block_size: config.max_block_size,
2653 ..DecodingConfig::without_auth()
2654 });
2655 let err = pipeline
2656 .set_object_params(ObjectParams::new(object_id, 1536, config.symbol_size, 1, 4))
2657 .unwrap_err();
2658 assert!(matches!(err, DecodingError::InconsistentMetadata { .. }));
2659 assert!(
2660 err.to_string().contains("block count mismatch"),
2661 "unexpected error: {err}"
2662 );
2663 }
2664
2665 #[test]
2666 fn pipeline_set_object_params_rejects_total_k_metadata_for_multi_block_object() {
2667 let config = encoding_config();
2668 let object_id = ObjectId::new_for_test(105);
2669
2670 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2671 symbol_size: config.symbol_size,
2672 max_block_size: config.max_block_size,
2673 ..DecodingConfig::without_auth()
2674 });
2675 let err = pipeline
2676 .set_object_params(ObjectParams::new(object_id, 2048, config.symbol_size, 2, 8))
2677 .unwrap_err();
2678 assert!(matches!(err, DecodingError::InconsistentMetadata { .. }));
2679 assert!(
2680 err.to_string().contains("symbols_per_block mismatch"),
2681 "unexpected error: {err}"
2682 );
2683 }
2684
2685 #[test]
2686 fn pipeline_set_object_params_failure_does_not_latch_object_identity() {
2687 let config = encoding_config();
2688 let invalid_object_id = ObjectId::new_for_test(106);
2689 let valid_object_id = ObjectId::new_for_test(107);
2690
2691 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2692 symbol_size: config.symbol_size,
2693 max_block_size: config.max_block_size,
2694 ..DecodingConfig::without_auth()
2695 });
2696 let err = pipeline
2697 .set_object_params(ObjectParams::new(
2698 invalid_object_id,
2699 2048,
2700 config.symbol_size,
2701 2,
2702 8,
2703 ))
2704 .unwrap_err();
2705 assert!(matches!(err, DecodingError::InconsistentMetadata { .. }));
2706
2707 pipeline
2708 .set_object_params(ObjectParams::new(
2709 valid_object_id,
2710 512,
2711 config.symbol_size,
2712 1,
2713 2,
2714 ))
2715 .expect("failed set_object_params must not poison object identity");
2716 }
2717
2718 #[test]
2719 fn pipeline_set_object_params_accepts_empty_object_single_block_sentinel_metadata() {
2720 let config = encoding_config();
2721 let object_id = ObjectId::new_for_test(108);
2722
2723 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2724 symbol_size: config.symbol_size,
2725 max_block_size: config.max_block_size,
2726 ..DecodingConfig::without_auth()
2727 });
2728 pipeline
2729 .set_object_params(ObjectParams::new(
2730 object_id,
2731 0,
2732 config.symbol_size,
2733 1,
2734 config
2735 .max_block_size
2736 .div_ceil(usize::from(config.symbol_size))
2737 .try_into()
2738 .expect("sentinel block K should fit in u16"),
2739 ))
2740 .expect("empty object sentinel metadata should be accepted");
2741
2742 assert!(pipeline.is_complete());
2743 assert_eq!(pipeline.progress().blocks_total, Some(0));
2744 assert_eq!(
2745 pipeline.into_data().expect("empty object should decode"),
2746 Vec::<u8>::new()
2747 );
2748 }
2749
2750 #[test]
2751 fn pipeline_set_object_params_accepts_full_256_block_boundary() {
2752 let config = crate::config::EncodingConfig {
2753 symbol_size: 8,
2754 max_block_size: 8,
2755 ..encoding_config()
2756 };
2757 let object_id = ObjectId::new_for_test(109);
2758
2759 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2760 symbol_size: config.symbol_size,
2761 max_block_size: config.max_block_size,
2762 ..DecodingConfig::without_auth()
2763 });
2764 pipeline
2765 .set_object_params(ObjectParams::new(
2766 object_id,
2767 u64::try_from(config.max_block_size * 256).expect("boundary object size fits u64"),
2768 config.symbol_size,
2769 256,
2770 1,
2771 ))
2772 .expect("256-block metadata boundary should be representable");
2773
2774 assert_eq!(pipeline.progress().blocks_total, Some(256));
2775 }
2776
2777 #[test]
2780 fn feed_batch_returns_results_per_symbol() {
2781 init_test("feed_batch_returns_results_per_symbol");
2782 let config = encoding_config();
2783 let mut encoder = EncodingPipeline::new(config.clone(), pool());
2784 let object_id = ObjectId::new_for_test(100);
2785 let data = vec![0xAAu8; 768]; let symbols: Vec<AuthenticatedSymbol> = encoder
2788 .encode_with_repair(object_id, &data, 0)
2789 .map(|res| {
2790 AuthenticatedSymbol::from_parts(
2791 res.unwrap().into_symbol(),
2792 crate::security::tag::AuthenticationTag::zero(),
2793 )
2794 })
2795 .take(3)
2796 .collect();
2797
2798 let mut decoder = decoder_with_params(&config, object_id, data.len(), 1.5, 1);
2799
2800 let results = decoder.feed_batch(symbols.into_iter());
2801 let len = results.len();
2802 let expected_len = 3usize;
2803 crate::assert_with_log!(len == expected_len, "batch length", expected_len, len);
2804 for (i, r) in results.iter().enumerate() {
2805 let is_ok = r.is_ok();
2806 crate::assert_with_log!(is_ok, &format!("result[{i}] is Ok"), true, is_ok);
2807 }
2808 crate::test_complete!("feed_batch_returns_results_per_symbol");
2809 }
2810
2811 #[test]
2812 fn skipped_verifications_count_only_inserted_symbols() {
2813 init_test("skipped_verifications_count_only_inserted_symbols");
2814 let config = encoding_config();
2815 let object_id = ObjectId::new_for_test(103);
2816 let mut decoder = DecodingPipeline::new(DecodingConfig {
2817 symbol_size: config.symbol_size,
2818 max_block_size: config.max_block_size,
2819 verify_auth: false,
2820 ..DecodingConfig::without_auth()
2821 });
2822 decoder
2823 .set_object_params(ObjectParams::new(object_id, 512, config.symbol_size, 1, 2))
2824 .expect("set object params");
2825
2826 let wrong_object = Symbol::new(
2827 SymbolId::new(ObjectId::new_for_test(104), 0, 0),
2828 vec![0u8; usize::from(config.symbol_size)],
2829 SymbolKind::Source,
2830 );
2831 let result = decoder
2832 .feed(AuthenticatedSymbol::from_parts(
2833 wrong_object,
2834 crate::security::tag::AuthenticationTag::zero(),
2835 ))
2836 .expect("wrong-object feed should not error");
2837 assert_eq!(
2838 result,
2839 SymbolAcceptResult::Rejected(RejectReason::WrongObjectId)
2840 );
2841 assert_eq!(decoder.skipped_verifications(), 0);
2842
2843 let valid = Symbol::new(
2844 SymbolId::new(object_id, 0, 0),
2845 vec![1u8; usize::from(config.symbol_size)],
2846 SymbolKind::Source,
2847 );
2848 let result = decoder
2849 .feed(AuthenticatedSymbol::from_parts(
2850 valid.clone(),
2851 crate::security::tag::AuthenticationTag::zero(),
2852 ))
2853 .expect("valid feed should not error");
2854 assert!(matches!(result, SymbolAcceptResult::Accepted { .. }));
2855 assert_eq!(decoder.skipped_verifications(), 1);
2856
2857 let result = decoder
2858 .feed(AuthenticatedSymbol::from_parts(
2859 valid,
2860 crate::security::tag::AuthenticationTag::zero(),
2861 ))
2862 .expect("duplicate feed should not error");
2863 assert_eq!(result, SymbolAcceptResult::Duplicate);
2864 assert_eq!(decoder.skipped_verifications(), 1);
2865
2866 crate::test_complete!("skipped_verifications_count_only_inserted_symbols");
2867 }
2868
2869 #[test]
2870 fn is_complete_false_without_params() {
2871 init_test("is_complete_false_without_params");
2872 let pipeline = DecodingPipeline::new(DecodingConfig::default());
2873 let complete = pipeline.is_complete();
2874 crate::assert_with_log!(!complete, "is_complete without params", false, complete);
2875 crate::test_complete!("is_complete_false_without_params");
2876 }
2877
2878 #[test]
2879 fn is_complete_true_after_all_blocks_decoded() {
2880 init_test("is_complete_true_after_all_blocks_decoded");
2881 let config = encoding_config();
2882 let mut encoder = EncodingPipeline::new(config.clone(), pool());
2883 let object_id = ObjectId::new_for_test(101);
2884 let data = vec![42u8; 512];
2885 let symbols: Vec<Symbol> = encoder
2886 .encode_with_repair(object_id, &data, 0)
2887 .map(|res| res.unwrap().into_symbol())
2888 .collect();
2889
2890 let mut decoder = decoder_with_params(&config, object_id, data.len(), 1.0, 0);
2891
2892 for symbol in symbols {
2893 let auth = AuthenticatedSymbol::from_parts(
2894 symbol,
2895 crate::security::tag::AuthenticationTag::zero(),
2896 );
2897 let _ = decoder.feed(auth).unwrap();
2898 }
2899
2900 let complete = decoder.is_complete();
2901 crate::assert_with_log!(complete, "is_complete after all blocks", true, complete);
2902 crate::test_complete!("is_complete_true_after_all_blocks_decoded");
2903 }
2904
2905 #[test]
2906 fn progress_reports_blocks_total_after_params() {
2907 init_test("progress_reports_blocks_total_after_params");
2908 let config = encoding_config();
2909 let object_id = ObjectId::new_for_test(102);
2910
2911 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2912 symbol_size: config.symbol_size,
2913 max_block_size: 1024,
2914 ..DecodingConfig::without_auth()
2915 });
2916 let k = (512usize).div_ceil(usize::from(config.symbol_size)) as u16;
2918 pipeline
2919 .set_object_params(ObjectParams::new(object_id, 512, config.symbol_size, 1, k))
2920 .expect("set params");
2921
2922 let progress = pipeline.progress();
2923 let blocks_total = progress.blocks_total;
2924 let expected_blocks = Some(1usize);
2925 crate::assert_with_log!(
2926 blocks_total == expected_blocks,
2927 "blocks_total",
2928 expected_blocks,
2929 blocks_total
2930 );
2931 let estimate = progress.symbols_needed_estimate;
2932 let positive = estimate > 0;
2933 crate::assert_with_log!(positive, "symbols_needed_estimate > 0", true, positive);
2934 crate::test_complete!("progress_reports_blocks_total_after_params");
2935 }
2936
2937 #[test]
2938 fn progress_symbols_needed_estimate_does_not_double_count_min_overhead() {
2939 init_test("progress_symbols_needed_estimate_does_not_double_count_min_overhead");
2940 let object_id = ObjectId::new_for_test(1020);
2941 let symbol_size = 256u16;
2942 let k = 10u16;
2943 let data_len = usize::from(symbol_size) * usize::from(k);
2944
2945 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2946 symbol_size,
2947 max_block_size: 4096,
2948 repair_overhead: 1.05,
2949 min_overhead: 3,
2950 max_buffered_symbols: 8192,
2951 block_timeout: Duration::from_secs(30),
2952 verify_auth: false,
2953 });
2954 pipeline
2955 .set_object_params(ObjectParams::new(
2956 object_id,
2957 data_len as u64,
2958 symbol_size,
2959 1,
2960 k,
2961 ))
2962 .expect("set params");
2963
2964 let progress = pipeline.progress();
2965 assert_eq!(progress.blocks_total, Some(1));
2966 assert_eq!(progress.symbols_needed_estimate, 13);
2967 crate::test_complete!(
2968 "progress_symbols_needed_estimate_does_not_double_count_min_overhead"
2969 );
2970 }
2971
2972 #[test]
2973 fn progress_symbols_needed_estimate_saturates_for_infinite_overhead() {
2974 init_test("progress_symbols_needed_estimate_saturates_for_infinite_overhead");
2975 let object_id = ObjectId::new_for_test(1021);
2976 let symbol_size = 256u16;
2977 let data_len = 2048usize;
2978
2979 let mut pipeline = DecodingPipeline::new(DecodingConfig {
2980 symbol_size,
2981 max_block_size: 1024,
2982 repair_overhead: f64::INFINITY,
2983 min_overhead: 0,
2984 max_buffered_symbols: 8192,
2985 block_timeout: Duration::from_secs(30),
2986 verify_auth: false,
2987 });
2988 pipeline
2989 .set_object_params(ObjectParams::new(
2990 object_id,
2991 data_len as u64,
2992 symbol_size,
2993 2,
2994 4,
2995 ))
2996 .expect("set params");
2997
2998 let progress = pipeline.progress();
2999 assert_eq!(progress.blocks_total, Some(2));
3000 assert_eq!(progress.symbols_needed_estimate, usize::MAX);
3001 crate::test_complete!("progress_symbols_needed_estimate_saturates_for_infinite_overhead");
3002 }
3003
3004 #[test]
3005 fn block_status_none_for_unknown_block() {
3006 init_test("block_status_none_for_unknown_block");
3007 let config = encoding_config();
3008 let object_id = ObjectId::new_for_test(103);
3009
3010 let mut pipeline = DecodingPipeline::new(DecodingConfig {
3011 symbol_size: config.symbol_size,
3012 max_block_size: config.max_block_size,
3013 ..DecodingConfig::without_auth()
3014 });
3015 let k = (512usize).div_ceil(usize::from(config.symbol_size)) as u16;
3016 pipeline
3017 .set_object_params(ObjectParams::new(object_id, 512, config.symbol_size, 1, k))
3018 .expect("set params");
3019
3020 let status = pipeline.block_status(99);
3021 let is_none = status.is_none();
3022 crate::assert_with_log!(is_none, "block_status(99) is None", true, is_none);
3023 crate::test_complete!("block_status_none_for_unknown_block");
3024 }
3025
3026 #[test]
3027 fn block_status_collecting_after_partial_feed() {
3028 init_test("block_status_collecting_after_partial_feed");
3029 let config = encoding_config();
3030 let mut encoder = EncodingPipeline::new(config.clone(), pool());
3031 let object_id = ObjectId::new_for_test(104);
3032 let data = vec![0xBBu8; 512];
3033
3034 let first_symbol = encoder
3035 .encode_with_repair(object_id, &data, 0)
3036 .next()
3037 .expect("symbol")
3038 .expect("encode")
3039 .into_symbol();
3040
3041 let mut decoder = decoder_with_params(&config, object_id, data.len(), 1.5, 1);
3043
3044 let auth = AuthenticatedSymbol::from_parts(
3045 first_symbol,
3046 crate::security::tag::AuthenticationTag::zero(),
3047 );
3048 let _ = decoder.feed(auth).expect("feed");
3049
3050 let status = decoder.block_status(0);
3051 let is_some = status.is_some();
3052 crate::assert_with_log!(is_some, "block_status(0) is Some", true, is_some);
3053
3054 let status = status.unwrap();
3055 let state = status.state;
3056 let expected_state = BlockStateKind::Collecting;
3057 crate::assert_with_log!(
3058 state == expected_state,
3059 "state is Collecting",
3060 expected_state,
3061 state
3062 );
3063 let received = status.symbols_received;
3064 let expected_received = 1usize;
3065 crate::assert_with_log!(
3066 received == expected_received,
3067 "symbols_received",
3068 expected_received,
3069 received
3070 );
3071 let rank_is_available = status.rank.is_some();
3072 crate::assert_with_log!(rank_is_available, "rank available", true, rank_is_available);
3073 let rank_deficit_positive = status.rank_deficit.is_some_and(|deficit| deficit > 0);
3074 crate::assert_with_log!(
3075 rank_deficit_positive,
3076 "rank_deficit positive",
3077 true,
3078 rank_deficit_positive
3079 );
3080 crate::test_complete!("block_status_collecting_after_partial_feed");
3081 }
3082
3083 #[test]
3084 fn block_status_decoded_after_complete() {
3085 init_test("block_status_decoded_after_complete");
3086 let config = encoding_config();
3087 let mut encoder = EncodingPipeline::new(config.clone(), pool());
3088 let object_id = ObjectId::new_for_test(105);
3089 let data = vec![42u8; 512];
3090 let symbols: Vec<Symbol> = encoder
3091 .encode_with_repair(object_id, &data, 0)
3092 .map(|res| res.unwrap().into_symbol())
3093 .collect();
3094
3095 let mut decoder = decoder_with_params(&config, object_id, data.len(), 1.0, 0);
3096
3097 for symbol in symbols {
3098 let auth = AuthenticatedSymbol::from_parts(
3099 symbol,
3100 crate::security::tag::AuthenticationTag::zero(),
3101 );
3102 let _ = decoder.feed(auth).unwrap();
3103 }
3104
3105 let _status = decoder.block_status(0);
3109 let complete = decoder.is_complete();
3110 crate::assert_with_log!(complete, "is_complete", true, complete);
3111
3112 let extra = Symbol::new(
3115 SymbolId::new(object_id, 0, 99),
3116 vec![0u8; usize::from(config.symbol_size)],
3117 SymbolKind::Source,
3118 );
3119 let auth =
3120 AuthenticatedSymbol::from_parts(extra, crate::security::tag::AuthenticationTag::zero());
3121 let result = decoder.feed(auth).expect("feed");
3122 let expected = SymbolAcceptResult::Rejected(RejectReason::BlockAlreadyDecoded);
3123 let ok = result == expected;
3124 crate::assert_with_log!(ok, "block already decoded", expected, result);
3125 crate::test_complete!("block_status_decoded_after_complete");
3126 }
3127
3128 #[test]
3129 fn streaming_source_complete_block_returns_data_without_retaining_copy() {
3130 init_test("streaming_source_complete_block_returns_data_without_retaining_copy");
3131 let config = encoding_config();
3132 let mut encoder = EncodingPipeline::new(config.clone(), pool());
3133 let object_id = ObjectId::new_for_test(205);
3134 let data = (0..700).map(|i| (i % 251) as u8).collect::<Vec<_>>();
3135 let symbols = encoder
3136 .encode_with_repair(object_id, &data, 0)
3137 .map(|res| res.expect("source symbol").into_symbol())
3138 .collect::<Vec<_>>();
3139
3140 let mut decoder = decoder_with_params(&config, object_id, data.len(), 1.0, 0);
3141 let mut completed = None;
3142 let mut deferred_jobs = 0usize;
3143 for symbol in symbols {
3144 let auth = AuthenticatedSymbol::from_parts(
3145 symbol,
3146 crate::security::tag::AuthenticationTag::zero(),
3147 );
3148 match decoder
3149 .feed_streaming_block_deferred(auth)
3150 .expect("feed streaming source")
3151 {
3152 DeferredSymbolAcceptResult::Immediate(SymbolAcceptResult::BlockComplete {
3153 ..
3154 }) => {
3155 panic!("source-complete deferred feed must return a decode job");
3156 }
3157 DeferredSymbolAcceptResult::Immediate(_) => {}
3158 DeferredSymbolAcceptResult::Decode(job) => {
3159 deferred_jobs = deferred_jobs.saturating_add(1);
3160 let expected_sources = data.len().div_ceil(usize::from(config.symbol_size));
3161 assert_eq!(
3162 job.source_symbols, expected_sources,
3163 "source-complete deferred jobs must carry the exact source count"
3164 );
3165 let outcome = run_block_decode_job(job);
3166 assert_eq!(outcome.kind(), BlockDecodeKind::SourceComplete);
3167 let result = decoder.finish_decode_job(outcome);
3168 if let SymbolAcceptResult::BlockComplete { data, .. } = result {
3169 completed = Some(data);
3170 }
3171 }
3172 }
3173 }
3174
3175 crate::assert_with_log!(
3176 deferred_jobs == 1,
3177 "source-complete deferred feed queues one blocking job",
3178 1,
3179 deferred_jobs
3180 );
3181 let decoded = completed.expect("source-complete block");
3182 crate::assert_with_log!(decoded == data, "decoded source block", data, decoded);
3183 let complete = decoder.is_complete();
3184 crate::assert_with_log!(complete, "decoder complete", true, complete);
3185 let retained = decoder
3186 .blocks
3187 .get(&0)
3188 .and_then(|block| block.decoded.as_ref());
3189 crate::assert_with_log!(
3190 retained.is_none(),
3191 "streaming decode should not retain block copy",
3192 true,
3193 retained.is_none()
3194 );
3195 crate::test_complete!(
3196 "streaming_source_complete_block_returns_data_without_retaining_copy"
3197 );
3198 }
3199
3200 #[test]
3201 fn block_already_decoded_reject() {
3202 init_test("block_already_decoded_reject");
3203 let config = encoding_config();
3204 let mut encoder = EncodingPipeline::new(config.clone(), pool());
3205 let object_id = ObjectId::new_for_test(106);
3206 let data = vec![42u8; 512];
3207 let symbols: Vec<Symbol> = encoder
3208 .encode_with_repair(object_id, &data, 0)
3209 .map(|res| res.unwrap().into_symbol())
3210 .collect();
3211
3212 let mut decoder = decoder_with_params(&config, object_id, data.len(), 1.0, 0);
3213
3214 for symbol in symbols {
3215 let auth = AuthenticatedSymbol::from_parts(
3216 symbol,
3217 crate::security::tag::AuthenticationTag::zero(),
3218 );
3219 let _ = decoder.feed(auth).unwrap();
3220 }
3221
3222 let extra = Symbol::new(
3224 SymbolId::new(object_id, 0, 0),
3225 vec![0u8; usize::from(config.symbol_size)],
3226 SymbolKind::Source,
3227 );
3228 let auth =
3229 AuthenticatedSymbol::from_parts(extra, crate::security::tag::AuthenticationTag::zero());
3230 let result = decoder.feed(auth).expect("feed");
3231 let expected = SymbolAcceptResult::Rejected(RejectReason::BlockAlreadyDecoded);
3232 let ok = result == expected;
3233 crate::assert_with_log!(ok, "block already decoded reject", expected, result);
3234 crate::test_complete!("block_already_decoded_reject");
3235 }
3236
3237 #[test]
3238 fn verify_auth_no_context_unverified_symbol_errors() {
3239 init_test("verify_auth_no_context_unverified_symbol_errors");
3240 let config = encoding_config();
3241 let mut decoder = DecodingPipeline::new(DecodingConfig {
3242 symbol_size: config.symbol_size,
3243 max_block_size: config.max_block_size,
3244 verify_auth: true,
3245 ..DecodingConfig::without_auth()
3246 });
3247
3248 let symbol = Symbol::new(
3249 SymbolId::new(ObjectId::new_for_test(107), 0, 0),
3250 vec![0u8; usize::from(config.symbol_size)],
3251 SymbolKind::Source,
3252 );
3253 let auth = AuthenticatedSymbol::from_parts(
3255 symbol,
3256 crate::security::tag::AuthenticationTag::zero(),
3257 );
3258
3259 let result = decoder.feed(auth);
3260 let is_ok = result.is_ok();
3261 crate::assert_with_log!(
3262 is_ok,
3263 "unverified with no context is rejected safely",
3264 true,
3265 is_ok
3266 );
3267
3268 let accept = result.unwrap();
3269 let expected = SymbolAcceptResult::Rejected(RejectReason::AuthenticationFailed);
3270 crate::assert_with_log!(
3271 accept == expected,
3272 "rejected as auth failed",
3273 expected,
3274 accept
3275 );
3276 crate::test_complete!("verify_auth_no_context_unverified_symbol_errors");
3277 }
3278
3279 #[test]
3280 fn verify_auth_no_context_preverified_symbol_rejected() {
3281 init_test("verify_auth_no_context_preverified_symbol_rejected");
3282 let config = encoding_config();
3283 let mut decoder = DecodingPipeline::new(DecodingConfig {
3284 symbol_size: config.symbol_size,
3285 max_block_size: config.max_block_size,
3286 verify_auth: true,
3287 ..DecodingConfig::without_auth()
3288 });
3289
3290 let symbol = Symbol::new(
3291 SymbolId::new(ObjectId::new_for_test(108), 0, 0),
3292 vec![0u8; usize::from(config.symbol_size)],
3293 SymbolKind::Source,
3294 );
3295 let auth = crate::security::SecurityContext::for_testing(108).sign_symbol(&symbol);
3296
3297 let result = decoder.feed(auth);
3298 let is_ok = result.is_ok();
3299 crate::assert_with_log!(is_ok, "preverified symbol rejected safely", true, is_ok);
3300 let accept = result.unwrap();
3301 let expected = SymbolAcceptResult::Rejected(RejectReason::AuthenticationFailed);
3302 crate::assert_with_log!(
3303 accept == expected,
3304 "result is auth rejection without verifier context",
3305 expected,
3306 accept
3307 );
3308 crate::test_complete!("verify_auth_no_context_preverified_symbol_rejected");
3309 }
3310
3311 #[test]
3312 fn with_auth_rejects_bad_tag() {
3313 init_test("with_auth_rejects_bad_tag");
3314 let config = encoding_config();
3315 let mut decoder = DecodingPipeline::with_auth(
3316 DecodingConfig {
3317 symbol_size: config.symbol_size,
3318 max_block_size: config.max_block_size,
3319 verify_auth: true,
3320 ..DecodingConfig::without_auth()
3321 },
3322 crate::security::SecurityContext::for_testing(42),
3323 );
3324
3325 let symbol = Symbol::new(
3326 SymbolId::new(ObjectId::new_for_test(109), 0, 0),
3327 vec![0u8; usize::from(config.symbol_size)],
3328 SymbolKind::Source,
3329 );
3330 let auth = AuthenticatedSymbol::from_parts(
3332 symbol,
3333 crate::security::tag::AuthenticationTag::zero(),
3334 );
3335
3336 let result = decoder.feed(auth).expect("feed should not return Err");
3337 let expected = SymbolAcceptResult::Rejected(RejectReason::AuthenticationFailed);
3338 let ok = result == expected;
3339 crate::assert_with_log!(ok, "bad tag rejected", expected, result);
3340 crate::test_complete!("with_auth_rejects_bad_tag");
3341 }
3342
3343 #[test]
3344 fn source_first_with_auth_verifies_hmac_before_fast_path_completion() {
3345 init_test("source_first_with_auth_verifies_hmac_before_fast_path_completion");
3346 let config = encoding_config();
3347 let mut encoder = EncodingPipeline::new(config.clone(), pool());
3348 let object_id = ObjectId::new_for_test(111);
3349 let data = (0..700).map(|i| (i % 251) as u8).collect::<Vec<_>>();
3350 let security = crate::security::SecurityContext::for_testing(111);
3351 let mut decoder = DecodingPipeline::with_auth(
3352 DecodingConfig {
3353 symbol_size: config.symbol_size,
3354 max_block_size: config.max_block_size,
3355 repair_overhead: 1.0,
3356 min_overhead: 0,
3357 max_buffered_symbols: 8192,
3358 block_timeout: Duration::from_secs(30),
3359 verify_auth: true,
3360 },
3361 security.clone(),
3362 );
3363 decoder
3364 .set_object_params(ObjectParams::new(
3365 object_id,
3366 data.len() as u64,
3367 config.symbol_size,
3368 1,
3369 data.len().div_ceil(usize::from(config.symbol_size)) as u16,
3370 ))
3371 .expect("params");
3372
3373 let source_symbols = encoder
3374 .encode_with_repair(object_id, &data, 0)
3375 .map(|res| res.expect("encode source").into_symbol())
3376 .collect::<Vec<_>>();
3377
3378 let mut completed = None;
3379 for symbol in source_symbols {
3380 let signed = security.sign_symbol(&symbol);
3381 let tag = *signed.tag();
3382 if let SymbolAcceptResult::BlockComplete { data, .. } = decoder
3383 .feed(AuthenticatedSymbol::from_parts(signed.into_symbol(), tag))
3384 .expect("feed signed source")
3385 {
3386 completed = Some(data);
3387 }
3388 }
3389
3390 assert_eq!(completed.expect("source-first completion"), data);
3391 assert!(decoder.is_complete());
3392 assert_eq!(decoder.skipped_verifications(), 0);
3393 crate::test_complete!("source_first_with_auth_verifies_hmac_before_fast_path_completion");
3394 }
3395
3396 #[test]
3397 fn source_first_with_auth_rejects_permissive_hmac_mismatch() {
3398 init_test("source_first_with_auth_rejects_permissive_hmac_mismatch");
3399 let config = encoding_config();
3400 let object_id = ObjectId::new_for_test(112);
3401 let signer = crate::security::SecurityContext::for_testing(112);
3402 let verifier = crate::security::SecurityContext::for_testing_with_mode(
3403 113,
3404 crate::security::AuthMode::Permissive,
3405 );
3406 let mut decoder = DecodingPipeline::with_auth(
3407 DecodingConfig {
3408 symbol_size: config.symbol_size,
3409 max_block_size: config.max_block_size,
3410 repair_overhead: 1.0,
3411 min_overhead: 0,
3412 max_buffered_symbols: 8192,
3413 block_timeout: Duration::from_secs(30),
3414 verify_auth: true,
3415 },
3416 verifier,
3417 );
3418 decoder
3419 .set_object_params(ObjectParams::new(object_id, 512, config.symbol_size, 1, 2))
3420 .expect("params");
3421
3422 let symbol = Symbol::new(
3423 SymbolId::new(object_id, 0, 0),
3424 vec![7u8; usize::from(config.symbol_size)],
3425 SymbolKind::Source,
3426 );
3427 let signed = signer.sign_symbol(&symbol);
3428 let tag = *signed.tag();
3429 let result = decoder
3430 .feed(AuthenticatedSymbol::from_parts(signed.into_symbol(), tag))
3431 .expect("feed mismatched signed source");
3432
3433 assert_eq!(
3434 result,
3435 SymbolAcceptResult::Rejected(RejectReason::AuthenticationFailed)
3436 );
3437 assert!(!decoder.is_complete());
3438 assert_eq!(decoder.progress().symbols_received, 0);
3439 crate::test_complete!("source_first_with_auth_rejects_permissive_hmac_mismatch");
3440 }
3441
3442 #[test]
3449 fn default_config_is_fail_closed() {
3450 init_test("default_config_is_fail_closed");
3451 let secure = DecodingConfig::default();
3452 crate::assert_with_log!(
3453 secure.verify_auth,
3454 "DecodingConfig::default() is fail-closed (verify_auth=true)",
3455 true,
3456 secure.verify_auth
3457 );
3458
3459 let insecure = DecodingConfig::without_auth();
3460 crate::assert_with_log!(
3461 !insecure.verify_auth,
3462 "DecodingConfig::without_auth() opts out (verify_auth=false)",
3463 false,
3464 insecure.verify_auth
3465 );
3466
3467 let fields_match = insecure.symbol_size == secure.symbol_size
3468 && insecure.max_block_size == secure.max_block_size
3469 && insecure.repair_overhead.to_bits() == secure.repair_overhead.to_bits()
3470 && insecure.min_overhead == secure.min_overhead
3471 && insecure.max_buffered_symbols == secure.max_buffered_symbols
3472 && insecure.block_timeout == secure.block_timeout;
3473 crate::assert_with_log!(
3474 fields_match,
3475 "without_auth differs from default only in verify_auth",
3476 true,
3477 fields_match
3478 );
3479 crate::test_complete!("default_config_is_fail_closed");
3480 }
3481
3482 #[test]
3488 fn default_config_pipeline_rejects_unauthenticated_symbol() {
3489 init_test("default_config_pipeline_rejects_unauthenticated_symbol");
3490 let mut decoder = DecodingPipeline::new(DecodingConfig::default());
3491 let symbol = Symbol::new(
3492 SymbolId::new(ObjectId::new_for_test(201), 0, 0),
3493 vec![0u8; usize::from(DecodingConfig::default().symbol_size)],
3494 SymbolKind::Source,
3495 );
3496 let auth = AuthenticatedSymbol::from_parts(
3497 symbol,
3498 crate::security::tag::AuthenticationTag::zero(),
3499 );
3500 let result = decoder.feed(auth).expect("feed should not return Err");
3501 let expected = SymbolAcceptResult::Rejected(RejectReason::AuthenticationFailed);
3502 let ok = result == expected;
3503 crate::assert_with_log!(
3504 ok,
3505 "default-config pipeline rejects unauthenticated symbol",
3506 expected,
3507 result
3508 );
3509 crate::test_complete!("default_config_pipeline_rejects_unauthenticated_symbol");
3510 }
3511
3512 #[test]
3513 fn multi_block_roundtrip() {
3514 init_test("multi_block_roundtrip");
3515 let config = crate::config::EncodingConfig {
3516 symbol_size: 256,
3517 max_block_size: 1024,
3518 repair_overhead: 1.05,
3519 encoding_parallelism: 1,
3520 decoding_parallelism: 1,
3521 };
3522 let mut encoder = EncodingPipeline::new(config.clone(), pool());
3523 let object_id = ObjectId::new_for_test(110);
3524 let data: Vec<u8> = (0u32..2048).map(|i| (i % 251) as u8).collect();
3525
3526 let symbols: Vec<Symbol> = encoder
3527 .encode_with_repair(object_id, &data, 0)
3528 .map(|res| res.unwrap().into_symbol())
3529 .collect();
3530
3531 let mut decoder = DecodingPipeline::new(DecodingConfig {
3532 symbol_size: config.symbol_size,
3533 max_block_size: config.max_block_size,
3534 repair_overhead: 1.0,
3535 min_overhead: 0,
3536 max_buffered_symbols: 8192,
3537 block_timeout: Duration::from_secs(30),
3538 verify_auth: false,
3539 });
3540
3541 let symbol_size = usize::from(config.symbol_size);
3543 let num_blocks = data.len().div_ceil(config.max_block_size);
3544 let mut full_block_k: u16 = 0;
3545 for b in 0..num_blocks {
3546 let block_start = b * config.max_block_size;
3547 let block_len = usize::min(config.max_block_size, data.len() - block_start);
3548 let k = block_len.div_ceil(symbol_size) as u16;
3549 full_block_k = full_block_k.max(k);
3550 }
3551 decoder
3552 .set_object_params(ObjectParams::new(
3553 object_id,
3554 data.len() as u64,
3555 config.symbol_size,
3556 num_blocks as u16,
3557 full_block_k,
3558 ))
3559 .expect("set params");
3560
3561 for symbol in symbols {
3562 let auth = AuthenticatedSymbol::from_parts(
3563 symbol,
3564 crate::security::tag::AuthenticationTag::zero(),
3565 );
3566 let _ = decoder.feed(auth).unwrap();
3567 }
3568
3569 let complete = decoder.is_complete();
3570 crate::assert_with_log!(complete, "multi-block is_complete", true, complete);
3571
3572 let decoded_data = decoder.into_data().expect("decoded");
3573 let ok = decoded_data == data;
3574 crate::assert_with_log!(
3575 ok,
3576 "multi-block roundtrip data",
3577 data.len(),
3578 decoded_data.len()
3579 );
3580 crate::test_complete!("multi_block_roundtrip");
3581 }
3582
3583 #[test]
3584 fn deferred_streaming_feed_finishes_via_decode_job() {
3585 init_test("deferred_streaming_feed_finishes_via_decode_job");
3586 let config = crate::config::EncodingConfig {
3587 symbol_size: 4,
3588 max_block_size: 8,
3589 repair_overhead: 1.0,
3590 encoding_parallelism: 1,
3591 decoding_parallelism: 1,
3592 };
3593 let object_id = ObjectId::new_for_test(113);
3594 let data = b"ABCDEFGH".to_vec();
3595 let encoder_pool = SymbolPool::new(PoolConfig {
3596 symbol_size: config.symbol_size,
3597 initial_size: 16,
3598 max_size: 16,
3599 allow_growth: false,
3600 growth_increment: 0,
3601 });
3602 let mut encoder = EncodingPipeline::new(config.clone(), encoder_pool);
3603 let mut source_zero = None;
3604 let mut first_repair = None;
3605 for encoded in encoder.encode_single_block_with_repair(object_id, 0, &data, 1) {
3606 let symbol = encoded.expect("encode").into_symbol();
3607 match symbol.kind() {
3608 SymbolKind::Source if symbol.esi() == 0 => source_zero = Some(symbol),
3609 SymbolKind::Repair if first_repair.is_none() => first_repair = Some(symbol),
3610 _ => {}
3611 }
3612 }
3613
3614 let mut decoder = DecodingPipeline::new(DecodingConfig {
3615 symbol_size: config.symbol_size,
3616 max_block_size: config.max_block_size,
3617 repair_overhead: 1.0,
3618 min_overhead: 0,
3619 max_buffered_symbols: 8192,
3620 block_timeout: Duration::from_secs(30),
3621 verify_auth: false,
3622 });
3623 decoder
3624 .set_object_params(ObjectParams::new(
3625 object_id,
3626 data.len() as u64,
3627 config.symbol_size,
3628 1,
3629 2,
3630 ))
3631 .expect("set params");
3632
3633 let first = decoder
3634 .feed_streaming_block_deferred(AuthenticatedSymbol::new_unauthenticated(
3635 source_zero.expect("source zero"),
3636 ))
3637 .expect("feed source");
3638 assert!(matches!(
3639 first,
3640 DeferredSymbolAcceptResult::Immediate(SymbolAcceptResult::Accepted { .. })
3641 ));
3642
3643 let second = decoder
3644 .feed_streaming_block_deferred(AuthenticatedSymbol::new_unauthenticated(
3645 first_repair.expect("repair"),
3646 ))
3647 .expect("feed repair");
3648 let DeferredSymbolAcceptResult::Decode(job) = second else {
3649 panic!("second symbol should start deferred decode");
3650 };
3651 assert_eq!(
3652 job.source_symbols, 1,
3653 "repair decode jobs must carry source count below k"
3654 );
3655
3656 let outcome = run_block_decode_job(job);
3657 assert_eq!(outcome.kind(), BlockDecodeKind::RaptorQRepair);
3658 assert!(
3659 outcome.elapsed().as_nanos() > 0,
3660 "deferred decode jobs must record solve wall time for receiver profiling"
3661 );
3662 let result = decoder.finish_decode_job(outcome);
3663 match result {
3664 SymbolAcceptResult::BlockComplete {
3665 block_sbn,
3666 data: got,
3667 } => {
3668 assert_eq!(block_sbn, 0);
3669 assert_eq!(got, data);
3670 }
3671 other => panic!("deferred decode should complete block, got {other:?}"),
3672 }
3673 assert!(decoder.is_complete());
3674 crate::test_complete!("deferred_streaming_feed_finishes_via_decode_job");
3675 }
3676
3677 #[test]
3678 fn deferred_streaming_feed_does_not_spawn_duplicate_decode_for_pending_block() {
3679 init_test("deferred_streaming_feed_does_not_spawn_duplicate_decode_for_pending_block");
3680 let config = crate::config::EncodingConfig {
3681 symbol_size: 4,
3682 max_block_size: 8,
3683 repair_overhead: 1.0,
3684 encoding_parallelism: 1,
3685 decoding_parallelism: 1,
3686 };
3687 let object_id = ObjectId::new_for_test(114);
3688 let data = b"ABCDEFGH".to_vec();
3689 let encoder_pool = SymbolPool::new(PoolConfig {
3690 symbol_size: config.symbol_size,
3691 initial_size: 16,
3692 max_size: 16,
3693 allow_growth: false,
3694 growth_increment: 0,
3695 });
3696 let mut encoder = EncodingPipeline::new(config.clone(), encoder_pool);
3697 let mut source_zero = None;
3698 let mut repairs = Vec::new();
3699 for encoded in encoder.encode_single_block_with_repair(object_id, 0, &data, 2) {
3700 let symbol = encoded.expect("encode").into_symbol();
3701 match symbol.kind() {
3702 SymbolKind::Source if symbol.esi() == 0 => source_zero = Some(symbol),
3703 SymbolKind::Repair => repairs.push(symbol),
3704 _ => {}
3705 }
3706 }
3707 assert!(
3708 repairs.len() >= 2,
3709 "test fixture must provide at least two repair symbols"
3710 );
3711
3712 let mut decoder = DecodingPipeline::new(DecodingConfig {
3713 symbol_size: config.symbol_size,
3714 max_block_size: config.max_block_size,
3715 repair_overhead: 1.0,
3716 min_overhead: 0,
3717 max_buffered_symbols: 8192,
3718 block_timeout: Duration::from_secs(30),
3719 verify_auth: false,
3720 });
3721 decoder
3722 .set_object_params(ObjectParams::new(
3723 object_id,
3724 data.len() as u64,
3725 config.symbol_size,
3726 1,
3727 2,
3728 ))
3729 .expect("set params");
3730
3731 let first = decoder
3732 .feed_streaming_block_deferred(AuthenticatedSymbol::new_unauthenticated(
3733 source_zero.expect("source zero"),
3734 ))
3735 .expect("feed source");
3736 assert!(matches!(
3737 first,
3738 DeferredSymbolAcceptResult::Immediate(SymbolAcceptResult::Accepted { .. })
3739 ));
3740
3741 let started = decoder
3742 .feed_streaming_block_deferred(AuthenticatedSymbol::new_unauthenticated(
3743 repairs.remove(0),
3744 ))
3745 .expect("feed first repair");
3746 let DeferredSymbolAcceptResult::Decode(job) = started else {
3747 panic!("first repair should start deferred decode");
3748 };
3749
3750 let duplicate = decoder
3751 .feed_streaming_block_deferred(AuthenticatedSymbol::new_unauthenticated(
3752 repairs.remove(0),
3753 ))
3754 .expect("feed second repair while decode pending");
3755 assert!(
3756 matches!(
3757 duplicate,
3758 DeferredSymbolAcceptResult::Immediate(SymbolAcceptResult::Accepted { .. })
3759 ),
3760 "extra symbols for a pending block must not spawn duplicate decode jobs: {duplicate:?}"
3761 );
3762
3763 let outcome = run_block_decode_job(job);
3764 let result = decoder.finish_decode_job(outcome);
3765 match result {
3766 SymbolAcceptResult::BlockComplete {
3767 block_sbn,
3768 data: got,
3769 } => {
3770 assert_eq!(block_sbn, 0);
3771 assert_eq!(got, data);
3772 }
3773 other => panic!("deferred decode should complete block, got {other:?}"),
3774 }
3775 assert!(decoder.is_complete());
3776 crate::test_complete!(
3777 "deferred_streaming_feed_does_not_spawn_duplicate_decode_for_pending_block"
3778 );
3779 }
3780
3781 #[test]
3782 fn deferred_retry_rechecks_symbols_buffered_during_pending_decode() {
3783 init_test("deferred_retry_rechecks_symbols_buffered_during_pending_decode");
3784 let config = crate::config::EncodingConfig {
3785 symbol_size: 4,
3786 max_block_size: 8,
3787 repair_overhead: 1.0,
3788 encoding_parallelism: 1,
3789 decoding_parallelism: 1,
3790 };
3791 let object_id = ObjectId::new_for_test(115);
3792 let data = b"ABCDEFGH".to_vec();
3793 let encoder_pool = SymbolPool::new(PoolConfig {
3794 symbol_size: config.symbol_size,
3795 initial_size: 16,
3796 max_size: 16,
3797 allow_growth: false,
3798 growth_increment: 0,
3799 });
3800 let mut encoder = EncodingPipeline::new(config.clone(), encoder_pool);
3801 let mut source_zero = None;
3802 let mut repairs = Vec::new();
3803 for encoded in encoder.encode_single_block_with_repair(object_id, 0, &data, 2) {
3804 let symbol = encoded.expect("encode").into_symbol();
3805 match symbol.kind() {
3806 SymbolKind::Source if symbol.esi() == 0 => source_zero = Some(symbol),
3807 SymbolKind::Repair => repairs.push(symbol),
3808 _ => {}
3809 }
3810 }
3811 assert!(
3812 repairs.len() >= 2,
3813 "test fixture must provide two repair symbols"
3814 );
3815
3816 let mut decoder = DecodingPipeline::new(DecodingConfig {
3817 symbol_size: config.symbol_size,
3818 max_block_size: config.max_block_size,
3819 repair_overhead: 1.0,
3820 min_overhead: 0,
3821 max_buffered_symbols: 8192,
3822 block_timeout: Duration::from_secs(30),
3823 verify_auth: false,
3824 });
3825 decoder
3826 .set_object_params(ObjectParams::new(
3827 object_id,
3828 data.len() as u64,
3829 config.symbol_size,
3830 1,
3831 2,
3832 ))
3833 .expect("set params");
3834
3835 let first = decoder
3836 .feed_streaming_block_deferred(AuthenticatedSymbol::new_unauthenticated(
3837 source_zero.expect("source zero"),
3838 ))
3839 .expect("feed source zero");
3840 assert!(matches!(
3841 first,
3842 DeferredSymbolAcceptResult::Immediate(SymbolAcceptResult::Accepted { .. })
3843 ));
3844
3845 let started = decoder
3846 .feed_streaming_block_deferred(AuthenticatedSymbol::new_unauthenticated(
3847 repairs.remove(0),
3848 ))
3849 .expect("feed repair");
3850 let DeferredSymbolAcceptResult::Decode(job) = started else {
3851 panic!("repair should start deferred decode");
3852 };
3853 let stale_sbn = job.sbn();
3854 let stale_symbols = job.symbols.clone();
3855
3856 let buffered = decoder
3857 .feed_streaming_block_deferred(AuthenticatedSymbol::new_unauthenticated(
3858 repairs.remove(0),
3859 ))
3860 .expect("feed second repair while decode pending");
3861 assert!(
3862 matches!(
3863 buffered,
3864 DeferredSymbolAcceptResult::Immediate(SymbolAcceptResult::Accepted { .. })
3865 ),
3866 "new symbols accepted during a pending decode must stay buffered: {buffered:?}"
3867 );
3868
3869 let stale_retry = BlockDecodeOutcome {
3870 sbn: stale_sbn,
3871 input_symbols: stale_symbols.len(),
3872 retain_decoded_block: false,
3873 kind: BlockDecodeKind::RaptorQRepair,
3874 elapsed: Duration::ZERO,
3875 resolution: BlockDecodeResolution::Retry {
3876 reason: RejectReason::InconsistentEquations,
3877 symbols: stale_symbols,
3878 },
3879 };
3880 let retry = decoder.finish_decode_job_deferred(stale_retry);
3881 let DeferredSymbolAcceptResult::Decode(retry_job) = retry else {
3882 panic!("stale deferred retry should return a fresh decode job, got {retry:?}");
3883 };
3884 assert!(
3885 !decoder.is_complete(),
3886 "deferred retry must not run the heavy decode inline"
3887 );
3888
3889 let result = decoder.finish_decode_job(run_block_decode_job(retry_job));
3890 match result {
3891 SymbolAcceptResult::BlockComplete {
3892 block_sbn,
3893 data: got,
3894 } => {
3895 assert_eq!(block_sbn, 0);
3896 assert_eq!(got, data);
3897 }
3898 other => panic!("stale deferred retry should recheck buffered symbols, got {other:?}"),
3899 }
3900 assert!(decoder.is_complete());
3901 crate::test_complete!("deferred_retry_rechecks_symbols_buffered_during_pending_decode");
3902 }
3903
3904 #[test]
3905 fn multi_block_roundtrip_respects_partial_last_block_metadata() {
3906 init_test("multi_block_roundtrip_respects_partial_last_block_metadata");
3907 let config = crate::config::EncodingConfig {
3908 symbol_size: 4,
3909 max_block_size: 6,
3910 repair_overhead: 1.0,
3911 encoding_parallelism: 1,
3912 decoding_parallelism: 1,
3913 };
3914 let encoder_pool = SymbolPool::new(PoolConfig {
3915 symbol_size: config.symbol_size,
3916 initial_size: 16,
3917 max_size: 16,
3918 allow_growth: false,
3919 growth_increment: 0,
3920 });
3921 let mut encoder = EncodingPipeline::new(config.clone(), encoder_pool);
3922 let object_id = ObjectId::new_for_test(112);
3923 let data = b"ABCDEFGHIJKLM".to_vec();
3924
3925 let symbols: Vec<Symbol> = encoder
3926 .encode_with_repair(object_id, &data, 0)
3927 .map(|res| res.expect("encode").into_symbol())
3928 .collect();
3929
3930 let mut decoder = DecodingPipeline::new(DecodingConfig {
3931 symbol_size: config.symbol_size,
3932 max_block_size: config.max_block_size,
3933 repair_overhead: 1.0,
3934 min_overhead: 0,
3935 max_buffered_symbols: 8192,
3936 block_timeout: Duration::from_secs(30),
3937 verify_auth: false,
3938 });
3939 decoder
3940 .set_object_params(ObjectParams::new(
3941 object_id,
3942 data.len() as u64,
3943 config.symbol_size,
3944 3,
3945 2,
3946 ))
3947 .expect("set params for uneven multi-block object");
3948
3949 let expected_blocks = Some(3usize);
3950 let blocks_total = decoder.progress().blocks_total;
3951 crate::assert_with_log!(
3952 blocks_total == expected_blocks,
3953 "partial last block count",
3954 expected_blocks,
3955 blocks_total
3956 );
3957
3958 for symbol in symbols {
3959 let auth = AuthenticatedSymbol::from_parts(
3960 symbol,
3961 crate::security::tag::AuthenticationTag::zero(),
3962 );
3963 let _ = decoder.feed(auth).expect("feed");
3964 }
3965
3966 let complete = decoder.is_complete();
3967 crate::assert_with_log!(
3968 complete,
3969 "partial last block roundtrip is_complete",
3970 true,
3971 complete
3972 );
3973
3974 let decoded_data = decoder.into_data().expect("decoded");
3975 let ok = decoded_data == data;
3976 crate::assert_with_log!(
3977 ok,
3978 "partial last block roundtrip data",
3979 data.len(),
3980 decoded_data.len()
3981 );
3982 crate::test_complete!("multi_block_roundtrip_respects_partial_last_block_metadata");
3983 }
3984
3985 #[test]
3986 fn multi_block_progress_retains_cumulative_symbols_after_block_completion() {
3987 init_test("multi_block_progress_retains_cumulative_symbols_after_block_completion");
3988 let config = crate::config::EncodingConfig {
3989 symbol_size: 256,
3990 max_block_size: 1024,
3991 repair_overhead: 1.05,
3992 encoding_parallelism: 1,
3993 decoding_parallelism: 1,
3994 };
3995 let mut encoder = EncodingPipeline::new(config.clone(), pool());
3996 let object_id = ObjectId::new_for_test(111);
3997 let data: Vec<u8> = (0u32..2048).map(|i| (i % 251) as u8).collect();
3998
3999 let mut block_zero_symbols: Vec<Symbol> = encoder
4000 .encode_with_repair(object_id, &data, 0)
4001 .map(|res| res.expect("encode").into_symbol())
4002 .filter(|symbol| symbol.sbn() == 0)
4003 .collect();
4004 block_zero_symbols.sort_by_key(Symbol::esi);
4005 assert_eq!(block_zero_symbols.len(), 4);
4006
4007 let mut decoder = DecodingPipeline::new(DecodingConfig {
4008 symbol_size: config.symbol_size,
4009 max_block_size: config.max_block_size,
4010 repair_overhead: 1.0,
4011 min_overhead: 0,
4012 max_buffered_symbols: 8192,
4013 block_timeout: Duration::from_secs(30),
4014 verify_auth: false,
4015 });
4016 decoder
4017 .set_object_params(ObjectParams::new(
4018 object_id,
4019 data.len() as u64,
4020 config.symbol_size,
4021 2,
4022 4,
4023 ))
4024 .expect("set params");
4025
4026 for symbol in block_zero_symbols {
4027 let auth = AuthenticatedSymbol::from_parts(
4028 symbol,
4029 crate::security::tag::AuthenticationTag::zero(),
4030 );
4031 let _ = decoder.feed(auth).expect("feed");
4032 }
4033
4034 assert_eq!(decoder.progress().blocks_complete, 1);
4035 assert_eq!(decoder.progress().blocks_total, Some(2));
4036 assert_eq!(decoder.progress().symbols_received, 4);
4037 assert_eq!(decoder.progress().symbols_needed_estimate, 8);
4038
4039 let err = decoder.into_data().expect_err("block one is still missing");
4040 assert!(matches!(
4041 err,
4042 DecodingError::InsufficientSymbols {
4043 received: 4,
4044 needed: 8
4045 }
4046 ));
4047 crate::test_complete!(
4048 "multi_block_progress_retains_cumulative_symbols_after_block_completion"
4049 );
4050 }
4051
4052 #[test]
4053 fn into_data_no_params_errors() {
4054 init_test("into_data_no_params_errors");
4055 let pipeline = DecodingPipeline::new(DecodingConfig::default());
4056 let result = pipeline.into_data();
4057 let is_err = result.is_err();
4058 crate::assert_with_log!(is_err, "into_data without params errors", true, is_err);
4059 let err = result.unwrap_err();
4060 let msg = err.to_string();
4061 let contains = msg.contains("object parameters not set");
4062 crate::assert_with_log!(
4063 contains,
4064 "error message contains expected text",
4065 true,
4066 contains
4067 );
4068 crate::test_complete!("into_data_no_params_errors");
4069 }
4070
4071 #[test]
4074 fn reject_reason_debug_clone_copy_eq() {
4075 let r = RejectReason::WrongObjectId;
4076 let r2 = r; let r3 = r;
4078 assert_eq!(r, r2);
4079 assert_eq!(r, r3);
4080 assert_ne!(r, RejectReason::AuthenticationFailed);
4081 assert_ne!(r, RejectReason::SymbolSizeMismatch);
4082 assert_ne!(r, RejectReason::BlockAlreadyDecoded);
4083 assert_ne!(r, RejectReason::InsufficientRank);
4084 assert_ne!(r, RejectReason::InconsistentEquations);
4085 assert_ne!(r, RejectReason::InvalidMetadata);
4086 assert_ne!(r, RejectReason::MemoryLimitReached);
4087 let dbg = format!("{r:?}");
4088 assert!(dbg.contains("WrongObjectId"));
4089 }
4090
4091 #[test]
4092 fn symbol_accept_result_debug_clone_eq() {
4093 let a = SymbolAcceptResult::Accepted {
4094 received: 3,
4095 needed: 5,
4096 };
4097 let a2 = a.clone();
4098 assert_eq!(a, a2);
4099 assert_ne!(a, SymbolAcceptResult::Duplicate);
4100 let r = SymbolAcceptResult::Rejected(RejectReason::InvalidMetadata);
4101 let r2 = r.clone();
4102 assert_eq!(r, r2);
4103 let dbg = format!("{a:?}");
4104 assert!(dbg.contains("Accepted"));
4105 }
4106
4107 #[test]
4108 fn block_state_kind_debug_clone_copy_eq() {
4109 let s = BlockStateKind::Collecting;
4110 let s2 = s; let s3 = s;
4112 assert_eq!(s, s2);
4113 assert_eq!(s, s3);
4114 assert_ne!(s, BlockStateKind::Decoding);
4115 assert_ne!(s, BlockStateKind::Decoded);
4116 assert_ne!(s, BlockStateKind::Failed);
4117 let dbg = format!("{s:?}");
4118 assert!(dbg.contains("Collecting"));
4119 }
4120
4121 #[test]
4129 fn honest_symbols_never_inconsistent_across_stale_requeue_cycles() {
4130 init_test("honest_symbols_never_inconsistent_across_stale_requeue_cycles");
4131 let mut rng_state: u64 = 0xC54_707;
4133 let mut next_rand = move |bound: usize| -> usize {
4134 rng_state = rng_state
4135 .wrapping_mul(6364136223846793005)
4136 .wrapping_add(1442695040888963407);
4137 usize::try_from((rng_state >> 33) % (bound.max(1) as u64)).unwrap_or(0)
4138 };
4139
4140 let shapes: [(u16, usize); 6] = [
4143 (8, 64), (8, 61), (8, 9), (16, 33), (4, 4), (8, 57), ];
4150
4151 for (shape_index, (symbol_size, data_len)) in shapes.iter().enumerate() {
4152 let config = crate::config::EncodingConfig {
4153 symbol_size: *symbol_size,
4154 max_block_size: 1 << 20,
4155 repair_overhead: 1.0,
4156 encoding_parallelism: 1,
4157 decoding_parallelism: 1,
4158 };
4159 let object_id = ObjectId::new_for_test(4200 + shape_index as u64);
4160 let data: Vec<u8> = (0..*data_len).map(|i| (i * 31 + 7) as u8).collect();
4161 let encoder_pool = SymbolPool::new(PoolConfig {
4162 symbol_size: config.symbol_size,
4163 initial_size: 64,
4164 max_size: 64,
4165 allow_growth: false,
4166 growth_increment: 0,
4167 });
4168 let mut encoder = EncodingPipeline::new(config.clone(), encoder_pool);
4169 let mut sources = Vec::new();
4170 let mut repairs = Vec::new();
4171 for encoded in encoder.encode_single_block_with_repair(object_id, 0, &data, 8) {
4172 let symbol = encoded.expect("encode").into_symbol();
4173 match symbol.kind() {
4174 SymbolKind::Source => sources.push(symbol),
4175 SymbolKind::Repair => repairs.push(symbol),
4176 }
4177 }
4178 let k = sources.len();
4179
4180 for trial in 0..24usize {
4183 let mut decoder = DecodingPipeline::new(DecodingConfig {
4184 symbol_size: config.symbol_size,
4185 max_block_size: config.max_block_size,
4186 repair_overhead: 1.0,
4187 min_overhead: 0,
4188 max_buffered_symbols: 8192,
4189 block_timeout: Duration::from_secs(30),
4190 verify_auth: false,
4191 });
4192 decoder
4193 .set_object_params(ObjectParams::new(
4194 object_id,
4195 data.len() as u64,
4196 config.symbol_size,
4197 1,
4198 u16::try_from(k).unwrap_or(u16::MAX),
4199 ))
4200 .expect("set params");
4201
4202 let dropped = trial % (k + 1);
4204 let mut feed: Vec<Symbol> = sources
4205 .iter()
4206 .skip(dropped)
4207 .chain(repairs.iter().take(dropped + 2))
4208 .cloned()
4209 .collect();
4210 for i in (1..feed.len()).rev() {
4212 feed.swap(i, next_rand(i + 1));
4213 }
4214
4215 let mut pending_job: Option<BlockDecodeJob> = None;
4216 let mut complete = false;
4217 for (feed_index, symbol) in feed.iter().enumerate() {
4218 if complete {
4219 break;
4220 }
4221 let result = decoder
4222 .feed_streaming_block_deferred(AuthenticatedSymbol::new_unauthenticated(
4223 symbol.clone(),
4224 ))
4225 .expect("feed");
4226 match result {
4227 DeferredSymbolAcceptResult::Decode(job) => {
4228 if feed_index % 2 == 0 {
4231 let outcome = run_block_decode_job(job);
4232 match decoder.finish_decode_job(outcome) {
4233 SymbolAcceptResult::Rejected(reason) => {
4234 assert_ne!(
4235 reason,
4236 RejectReason::InconsistentEquations,
4237 "honest symbols must never be inconsistent \
4238 (shape {shape_index}, trial {trial})"
4239 );
4240 }
4241 SymbolAcceptResult::BlockComplete { data: got, .. } => {
4242 assert_eq!(got, data, "decoded bytes must match");
4243 complete = true;
4244 }
4245 _ => {}
4246 }
4247 } else {
4248 pending_job = Some(job);
4249 }
4250 }
4251 DeferredSymbolAcceptResult::Immediate(SymbolAcceptResult::Rejected(
4252 reason,
4253 )) => {
4254 assert_ne!(
4255 reason,
4256 RejectReason::InconsistentEquations,
4257 "honest feed must never be inconsistent \
4258 (shape {shape_index}, trial {trial})"
4259 );
4260 }
4261 DeferredSymbolAcceptResult::Immediate(
4262 SymbolAcceptResult::BlockComplete { data: got, .. },
4263 ) => {
4264 assert_eq!(got, data, "decoded bytes must match");
4265 complete = true;
4266 }
4267 DeferredSymbolAcceptResult::Immediate(_) => {}
4268 }
4269 if let Some(job) = pending_job.take() {
4273 let outcome = run_block_decode_job(job);
4274 match decoder.finish_decode_job_deferred(outcome) {
4275 DeferredSymbolAcceptResult::Decode(fresh) => {
4276 let outcome = run_block_decode_job(fresh);
4277 match decoder.finish_decode_job(outcome) {
4278 SymbolAcceptResult::Rejected(reason) => {
4279 assert_ne!(
4280 reason,
4281 RejectReason::InconsistentEquations,
4282 "stale-requeue cycle must stay consistent \
4283 (shape {shape_index}, trial {trial})"
4284 );
4285 }
4286 SymbolAcceptResult::BlockComplete { data: got, .. } => {
4287 assert_eq!(got, data);
4288 complete = true;
4289 }
4290 _ => {}
4291 }
4292 }
4293 DeferredSymbolAcceptResult::Immediate(
4294 SymbolAcceptResult::Rejected(reason),
4295 ) => {
4296 assert_ne!(
4297 reason,
4298 RejectReason::InconsistentEquations,
4299 "stale finish must stay consistent \
4300 (shape {shape_index}, trial {trial})"
4301 );
4302 }
4303 DeferredSymbolAcceptResult::Immediate(
4304 SymbolAcceptResult::BlockComplete { data: got, .. },
4305 ) => {
4306 assert_eq!(got, data);
4307 complete = true;
4308 }
4309 DeferredSymbolAcceptResult::Immediate(_) => {}
4310 }
4311 }
4312 }
4313 if feed.len() > k {
4315 assert!(
4316 complete || feed.len() < k,
4317 "block should complete with {} symbols for k={k} \
4318 (shape {shape_index}, trial {trial})",
4319 feed.len()
4320 );
4321 }
4322 }
4323 }
4324 crate::test_complete!("honest_symbols_never_inconsistent_across_stale_requeue_cycles");
4325 }
4326}