Skip to main content

asupersync/
decoding.rs

1//! RaptorQ decoding pipeline (Phase 0).
2//!
3//! This module provides a deterministic, block-oriented decoding pipeline that
4//! reconstructs original data from a set of received symbols. The current
5//! implementation mirrors the systematic RaptorQ encoder: it solves for
6//! intermediate symbols using the precode constraints and LT repair rows, then
7//! reconstitutes source symbols deterministically for testing.
8
9use 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/// Errors produced by the decoding pipeline.
27#[derive(Debug, thiserror::Error)]
28pub enum DecodingError {
29    /// Authentication failed for a symbol.
30    #[error("authentication failed for symbol {symbol_id}")]
31    AuthenticationFailed {
32        /// The symbol that failed authentication.
33        symbol_id: SymbolId,
34    },
35    /// Not enough symbols to decode.
36    #[error("insufficient symbols: have {received}, need {needed}")]
37    InsufficientSymbols {
38        /// Received symbol count.
39        received: usize,
40        /// Needed symbol count.
41        needed: usize,
42    },
43    /// Matrix inversion failed during decoding.
44    #[error("matrix inversion failed: {reason}")]
45    MatrixInversionFailed {
46        /// Reason for failure.
47        reason: String,
48    },
49    /// Block timed out before decoding completed.
50    #[error("block timeout after {elapsed:?}")]
51    BlockTimeout {
52        /// Block number.
53        sbn: u8,
54        /// Elapsed time.
55        elapsed: Duration,
56    },
57    /// Inconsistent metadata for a block or object.
58    #[error("inconsistent block metadata: {sbn} {details}")]
59    InconsistentMetadata {
60        /// Block number.
61        sbn: u8,
62        /// Details of the inconsistency.
63        details: String,
64    },
65    /// Symbol size mismatch.
66    #[error("symbol size mismatch: expected {expected}, got {actual}")]
67    SymbolSizeMismatch {
68        /// Expected size in bytes.
69        expected: u16,
70        /// Actual size in bytes.
71        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/// Reasons a symbol may be rejected by the decoder.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum RejectReason {
92    /// Symbol belongs to a different object.
93    WrongObjectId,
94    /// Authentication failed.
95    AuthenticationFailed,
96    /// Symbol size mismatch.
97    SymbolSizeMismatch,
98    /// Block already decoded.
99    BlockAlreadyDecoded,
100    /// Decode failed due to insufficient rank.
101    InsufficientRank,
102    /// Decode failed due to inconsistent equations.
103    InconsistentEquations,
104    /// Invalid or inconsistent metadata.
105    InvalidMetadata,
106    /// Memory or buffer limit reached.
107    MemoryLimitReached,
108}
109
110/// Result of feeding a symbol into the decoder.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum SymbolAcceptResult {
113    /// Symbol accepted and stored.
114    Accepted {
115        /// Symbols received for the block.
116        received: usize,
117        /// Estimated symbols needed for decode.
118        needed: usize,
119    },
120    /// Decoding started for the block.
121    DecodingStarted {
122        /// Block number being decoded.
123        block_sbn: u8,
124    },
125    /// Block fully decoded.
126    BlockComplete {
127        /// Block number.
128        block_sbn: u8,
129        /// Decoded block data.
130        data: Vec<u8>,
131    },
132    /// Duplicate symbol ignored.
133    Duplicate,
134    /// Symbol rejected.
135    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    // The pre-verified feed path has no wasm caller yet (browser feeds
149    // always verify in-pipeline); native transports construct it.
150    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
151    CallerVerified,
152}
153
154/// Result of a feed that may defer the CPU-heavy RaptorQ solve to a blocking
155/// worker.
156#[derive(Debug)]
157pub(crate) enum DeferredSymbolAcceptResult {
158    /// The symbol was handled synchronously.
159    Immediate(SymbolAcceptResult),
160    /// The block reached decode threshold and should be solved off the caller's
161    /// hot receive path.
162    Decode(BlockDecodeJob),
163}
164
165/// Owned RaptorQ block-decode job.
166#[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/// Which path produced a block-decode outcome.
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub(crate) enum BlockDecodeKind {
199    /// The block had every source symbol, so no RaptorQ matrix solve was needed.
200    SourceComplete,
201    /// The block required repair rows and a RaptorQ matrix solve.
202    RaptorQRepair,
203}
204
205/// Output from a [`BlockDecodeJob`]. Feed it back through
206/// [`DecodingPipeline::finish_decode_job`] to update pipeline state.
207#[derive(Debug)]
208pub(crate) struct BlockDecodeOutcome {
209    sbn: u8,
210    input_symbols: usize,
211    retain_decoded_block: bool,
212    // Read only by the native audit/telemetry consumers; the browser
213    // profile records outcomes without re-reading these fields.
214    #[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/// FNV-1a 64 over a byte slice — the cheap payload fingerprint used by the
222/// `ATP_RQ_INCONSISTENT_AUDIT` probe (c54to7): stable, dependency-free, and
223/// enough to tell "same bytes" from "poisoned bytes" across audit dumps.
224#[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
234/// Env-gated (`ATP_RQ_INCONSISTENT_AUDIT`) equation-set dump when a block
235/// solve rejects: one line per input symbol with its payload fingerprint.
236/// Offline cross-check against the transport layer's staged-bytes dump for
237/// the same block identifies WHICH symbol carried poisoned bytes (seeded
238/// source vs wire repair) — the c54to7 residual-inconsistency probe.
239fn 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/// Runs an owned block-decode job. Intended for `Cx::spawn_blocking`.
260#[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                // A singular matrix is RANK DEFICIENCY, not inconsistency —
288                // and at N == K it is EXPECTED RaptorQ behavior, not a bug:
289                // RFC 6330's decode failure probability at exactly K symbols
290                // is ~1e-2 (~1e-4 at K+1, ~1e-6 at K+2). With
291                // repair_overhead = 1.0 every block's first solve fires at
292                // exactly K, so a 500M sharded transfer (~900-1000 K-exact
293                // solves) sees a handful of these per run; the Retry path
294                // collects one more symbol and succeeds. Labeling this
295                // InconsistentEquations sent a P1 correctness hunt after
296                // spec-expected noise (br-asupersync-c54to7: traced audit
297                // showed all residual rejects rank-K-exact with ZERO payload
298                // mismatches across 1,192 seeded-vs-staged comparisons).
299                // InconsistentEquations is reserved for the unexpected-error
300                // fallback below, where the ATP_RQ_INCONSISTENT_AUDIT dump
301                // fires loudly.
302                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    // The three accessors below serve native audit/staging consumers
340    // (the c54to7 cross-dump and transport telemetry); the browser
341    // profile has no caller yet, mirroring this file's wasm precedent.
342    #[must_use]
343    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
344    pub(crate) fn elapsed(&self) -> Duration {
345        self.elapsed
346    }
347
348    /// Block this outcome belongs to (the `ATP_RQ_INCONSISTENT_AUDIT`
349    /// staging cross-dump needs it at the reject site, c54to7).
350    #[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/// Configuration for decoding operations.
364#[derive(Debug, Clone)]
365pub struct DecodingConfig {
366    /// Symbol size in bytes (must match encoding).
367    pub symbol_size: u16,
368    /// Maximum source block size in bytes.
369    pub max_block_size: usize,
370    /// Repair overhead factor (e.g., 1.05 = 5% extra symbols).
371    pub repair_overhead: f64,
372    /// Minimum extra symbols beyond K.
373    pub min_overhead: usize,
374    /// Maximum symbols to buffer per block (0 = unlimited).
375    pub max_buffered_symbols: usize,
376    /// Block timeout (not enforced in Phase 0).
377    pub block_timeout: Duration,
378    /// Whether to verify authentication tags.
379    pub verify_auth: bool,
380}
381
382impl Default for DecodingConfig {
383    /// br-asupersync-b1fojq: the default is **fail-closed** —
384    /// `verify_auth: true`. A `DecodingPipeline` built from
385    /// `DecodingConfig::default()` rejects every symbol unless an
386    /// [`SecurityContext`] is installed (see [`DecodingPipeline::with_auth`])
387    /// and the symbol authenticates. Previously the default was
388    /// `verify_auth: false`, so a default-config pipeline authenticated
389    /// NOTHING and silently accepted forged/unauthenticated symbols
390    /// (decode-matrix poisoning). Callers that legitimately decode without
391    /// per-symbol authentication (erasure-only / integrity-vs-manifest
392    /// transports, or paths that authenticate each symbol upstream) must opt
393    /// out **explicitly** via [`DecodingConfig::without_auth`] or by setting
394    /// `verify_auth: false` in a literal — the insecure choice is no longer
395    /// the default.
396    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    /// Explicit, **insecure** opt-out from per-symbol authentication.
411    ///
412    /// br-asupersync-b1fojq: returns the same configuration as
413    /// [`DecodingConfig::default`] except `verify_auth` is `false`, so the
414    /// resulting [`DecodingPipeline`] accepts symbols WITHOUT verifying an
415    /// authentication tag. This is the correct configuration only when the
416    /// caller does not need anti-forgery at the symbol layer — e.g.
417    /// erasure-only recovery, integrity-vs-manifest transports, or pipelines
418    /// that authenticate every symbol upstream before feeding it. Acceptance
419    /// is still surfaced via [`DecodingPipeline::skipped_verifications`] and a
420    /// one-time WARN. Prefer [`DecodingConfig::default`] (fail-closed) for any
421    /// path that ingests symbols from an untrusted peer.
422    #[must_use]
423    pub fn without_auth() -> Self {
424        Self {
425            verify_auth: false,
426            ..Self::default()
427        }
428    }
429}
430
431/// Progress summary for decoding.
432#[derive(Debug, Clone, Copy)]
433pub struct DecodingProgress {
434    /// Blocks fully decoded.
435    pub blocks_complete: usize,
436    /// Total blocks expected (if known).
437    pub blocks_total: Option<usize>,
438    /// Total symbols received.
439    pub symbols_received: usize,
440    /// Estimated symbols needed to complete decode.
441    pub symbols_needed_estimate: usize,
442}
443
444/// Per-block status.
445#[derive(Debug, Clone, Copy)]
446pub struct BlockStatus {
447    /// Block number.
448    pub sbn: u8,
449    /// Symbols received for this block.
450    pub symbols_received: usize,
451    /// Estimated symbols needed for this block.
452    pub symbols_needed: usize,
453    /// Independent equation rank for this block, when computable.
454    pub rank: Option<usize>,
455    /// Additional independent equations required for full rank, when computable.
456    pub rank_deficit: Option<usize>,
457    /// Block state.
458    pub state: BlockStateKind,
459}
460
461/// Missing systematic source symbol for an incomplete source block.
462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
463pub struct MissingSourceSymbol {
464    /// Source block number.
465    pub sbn: u8,
466    /// Encoding symbol id within the source block.
467    pub esi: u32,
468}
469
470/// High-level block state.
471#[derive(Debug, Clone, Copy, PartialEq, Eq)]
472pub enum BlockStateKind {
473    /// Collecting symbols.
474    Collecting,
475    /// Decoding in progress.
476    Decoding,
477    /// Decoded successfully.
478    Decoded,
479    /// Decoding failed.
480    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/// Main decoding pipeline.
510#[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    /// br-asupersync-f4mdcr: count of symbols accepted with
525    /// authentication INTENTIONALLY skipped because
526    /// `config.verify_auth = false`. Surfaced via
527    /// [`Self::skipped_verifications`] so operators alerting on
528    /// "authenticated symbol pipeline" health have an audit trail
529    /// instead of silent acceptance.
530    skipped_verifications: u64,
531    /// br-asupersync-f4mdcr: tracks whether we have already emitted
532    /// the one-time WARN log for the verify-auth-disabled path. The
533    /// log is emitted once per pipeline instance to avoid spamming
534    /// per-symbol log lines while still giving operators a visible
535    /// signal that auth is off.
536    verify_auth_disabled_warned: bool,
537}
538
539impl DecodingPipeline {
540    /// Configured symbol size — the `ATP_RQ_INCONSISTENT_AUDIT` staging
541    /// cross-dump (c54to7) needs it to mirror seed-symbol zero-padding.
542    #[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    /// Creates a new decoding pipeline.
549    #[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    /// br-asupersync-f4mdcr: total number of `feed()` calls that
575    /// accepted a symbol with authentication INTENTIONALLY skipped
576    /// because `config.verify_auth = false`. Operators can scrape
577    /// this counter via the runtime's observability surface to alert
578    /// on misconfigured pipelines that quietly disable auth in
579    /// production. Pre-fix the skip was silent — no log, no counter,
580    /// no observability hook fired — so a deployment that misset
581    /// `verify_auth = false` accepted unauthenticated symbols
582    /// without any operator-visible signal.
583    #[must_use]
584    #[inline]
585    pub const fn skipped_verifications(&self) -> u64 {
586        self.skipped_verifications
587    }
588
589    /// Creates a new decoding pipeline with authentication enabled.
590    #[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    /// Sets object parameters (object size, symbol size, and block layout).
598    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    /// The effective per-block symbol-accept cap (`0` means unbounded).
633    ///
634    /// With `max_buffered_symbols == 0`, [`set_object_params`](Self::set_object_params)
635    /// sizes this to cover K (plus repair slack) via `configure_auto_buffer_limit`;
636    /// with a fixed nonzero `max_buffered_symbols` it stays at that value and does
637    /// not scale with K.
638    #[must_use]
639    pub fn block_accept_cap(&self) -> usize {
640        self.symbols.max_per_block()
641    }
642
643    /// Feeds a received authenticated symbol into the pipeline.
644    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    /// Feeds a received authenticated symbol and returns an owned decode job
652    /// when the block reaches threshold. Unlike
653    /// [`Self::feed_streaming_block_deferred`], completed block data is retained
654    /// in this pipeline so existing full-object commit paths can still call
655    /// [`Self::into_data`] after joining the decode job.
656    #[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    /// Feeds a streaming symbol and returns an owned decode job when the block
670    /// reaches threshold. The caller must run the job and pass its outcome to
671    /// [`Self::finish_decode_job`].
672    #[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    /// Feeds a streaming symbol that the caller has already authenticated with
686    /// the same security context guarding this receive pipeline.
687    ///
688    /// This is intentionally `pub(crate)` and separate from the normal feed
689    /// methods: a bare verified bit is not generally transferable between
690    /// trust domains, but transport-level batch verifiers can prove the tag once
691    /// at the receiver boundary and then preserve decoder ordering without a
692    /// second serial HMAC.
693    #[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                    // A bare `verified` bit does not identify which key or verifier vouched for
750                    // the symbol. Without an auth context, we cannot authenticate deterministically
751                    // and must fail closed.
752                    return Ok(DeferredSymbolAcceptResult::Immediate(
753                        SymbolAcceptResult::Rejected(RejectReason::AuthenticationFailed),
754                    ));
755                }
756            }
757        } else if !self.verify_auth_disabled_warned {
758            // br-asupersync-f4mdcr: auth is disabled by configuration.
759            // The pre-fix shape silently accepted the symbol with NO
760            // log, NO counter, NO observability hook — operators
761            // alerting on `starvation_events: 0, priority_inversions: 0`
762            // had no way to detect a deployment that quietly turned
763            // off auth. Now we emit a one-time WARN per pipeline
764            // instance and expose accepted-symbol counts via
765            // [`Self::skipped_verifications`].
766            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        // Ensure block entry exists
822        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                    // Update state to Decoding
881                    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                // Reset state to Collecting (if not decoded)
894                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    /// Feeds a batch of symbols.
910    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    /// Returns true if all expected blocks are decoded.
918    #[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    /// Returns decoding progress.
927    #[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    /// Returns per-block status if known.
944    #[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    /// Consumes the pipeline and returns decoded data if complete.
973    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        // O(1) gate (br-asupersync-atp-dataplane-redesign-317hxr.29, drhadc lever):
1056        // try_complete_from_source_symbols allocates + copies the ENTIRE block on every
1057        // call and returns None until all k source symbols are present. Called per source
1058        // symbol, that is O(k^2) alloc/copy per block — the clean-link receiver-intake
1059        // wall (MATRIX-23 feed_micros). Skip it until the maintained source count reaches
1060        // k; source symbols are never evicted, so the count is exact.
1061        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        // O(1) gate (br-asupersync-atp-dataplane-redesign-317hxr.29, drhadc lever):
1090        // skip cloning every received block symbol until at least k symbols
1091        // (source+repair) are available to decode. Called per symbol, the old
1092        // clone-then-`len() < k` was O(k^2) clone per block. The authoritative
1093        // `symbols.len() < k` check below is retained as a backstop in case repair
1094        // retention eviction leaves the count ahead of the live symbol set.
1095        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    /// Finalizes a previously deferred decode job and updates block state.
1196    #[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    /// Finalizes a previously deferred decode job without running any CPU-heavy
1209    /// retry inline. If newer symbols arrived while the job was in flight, the
1210    /// caller gets a fresh owned job that can be sent back through its blocking
1211    /// decode queue.
1212    #[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    /// Returns missing systematic source symbols for incomplete blocks.
1353    ///
1354    /// This is used by ATP-RQ as a cheap first feedback step: retransmitting a
1355    /// sparse set of missing systematic symbols avoids constructing the
1356    /// CPU-heavy RaptorQ repair encoder when the receiver only dropped a few
1357    /// datagrams. `limit == 0` means unbounded.
1358    #[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    // br-asupersync-qokghh: reject K outside the RFC 6330 systematic-index
1487    // table BEFORE decode_block reaches InactivationDecoder::new, which
1488    // would otherwise panic via SystematicParams::for_source_block. A
1489    // peer-supplied symbols_per_block (u16, 0..65535) or a misconfigured
1490    // DecodingConfig (e.g., symbol_size=1 with default max_block_size)
1491    // can drive K above the 56403 max; surface that as a typed
1492    // InconsistentMetadata at validation time.
1493    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    // `overhead` already encodes the total-symbol target; `min_overhead` is a
1529    // floor on extra symbols beyond K, not an additional increment on top.
1530    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    // 4. Construct decoded symbols from the source data returned by the decoder.
1773    // InactivationDecoder::decode already extracts the first K intermediate symbols
1774    // into `result.source`, which corresponds exactly to the systematic source data.
1775    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        // A single block with K > the fixed 8192 default. With
2102        // max_buffered_symbols == 0, set_object_params must size the per-block
2103        // accept cap to cover K (configure_auto_buffer_limit); with the fixed
2104        // default it stays at 8192 and would reject legitimately-received
2105        // symbols past 8192 so the block never decodes (the receive_object /
2106        // erasure / recovery class of bug).
2107        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        // Ensure K > 1 so the first symbol cannot complete the block decode.
2264        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    // ---- DecodingError Display ----
2331
2332    #[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    // ---- DecodingError -> Error conversion ----
2393
2394    #[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    // ---- RejectReason ----
2453
2454    #[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    // ---- SymbolAcceptResult ----
2470
2471    #[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    // ---- DecodingConfig default ----
2505
2506    #[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    // ---- BlockStateKind ----
2529
2530    #[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    // ---- DecodingPipeline construction ----
2539
2540    #[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        // br-asupersync-qokghh: a misconfigured DecodingConfig (here:
2617        // symbol_size=1 with default max_block_size=1MB) drives K above
2618        // the RFC 6330 systematic-index table maximum (56,403). Without
2619        // the validation guard, decode_block would later panic via
2620        // SystematicParams::for_source_block; with the guard the error
2621        // is surfaced as a typed InconsistentMetadata at the validation
2622        // boundary so callers can react instead of crashing the
2623        // decoder.
2624        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        // 65_000 bytes / 1-byte-symbols = 65_000 symbols/block — exceeds
2631        // the RFC max of 56,403.
2632        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    // ---- Gap tests ----
2778
2779    #[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]; // 3 source symbols at 256 bytes each
2786
2787        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        // data_len=512 < max_block_size=1024 => 1 block
2917        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        // Use high overhead so 1 symbol doesn't trigger decode
3042        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        // Block 0 should now be decoded; symbols are cleared but block state persists.
3106        // After decode, symbols are cleared so block_progress returns None.
3107        // The completed_blocks set tracks completion separately.
3108        let _status = decoder.block_status(0);
3109        let complete = decoder.is_complete();
3110        crate::assert_with_log!(complete, "is_complete", true, complete);
3111
3112        // Verify via completed_blocks indirectly: feeding another sbn=0 symbol
3113        // should give BlockAlreadyDecoded
3114        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        // Feed one more symbol for sbn=0
3223        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        // from_parts creates an unverified symbol
3254        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        // zero tag is wrong for any real key
3331        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    /// br-asupersync-b1fojq: the default decode configuration MUST be
3443    /// fail-closed (`verify_auth = true`). This test locks the secure default
3444    /// in place so a future change cannot silently reintroduce the fail-open
3445    /// posture, and verifies the explicit opt-out
3446    /// [`DecodingConfig::without_auth`] differs from the default ONLY in
3447    /// `verify_auth`.
3448    #[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    /// br-asupersync-b1fojq: end-to-end proof that a pipeline built from the
3483    /// default config (no [`SecurityContext`] installed) REJECTS an
3484    /// unauthenticated symbol instead of silently accepting it. Pre-fix the
3485    /// default was `verify_auth = false`, so this exact symbol would have been
3486    /// accepted (decode-matrix poisoning).
3487    #[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        // Compute block plan matching what the encoder does
3542        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    // --- wave 76 trait coverage ---
4072
4073    #[test]
4074    fn reject_reason_debug_clone_copy_eq() {
4075        let r = RejectReason::WrongObjectId;
4076        let r2 = r; // Copy
4077        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; // Copy
4111        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    /// c54to7 residual hunt: honest symbols fed through arbitrary orders,
4122    /// duplicate re-feeds, and repeated STALE-REQUEUE cycles must NEVER
4123    /// produce InconsistentEquations — across block shapes including short
4124    /// final symbols (the last-shard boundary class the residual skews to).
4125    /// If this holds, the decoder core + snapshot/restore machinery is
4126    /// exonerated deterministically and the residual source is
4127    /// transport-side state.
4128    #[test]
4129    fn honest_symbols_never_inconsistent_across_stale_requeue_cycles() {
4130        init_test("honest_symbols_never_inconsistent_across_stale_requeue_cycles");
4131        // Deterministic LCG (no external RNG; Date/random are banned).
4132        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        // Block shapes: (symbol_size, data_len) — full blocks, short final
4141        // symbol, single-symbol blocks, and K around the observed class.
4142        let shapes: [(u16, usize); 6] = [
4143            (8, 64),  // k=8 exact
4144            (8, 61),  // k=8, short final symbol (the shard-boundary class)
4145            (8, 9),   // k=2, tiny short final
4146            (16, 33), // k=3, one-past boundary
4147            (4, 4),   // k=1 single symbol
4148            (8, 57),  // k=8, 1-byte final symbol
4149        ];
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            // Several randomized trials per shape: shuffle a mixed subset,
4181            // inject staleness mid-stream, re-feed duplicates.
4182            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                // Feed order: drop `dropped` sources, replace with repairs.
4203                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                // Deterministic shuffle.
4211                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                            // Sometimes run it now; sometimes hold it so more
4229                            // symbols land first, then report it back STALE.
4230                            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                    // Stale cycle: resolve the held job AFTER newer symbols
4270                    // arrived — the deferred finish must produce a fresh job
4271                    // (or an honest outcome), never an inconsistency.
4272                    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                // With ≥ K honest symbols delivered the block must complete.
4314                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}