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// ---------------------------------------------------------------------------
34// StreamOptions
35// ---------------------------------------------------------------------------
36
37/// I/O scheduling mode for streaming operations.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum StreamIoMode {
40    /// Choose serial or parallel I/O using conservative built-in thresholds.
41    Auto,
42    /// Read and write shard streams serially.
43    Serial,
44    /// Read and write shard streams with rayon parallel iterators.
45    Parallel,
46}
47
48/// Configuration for streaming encode/verify/reconstruct operations.
49#[derive(Debug, Clone)]
50pub struct StreamOptions {
51    /// Block size in bytes for each read/write cycle. Default: 4 MiB.
52    ///
53    /// Larger blocks reduce syscall overhead but increase memory usage.
54    /// Recommended range: 256 KiB – 16 MiB.
55    pub block_size: usize,
56    /// I/O scheduling mode for stream reads and writes. Default: Auto.
57    pub io_mode: StreamIoMode,
58}
59
60impl Default for StreamOptions {
61    fn default() -> Self {
62        Self {
63            block_size: 4 * 1024 * 1024,
64            io_mode: StreamIoMode::Auto,
65        }
66    }
67}
68
69impl StreamOptions {
70    /// Create a new `StreamOptions` with default settings.
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Set the block size (minimum 1 KiB).
76    pub fn with_block_size(mut self, size: usize) -> Self {
77        const MIN_BLOCK_SIZE_BYTES: usize = 1024;
78        const MAX_BLOCK_SIZE_BYTES: usize = 16 * 1024 * 1024;
79        self.block_size = size.clamp(MIN_BLOCK_SIZE_BYTES, MAX_BLOCK_SIZE_BYTES);
80        self
81    }
82
83    /// Set the stream I/O scheduling mode.
84    pub fn with_io_mode(mut self, mode: StreamIoMode) -> Self {
85        self.io_mode = mode;
86        self
87    }
88}
89
90// ---------------------------------------------------------------------------
91// StreamError
92// ---------------------------------------------------------------------------
93
94/// Error returned by streaming operations.
95#[derive(Debug)]
96pub struct StreamError {
97    /// Index of the shard that caused the error.
98    pub shard_index: usize,
99    /// The kind of error.
100    pub kind: StreamErrorKind,
101}
102
103/// Classification of [`StreamError`].
104#[derive(Debug)]
105pub enum StreamErrorKind {
106    /// I/O error while reading a shard.
107    Read(std::io::Error),
108    /// I/O error while writing a shard.
109    Write(std::io::Error),
110    /// Error from the underlying codec (encode/verify/reconstruct).
111    Codec(crate::Error),
112}
113
114impl core::fmt::Display for StreamError {
115    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
116        match &self.kind {
117            StreamErrorKind::Read(e) => {
118                write!(f, "read error on shard {}: {}", self.shard_index, e)
119            }
120            StreamErrorKind::Write(e) => {
121                write!(f, "write error on shard {}: {}", self.shard_index, e)
122            }
123            StreamErrorKind::Codec(e) => {
124                write!(f, "codec error on shard {}: {}", self.shard_index, e)
125            }
126        }
127    }
128}
129
130impl std::error::Error for StreamError {
131    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
132        match &self.kind {
133            StreamErrorKind::Read(e) => Some(e),
134            StreamErrorKind::Write(e) => Some(e),
135            StreamErrorKind::Codec(e) => Some(e),
136        }
137    }
138}
139
140impl StreamError {
141    fn read(shard_index: usize, e: std::io::Error) -> Self {
142        Self {
143            shard_index,
144            kind: StreamErrorKind::Read(e),
145        }
146    }
147
148    fn write(shard_index: usize, e: std::io::Error) -> Self {
149        Self {
150            shard_index,
151            kind: StreamErrorKind::Write(e),
152        }
153    }
154
155    fn codec(shard_index: usize, e: crate::Error) -> Self {
156        Self {
157            shard_index,
158            kind: StreamErrorKind::Codec(e),
159        }
160    }
161}
162
163fn take_stream_error(
164    first_error: &std::sync::Mutex<Option<StreamError>>,
165    fallback_message: &'static str,
166) -> StreamError {
167    match first_error.lock() {
168        Ok(mut guard) => guard
169            .take()
170            .unwrap_or_else(|| StreamError::read(0, Error::other(fallback_message))),
171        Err(poisoned) => {
172            let mut guard = poisoned.into_inner();
173            guard
174                .take()
175                .unwrap_or_else(|| StreamError::read(0, Error::other(fallback_message)))
176        }
177    }
178}
179
180// ---------------------------------------------------------------------------
181// Helpers
182// ---------------------------------------------------------------------------
183
184fn use_parallel_stream_io(options: &StreamOptions, stream_count: usize) -> bool {
185    match options.io_mode {
186        StreamIoMode::Serial => false,
187        StreamIoMode::Parallel => true,
188        StreamIoMode::Auto => use_parallel_stream_io_auto(options.block_size, stream_count),
189    }
190}
191
192fn use_parallel_stream_io_auto(block_size: usize, stream_count: usize) -> bool {
193    if stream_count < 2 || block_size < 256 * 1024 {
194        return false;
195    }
196    if stream_count <= 6 && block_size <= 1024 * 1024 {
197        return false;
198    }
199
200    block_size >= 4 * 1024 * 1024 && stream_count >= 10
201}
202
203fn read_block_with_mode<R: std::io::Read + Send>(
204    readers: &mut [R],
205    buffers: &mut [Vec<u8>],
206    max_len: usize,
207    use_parallel_io: bool,
208    read_lengths: &mut Vec<(usize, usize)>,
209) -> Result<(bool, usize), StreamError> {
210    if use_parallel_io {
211        read_block_par(readers, buffers, max_len)
212    } else {
213        read_block(readers, buffers, max_len, read_lengths)
214    }
215}
216
217fn write_block_with_mode<W: std::io::Write + Send>(
218    writers: &mut [W],
219    buffers: &[Vec<u8>],
220    len: usize,
221    shard_offset: usize,
222    use_parallel_io: bool,
223) -> Result<(), StreamError> {
224    if use_parallel_io {
225        write_block_par(writers, buffers, len, shard_offset)
226    } else {
227        write_block(writers, buffers, len, shard_offset)
228    }
229}
230
231/// Read up to `max_len` bytes from each reader into the corresponding
232/// buffer, retrying on `Interrupted`.  Returns `Ok((all_eof, actual_len))`
233/// where `all_eof` is `true` if every reader was already at EOF, and
234/// `actual_len` is the number of bytes read (same across all readers,
235/// with short reads zero-padded).
236fn read_block<R: std::io::Read>(
237    readers: &mut [R],
238    buffers: &mut [Vec<u8>],
239    max_len: usize,
240    read_lengths: &mut Vec<(usize, usize)>,
241) -> Result<(bool, usize), StreamError> {
242    read_lengths.clear();
243
244    for (i, (reader, buf)) in readers.iter_mut().zip(buffers.iter_mut()).enumerate() {
245        let total = read_one_stream(reader, buf, max_len).map_err(|e| StreamError::read(i, e))?;
246        read_lengths.push((i, total));
247    }
248
249    let actual_len = read_lengths
250        .iter()
251        .map(|(_, total)| *total)
252        .max()
253        .unwrap_or(0);
254    zero_pad_short_buffers(buffers, read_lengths, actual_len);
255
256    Ok((actual_len == 0, actual_len))
257}
258
259fn prepare_read_buffer(buf: &mut Vec<u8>, max_len: usize) {
260    match buf.len().cmp(&max_len) {
261        core::cmp::Ordering::Less => buf.resize(max_len, 0),
262        core::cmp::Ordering::Greater => buf.truncate(max_len),
263        core::cmp::Ordering::Equal => {}
264    }
265}
266
267fn read_one_stream<R: std::io::Read>(
268    reader: &mut R,
269    buf: &mut Vec<u8>,
270    max_len: usize,
271) -> Result<usize, std::io::Error> {
272    prepare_read_buffer(buf, max_len);
273    let mut total = 0;
274
275    while total < max_len {
276        match reader.read(&mut buf[total..]) {
277            Ok(0) => break,
278            Ok(n) => total += n,
279            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
280            Err(e) => return Err(e),
281        }
282    }
283
284    Ok(total)
285}
286
287fn zero_pad_short_buffers(
288    buffers: &mut [Vec<u8>],
289    read_lengths: &[(usize, usize)],
290    actual_len: usize,
291) {
292    for &(i, total) in read_lengths {
293        if total < actual_len {
294            buffers[i][total..actual_len].fill(0);
295        }
296    }
297}
298
299fn read_present_cursors_with_mode(
300    shards: &mut [std::io::Cursor<Vec<u8>>],
301    buffers: &mut [Vec<u8>],
302    present: &[bool],
303    present_indices: &[usize],
304    max_len: usize,
305    use_parallel_io: bool,
306    read_lengths: &mut Vec<(usize, usize)>,
307) -> Result<(bool, usize), StreamError> {
308    read_lengths.clear();
309
310    if use_parallel_io {
311        use rayon::prelude::*;
312
313        *read_lengths = shards
314            .par_iter_mut()
315            .zip(buffers.par_iter_mut())
316            .enumerate()
317            .filter_map(|(i, item)| present[i].then_some((i, item)))
318            .map(|(i, (shard, buf))| {
319                let total =
320                    read_one_stream(shard, buf, max_len).map_err(|e| StreamError::read(i, e))?;
321                buf.truncate(total);
322                Ok::<_, StreamError>((i, total))
323            })
324            .collect::<Result<Vec<_>, _>>()?;
325    } else {
326        for &i in present_indices {
327            let total = read_one_stream(&mut shards[i], &mut buffers[i], max_len)
328                .map_err(|e| StreamError::read(i, e))?;
329            buffers[i].truncate(total);
330            read_lengths.push((i, total));
331        }
332    }
333
334    let actual_len = read_lengths
335        .iter()
336        .map(|(_, total)| *total)
337        .max()
338        .unwrap_or(0);
339    if actual_len != 0 {
340        for &idx in present_indices {
341            buffers[idx].resize(actual_len, 0);
342        }
343    }
344
345    Ok((actual_len == 0, actual_len))
346}
347
348/// Write `len` bytes from each buffer to the corresponding writer.
349fn write_block<W: std::io::Write>(
350    writers: &mut [W],
351    buffers: &[Vec<u8>],
352    len: usize,
353    shard_offset: usize,
354) -> Result<(), StreamError> {
355    for (i, (writer, buf)) in writers.iter_mut().zip(buffers.iter()).enumerate() {
356        let mut written = 0;
357        while written < len {
358            match writer.write(&buf[written..len]) {
359                Ok(0) => {
360                    return Err(StreamError::write(
361                        shard_offset + i,
362                        std::io::Error::new(std::io::ErrorKind::WriteZero, "write returned 0"),
363                    ));
364                }
365                Ok(n) => written += n,
366                Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
367                Err(e) => return Err(StreamError::write(shard_offset + i, e)),
368            }
369        }
370    }
371    Ok(())
372}
373
374// ---------------------------------------------------------------------------
375// Parallel I/O helpers (rayon)
376// ---------------------------------------------------------------------------
377
378/// Parallel version of `read_block` — reads all shards concurrently.
379fn read_block_par<R: std::io::Read + Send>(
380    readers: &mut [R],
381    buffers: &mut [Vec<u8>],
382    max_len: usize,
383) -> Result<(bool, usize), StreamError> {
384    use rayon::prelude::*;
385
386    let read_lengths: Vec<(usize, usize)> = readers
387        .par_iter_mut()
388        .zip(buffers.par_iter_mut())
389        .enumerate()
390        .map(|(i, (reader, buf))| {
391            read_one_stream(reader, buf, max_len)
392                .map(|total| (i, total))
393                .map_err(|e| StreamError::read(i, e))
394        })
395        .collect::<Result<Vec<_>, _>>()?;
396
397    let actual_len = read_lengths
398        .iter()
399        .map(|(_, total)| *total)
400        .max()
401        .unwrap_or(0);
402    zero_pad_short_buffers(buffers, &read_lengths, actual_len);
403
404    Ok((actual_len == 0, actual_len))
405}
406
407/// Parallel version of `write_block` — writes all shards concurrently.
408fn write_block_par<W: std::io::Write + Send>(
409    writers: &mut [W],
410    buffers: &[Vec<u8>],
411    len: usize,
412    shard_offset: usize,
413) -> Result<(), StreamError> {
414    use rayon::prelude::*;
415
416    let first_error: std::sync::Mutex<Option<StreamError>> = std::sync::Mutex::new(None);
417
418    writers
419        .par_iter_mut()
420        .zip(buffers.par_iter())
421        .enumerate()
422        .try_for_each(|(i, (writer, buf))| {
423            let mut written = 0;
424            while written < len {
425                match writer.write(&buf[written..len]) {
426                    Ok(0) => {
427                        if let Ok(mut guard) = first_error.lock()
428                            && guard.is_none()
429                        {
430                            *guard = Some(StreamError::write(
431                                shard_offset + i,
432                                std::io::Error::new(
433                                    std::io::ErrorKind::WriteZero,
434                                    "write returned 0",
435                                ),
436                            ));
437                        }
438                        return Err(());
439                    }
440                    Ok(n) => written += n,
441                    Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
442                    Err(e) => {
443                        if let Ok(mut guard) = first_error.lock()
444                            && guard.is_none()
445                        {
446                            *guard = Some(StreamError::write(shard_offset + i, e));
447                        }
448                        return Err(());
449                    }
450                }
451            }
452            Ok(())
453        })
454        .map_err(|()| {
455            take_stream_error(
456                &first_error,
457                "parallel stream writer error was not reported",
458            )
459        })
460}
461
462// ---------------------------------------------------------------------------
463// ReedSolomon streaming methods
464// ---------------------------------------------------------------------------
465
466impl super::ReedSolomon<crate::galois_8::Field> {
467    /// Stream-encode data from readers into parity writers.
468    ///
469    /// Reads data shards in blocks of `options.block_size` bytes, encodes
470    /// each block, and writes the resulting parity blocks.  This avoids
471    /// loading the entire dataset into memory.
472    ///
473    /// # Errors
474    ///
475    /// Returns [`StreamError`] on I/O failure or codec error.
476    pub fn encode_stream(
477        &self,
478        data: &mut [impl std::io::Read + Send],
479        parity: &mut [impl std::io::Write + Send],
480        options: &StreamOptions,
481    ) -> Result<(), StreamError> {
482        let block_size = options.block_size;
483        let data_count = self.data_shard_count;
484        let parity_count = self.parity_shard_count;
485        let use_parallel_read = use_parallel_stream_io(options, data_count);
486        let use_parallel_write = use_parallel_stream_io(options, parity_count);
487
488        debug_assert_eq!(data.len(), data_count);
489        debug_assert_eq!(parity.len(), parity_count);
490
491        let mut data_bufs: Vec<Vec<u8>> = (0..data_count)
492            .map(|_| Vec::with_capacity(block_size))
493            .collect();
494        let mut parity_bufs: Vec<Vec<u8>> = (0..parity_count)
495            .map(|_| Vec::with_capacity(block_size))
496            .collect();
497        let mut read_lengths = Vec::with_capacity(data_count);
498
499        loop {
500            let (all_eof, actual_len) = read_block_with_mode(
501                data,
502                &mut data_bufs,
503                block_size,
504                use_parallel_read,
505                &mut read_lengths,
506            )?;
507            if all_eof {
508                break;
509            }
510
511            // Resize parity buffers to match actual length.
512            for buf in parity_bufs.iter_mut() {
513                buf.resize(actual_len, 0);
514            }
515
516            // Encode (parallel codec).
517            let data_refs: Vec<&[u8]> = data_bufs.iter().map(|b| &b[..actual_len]).collect();
518            let mut parity_refs: Vec<&mut [u8]> = parity_bufs
519                .iter_mut()
520                .map(|b| &mut b[..actual_len])
521                .collect();
522
523            self.encode_sep_par(&data_refs, &mut parity_refs)
524                .map_err(|e| StreamError::codec(0, e))?;
525
526            // Write parity using the selected stream I/O mode.
527            write_block_with_mode(
528                parity,
529                &parity_bufs,
530                actual_len,
531                data_count,
532                use_parallel_write,
533            )?;
534        }
535
536        Ok(())
537    }
538
539    /// Stream-verify data + parity from readers.
540    ///
541    /// Reads all shards in blocks, verifying each block independently.
542    /// Returns `Ok(true)` if every block is valid, `Ok(false)` if any block
543    /// fails verification.
544    pub fn verify_stream(
545        &self,
546        shards: &mut [impl std::io::Read + Send],
547        options: &StreamOptions,
548    ) -> Result<bool, StreamError> {
549        let block_size = options.block_size;
550        let total = self.total_shard_count;
551        let use_parallel_read = use_parallel_stream_io(options, total);
552
553        debug_assert_eq!(shards.len(), total);
554
555        let mut bufs: Vec<Vec<u8>> = (0..total).map(|_| Vec::with_capacity(block_size)).collect();
556        let mut read_lengths = Vec::with_capacity(total);
557
558        loop {
559            let (all_eof, actual_len) = read_block_with_mode(
560                shards,
561                &mut bufs,
562                block_size,
563                use_parallel_read,
564                &mut read_lengths,
565            )?;
566            if all_eof {
567                break;
568            }
569
570            let refs: Vec<&[u8]> = bufs.iter().map(|b| &b[..actual_len]).collect();
571
572            let valid = self
573                .verify_par(&refs)
574                .map_err(|e| StreamError::codec(0, e))?;
575            if !valid {
576                return Ok(false);
577            }
578        }
579
580        Ok(true)
581    }
582
583    /// Stream-reconstruct missing shards.
584    ///
585    /// `shards` has one entry per total shard.  Present shards contain their
586    /// data in a `Cursor<Vec<u8>>`; missing shards use an empty cursor
587    /// (`Cursor::new(Vec::new())`).  Recovered data is written into the
588    /// missing shards' cursors.
589    ///
590    /// The function reads blocks from present shards, reconstructs missing
591    /// blocks, and writes recovered data into the missing cursors.  The set
592    /// of missing shard indices must be consistent across all blocks.
593    ///
594    /// # Limitations
595    ///
596    /// For Leopard-family codecs the entire dataset must fit in memory; this
597    /// streaming path is only supported for the classic Reed-Solomon family.
598    pub fn reconstruct_stream(
599        &self,
600        shards: &mut [std::io::Cursor<Vec<u8>>],
601        options: &StreamOptions,
602    ) -> Result<(), StreamError> {
603        let block_size = options.block_size;
604        let total = self.total_shard_count;
605
606        debug_assert_eq!(shards.len(), total);
607
608        // Determine which shards are present (non-empty cursor).
609        let mut present = vec![false; total];
610        for (i, shard) in shards.iter().enumerate() {
611            present[i] = !shard.get_ref().is_empty();
612        }
613
614        let missing_count = present.iter().filter(|&&p| !p).count();
615        if missing_count > self.parity_shard_count {
616            return Err(StreamError::codec(0, crate::Error::TooFewShardsPresent));
617        }
618        let present_count = total - missing_count;
619        let use_parallel_read = use_parallel_stream_io(options, present_count);
620        let present_indices: Vec<usize> = present
621            .iter()
622            .enumerate()
623            .filter_map(|(i, is_present)| is_present.then_some(i))
624            .collect();
625        let missing_indices: Vec<usize> = present
626            .iter()
627            .enumerate()
628            .filter_map(|(i, is_present)| (!is_present).then_some(i))
629            .collect();
630
631        // Strategy: read present shards into buffers per block, reconstruct,
632        // then write recovered data into missing shards' cursors.
633
634        // Pre-allocate read buffers and the reconstruct container once. Present
635        // shard buffers are moved into the reconstruct call and then moved back
636        // after each block so their allocations survive across iterations.
637        let mut bufs: Vec<Vec<u8>> = (0..total)
638            .map(|i| {
639                if present[i] {
640                    Vec::with_capacity(block_size)
641                } else {
642                    Vec::new()
643                }
644            })
645            .collect();
646        let mut reconstruct_bufs: Vec<Option<Vec<u8>>> = (0..total).map(|_| None).collect();
647        let mut read_lengths = Vec::with_capacity(total);
648
649        loop {
650            let (all_eof, actual_len) = read_present_cursors_with_mode(
651                shards,
652                &mut bufs,
653                &present,
654                &present_indices,
655                block_size,
656                use_parallel_read,
657                &mut read_lengths,
658            )?;
659            if all_eof {
660                break;
661            }
662
663            // Zero-pad present shards to actual_len and reuse the outer
664            // Option<Vec<_>> container for reconstructing this block.
665            for &idx in &present_indices {
666                bufs[idx].resize(actual_len, 0);
667                reconstruct_bufs[idx] = Some(mem::take(&mut bufs[idx]));
668            }
669            for &idx in &missing_indices {
670                reconstruct_bufs[idx] = None;
671            }
672
673            self.reconstruct(&mut reconstruct_bufs)
674                .map_err(|e| StreamError::codec(0, e))?;
675
676            // Write recovered data into missing shards' cursors.
677            // (reconstruct fills in all missing shards — data and parity)
678            for &idx in &missing_indices {
679                let recovered = reconstruct_bufs[idx]
680                    .as_ref()
681                    .expect("missing shard buffer");
682                shards[idx]
683                    .get_mut()
684                    .extend_from_slice(&recovered[..actual_len]);
685            }
686
687            for idx in 0..total {
688                if let Some(buf) = reconstruct_bufs[idx].take() {
689                    bufs[idx] = buf;
690                }
691            }
692        }
693
694        Ok(())
695    }
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701    use crate::galois_8::ReedSolomon;
702
703    fn make_codec(data: usize, parity: usize) -> ReedSolomon {
704        ReedSolomon::new(data, parity).unwrap()
705    }
706
707    fn random_data(len: usize) -> Vec<u8> {
708        // Simple deterministic fill for reproducibility.
709        (0..len)
710            .map(|i| (i.wrapping_mul(73).wrapping_add(17)) as u8)
711            .collect()
712    }
713
714    #[test]
715    fn test_encode_stream_basic() {
716        let rs = make_codec(3, 2);
717        let shard_size = 4096;
718
719        let d0 = random_data(shard_size);
720        let d1 = random_data(shard_size);
721        let d2 = random_data(shard_size);
722
723        let mut readers: Vec<&[u8]> = vec![d0.as_slice(), d1.as_slice(), d2.as_slice()];
724        let mut writers: Vec<Vec<u8>> = vec![Vec::new(), Vec::new()];
725
726        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
727            .unwrap();
728
729        assert_eq!(writers[0].len(), shard_size);
730        assert_eq!(writers[1].len(), shard_size);
731
732        // Verify parity is correct by verifying the full shard set.
733        let all: Vec<&[u8]> = vec![
734            d0.as_slice(),
735            d1.as_slice(),
736            d2.as_slice(),
737            writers[0].as_slice(),
738            writers[1].as_slice(),
739        ];
740        assert!(rs.verify(&all).unwrap());
741    }
742
743    #[test]
744    fn test_encode_stream_multi_block() {
745        let rs = make_codec(3, 2);
746        let total_size = 10 * 1024; // 10 KiB
747        let block_size = 4096;
748
749        let d0 = random_data(total_size);
750        let d1 = random_data(total_size);
751        let d2 = random_data(total_size);
752
753        let mut readers: Vec<&[u8]> = vec![d0.as_slice(), d1.as_slice(), d2.as_slice()];
754        let mut writers: Vec<Vec<u8>> = vec![Vec::new(), Vec::new()];
755
756        let opts = StreamOptions::new().with_block_size(block_size);
757        rs.encode_stream(&mut readers, &mut writers, &opts).unwrap();
758
759        assert_eq!(writers[0].len(), total_size);
760        assert_eq!(writers[1].len(), total_size);
761    }
762
763    #[test]
764    fn test_encode_stream_empty() {
765        let rs = make_codec(3, 2);
766        let empty: Vec<&[u8]> = vec![&[], &[], &[]];
767        let mut readers = empty.clone();
768        let mut writers: Vec<Vec<u8>> = vec![Vec::new(), Vec::new()];
769
770        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
771            .unwrap();
772
773        // Empty input → empty output.
774        assert!(writers[0].is_empty());
775        assert!(writers[1].is_empty());
776    }
777
778    #[test]
779    fn test_encode_stream_unequal_lengths() {
780        let rs = make_codec(3, 2);
781
782        let d0 = random_data(1000);
783        let d1 = random_data(500);
784        let d2 = random_data(800);
785
786        let mut readers: Vec<&[u8]> = vec![d0.as_slice(), d1.as_slice(), d2.as_slice()];
787        let mut writers: Vec<Vec<u8>> = vec![Vec::new(), Vec::new()];
788
789        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
790            .unwrap();
791
792        // All outputs should be 1000 bytes (max of input lengths, since we
793        // zero-pad short shards).
794        assert_eq!(writers[0].len(), 1000);
795        assert_eq!(writers[1].len(), 1000);
796    }
797
798    #[test]
799    fn test_encode_stream_10x4() {
800        let rs = make_codec(10, 4);
801        let shard_size = 64 * 1024;
802
803        let data: Vec<Vec<u8>> = (0..10).map(|_| random_data(shard_size)).collect();
804        let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
805        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 4];
806
807        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
808            .unwrap();
809
810        // Verify.
811        let mut all: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
812        for w in &writers {
813            all.push(w.as_slice());
814        }
815        assert!(rs.verify(&all).unwrap());
816    }
817
818    #[test]
819    fn test_verify_stream_valid() {
820        let rs = make_codec(3, 2);
821        let shard_size = 4096;
822
823        let d = vec![random_data(shard_size); 3];
824        let mut readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
825        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
826        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
827            .unwrap();
828
829        // Build full shard set for verification.
830        let mut all_data: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
831        for w in &writers {
832            all_data.push(w.as_slice());
833        }
834        let mut all_readers: Vec<&[u8]> = all_data;
835
836        assert!(
837            rs.verify_stream(&mut all_readers, &StreamOptions::default())
838                .unwrap()
839        );
840    }
841
842    #[test]
843    fn test_verify_stream_corrupted() {
844        let rs = make_codec(3, 2);
845        let shard_size = 4096;
846
847        let d = vec![random_data(shard_size); 3];
848        let mut readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
849        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
850        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
851            .unwrap();
852
853        // Corrupt a parity shard.
854        writers[0][0] ^= 0xFF;
855
856        let mut all_data: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
857        for w in &writers {
858            all_data.push(w.as_slice());
859        }
860        let mut all_readers: Vec<&[u8]> = all_data;
861
862        assert!(
863            !rs.verify_stream(&mut all_readers, &StreamOptions::default())
864                .unwrap()
865        );
866    }
867
868    #[test]
869    fn test_reconstruct_stream_single_missing() {
870        let rs = make_codec(3, 2);
871        let shard_size = 4096;
872
873        let d: Vec<Vec<u8>> = (0..3).map(|_| random_data(shard_size)).collect();
874        let mut readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
875        let mut parity_writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
876        rs.encode_stream(&mut readers, &mut parity_writers, &StreamOptions::default())
877            .unwrap();
878
879        // data[0] is missing — use empty Cursor.
880        let mut shards: Vec<std::io::Cursor<Vec<u8>>> = vec![
881            std::io::Cursor::new(Vec::new()), // missing
882            std::io::Cursor::new(d[1].clone()),
883            std::io::Cursor::new(d[2].clone()),
884            std::io::Cursor::new(parity_writers[0].clone()),
885            std::io::Cursor::new(parity_writers[1].clone()),
886        ];
887
888        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
889            .unwrap();
890
891        // Shard 0 should have been recovered into the cursor's inner Vec.
892        assert_eq!(shards[0].get_ref(), &d[0]);
893    }
894
895    #[test]
896    fn test_reconstruct_non_streaming() {
897        // Verify basic encode + reconstruct works without streaming.
898        let rs = make_codec(3, 2);
899        let shard_size = 4096;
900
901        let d: Vec<Vec<u8>> = (0..3).map(|_| random_data(shard_size)).collect();
902        let data_refs: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
903        let mut p0 = vec![0u8; shard_size];
904        let mut p1 = vec![0u8; shard_size];
905        let mut parity_refs: Vec<&mut [u8]> = vec![&mut p0, &mut p1];
906        rs.encode_sep(&data_refs, &mut parity_refs).unwrap();
907
908        // Now reconstruct with shard 0 missing.
909        let mut shards: Vec<Option<Vec<u8>>> = vec![
910            None,
911            Some(d[1].clone()),
912            Some(d[2].clone()),
913            Some(p0.clone()),
914            Some(p1.clone()),
915        ];
916        rs.reconstruct(&mut shards).unwrap();
917
918        assert_eq!(shards[0].as_ref().unwrap(), &d[0]);
919    }
920
921    #[test]
922    fn test_reconstruct_stream_basic() {
923        let rs = make_codec(3, 2);
924        let shard_size = 64;
925
926        let d: Vec<Vec<u8>> = (0..3).map(|_| random_data(shard_size)).collect();
927        let mut readers: Vec<&[u8]> = d.iter().map(|s| s.as_slice()).collect();
928        let mut parity_writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
929        rs.encode_stream(&mut readers, &mut parity_writers, &StreamOptions::default())
930            .unwrap();
931
932        // Verify parity is correct.
933        let all: Vec<&[u8]> = vec![
934            d[0].as_slice(),
935            d[1].as_slice(),
936            d[2].as_slice(),
937            parity_writers[0].as_slice(),
938            parity_writers[1].as_slice(),
939        ];
940        assert!(rs.verify(&all).unwrap());
941
942        // Missing shard 0 — empty Cursor; present shards have data.
943        let mut shards: Vec<std::io::Cursor<Vec<u8>>> = vec![
944            std::io::Cursor::new(Vec::new()), // missing
945            std::io::Cursor::new(d[1].clone()),
946            std::io::Cursor::new(d[2].clone()),
947            std::io::Cursor::new(parity_writers[0].clone()),
948            std::io::Cursor::new(parity_writers[1].clone()),
949        ];
950
951        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
952            .unwrap();
953
954        // Shard 0 recovered into the empty cursor's inner Vec.
955        let recovered = shards[0].get_ref();
956        assert_eq!(
957            recovered.len(),
958            d[0].len(),
959            "recovered len {} != expected len {}",
960            recovered.len(),
961            d[0].len()
962        );
963        assert_eq!(
964            recovered,
965            &d[0],
966            "recovered: {:?}, expected: {:?}",
967            &recovered[..8],
968            &d[0][..8]
969        );
970    }
971
972    #[test]
973    fn test_stream_options_builder() {
974        let opts = StreamOptions::new().with_block_size(1024 * 1024);
975        assert_eq!(opts.block_size, 1024 * 1024);
976        assert_eq!(opts.io_mode, StreamIoMode::Auto);
977
978        // Minimum is 1 KiB.
979        let opts = StreamOptions::new().with_block_size(100);
980        assert_eq!(opts.block_size, 1024);
981
982        let opts = StreamOptions::new().with_io_mode(StreamIoMode::Serial);
983        assert_eq!(opts.io_mode, StreamIoMode::Serial);
984    }
985
986    #[test]
987    fn test_stream_io_auto_decision_thresholds() {
988        let opts = StreamOptions::new()
989            .with_block_size(64 * 1024)
990            .with_io_mode(StreamIoMode::Auto);
991        assert!(!use_parallel_stream_io(&opts, 14));
992
993        let opts = StreamOptions::new()
994            .with_block_size(1024 * 1024)
995            .with_io_mode(StreamIoMode::Auto);
996        assert!(!use_parallel_stream_io(&opts, 6));
997
998        let opts = StreamOptions::new()
999            .with_block_size(4 * 1024 * 1024)
1000            .with_io_mode(StreamIoMode::Auto);
1001        assert!(use_parallel_stream_io(&opts, 10));
1002
1003        let opts = StreamOptions::new()
1004            .with_block_size(64 * 1024)
1005            .with_io_mode(StreamIoMode::Parallel);
1006        assert!(use_parallel_stream_io(&opts, 1));
1007    }
1008
1009    #[test]
1010    fn test_stream_error_display() {
1011        let e = StreamError::read(
1012            3,
1013            std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "eof"),
1014        );
1015        let s = format!("{e}");
1016        assert!(s.contains("shard 3"));
1017        assert!(s.contains("eof"));
1018
1019        let e = StreamError::codec(0, crate::Error::TooFewShardsPresent);
1020        let s = format!("{e}");
1021        assert!(s.contains("codec"));
1022    }
1023
1024    // -----------------------------------------------------------------------
1025    // Concurrent stream tests (P0-2e-3)
1026    // -----------------------------------------------------------------------
1027
1028    #[test]
1029    fn test_encode_stream_concurrent() {
1030        let rs = make_codec(4, 2);
1031        let shard_size = 8 * 1024;
1032
1033        let data: Vec<Vec<u8>> = (0..4).map(|_| random_data(shard_size)).collect();
1034        let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1035        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1036
1037        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
1038            .unwrap();
1039
1040        // Verify parity correctness.
1041        let mut all: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1042        for w in &writers {
1043            all.push(w.as_slice());
1044        }
1045        assert!(rs.verify(&all).unwrap());
1046    }
1047
1048    #[test]
1049    fn test_verify_stream_concurrent() {
1050        let rs = make_codec(4, 2);
1051        let shard_size = 8 * 1024;
1052
1053        let data: Vec<Vec<u8>> = (0..4).map(|_| random_data(shard_size)).collect();
1054        let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1055        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1056        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
1057            .unwrap();
1058
1059        // Valid case.
1060        let mut all: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1061        for w in &writers {
1062            all.push(w.as_slice());
1063        }
1064        let mut all_readers: Vec<&[u8]> = all;
1065        assert!(
1066            rs.verify_stream(&mut all_readers, &StreamOptions::default())
1067                .unwrap()
1068        );
1069
1070        // Corrupted case.
1071        let mut corrupted = writers[0].clone();
1072        corrupted[0] ^= 0xFF;
1073        let mut all_corrupt: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1074        all_corrupt.push(corrupted.as_slice());
1075        all_corrupt.push(writers[1].as_slice());
1076        assert!(
1077            !rs.verify_stream(&mut all_corrupt, &StreamOptions::default())
1078                .unwrap()
1079        );
1080    }
1081
1082    #[test]
1083    fn test_reconstruct_stream_concurrent() {
1084        let rs = make_codec(4, 2);
1085        let shard_size = 8 * 1024;
1086
1087        let data: Vec<Vec<u8>> = (0..4).map(|_| random_data(shard_size)).collect();
1088        let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1089        let mut parity_writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1090        rs.encode_stream(&mut readers, &mut parity_writers, &StreamOptions::default())
1091            .unwrap();
1092
1093        // Missing shards: data[1] and parity[0].
1094        let mut shards: Vec<std::io::Cursor<Vec<u8>>> = vec![
1095            std::io::Cursor::new(data[0].clone()),
1096            std::io::Cursor::new(Vec::new()), // missing
1097            std::io::Cursor::new(data[2].clone()),
1098            std::io::Cursor::new(data[3].clone()),
1099            std::io::Cursor::new(Vec::new()), // missing
1100            std::io::Cursor::new(parity_writers[1].clone()),
1101        ];
1102
1103        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
1104            .unwrap();
1105
1106        assert_eq!(shards[1].get_ref(), &data[1]);
1107    }
1108
1109    #[test]
1110    fn test_concurrent_stream_large_blocks() {
1111        let rs = make_codec(10, 4);
1112        let total_size = 1024 * 1024; // 1 MiB
1113        let block_size = 256 * 1024; // 256 KiB blocks
1114
1115        let data: Vec<Vec<u8>> = (0..10).map(|_| random_data(total_size)).collect();
1116        let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1117        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 4];
1118
1119        let opts = StreamOptions::new().with_block_size(block_size);
1120        rs.encode_stream(&mut readers, &mut writers, &opts).unwrap();
1121
1122        // Verify all blocks.
1123        let mut all: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1124        for w in &writers {
1125            all.push(w.as_slice());
1126        }
1127        assert!(rs.verify(&all).unwrap());
1128
1129        // Reconstruct with 2 missing data shards.
1130        let mut shards: Vec<std::io::Cursor<Vec<u8>>> = Vec::new();
1131        for d in &data {
1132            shards.push(std::io::Cursor::new(d.clone()));
1133        }
1134        shards[0] = std::io::Cursor::new(Vec::new());
1135        shards[5] = std::io::Cursor::new(Vec::new());
1136        for w in &writers {
1137            shards.push(std::io::Cursor::new(w.clone()));
1138        }
1139
1140        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
1141            .unwrap();
1142
1143        assert_eq!(shards[0].get_ref(), &data[0]);
1144        assert_eq!(shards[5].get_ref(), &data[5]);
1145    }
1146
1147    #[test]
1148    fn test_encode_stream_zero_length() {
1149        let rs = ReedSolomon::new(3, 2).unwrap();
1150        let mut readers: Vec<&[u8]> = vec![b""; 3];
1151        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1152        // Zero-length data should succeed (encode produces empty parity).
1153        rs.encode_stream(&mut readers, &mut writers, &StreamOptions::default())
1154            .unwrap();
1155        assert!(writers.iter().all(|w| w.is_empty()));
1156    }
1157
1158    #[test]
1159    fn test_encode_stream_single_byte_block() {
1160        let rs = ReedSolomon::new(2, 1).unwrap();
1161        let d0 = vec![0xABu8; 4];
1162        let d1 = vec![0xCDu8; 4];
1163        let mut readers: Vec<&[u8]> = vec![d0.as_slice(), d1.as_slice()];
1164        let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 1];
1165        let opts = StreamOptions::new().with_block_size(1);
1166        rs.encode_stream(&mut readers, &mut writers, &opts).unwrap();
1167
1168        // Verify with the smallest possible block size.
1169        let mut all: Vec<&[u8]> = vec![d0.as_slice(), d1.as_slice()];
1170        for w in &writers {
1171            all.push(w.as_slice());
1172        }
1173        assert!(rs.verify(&all).unwrap());
1174    }
1175
1176    #[test]
1177    fn test_stream_io_modes_encode_verify_match() {
1178        let rs = ReedSolomon::new(4, 2).unwrap();
1179        let shard_size = 32 * 1024;
1180        let data: Vec<Vec<u8>> = (0..4).map(|_| random_data(shard_size)).collect();
1181
1182        let mut expected_parity = Vec::new();
1183        for mode in [
1184            StreamIoMode::Auto,
1185            StreamIoMode::Serial,
1186            StreamIoMode::Parallel,
1187        ] {
1188            let opts = StreamOptions::new()
1189                .with_block_size(8 * 1024)
1190                .with_io_mode(mode);
1191            let mut readers: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1192            let mut writers: Vec<Vec<u8>> = vec![Vec::new(); 2];
1193
1194            rs.encode_stream(&mut readers, &mut writers, &opts).unwrap();
1195            if expected_parity.is_empty() {
1196                expected_parity = writers.clone();
1197            } else {
1198                assert_eq!(writers, expected_parity);
1199            }
1200
1201            let mut all: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1202            for parity in &writers {
1203                all.push(parity.as_slice());
1204            }
1205            let mut verify_readers = all;
1206            assert!(rs.verify_stream(&mut verify_readers, &opts).unwrap());
1207        }
1208    }
1209
1210    #[test]
1211    fn test_reconstruct_stream_minimum_present() {
1212        let rs = ReedSolomon::new(3, 2).unwrap();
1213        let shard_len = 16usize;
1214        let data: Vec<Vec<u8>> = (0..3).map(|i| vec![i as u8 + 1; shard_len]).collect();
1215
1216        // Encode to get parity.
1217        let refs: Vec<&[u8]> = data.iter().map(|d| d.as_slice()).collect();
1218        let mut parity = vec![vec![0u8; shard_len]; 2];
1219        let mut par_refs: Vec<&mut [u8]> = parity.iter_mut().map(|p| &mut p[..]).collect();
1220        rs.encode_sep(&refs, &mut par_refs).unwrap();
1221
1222        // Keep minimum viable: 3 of 5 shards (data_shard_count).
1223        // Present: shard 0 (data), shard 3 (parity), shard 4 (parity).
1224        // Missing: shard 1 (data), shard 2 (data).
1225        let mut shards: Vec<std::io::Cursor<Vec<u8>>> = vec![
1226            std::io::Cursor::new(data[0].clone()),
1227            std::io::Cursor::new(Vec::new()),
1228            std::io::Cursor::new(Vec::new()),
1229            std::io::Cursor::new(parity[0].clone()),
1230            std::io::Cursor::new(parity[1].clone()),
1231        ];
1232
1233        rs.reconstruct_stream(&mut shards, &StreamOptions::default())
1234            .unwrap();
1235
1236        // Verify reconstructed data shards match originals.
1237        assert_eq!(shards[1].get_ref(), &data[1], "shard 1 mismatch");
1238        assert_eq!(shards[2].get_ref(), &data[2], "shard 2 mismatch");
1239    }
1240}