Skip to main content

rustfs_erasure_codec/core/
stream.rs

1//! Streaming encode/verify/reconstruct for large data.
2//!
3//! Provides [`encode_stream`](crate::galois_8::ReedSolomon::encode_stream),
4//! [`verify_stream`](crate::galois_8::ReedSolomon::verify_stream), and
5//! [`reconstruct_stream`](crate::galois_8::ReedSolomon::reconstruct_stream) methods that
6//! process data in configurable blocks, avoiding the need to load entire
7//! files into memory.
8//!
9//! # Example
10//!
11//! ```no_run
12//! use rustfs_erasure_codec::galois_8::ReedSolomon;
13//! use rustfs_erasure_codec::stream::StreamOptions;
14//! use std::fs::File;
15//! use std::io::BufReader;
16//!
17//! let rs = ReedSolomon::new(10, 4).unwrap();
18//!
19//! let mut data_readers: Vec<BufReader<File>> = (0..10)
20//!     .map(|i| BufReader::new(File::open(format!("data_{i}.bin")).unwrap()))
21//!     .collect();
22//! let mut parity_writers: Vec<File> = (0..4)
23//!     .map(|i| File::create(format!("parity_{i}.bin")).unwrap())
24//!     .collect();
25//!
26//! let opts = StreamOptions::new().with_block_size(4 * 1024 * 1024);
27//! rs.encode_stream(&mut data_readers, &mut parity_writers, &opts).unwrap();
28//! ```
29
30use std::io::Error;
31use std::mem;
32
33/// Lower bound for the read/write block size (1 KiB).
34const MIN_BLOCK_SIZE_BYTES: usize = 1024;
35/// Upper bound for the read/write block size (16 MiB).
36const MAX_BLOCK_SIZE_BYTES: usize = 16 * 1024 * 1024;
37
38// ---------------------------------------------------------------------------
39// StreamOptions
40// ---------------------------------------------------------------------------
41
42/// I/O scheduling mode for streaming operations.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum StreamIoMode {
45    /// Choose serial or parallel I/O using conservative built-in thresholds.
46    Auto,
47    /// Read and write shard streams serially.
48    Serial,
49    /// Read and write shard streams with rayon parallel iterators.
50    Parallel,
51}
52
53/// Configuration for streaming encode/verify/reconstruct operations.
54#[derive(Debug, Clone)]
55pub struct StreamOptions {
56    /// Block size in bytes for each read/write cycle. Default: 4 MiB.
57    ///
58    /// Larger blocks reduce syscall overhead but increase memory usage.
59    /// Recommended range: 256 KiB – 16 MiB.
60    pub block_size: usize,
61    /// I/O scheduling mode for stream reads and writes. Default: Auto.
62    pub io_mode: StreamIoMode,
63}
64
65impl Default for StreamOptions {
66    fn default() -> Self {
67        Self {
68            block_size: 4 * 1024 * 1024,
69            io_mode: StreamIoMode::Auto,
70        }
71    }
72}
73
74impl StreamOptions {
75    /// Create a new `StreamOptions` with default settings.
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    /// Set the block size (clamped to `[1 KiB, 16 MiB]`).
81    pub fn with_block_size(mut self, size: usize) -> Self {
82        self.block_size = size.clamp(MIN_BLOCK_SIZE_BYTES, MAX_BLOCK_SIZE_BYTES);
83        self
84    }
85
86    /// Set the stream I/O scheduling mode.
87    pub fn with_io_mode(mut self, mode: StreamIoMode) -> Self {
88        self.io_mode = mode;
89        self
90    }
91}
92
93// ---------------------------------------------------------------------------
94// StreamError
95// ---------------------------------------------------------------------------
96
97/// Error returned by streaming operations.
98#[derive(Debug)]
99pub struct StreamError {
100    /// Index of the shard that caused the error.
101    pub shard_index: usize,
102    /// The kind of error.
103    pub kind: StreamErrorKind,
104}
105
106/// Classification of [`StreamError`].
107#[derive(Debug)]
108pub enum StreamErrorKind {
109    /// I/O error while reading a shard.
110    Read(std::io::Error),
111    /// I/O error while writing a shard.
112    Write(std::io::Error),
113    /// Error from the underlying codec (encode/verify/reconstruct).
114    Codec(crate::Error),
115}
116
117impl core::fmt::Display for StreamError {
118    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
119        match &self.kind {
120            StreamErrorKind::Read(e) => {
121                write!(f, "read error on shard {}: {}", self.shard_index, e)
122            }
123            StreamErrorKind::Write(e) => {
124                write!(f, "write error on shard {}: {}", self.shard_index, e)
125            }
126            StreamErrorKind::Codec(e) => {
127                write!(f, "codec error on shard {}: {}", self.shard_index, e)
128            }
129        }
130    }
131}
132
133impl std::error::Error for StreamError {
134    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
135        match &self.kind {
136            StreamErrorKind::Read(e) => Some(e),
137            StreamErrorKind::Write(e) => Some(e),
138            StreamErrorKind::Codec(e) => Some(e),
139        }
140    }
141}
142
143impl StreamError {
144    fn read(shard_index: usize, e: std::io::Error) -> Self {
145        Self {
146            shard_index,
147            kind: StreamErrorKind::Read(e),
148        }
149    }
150
151    fn write(shard_index: usize, e: std::io::Error) -> Self {
152        Self {
153            shard_index,
154            kind: StreamErrorKind::Write(e),
155        }
156    }
157
158    fn codec(shard_index: usize, e: crate::Error) -> Self {
159        Self {
160            shard_index,
161            kind: StreamErrorKind::Codec(e),
162        }
163    }
164}
165
166fn take_stream_error(
167    first_error: &std::sync::Mutex<Option<StreamError>>,
168    fallback: impl FnOnce() -> StreamError,
169) -> StreamError {
170    let mut guard = match first_error.lock() {
171        Ok(guard) => guard,
172        Err(poisoned) => poisoned.into_inner(),
173    };
174    guard.take().unwrap_or_else(fallback)
175}
176
177/// Record a parallel-write error, keeping the one with the smallest
178/// `shard_index`. When multiple writers fail at once, the reported error no
179/// longer depends on thread scheduling but deterministically points at the
180/// lowest shard index, aiding diagnostics and reproduction.
181fn record_min_error(first_error: &std::sync::Mutex<Option<StreamError>>, cand: StreamError) {
182    if let Ok(mut guard) = first_error.lock() {
183        let replace = match guard.as_ref() {
184            Some(existing) => cand.shard_index < existing.shard_index,
185            None => true,
186        };
187        if replace {
188            *guard = Some(cand);
189        }
190    }
191}
192
193// ---------------------------------------------------------------------------
194// Helpers
195// ---------------------------------------------------------------------------
196
197fn use_parallel_stream_io(options: &StreamOptions, stream_count: usize) -> bool {
198    match options.io_mode {
199        StreamIoMode::Serial => false,
200        StreamIoMode::Parallel => true,
201        StreamIoMode::Auto => use_parallel_stream_io_auto(options.block_size, stream_count),
202    }
203}
204
205fn use_parallel_stream_io_auto(block_size: usize, stream_count: usize) -> bool {
206    // Conservative single threshold: enable parallel I/O only when there are at
207    // least 10 streams and blocks are at least 4 MiB. The two earlier
208    // early-return guards (stream_count<2||block<256KiB and
209    // stream_count<=6&&block<=1MiB) were strictly dominated by this predicate,
210    // dead code that never changed the result, so they are removed to avoid
211    // implying a tiered policy.
212    block_size >= 4 * 1024 * 1024 && stream_count >= 10
213}
214
215fn read_block_with_mode<R: std::io::Read + Send>(
216    readers: &mut [R],
217    buffers: &mut [Vec<u8>],
218    max_len: usize,
219    use_parallel_io: bool,
220    read_lengths: &mut Vec<(usize, usize)>,
221) -> Result<(bool, usize), StreamError> {
222    if use_parallel_io {
223        read_block_par(readers, buffers, max_len)
224    } else {
225        read_block(readers, buffers, max_len, read_lengths)
226    }
227}
228
229fn write_block_with_mode<W: std::io::Write + Send>(
230    writers: &mut [W],
231    buffers: &[Vec<u8>],
232    len: usize,
233    shard_offset: usize,
234    use_parallel_io: bool,
235) -> Result<(), StreamError> {
236    if use_parallel_io {
237        write_block_par(writers, buffers, len, shard_offset)
238    } else {
239        write_block(writers, buffers, len, shard_offset)
240    }
241}
242
243/// Read up to `max_len` bytes from each reader into the corresponding
244/// buffer, retrying on `Interrupted`.  Returns `Ok((all_eof, actual_len))`
245/// where `all_eof` is `true` if every reader was already at EOF, and
246/// `actual_len` is the number of bytes read (same across all readers,
247/// with short reads zero-padded).
248fn read_block<R: std::io::Read>(
249    readers: &mut [R],
250    buffers: &mut [Vec<u8>],
251    max_len: usize,
252    read_lengths: &mut Vec<(usize, usize)>,
253) -> Result<(bool, usize), StreamError> {
254    read_lengths.clear();
255
256    for (i, (reader, buf)) in readers.iter_mut().zip(buffers.iter_mut()).enumerate() {
257        let total = read_one_stream(reader, buf, max_len).map_err(|e| StreamError::read(i, e))?;
258        read_lengths.push((i, total));
259    }
260
261    let actual_len = read_lengths
262        .iter()
263        .map(|(_, total)| *total)
264        .max()
265        .unwrap_or(0);
266    zero_pad_short_buffers(buffers, read_lengths, actual_len);
267
268    Ok((actual_len == 0, actual_len))
269}
270
271fn prepare_read_buffer(buf: &mut Vec<u8>, max_len: usize) {
272    match buf.len().cmp(&max_len) {
273        core::cmp::Ordering::Less => buf.resize(max_len, 0),
274        core::cmp::Ordering::Greater => buf.truncate(max_len),
275        core::cmp::Ordering::Equal => {}
276    }
277}
278
279fn read_one_stream<R: std::io::Read>(
280    reader: &mut R,
281    buf: &mut Vec<u8>,
282    max_len: usize,
283) -> Result<usize, std::io::Error> {
284    prepare_read_buffer(buf, max_len);
285    let mut total = 0;
286
287    while total < max_len {
288        match reader.read(&mut buf[total..]) {
289            Ok(0) => break,
290            Ok(n) => total += n,
291            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
292            Err(e) => return Err(e),
293        }
294    }
295
296    Ok(total)
297}
298
299fn zero_pad_short_buffers(
300    buffers: &mut [Vec<u8>],
301    read_lengths: &[(usize, usize)],
302    actual_len: usize,
303) {
304    for &(i, total) in read_lengths {
305        if total < actual_len {
306            buffers[i][total..actual_len].fill(0);
307        }
308    }
309}
310
311fn read_present_cursors_with_mode(
312    shards: &mut [std::io::Cursor<Vec<u8>>],
313    buffers: &mut [Vec<u8>],
314    present: &[bool],
315    present_indices: &[usize],
316    max_len: usize,
317    use_parallel_io: bool,
318    read_lengths: &mut Vec<(usize, usize)>,
319) -> Result<(bool, usize), StreamError> {
320    read_lengths.clear();
321
322    if use_parallel_io {
323        use rayon::prelude::*;
324
325        *read_lengths = shards
326            .par_iter_mut()
327            .zip(buffers.par_iter_mut())
328            .enumerate()
329            .filter_map(|(i, item)| present[i].then_some((i, item)))
330            .map(|(i, (shard, buf))| {
331                let total =
332                    read_one_stream(shard, buf, max_len).map_err(|e| StreamError::read(i, e))?;
333                buf.truncate(total);
334                Ok::<_, StreamError>((i, total))
335            })
336            .collect::<Result<Vec<_>, _>>()?;
337    } else {
338        for &i in present_indices {
339            let total = read_one_stream(&mut shards[i], &mut buffers[i], max_len)
340                .map_err(|e| StreamError::read(i, e))?;
341            buffers[i].truncate(total);
342            read_lengths.push((i, total));
343        }
344    }
345
346    let actual_len = read_lengths
347        .iter()
348        .map(|(_, total)| *total)
349        .max()
350        .unwrap_or(0);
351    if actual_len != 0 {
352        // Within a block, every present shard must read the same number of
353        // bytes. A present shard that hits EOF early (fewer bytes than the
354        // others) is truncated or length-mismatched and must not be silently
355        // zero-padded into reconstruction, which would produce wrong recovered
356        // data while returning Ok. This matches the in-memory `reconstruct`'s
357        // `IncorrectShardSize` check.
358        if let Some(&(bad, _)) = read_lengths.iter().find(|(_, total)| *total != actual_len) {
359            return Err(StreamError::codec(bad, crate::Error::IncorrectShardSize));
360        }
361    }
362
363    Ok((actual_len == 0, actual_len))
364}
365
366/// Write `len` bytes from each buffer to the corresponding writer.
367fn write_block<W: std::io::Write>(
368    writers: &mut [W],
369    buffers: &[Vec<u8>],
370    len: usize,
371    shard_offset: usize,
372) -> Result<(), StreamError> {
373    for (i, (writer, buf)) in writers.iter_mut().zip(buffers.iter()).enumerate() {
374        let mut written = 0;
375        while written < len {
376            match writer.write(&buf[written..len]) {
377                Ok(0) => {
378                    return Err(StreamError::write(
379                        shard_offset + i,
380                        std::io::Error::new(std::io::ErrorKind::WriteZero, "write returned 0"),
381                    ));
382                }
383                Ok(n) => written += n,
384                Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
385                Err(e) => return Err(StreamError::write(shard_offset + i, e)),
386            }
387        }
388    }
389    Ok(())
390}
391
392// ---------------------------------------------------------------------------
393// Parallel I/O helpers (rayon)
394// ---------------------------------------------------------------------------
395
396/// Parallel version of `read_block` — reads all shards concurrently.
397fn read_block_par<R: std::io::Read + Send>(
398    readers: &mut [R],
399    buffers: &mut [Vec<u8>],
400    max_len: usize,
401) -> Result<(bool, usize), StreamError> {
402    use rayon::prelude::*;
403
404    let read_lengths: Vec<(usize, usize)> = readers
405        .par_iter_mut()
406        .zip(buffers.par_iter_mut())
407        .enumerate()
408        .map(|(i, (reader, buf))| {
409            read_one_stream(reader, buf, max_len)
410                .map(|total| (i, total))
411                .map_err(|e| StreamError::read(i, e))
412        })
413        .collect::<Result<Vec<_>, _>>()?;
414
415    let actual_len = read_lengths
416        .iter()
417        .map(|(_, total)| *total)
418        .max()
419        .unwrap_or(0);
420    zero_pad_short_buffers(buffers, &read_lengths, actual_len);
421
422    Ok((actual_len == 0, actual_len))
423}
424
425/// Parallel version of `write_block` — writes all shards concurrently.
426fn write_block_par<W: std::io::Write + Send>(
427    writers: &mut [W],
428    buffers: &[Vec<u8>],
429    len: usize,
430    shard_offset: usize,
431) -> Result<(), StreamError> {
432    use rayon::prelude::*;
433
434    let first_error: std::sync::Mutex<Option<StreamError>> = std::sync::Mutex::new(None);
435
436    writers
437        .par_iter_mut()
438        .zip(buffers.par_iter())
439        .enumerate()
440        .try_for_each(|(i, (writer, buf))| {
441            let mut written = 0;
442            while written < len {
443                match writer.write(&buf[written..len]) {
444                    Ok(0) => {
445                        record_min_error(
446                            &first_error,
447                            StreamError::write(
448                                shard_offset + i,
449                                std::io::Error::new(
450                                    std::io::ErrorKind::WriteZero,
451                                    "write returned 0",
452                                ),
453                            ),
454                        );
455                        return Err(());
456                    }
457                    Ok(n) => written += n,
458                    Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
459                    Err(e) => {
460                        record_min_error(&first_error, StreamError::write(shard_offset + i, e));
461                        return Err(());
462                    }
463                }
464            }
465            Ok(())
466        })
467        .map_err(|()| {
468            // The fallback must also be a write error (not a fabricated read(0))
469            // and point at this batch's starting shard index.
470            take_stream_error(&first_error, || {
471                StreamError::write(
472                    shard_offset,
473                    Error::other("parallel stream writer error was not reported"),
474                )
475            })
476        })
477}
478
479// ---------------------------------------------------------------------------
480// ReedSolomon streaming methods
481// ---------------------------------------------------------------------------
482
483impl super::ReedSolomon<crate::galois_8::Field> {
484    /// Stream-encode data from readers into parity writers.
485    ///
486    /// Reads data shards in blocks of `options.block_size` bytes, encodes
487    /// each block, and writes the resulting parity blocks.  This avoids
488    /// loading the entire dataset into memory.
489    ///
490    /// All data streams should supply the same number of bytes. When they do
491    /// not, shorter streams are zero-padded to the longest stream's length for
492    /// each block, and no original-length metadata is persisted. The generated
493    /// parity therefore only round-trips within this streaming API; interop
494    /// with the in-memory API (which rejects unequal lengths with
495    /// [`crate::Error::IncorrectShardSize`]) or with other Reed-Solomon
496    /// implementations requires the caller to track the original shard lengths
497    /// out of band.
498    ///
499    /// # Errors
500    ///
501    /// Returns [`StreamError`] on I/O failure or codec error.
502    ///
503    /// # Limitations
504    ///
505    /// Only the classic Reed-Solomon family is supported. Leopard-family codecs
506    /// return [`crate::Error::UnsupportedCodecFamily`] (wrapped in a
507    /// [`StreamError`]), because their FFT engines require whole-shard,
508    /// 64-byte-aligned processing that the block-wise streaming path cannot
509    /// satisfy.
510    pub fn encode_stream(
511        &self,
512        data: &mut [impl std::io::Read + Send],
513        parity: &mut [impl std::io::Write + Send],
514        options: &StreamOptions,
515    ) -> Result<(), StreamError> {
516        // block_size is a public field and may bypass with_block_size's clamp
517        // (e.g. a struct literal). Clamp it to the valid range here: 0 would be
518        // raised to the lower bound (otherwise reads hit EOF immediately and
519        // silently produce empty output), and a huge value is capped to the
520        // upper bound (otherwise Vec::with_capacity may OOM).
521        let block_size = options
522            .block_size
523            .clamp(MIN_BLOCK_SIZE_BYTES, MAX_BLOCK_SIZE_BYTES);
524        // Leopard families (GF8/GF16, both built on ReedSolomon<galois_8::Field>)
525        // dispatch to FFT codecs that require whole-shard, 64-byte-aligned
526        // processing: a non-64-multiple final block fails with IncorrectShardSize,
527        // so streaming is effectively unusable. Reject them explicitly, matching
528        // the documented limitation.
529        if self.is_leopard_gf8_family() || self.is_leopard_gf16_family() {
530            return Err(StreamError::codec(0, crate::Error::UnsupportedCodecFamily));
531        }
532
533        let data_count = self.data_shard_count;
534        let parity_count = self.parity_shard_count;
535        let use_parallel_read = use_parallel_stream_io(options, data_count);
536        let use_parallel_write = use_parallel_stream_io(options, parity_count);
537
538        // Validate stream counts at runtime (debug_assert is removed in release
539        // builds, which would otherwise lead to slice out-of-bounds panics or
540        // silent partial encoding). Consistent with the crate's
541        // check_piece_count! used on non-streaming paths.
542        if data.len() != data_count {
543            return Err(StreamError::codec(0, crate::Error::TooFewShards));
544        }
545        if parity.len() != parity_count {
546            return Err(StreamError::codec(0, crate::Error::TooFewShards));
547        }
548
549        let mut data_bufs: Vec<Vec<u8>> = (0..data_count)
550            .map(|_| Vec::with_capacity(block_size))
551            .collect();
552        let mut parity_bufs: Vec<Vec<u8>> = (0..parity_count)
553            .map(|_| Vec::with_capacity(block_size))
554            .collect();
555        let mut read_lengths = Vec::with_capacity(data_count);
556
557        loop {
558            let (all_eof, actual_len) = read_block_with_mode(
559                data,
560                &mut data_bufs,
561                block_size,
562                use_parallel_read,
563                &mut read_lengths,
564            )?;
565            if all_eof {
566                break;
567            }
568
569            // Resize parity buffers to match actual length.
570            for buf in parity_bufs.iter_mut() {
571                buf.resize(actual_len, 0);
572            }
573
574            // Encode (parallel codec).
575            let data_refs: Vec<&[u8]> = data_bufs.iter().map(|b| &b[..actual_len]).collect();
576            let mut parity_refs: Vec<&mut [u8]> = parity_bufs
577                .iter_mut()
578                .map(|b| &mut b[..actual_len])
579                .collect();
580
581            self.encode_sep_par(&data_refs, &mut parity_refs)
582                .map_err(|e| StreamError::codec(0, e))?;
583
584            // Write parity using the selected stream I/O mode.
585            write_block_with_mode(
586                parity,
587                &parity_bufs,
588                actual_len,
589                data_count,
590                use_parallel_write,
591            )?;
592        }
593
594        Ok(())
595    }
596
597    /// Stream-verify data + parity from readers.
598    ///
599    /// Reads all shards in blocks, verifying each block independently.
600    /// Returns `Ok(true)` if every block is valid, `Ok(false)` if any block
601    /// fails verification.
602    ///
603    /// # Limitations
604    ///
605    /// Only the classic Reed-Solomon family is supported; Leopard-family codecs
606    /// return [`crate::Error::UnsupportedCodecFamily`].
607    pub fn verify_stream(
608        &self,
609        shards: &mut [impl std::io::Read + Send],
610        options: &StreamOptions,
611    ) -> Result<bool, StreamError> {
612        // block_size is a public field and may bypass with_block_size's clamp
613        // (e.g. a struct literal). Clamp it to the valid range here: 0 would be
614        // raised to the lower bound (otherwise reads hit EOF immediately and
615        // silently produce empty output), and a huge value is capped to the
616        // upper bound (otherwise Vec::with_capacity may OOM).
617        let block_size = options
618            .block_size
619            .clamp(MIN_BLOCK_SIZE_BYTES, MAX_BLOCK_SIZE_BYTES);
620        // Leopard families are unsupported for streaming (see encode_stream).
621        if self.is_leopard_gf8_family() || self.is_leopard_gf16_family() {
622            return Err(StreamError::codec(0, crate::Error::UnsupportedCodecFamily));
623        }
624
625        let total = self.total_shard_count;
626        let use_parallel_read = use_parallel_stream_io(options, total);
627
628        // Validate the shard count at runtime (debug_assert is removed in
629        // release builds).
630        if shards.len() != total {
631            return Err(StreamError::codec(0, crate::Error::TooFewShards));
632        }
633
634        let mut bufs: Vec<Vec<u8>> = (0..total).map(|_| Vec::with_capacity(block_size)).collect();
635        let mut read_lengths = Vec::with_capacity(total);
636
637        loop {
638            let (all_eof, actual_len) = read_block_with_mode(
639                shards,
640                &mut bufs,
641                block_size,
642                use_parallel_read,
643                &mut read_lengths,
644            )?;
645            if all_eof {
646                break;
647            }
648
649            let refs: Vec<&[u8]> = bufs.iter().map(|b| &b[..actual_len]).collect();
650
651            let valid = self
652                .verify_par(&refs)
653                .map_err(|e| StreamError::codec(0, e))?;
654            if !valid {
655                return Ok(false);
656            }
657        }
658
659        Ok(true)
660    }
661
662    /// Stream-reconstruct missing shards.
663    ///
664    /// `shards` has one entry per total shard.  Present shards contain their
665    /// data in a `Cursor<Vec<u8>>`; missing shards use an empty cursor
666    /// (`Cursor::new(Vec::new())`).  Recovered data is written into the
667    /// missing shards' cursors.
668    ///
669    /// Present cursors are read from the beginning: their position is reset to
670    /// `0` before reading, so it is safe to pass cursors that were just written
671    /// to (whose position sits at the end).
672    ///
673    /// The function reads blocks from present shards, reconstructs missing
674    /// blocks, and writes recovered data into the missing cursors.  The set
675    /// of missing shard indices must be consistent across all blocks.
676    ///
677    /// # Limitations
678    ///
679    /// Only the classic Reed-Solomon family is supported; Leopard-family codecs
680    /// return [`crate::Error::UnsupportedCodecFamily`] rather than silently
681    /// producing incorrect results.
682    pub fn reconstruct_stream(
683        &self,
684        shards: &mut [std::io::Cursor<Vec<u8>>],
685        options: &StreamOptions,
686    ) -> Result<(), StreamError> {
687        // block_size is a public field and may bypass with_block_size's clamp
688        // (e.g. a struct literal). Clamp it to the valid range here: 0 would be
689        // raised to the lower bound (otherwise reads hit EOF immediately and
690        // silently produce empty output), and a huge value is capped to the
691        // upper bound (otherwise Vec::with_capacity may OOM).
692        let block_size = options
693            .block_size
694            .clamp(MIN_BLOCK_SIZE_BYTES, MAX_BLOCK_SIZE_BYTES);
695        let total = self.total_shard_count;
696
697        // Validate the shard count at runtime (debug_assert is removed in
698        // release builds).
699        if shards.len() != total {
700            return Err(StreamError::codec(0, crate::Error::TooFewShards));
701        }
702
703        // Leopard families are unsupported for streaming (see encode_stream).
704        if self.is_leopard_gf8_family() || self.is_leopard_gf16_family() {
705            return Err(StreamError::codec(0, crate::Error::UnsupportedCodecFamily));
706        }
707
708        // Determine which shards are present (non-empty cursor).
709        let mut present = vec![false; total];
710        for (i, shard) in shards.iter().enumerate() {
711            present[i] = !shard.get_ref().is_empty();
712        }
713
714        let missing_count = present.iter().filter(|&&p| !p).count();
715        let present_count = total - missing_count;
716        // An empty dataset (no present shards) has nothing to recover; return Ok,
717        // consistent with encode_stream returning Ok for empty input (rather than
718        // TooFewShardsPresent).
719        if present_count == 0 {
720            return Ok(());
721        }
722        if missing_count > self.parity_shard_count {
723            return Err(StreamError::codec(0, crate::Error::TooFewShardsPresent));
724        }
725        let use_parallel_read = use_parallel_stream_io(options, present_count);
726        let present_indices: Vec<usize> = present
727            .iter()
728            .enumerate()
729            .filter_map(|(i, is_present)| is_present.then_some(i))
730            .collect();
731        let missing_indices: Vec<usize> = present
732            .iter()
733            .enumerate()
734            .filter_map(|(i, is_present)| (!is_present).then_some(i))
735            .collect();
736
737        // Presence is decided from whether the underlying Vec is empty, but
738        // reads go through the Cursor's Read impl (from the current position).
739        // A present cursor whose position is not 0 (e.g. just written to, with
740        // the position at the end) would be misread as empty, causing silent
741        // no-recovery or wrong recovery. Reset every present cursor's position
742        // to 0 before reading.
743        for &idx in &present_indices {
744            shards[idx].set_position(0);
745        }
746
747        // Strategy: read present shards into buffers per block, reconstruct,
748        // then write recovered data into missing shards' cursors.
749
750        // Pre-allocate read buffers and the reconstruct container once. Present
751        // shard buffers are moved into the reconstruct call and then moved back
752        // after each block so their allocations survive across iterations.
753        let mut bufs: Vec<Vec<u8>> = (0..total)
754            .map(|i| {
755                if present[i] {
756                    Vec::with_capacity(block_size)
757                } else {
758                    Vec::new()
759                }
760            })
761            .collect();
762        let mut reconstruct_bufs: Vec<Option<Vec<u8>>> = (0..total).map(|_| None).collect();
763        let mut read_lengths = Vec::with_capacity(total);
764
765        loop {
766            let (all_eof, actual_len) = read_present_cursors_with_mode(
767                shards,
768                &mut bufs,
769                &present,
770                &present_indices,
771                block_size,
772                use_parallel_read,
773                &mut read_lengths,
774            )?;
775            if all_eof {
776                break;
777            }
778
779            // Zero-pad present shards to actual_len and reuse the outer
780            // Option<Vec<_>> container for reconstructing this block.
781            for &idx in &present_indices {
782                bufs[idx].resize(actual_len, 0);
783                reconstruct_bufs[idx] = Some(mem::take(&mut bufs[idx]));
784            }
785            for &idx in &missing_indices {
786                reconstruct_bufs[idx] = None;
787            }
788
789            self.reconstruct(&mut reconstruct_bufs)
790                .map_err(|e| StreamError::codec(0, e))?;
791
792            // Write recovered data into missing shards' cursors.
793            // (reconstruct fills in all missing shards — data and parity)
794            for &idx in &missing_indices {
795                let recovered = reconstruct_bufs[idx]
796                    .as_ref()
797                    .expect("missing shard buffer");
798                shards[idx]
799                    .get_mut()
800                    .extend_from_slice(&recovered[..actual_len]);
801            }
802
803            for idx in 0..total {
804                if let Some(buf) = reconstruct_bufs[idx].take() {
805                    bufs[idx] = buf;
806                }
807            }
808        }
809
810        Ok(())
811    }
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817    use crate::galois_8::ReedSolomon;
818
819    fn make_codec(data: usize, parity: usize) -> ReedSolomon {
820        ReedSolomon::new(data, parity).unwrap()
821    }
822
823    fn random_data(len: usize) -> Vec<u8> {
824        // Simple deterministic fill for reproducibility.
825        (0..len)
826            .map(|i| (i.wrapping_mul(73).wrapping_add(17)) as u8)
827            .collect()
828    }
829
830    #[test]
831    fn test_encode_stream_basic() {
832        let rs = make_codec(3, 2);
833        let shard_size = 4096;
834
835        let d0 = random_data(shard_size);
836        let d1 = random_data(shard_size);
837        let d2 = random_data(shard_size);
838
839        let mut readers: Vec<&[u8]> = vec![d0.as_slice(), d1.as_slice(), d2.as_slice()];
840        let mut writers: Vec<Vec<u8>> = vec![Vec::new(), Vec::new()];
841
842        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
843            .unwrap();
844
845        assert_eq!(writers[0].len(), shard_size);
846        assert_eq!(writers[1].len(), shard_size);
847
848        // Verify parity is correct by verifying the full shard set.
849        let all: Vec<&[u8]> = vec![
850            d0.as_slice(),
851            d1.as_slice(),
852            d2.as_slice(),
853            writers[0].as_slice(),
854            writers[1].as_slice(),
855        ];
856        assert!(rs.verify(&all).unwrap());
857    }
858
859    #[test]
860    fn test_encode_stream_multi_block() {
861        let rs = make_codec(3, 2);
862        let total_size = 10 * 1024; // 10 KiB
863        let block_size = 4096;
864
865        let d0 = random_data(total_size);
866        let d1 = random_data(total_size);
867        let d2 = random_data(total_size);
868
869        let mut readers: Vec<&[u8]> = vec![d0.as_slice(), d1.as_slice(), d2.as_slice()];
870        let mut writers: Vec<Vec<u8>> = vec![Vec::new(), Vec::new()];
871
872        let opts = StreamOptions::new().with_block_size(block_size);
873        rs.encode_stream(&mut readers, &mut writers, &opts).unwrap();
874
875        assert_eq!(writers[0].len(), total_size);
876        assert_eq!(writers[1].len(), total_size);
877    }
878
879    #[test]
880    fn test_encode_stream_empty() {
881        let rs = make_codec(3, 2);
882        let empty: Vec<&[u8]> = vec![&[], &[], &[]];
883        let mut readers = empty.clone();
884        let mut writers: Vec<Vec<u8>> = vec![Vec::new(), Vec::new()];
885
886        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
887            .unwrap();
888
889        // Empty input → empty output.
890        assert!(writers[0].is_empty());
891        assert!(writers[1].is_empty());
892    }
893
894    #[test]
895    fn test_encode_stream_unequal_lengths() {
896        let rs = make_codec(3, 2);
897
898        let d0 = random_data(1000);
899        let d1 = random_data(500);
900        let d2 = random_data(800);
901
902        let mut readers: Vec<&[u8]> = vec![d0.as_slice(), d1.as_slice(), d2.as_slice()];
903        let mut writers: Vec<Vec<u8>> = vec![Vec::new(), Vec::new()];
904
905        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
906            .unwrap();
907
908        // All outputs should be 1000 bytes (max of input lengths, since we
909        // zero-pad short shards).
910        assert_eq!(writers[0].len(), 1000);
911        assert_eq!(writers[1].len(), 1000);
912    }
913
914    #[test]
915    fn test_encode_stream_10x4() {
916        let rs = make_codec(10, 4);
917        let shard_size = 64 * 1024;
918
919        let data: Vec<Vec<u8>> = (0..10).map(|_| random_data(shard_size)).collect();
920        let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
921        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 4];
922
923        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
924            .unwrap();
925
926        // Verify.
927        let mut all: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
928        for w in &writers {
929            all.push(w.as_slice());
930        }
931        assert!(rs.verify(&all).unwrap());
932    }
933
934    #[test]
935    fn test_verify_stream_valid() {
936        let rs = make_codec(3, 2);
937        let shard_size = 4096;
938
939        let d = vec![random_data(shard_size); 3];
940        let mut readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
941        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
942        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
943            .unwrap();
944
945        // Build full shard set for verification.
946        let mut all_data: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
947        for w in &writers {
948            all_data.push(w.as_slice());
949        }
950        let mut all_readers: Vec<&[u8]> = all_data;
951
952        assert!(
953            rs.verify_stream(&mut all_readers, &StreamOptions::default())
954                .unwrap()
955        );
956    }
957
958    #[test]
959    fn test_verify_stream_corrupted() {
960        let rs = make_codec(3, 2);
961        let shard_size = 4096;
962
963        let d = vec![random_data(shard_size); 3];
964        let mut readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
965        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
966        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
967            .unwrap();
968
969        // Corrupt a parity shard.
970        writers[0][0] ^= 0xFF;
971
972        let mut all_data: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
973        for w in &writers {
974            all_data.push(w.as_slice());
975        }
976        let mut all_readers: Vec<&[u8]> = all_data;
977
978        assert!(
979            !rs.verify_stream(&mut all_readers, &StreamOptions::default())
980                .unwrap()
981        );
982    }
983
984    #[test]
985    fn test_reconstruct_stream_single_missing() {
986        let rs = make_codec(3, 2);
987        let shard_size = 4096;
988
989        let d: Vec<Vec<u8>> = (0..3).map(|_| random_data(shard_size)).collect();
990        let mut readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
991        let mut parity_writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
992        rs.encode_stream(&mut readers, &mut parity_writers, &StreamOptions::default())
993            .unwrap();
994
995        // data[0] is missing — use empty Cursor.
996        let mut shards: Vec<std::io::Cursor<Vec<u8>>> = vec![
997            std::io::Cursor::new(Vec::new()), // missing
998            std::io::Cursor::new(d[1].clone()),
999            std::io::Cursor::new(d[2].clone()),
1000            std::io::Cursor::new(parity_writers[0].clone()),
1001            std::io::Cursor::new(parity_writers[1].clone()),
1002        ];
1003
1004        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
1005            .unwrap();
1006
1007        // Shard 0 should have been recovered into the cursor's inner Vec.
1008        assert_eq!(shards[0].get_ref(), &d[0]);
1009    }
1010
1011    #[test]
1012    fn test_reconstruct_non_streaming() {
1013        // Verify basic encode + reconstruct works without streaming.
1014        let rs = make_codec(3, 2);
1015        let shard_size = 4096;
1016
1017        let d: Vec<Vec<u8>> = (0..3).map(|_| random_data(shard_size)).collect();
1018        let data_refs: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
1019        let mut p0 = vec![0u8; shard_size];
1020        let mut p1 = vec![0u8; shard_size];
1021        let mut parity_refs: Vec<&mut [u8]> = vec![&mut p0, &mut p1];
1022        rs.encode_sep(&data_refs, &mut parity_refs).unwrap();
1023
1024        // Now reconstruct with shard 0 missing.
1025        let mut shards: Vec<Option<Vec<u8>>> = vec![
1026            None,
1027            Some(d[1].clone()),
1028            Some(d[2].clone()),
1029            Some(p0.clone()),
1030            Some(p1.clone()),
1031        ];
1032        rs.reconstruct(&mut shards).unwrap();
1033
1034        assert_eq!(shards[0].as_ref().unwrap(), &d[0]);
1035    }
1036
1037    #[test]
1038    fn test_reconstruct_stream_basic() {
1039        let rs = make_codec(3, 2);
1040        let shard_size = 64;
1041
1042        let d: Vec<Vec<u8>> = (0..3).map(|_| random_data(shard_size)).collect();
1043        let mut readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
1044        let mut parity_writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1045        rs.encode_stream(&mut readers, &mut parity_writers, &StreamOptions::default())
1046            .unwrap();
1047
1048        // Verify parity is correct.
1049        let all: Vec<&[u8]> = vec![
1050            d[0].as_slice(),
1051            d[1].as_slice(),
1052            d[2].as_slice(),
1053            parity_writers[0].as_slice(),
1054            parity_writers[1].as_slice(),
1055        ];
1056        assert!(rs.verify(&all).unwrap());
1057
1058        // Missing shard 0 — empty Cursor; present shards have data.
1059        let mut shards: Vec<std::io::Cursor<Vec<u8>>> = vec![
1060            std::io::Cursor::new(Vec::new()), // missing
1061            std::io::Cursor::new(d[1].clone()),
1062            std::io::Cursor::new(d[2].clone()),
1063            std::io::Cursor::new(parity_writers[0].clone()),
1064            std::io::Cursor::new(parity_writers[1].clone()),
1065        ];
1066
1067        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
1068            .unwrap();
1069
1070        // Shard 0 recovered into the empty cursor's inner Vec.
1071        let recovered = shards[0].get_ref();
1072        assert_eq!(
1073            recovered.len(),
1074            d[0].len(),
1075            "recovered len {} != expected len {}",
1076            recovered.len(),
1077            d[0].len()
1078        );
1079        assert_eq!(
1080            recovered,
1081            &d[0],
1082            "recovered: {:?}, expected: {:?}",
1083            &recovered[..8],
1084            &d[0][..8]
1085        );
1086    }
1087
1088    #[test]
1089    fn test_stream_options_builder() {
1090        let opts = StreamOptions::new().with_block_size(1024 * 1024);
1091        assert_eq!(opts.block_size, 1024 * 1024);
1092        assert_eq!(opts.io_mode, StreamIoMode::Auto);
1093
1094        // Minimum is 1 KiB.
1095        let opts = StreamOptions::new().with_block_size(100);
1096        assert_eq!(opts.block_size, 1024);
1097
1098        let opts = StreamOptions::new().with_io_mode(StreamIoMode::Serial);
1099        assert_eq!(opts.io_mode, StreamIoMode::Serial);
1100    }
1101
1102    #[test]
1103    fn test_stream_io_auto_decision_thresholds() {
1104        let opts = StreamOptions::new()
1105            .with_block_size(64 * 1024)
1106            .with_io_mode(StreamIoMode::Auto);
1107        assert!(!use_parallel_stream_io(&opts, 14));
1108
1109        let opts = StreamOptions::new()
1110            .with_block_size(1024 * 1024)
1111            .with_io_mode(StreamIoMode::Auto);
1112        assert!(!use_parallel_stream_io(&opts, 6));
1113
1114        let opts = StreamOptions::new()
1115            .with_block_size(4 * 1024 * 1024)
1116            .with_io_mode(StreamIoMode::Auto);
1117        assert!(use_parallel_stream_io(&opts, 10));
1118
1119        let opts = StreamOptions::new()
1120            .with_block_size(64 * 1024)
1121            .with_io_mode(StreamIoMode::Parallel);
1122        assert!(use_parallel_stream_io(&opts, 1));
1123    }
1124
1125    #[test]
1126    fn test_stream_error_display() {
1127        let e = StreamError::read(
1128            3,
1129            std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "eof"),
1130        );
1131        let s = format!("{e}");
1132        assert!(s.contains("shard 3"));
1133        assert!(s.contains("eof"));
1134
1135        let e = StreamError::codec(0, crate::Error::TooFewShardsPresent);
1136        let s = format!("{e}");
1137        assert!(s.contains("codec"));
1138    }
1139
1140    // -----------------------------------------------------------------------
1141    // Concurrent stream tests (P0-2e-3)
1142    // -----------------------------------------------------------------------
1143
1144    #[test]
1145    fn test_encode_stream_concurrent() {
1146        let rs = make_codec(4, 2);
1147        let shard_size = 8 * 1024;
1148
1149        let data: Vec<Vec<u8>> = (0..4).map(|_| random_data(shard_size)).collect();
1150        let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1151        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1152
1153        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
1154            .unwrap();
1155
1156        // Verify parity correctness.
1157        let mut all: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1158        for w in &writers {
1159            all.push(w.as_slice());
1160        }
1161        assert!(rs.verify(&all).unwrap());
1162    }
1163
1164    #[test]
1165    fn test_verify_stream_concurrent() {
1166        let rs = make_codec(4, 2);
1167        let shard_size = 8 * 1024;
1168
1169        let data: Vec<Vec<u8>> = (0..4).map(|_| random_data(shard_size)).collect();
1170        let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1171        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1172        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
1173            .unwrap();
1174
1175        // Valid case.
1176        let mut all: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1177        for w in &writers {
1178            all.push(w.as_slice());
1179        }
1180        let mut all_readers: Vec<&[u8]> = all;
1181        assert!(
1182            rs.verify_stream(&mut all_readers, &StreamOptions::default())
1183                .unwrap()
1184        );
1185
1186        // Corrupted case.
1187        let mut corrupted = writers[0].clone();
1188        corrupted[0] ^= 0xFF;
1189        let mut all_corrupt: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1190        all_corrupt.push(corrupted.as_slice());
1191        all_corrupt.push(writers[1].as_slice());
1192        assert!(
1193            !rs.verify_stream(&mut all_corrupt, &StreamOptions::default())
1194                .unwrap()
1195        );
1196    }
1197
1198    #[test]
1199    fn test_reconstruct_stream_concurrent() {
1200        let rs = make_codec(4, 2);
1201        let shard_size = 8 * 1024;
1202
1203        let data: Vec<Vec<u8>> = (0..4).map(|_| random_data(shard_size)).collect();
1204        let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1205        let mut parity_writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1206        rs.encode_stream(&mut readers, &mut parity_writers, &StreamOptions::default())
1207            .unwrap();
1208
1209        // Missing shards: data[1] and parity[0].
1210        let mut shards: Vec<std::io::Cursor<Vec<u8>>> = vec![
1211            std::io::Cursor::new(data[0].clone()),
1212            std::io::Cursor::new(Vec::new()), // missing
1213            std::io::Cursor::new(data[2].clone()),
1214            std::io::Cursor::new(data[3].clone()),
1215            std::io::Cursor::new(Vec::new()), // missing
1216            std::io::Cursor::new(parity_writers[1].clone()),
1217        ];
1218
1219        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
1220            .unwrap();
1221
1222        assert_eq!(shards[1].get_ref(), &data[1]);
1223    }
1224
1225    #[test]
1226    fn test_concurrent_stream_large_blocks() {
1227        let rs = make_codec(10, 4);
1228        let total_size = 1024 * 1024; // 1 MiB
1229        let block_size = 256 * 1024; // 256 KiB blocks
1230
1231        let data: Vec<Vec<u8>> = (0..10).map(|_| random_data(total_size)).collect();
1232        let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1233        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 4];
1234
1235        let opts = StreamOptions::new().with_block_size(block_size);
1236        rs.encode_stream(&mut readers, &mut writers, &opts).unwrap();
1237
1238        // Verify all blocks.
1239        let mut all: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1240        for w in &writers {
1241            all.push(w.as_slice());
1242        }
1243        assert!(rs.verify(&all).unwrap());
1244
1245        // Reconstruct with 2 missing data shards.
1246        let mut shards: Vec<std::io::Cursor<Vec<u8>>> = Vec::new();
1247        for d in &data {
1248            shards.push(std::io::Cursor::new(d.clone()));
1249        }
1250        shards[0] = std::io::Cursor::new(Vec::new());
1251        shards[5] = std::io::Cursor::new(Vec::new());
1252        for w in &writers {
1253            shards.push(std::io::Cursor::new(w.clone()));
1254        }
1255
1256        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
1257            .unwrap();
1258
1259        assert_eq!(shards[0].get_ref(), &data[0]);
1260        assert_eq!(shards[5].get_ref(), &data[5]);
1261    }
1262
1263    #[test]
1264    fn test_encode_stream_zero_length() {
1265        let rs = ReedSolomon::new(3, 2).unwrap();
1266        let mut readers: Vec<&[u8]> = vec![b""; 3];
1267        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1268        // Zero-length data should succeed (encode produces empty parity).
1269        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
1270            .unwrap();
1271        assert!(writers.iter().all(|w| w.is_empty()));
1272    }
1273
1274    #[test]
1275    fn test_encode_stream_single_byte_block() {
1276        let rs = ReedSolomon::new(2, 1).unwrap();
1277        let d0 = vec![0xABu8; 4];
1278        let d1 = vec![0xCDu8; 4];
1279        let mut readers: Vec<&[u8]> = vec![d0.as_slice(), d1.as_slice()];
1280        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 1];
1281        let opts = StreamOptions::new().with_block_size(1);
1282        rs.encode_stream(&mut readers, &mut writers, &opts).unwrap();
1283
1284        // Verify with the smallest possible block size.
1285        let mut all: Vec<&[u8]> = vec![d0.as_slice(), d1.as_slice()];
1286        for w in &writers {
1287            all.push(w.as_slice());
1288        }
1289        assert!(rs.verify(&all).unwrap());
1290    }
1291
1292    #[test]
1293    fn test_stream_io_modes_encode_verify_match() {
1294        let rs = ReedSolomon::new(4, 2).unwrap();
1295        let shard_size = 32 * 1024;
1296        let data: Vec<Vec<u8>> = (0..4).map(|_| random_data(shard_size)).collect();
1297
1298        let mut expected_parity = Vec::new();
1299        for mode in [
1300            StreamIoMode::Auto,
1301            StreamIoMode::Serial,
1302            StreamIoMode::Parallel,
1303        ] {
1304            let opts = StreamOptions::new()
1305                .with_block_size(8 * 1024)
1306                .with_io_mode(mode);
1307            let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1308            let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1309
1310            rs.encode_stream(&mut readers, &mut writers, &opts).unwrap();
1311            if expected_parity.is_empty() {
1312                expected_parity = writers.clone();
1313            } else {
1314                assert_eq!(writers, expected_parity);
1315            }
1316
1317            let mut all: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1318            for parity in &writers {
1319                all.push(parity.as_slice());
1320            }
1321            let mut verify_readers = all;
1322            assert!(rs.verify_stream(&mut verify_readers, &opts).unwrap());
1323        }
1324    }
1325
1326    #[test]
1327    fn test_reconstruct_stream_minimum_present() {
1328        let rs = ReedSolomon::new(3, 2).unwrap();
1329        let shard_len = 16usize;
1330        let data: Vec<Vec<u8>> = (0..3).map(|i| vec![i as u8 + 1; shard_len]).collect();
1331
1332        // Encode to get parity.
1333        let refs: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1334        let mut parity = vec![vec![0u8; shard_len]; 2];
1335        let mut par_refs: Vec<&mut [u8]> = parity.iter_mut().map(|p| &mut p[..]).collect();
1336        rs.encode_sep(&refs, &mut par_refs).unwrap();
1337
1338        // Keep minimum viable: 3 of 5 shards (data_shard_count).
1339        // Present: shard 0 (data), shard 3 (parity), shard 4 (parity).
1340        // Missing: shard 1 (data), shard 2 (data).
1341        let mut shards: Vec<std::io::Cursor<Vec<u8>>> = vec![
1342            std::io::Cursor::new(data[0].clone()),
1343            std::io::Cursor::new(Vec::new()),
1344            std::io::Cursor::new(Vec::new()),
1345            std::io::Cursor::new(parity[0].clone()),
1346            std::io::Cursor::new(parity[1].clone()),
1347        ];
1348
1349        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
1350            .unwrap();
1351
1352        // Verify reconstructed data shards match originals.
1353        assert_eq!(shards[1].get_ref(), &data[1], "shard 1 mismatch");
1354        assert_eq!(shards[2].get_ref(), &data[2], "shard 2 mismatch");
1355    }
1356
1357    // -----------------------------------------------------------------------
1358    // Streaming-path audit regression tests
1359    // -----------------------------------------------------------------------
1360
1361    fn leopard_gf8_codec(data: usize, parity: usize) -> ReedSolomon {
1362        use crate::{CodecFamily, CodecOptions};
1363        ReedSolomon::with_options(
1364            data,
1365            parity,
1366            CodecOptions {
1367                codec_family: CodecFamily::LeopardGF8,
1368                ..CodecOptions::default()
1369            },
1370        )
1371        .unwrap()
1372    }
1373
1374    /// A truncated / length-mismatched present shard must error with
1375    /// `IncorrectShardSize` instead of silently recovering wrong data.
1376    #[test]
1377    fn test_reconstruct_stream_truncated_present_errors() {
1378        let rs = make_codec(3, 2);
1379        let shard = 4096;
1380        let d: Vec<Vec<u8>> = (0..3).map(|_| random_data(shard)).collect();
1381        let mut readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
1382        let mut parity: Vec<Vec<u8>> = vec![Vec::new(); 2];
1383        rs.encode_stream(&mut readers, &mut parity, &StreamOptions::default())
1384            .unwrap();
1385
1386        // shard0 missing; shard1 truncated to 50 bytes.
1387        let mut shards: Vec<std::io::Cursor<Vec<u8>>> = vec![
1388            std::io::Cursor::new(Vec::new()),
1389            std::io::Cursor::new(d[1][..50].to_vec()),
1390            std::io::Cursor::new(d[2].clone()),
1391            std::io::Cursor::new(parity[0].clone()),
1392            std::io::Cursor::new(parity[1].clone()),
1393        ];
1394        let err = rs
1395            .reconstruct_stream(&mut shards, &StreamOptions::default())
1396            .unwrap_err();
1397        assert!(matches!(
1398            err.kind,
1399            StreamErrorKind::Codec(crate::Error::IncorrectShardSize)
1400        ));
1401    }
1402
1403    /// A present cursor whose position is not 0 (written first) still recovers
1404    /// correctly.
1405    #[test]
1406    fn test_reconstruct_stream_nonzero_cursor_position() {
1407        use std::io::Write;
1408        let rs = make_codec(3, 2);
1409        let shard = 4096;
1410        let d: Vec<Vec<u8>> = (0..3).map(|_| random_data(shard)).collect();
1411        let mut readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
1412        let mut parity: Vec<Vec<u8>> = vec![Vec::new(); 2];
1413        rs.encode_stream(&mut readers, &mut parity, &StreamOptions::default())
1414            .unwrap();
1415
1416        let mk = |bytes: &[u8]| {
1417            let mut c = std::io::Cursor::new(Vec::new());
1418            c.write_all(bytes).unwrap(); // position left at the end
1419            c
1420        };
1421        let mut shards = vec![
1422            std::io::Cursor::new(Vec::new()), // missing
1423            mk(&d[1]),
1424            mk(&d[2]),
1425            mk(&parity[0]),
1426            mk(&parity[1]),
1427        ];
1428        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
1429            .unwrap();
1430        assert_eq!(shards[0].get_ref(), &d[0]);
1431    }
1432
1433    /// `block_size = 0` (public field) is clamped and encodes normally instead
1434    /// of silently producing empty output.
1435    #[test]
1436    fn test_encode_stream_zero_block_size_clamped() {
1437        let rs = make_codec(3, 2);
1438        let shard = 4096;
1439        let d: Vec<Vec<u8>> = (0..3).map(|_| random_data(shard)).collect();
1440        let mut readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
1441        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1442        let opts = StreamOptions {
1443            block_size: 0,
1444            io_mode: StreamIoMode::Serial,
1445        };
1446        rs.encode_stream(&mut readers, &mut writers, &opts).unwrap();
1447        assert_eq!(writers[0].len(), shard);
1448        assert_eq!(writers[1].len(), shard);
1449    }
1450
1451    /// A mismatched stream count returns an error at runtime (no panic, no
1452    /// reliance on `debug_assert`).
1453    #[test]
1454    fn test_encode_stream_wrong_count_errors() {
1455        let rs = make_codec(3, 2);
1456        let d0 = random_data(64);
1457        let mut readers: Vec<&[u8]> = vec![d0.as_slice(), d0.as_slice()]; // one short
1458        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1459        let err = rs
1460            .encode_stream(&mut readers, &mut writers, &StreamOptions::default())
1461            .unwrap_err();
1462        assert!(matches!(
1463            err.kind,
1464            StreamErrorKind::Codec(crate::Error::TooFewShards)
1465        ));
1466    }
1467
1468    /// Leopard families are rejected by all three streaming methods with
1469    /// `UnsupportedCodecFamily`.
1470    #[test]
1471    fn test_stream_rejects_leopard_family() {
1472        let rs = leopard_gf8_codec(4, 2);
1473        let d: Vec<Vec<u8>> = (0..6).map(|_| random_data(128)).collect();
1474
1475        let mut enc_readers: Vec<&[u8]> = d[..4].iter().map(|s| s.as_slice()).collect();
1476        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1477        let err = rs
1478            .encode_stream(&mut enc_readers, &mut writers, &StreamOptions::default())
1479            .unwrap_err();
1480        assert!(matches!(
1481            err.kind,
1482            StreamErrorKind::Codec(crate::Error::UnsupportedCodecFamily)
1483        ));
1484
1485        let mut ver_readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
1486        let err = rs
1487            .verify_stream(&mut ver_readers, &StreamOptions::default())
1488            .unwrap_err();
1489        assert!(matches!(
1490            err.kind,
1491            StreamErrorKind::Codec(crate::Error::UnsupportedCodecFamily)
1492        ));
1493
1494        let mut rec_shards: Vec<std::io::Cursor<Vec<u8>>> =
1495            d.iter().map(|s| std::io::Cursor::new(s.clone())).collect();
1496        let err = rs
1497            .reconstruct_stream(&mut rec_shards, &StreamOptions::default())
1498            .unwrap_err();
1499        assert!(matches!(
1500            err.kind,
1501            StreamErrorKind::Codec(crate::Error::UnsupportedCodecFamily)
1502        ));
1503    }
1504
1505    /// An empty dataset (all empty cursors) reconstructs to Ok, consistent with
1506    /// `encode_stream` on empty input.
1507    #[test]
1508    fn test_reconstruct_stream_empty_dataset_ok() {
1509        let rs = make_codec(3, 2);
1510        let mut shards: Vec<std::io::Cursor<Vec<u8>>> =
1511            (0..5).map(|_| std::io::Cursor::new(Vec::new())).collect();
1512        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
1513            .unwrap();
1514        assert!(shards.iter().all(|c| c.get_ref().is_empty()));
1515    }
1516}