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