// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Erasure coding implementation using reed-solomon-erasure (GF(2^8)). //! Supports legacy (reed-solomon-simd) for reading/healing old-version files. //! use bytes::{Bytes, BytesMut}; use reed_solomon_erasure::galois_8::ReedSolomon; use reed_solomon_simd; use smallvec::SmallVec; use std::io; use tokio::io::AsyncRead; use tracing::warn; use uuid::Uuid; const MODERN_MAX_TOTAL_SHARDS: usize = ::ORDER; /// Errors returned when constructing an [`Erasure`] codec. #[derive(Debug, thiserror::Error)] pub enum ErasureConstructionError { /// Erasure coding requires at least one data shard. #[error("data_shards must be greater than zero")] ZeroDataShards, /// Erasure coding requires a non-zero logical block size. #[error("block_size must be greater than zero")] ZeroBlockSize, /// The total shard count cannot be represented by `usize`. #[error("data_shards ({data_shards}) + parity_shards ({parity_shards}) overflows usize")] ShardCountOverflow { data_shards: usize, parity_shards: usize }, /// The modern GF(2^8) codec cannot represent this shard configuration. #[error("modern codec does not support {data_shards} data shards and {parity_shards} parity shards")] UnsupportedModernShardCount { data_shards: usize, parity_shards: usize }, /// The legacy SIMD codec cannot represent this shard configuration. #[error("legacy codec does not support {data_shards} data shards and {parity_shards} parity shards")] UnsupportedLegacyShardCount { data_shards: usize, parity_shards: usize }, /// The modern encoder rejected dimensions that passed the preflight checks. #[error("failed to construct modern Reed-Solomon encoder")] ModernEncoder { #[source] source: reed_solomon_erasure::Error, }, /// The legacy encoder wrapper failed to initialize. #[error("failed to construct legacy Reed-Solomon encoder")] LegacyEncoder { #[source] source: io::Error, }, } #[derive(Debug, thiserror::Error)] #[error("{source}")] struct ErasureConstructionIoSource { #[source] source: ErasureConstructionError, } impl ErasureConstructionError { pub(crate) fn into_io_error(self) -> io::Error { // `io::Error::source` forwards to the wrapped error's source rather // than exposing the wrapped error itself. This adapter keeps the // construction error as an observable link in the source chain. io::Error::other(ErasureConstructionIoSource { source: self }) } } /// Legacy calc_shard_size formula: (block_size.div_ceil(data_shards) + 1) & !1 /// Matches main branch and filemeta::ErasureInfo for old-version files. pub fn calc_shard_size_legacy(block_size: usize, data_shards: usize) -> usize { (block_size.div_ceil(data_shards) + 1) & !1 } /// Reed-Solomon encoder for legacy (main branch) format using reed-solomon-simd. /// Used when decoding/encoding files with uses_legacy_checksum == true. struct LegacyReedSolomonEncoder { data_shards: usize, parity_shards: usize, encoder_cache: std::sync::RwLock>, decoder_cache: std::sync::RwLock>, } impl Clone for LegacyReedSolomonEncoder { fn clone(&self) -> Self { Self { data_shards: self.data_shards, parity_shards: self.parity_shards, encoder_cache: std::sync::RwLock::new(None), decoder_cache: std::sync::RwLock::new(None), } } } impl LegacyReedSolomonEncoder { fn new(_data_shards: usize, _parity_shards: usize) -> io::Result { Ok(Self { data_shards: _data_shards, parity_shards: _parity_shards, encoder_cache: std::sync::RwLock::new(None), decoder_cache: std::sync::RwLock::new(None), }) } fn encode(&self, shards: SmallVec<[&mut [u8]; 16]>) -> io::Result<()> { let mut shards_vec: Vec<&mut [u8]> = shards.into_vec(); if shards_vec.is_empty() { return Ok(()); } let shard_len = shards_vec[0].len(); let mut encoder = { let mut cache_guard = self .encoder_cache .write() .map_err(|_| io::Error::other("Failed to acquire encoder cache lock"))?; match cache_guard.take() { Some(mut cached) => { if cached.reset(self.data_shards, self.parity_shards, shard_len).is_err() { reed_solomon_simd::ReedSolomonEncoder::new(self.data_shards, self.parity_shards, shard_len) .map_err(|e| io::Error::other(format!("Failed to create SIMD encoder: {e:?}")))? } else { cached } } None => reed_solomon_simd::ReedSolomonEncoder::new(self.data_shards, self.parity_shards, shard_len) .map_err(|e| io::Error::other(format!("Failed to create SIMD encoder: {e:?}")))?, } }; for (i, shard) in shards_vec.iter().enumerate().take(self.data_shards) { encoder .add_original_shard(shard) .map_err(|e| io::Error::other(format!("Failed to add shard {i}: {e:?}")))?; } let result = encoder .encode() .map_err(|e| io::Error::other(format!("SIMD encoding failed: {e:?}")))?; for (i, recovery_shard) in result.recovery_iter().enumerate() { if i + self.data_shards < shards_vec.len() { shards_vec[i + self.data_shards].copy_from_slice(recovery_shard); } } drop(result); *self .encoder_cache .write() .map_err(|_| io::Error::other("Failed to return encoder to cache"))? = Some(encoder); Ok(()) } fn reconstruct_data(&self, shards: &mut [Option>]) -> io::Result<()> { if recover_empty_payload_data_shards(shards, self.data_shards, self.parity_shards)? { return Ok(()); } let shard_len = shards .iter() .find_map(|s| s.as_ref().map(|v| v.len())) .ok_or_else(|| io::Error::other("No valid shards found for reconstruction"))?; let mut decoder = { let mut cache_guard = self .decoder_cache .write() .map_err(|_| io::Error::other("Failed to acquire decoder cache lock"))?; match cache_guard.take() { Some(mut cached_decoder) => { if let Err(e) = cached_decoder.reset(self.data_shards, self.parity_shards, shard_len) { warn!("Failed to reset SIMD decoder: {:?}, creating new one", e); reed_solomon_simd::ReedSolomonDecoder::new(self.data_shards, self.parity_shards, shard_len) .map_err(|e| io::Error::other(format!("Failed to create SIMD decoder: {e:?}")))? } else { cached_decoder } } None => reed_solomon_simd::ReedSolomonDecoder::new(self.data_shards, self.parity_shards, shard_len) .map_err(|e| io::Error::other(format!("Failed to create SIMD decoder: {e:?}")))?, } }; for (i, shard_opt) in shards.iter().enumerate() { if let Some(shard) = shard_opt { if i < self.data_shards { decoder .add_original_shard(i, shard) .map_err(|e| io::Error::other(format!("Failed to add original shard for reconstruction: {e:?}")))?; } else { let recovery_idx = i - self.data_shards; decoder .add_recovery_shard(recovery_idx, shard) .map_err(|e| io::Error::other(format!("Failed to add recovery shard for reconstruction: {e:?}")))?; } } } let result = decoder .decode() .map_err(|e| io::Error::other(format!("SIMD decode error: {e:?}")))?; for (i, shard_opt) in shards.iter_mut().enumerate() { if shard_opt.is_none() && i < self.data_shards { for (restored_index, restored_data) in result.restored_original_iter() { if restored_index == i { *shard_opt = Some(restored_data.to_vec()); break; } } } } drop(result); *self .decoder_cache .write() .map_err(|_| io::Error::other("Failed to return decoder to cache"))? = Some(decoder); Ok(()) } fn reconstruct(&self, shards: &mut [Option>]) -> io::Result<()> { self.reconstruct_data(shards)?; self.encode_parity(shards) } fn verify(&self, shards: &[&[u8]]) -> io::Result { let expected_shards = self.data_shards + self.parity_shards; if shards.len() != expected_shards { return Err(io::Error::other(format!( "invalid shard count: got {}, expected {}", shards.len(), expected_shards ))); } if shards.iter().all(|shard| shard.is_empty()) { return Ok(true); } let mut expected = shards.iter().map(|shard| Some(shard.to_vec())).collect::>(); self.encode_parity(&mut expected)?; for index in self.data_shards..expected_shards { let Some(expected_parity) = expected[index].as_ref() else { return Err(io::Error::other(format!("missing parity shard {index} after verification encode"))); }; if expected_parity.as_slice() != shards[index] { return Ok(false); } } Ok(true) } fn encode_parity(&self, shards: &mut [Option>]) -> io::Result<()> { encode_parity_shards(shards, self.data_shards, self.parity_shards, |shards| self.encode(shards)) } } /// Reed-Solomon encoder using reed-solomon-erasure pub struct ReedSolomonEncoder { data_shards: usize, parity_shards: usize, encoder: Option, } impl Clone for ReedSolomonEncoder { fn clone(&self) -> Self { Self { data_shards: self.data_shards, parity_shards: self.parity_shards, encoder: self.encoder.clone(), } } } impl ReedSolomonEncoder { fn try_new_typed(data_shards: usize, parity_shards: usize) -> Result { let encoder = if parity_shards > 0 { Some(ReedSolomon::new(data_shards, parity_shards)?) } else { None }; Ok(ReedSolomonEncoder { data_shards, parity_shards, encoder, }) } /// Create a new Reed-Solomon encoder with specified data and parity shards. pub fn new(data_shards: usize, parity_shards: usize) -> io::Result { Self::try_new_typed(data_shards, parity_shards) .map_err(|error| io::Error::other(format!("Failed to create Reed-Solomon encoder: {error:?}"))) } /// Encode data shards with parity. pub fn encode(&self, shards: SmallVec<[&mut [u8]; 16]>) -> io::Result<()> { let mut shards_vec: Vec<&mut [u8]> = shards.into_vec(); if shards_vec.is_empty() { return Ok(()); } if let Some(ref rs) = self.encoder { rs.encode(&mut shards_vec) .map_err(|e| io::Error::other(format!("Reed-Solomon encode failed: {e:?}"))) } else { Ok(()) } } /// Reconstruct missing data shards. pub fn reconstruct_data(&self, shards: &mut [Option>]) -> io::Result<()> { if recover_empty_payload_data_shards(shards, self.data_shards, self.parity_shards)? { return Ok(()); } if let Some(ref rs) = self.encoder { rs.reconstruct_data(shards) .map_err(|e| io::Error::other(format!("Reed-Solomon reconstruct failed: {e:?}"))) } else { Ok(()) } } /// Reconstruct missing data shards and regenerate parity shards. pub fn reconstruct(&self, shards: &mut [Option>]) -> io::Result<()> { self.reconstruct_data(shards)?; self.encode_parity(shards) } pub fn verify(&self, shards: &[&[u8]]) -> io::Result { if shards.iter().all(|shard| shard.is_empty()) { return Ok(true); } if let Some(ref rs) = self.encoder { rs.verify(shards) .map_err(|e| io::Error::other(format!("Reed-Solomon verify failed: {e:?}"))) } else { Ok(true) } } fn encode_parity(&self, shards: &mut [Option>]) -> io::Result<()> { encode_parity_shards(shards, self.data_shards, self.parity_shards, |shards| self.encode(shards)) } } fn encode_parity_shards(shards: &mut [Option>], data_shards: usize, parity_shards: usize, encode: F) -> io::Result<()> where F: FnOnce(SmallVec<[&mut [u8]; 16]>) -> io::Result<()>, { let expected_shards = data_shards + parity_shards; if shards.len() != expected_shards { return Err(io::Error::other(format!( "invalid shard count: got {}, expected {}", shards.len(), expected_shards ))); } let shard_len = shards .iter() .find_map(|s| s.as_ref().map(Vec::len)) .ok_or_else(|| io::Error::other("No valid shards found for parity encoding"))?; for shard in shards.iter_mut().skip(data_shards) { if shard.is_none() { *shard = Some(vec![0; shard_len]); } } if shard_len == 0 { for (index, shard) in shards.iter().enumerate() { let shard = shard .as_ref() .ok_or_else(|| io::Error::other(format!("missing shard {index} after data reconstruction")))?; if !shard.is_empty() { return Err(io::Error::other(format!( "inconsistent shard length at index {index}: got {}, expected {}", shard.len(), shard_len ))); } } return Ok(()); } let mut shard_refs: SmallVec<[&mut [u8]; 16]> = SmallVec::new(); for (index, shard) in shards.iter_mut().enumerate() { let shard = shard .as_mut() .ok_or_else(|| io::Error::other(format!("missing shard {index} after data reconstruction")))?; if shard.len() != shard_len { return Err(io::Error::other(format!( "inconsistent shard length at index {index}: got {}, expected {}", shard.len(), shard_len ))); } shard_refs.push(shard.as_mut_slice()); } encode(shard_refs) } fn recover_empty_payload_data_shards( shards: &mut [Option>], data_shards: usize, parity_shards: usize, ) -> io::Result { let expected_shards = data_shards + parity_shards; if shards.len() != expected_shards { return Err(io::Error::other(format!( "invalid shard count: got {}, expected {}", shards.len(), expected_shards ))); } let mut has_present_shard = false; for shard in shards.iter().filter_map(|shard| shard.as_ref()) { has_present_shard = true; if !shard.is_empty() { return Ok(false); } } if !has_present_shard { return Ok(false); } for shard in shards.iter_mut().take(data_shards) { if shard.is_none() { *shard = Some(Vec::new()); } } Ok(true) } /// Erasure coding utility for data reliability using Reed-Solomon codes. /// /// This struct provides encoding and decoding of data into data and parity shards. /// It supports splitting data into multiple shards, generating parity for fault tolerance, /// and reconstructing lost shards. /// /// # Fields /// - `data_shards`: Number of data shards. /// - `parity_shards`: Number of parity shards. /// - `encoder`: Optional ReedSolomon encoder instance. /// - `block_size`: Block size for each shard. /// - `_id`: Unique identifier for the erasure instance. /// /// # Example /// ```ignore /// use rustfs_ecstore::api::erasure::Erasure; /// let erasure = Erasure::new(4, 2, 8); /// let data = b"hello world"; /// let shards = erasure.encode_data(data).expect("operation should succeed"); /// // Simulate loss and recovery... /// ``` pub struct Erasure { pub data_shards: usize, pub parity_shards: usize, encoder: Option, legacy_encoder: Option, pub block_size: usize, uses_legacy: bool, _id: Uuid, } impl Default for Erasure { fn default() -> Self { Self { data_shards: 0, parity_shards: 0, encoder: None, legacy_encoder: None, block_size: 0, uses_legacy: false, _id: Uuid::nil(), } } } impl Clone for Erasure { fn clone(&self) -> Self { Self { data_shards: self.data_shards, parity_shards: self.parity_shards, encoder: self.encoder.clone(), legacy_encoder: self.legacy_encoder.clone(), block_size: self.block_size, uses_legacy: self.uses_legacy, _id: self._id, // Shared by clones; this field is unused in hot paths. } } } pub fn calc_shard_size(block_size: usize, data_shards: usize) -> usize { block_size.div_ceil(data_shards) } impl Erasure { /// Create a new Erasure instance /// /// # Arguments /// * `data_shards` - Number of data shards. /// * `parity_shards` - Number of parity shards. /// * `block_size` - Block size for each shard. /// /// # Panics /// Panics when the dimensions are invalid or unsupported. RustFS production /// paths use [`Self::try_new`] instead. pub fn new(data_shards: usize, parity_shards: usize, block_size: usize) -> Self { match Self::try_new(data_shards, parity_shards, block_size) { Ok(erasure) => erasure, Err(error) => panic!("invalid erasure codec configuration: {error}"), } } /// Create a new Erasure instance with legacy format support. /// /// When `uses_legacy` is true, uses main-branch shard_size formula and reed-solomon-simd /// for decode/reconstruct (for reading and healing old-version files). /// /// # Panics /// Panics when the dimensions are invalid or unsupported. RustFS production /// paths use [`Self::try_new_with_options`] instead. pub fn new_with_options(data_shards: usize, parity_shards: usize, block_size: usize, uses_legacy: bool) -> Self { match Self::try_new_with_options(data_shards, parity_shards, block_size, uses_legacy) { Ok(erasure) => erasure, Err(error) => panic!("invalid erasure codec configuration: {error}"), } } /// Tries to create a modern Erasure codec. /// /// # Errors /// Returns [`ErasureConstructionError`] when dimensions are zero, /// overflow the shard count, exceed the modern codec's supported range, or /// the underlying encoder rejects the configuration. pub fn try_new(data_shards: usize, parity_shards: usize, block_size: usize) -> Result { Self::try_new_with_options(data_shards, parity_shards, block_size, false) } /// Tries to create an Erasure codec using the selected format backend. /// /// # Errors /// Returns [`ErasureConstructionError`] when dimensions are zero, /// overflow the shard count, are unsupported by the selected codec, or the /// selected encoder fails to initialize. pub fn try_new_with_options( data_shards: usize, parity_shards: usize, block_size: usize, uses_legacy: bool, ) -> Result { if data_shards == 0 { return Err(ErasureConstructionError::ZeroDataShards); } if block_size == 0 { return Err(ErasureConstructionError::ZeroBlockSize); } let total_shards = data_shards .checked_add(parity_shards) .ok_or(ErasureConstructionError::ShardCountOverflow { data_shards, parity_shards, })?; if parity_shards > 0 { if uses_legacy && (total_shards > reed_solomon_simd::engine::GF_ORDER || !reed_solomon_simd::ReedSolomonEncoder::supports(data_shards, parity_shards)) { return Err(ErasureConstructionError::UnsupportedLegacyShardCount { data_shards, parity_shards, }); } if !uses_legacy && total_shards > MODERN_MAX_TOTAL_SHARDS { return Err(ErasureConstructionError::UnsupportedModernShardCount { data_shards, parity_shards, }); } } let encoder = if !uses_legacy && parity_shards > 0 { Some( ReedSolomonEncoder::try_new_typed(data_shards, parity_shards) .map_err(|source| ErasureConstructionError::ModernEncoder { source })?, ) } else { None }; let legacy_encoder = if uses_legacy && parity_shards > 0 { Some( LegacyReedSolomonEncoder::new(data_shards, parity_shards) .map_err(|source| ErasureConstructionError::LegacyEncoder { source })?, ) } else { None }; Ok(Erasure { data_shards, parity_shards, block_size, encoder, legacy_encoder, uses_legacy, _id: Uuid::nil(), // Unused in hot paths; avoid CSPRNG syscall }) } /// Encode data into data and parity shards. /// /// # Arguments /// * `data` - The input data to encode. /// /// # Returns /// A vector of encoded shards as `Bytes`. #[tracing::instrument(level = "debug", skip_all, fields(data_len=data.len()))] #[hotpath::measure(impl_type = "Erasure")] pub fn encode_data(&self, data: &[u8]) -> io::Result> { let shard_size_fn = if self.uses_legacy { calc_shard_size_legacy } else { calc_shard_size }; let per_shard_size = shard_size_fn(data.len(), self.data_shards); if per_shard_size == 0 { return Ok(vec![Bytes::new(); self.total_shard_count()]); } let need_total_size = per_shard_size * self.total_shard_count(); let mut data_buffer = BytesMut::with_capacity(need_total_size); data_buffer.extend_from_slice(data); data_buffer.resize(need_total_size, 0u8); { let data_slices: SmallVec<[&mut [u8]; 16]> = data_buffer.chunks_exact_mut(per_shard_size).collect(); if self.parity_shards > 0 { if self.uses_legacy { if let Some(encoder) = self.legacy_encoder.as_ref() { encoder.encode(data_slices)?; } else { warn!("parity_shards > 0, uses_legacy but legacy_encoder is None"); } } else if let Some(encoder) = self.encoder.as_ref() { encoder.encode(data_slices)?; } else { warn!("parity_shards > 0, but encoder is None"); } } } // Zero-copy split, all shards reference data_buffer let mut data_buffer = data_buffer.freeze(); let mut shards = Vec::with_capacity(self.total_shard_count()); for _ in 0..self.total_shard_count() { let shard = data_buffer.split_to(per_shard_size); shards.push(shard); } Ok(shards) } /// Encode owned data, avoiding a copy when the caller already has a heap buffer. /// Falls back to copying into a new buffer if zero-copy conversion fails. #[hotpath::measure(impl_type = "Erasure")] pub fn encode_data_owned(&self, data: Vec) -> io::Result> { let shard_size_fn = if self.uses_legacy { calc_shard_size_legacy } else { calc_shard_size }; let per_shard_size = shard_size_fn(data.len(), self.data_shards); if per_shard_size == 0 { return Ok(vec![Bytes::new(); self.total_shard_count()]); } let need_total_size = per_shard_size * self.total_shard_count(); // Try zero-copy: Vec -> Bytes -> BytesMut (succeeds when refcount == 1) let mut data_buffer = match Bytes::from(data).try_into_mut() { Ok(mut bm) => { bm.resize(need_total_size, 0u8); bm } Err(b) => { // Rare path: refcount != 1, fall back to copy let mut bm = BytesMut::with_capacity(need_total_size); bm.extend_from_slice(&b); bm.resize(need_total_size, 0u8); bm } }; { let data_slices: SmallVec<[&mut [u8]; 16]> = data_buffer.chunks_exact_mut(per_shard_size).collect(); if self.parity_shards > 0 { if self.uses_legacy { if let Some(encoder) = self.legacy_encoder.as_ref() { encoder.encode(data_slices)?; } else { warn!("parity_shards > 0, uses_legacy but legacy_encoder is None"); } } else if let Some(encoder) = self.encoder.as_ref() { encoder.encode(data_slices)?; } else { warn!("parity_shards > 0, but encoder is None"); } } } let mut data_buffer = data_buffer.freeze(); let mut shards = Vec::with_capacity(self.total_shard_count()); for _ in 0..self.total_shard_count() { let shard = data_buffer.split_to(per_shard_size); shards.push(shard); } Ok(shards) } /// Encode data from an owned `BytesMut` buffer, avoiding the initial copy /// from a borrowed slice into a fresh `BytesMut`. /// /// Capacity contract: when the caller pre-reserves /// `shard_size() * total_shard_count()` bytes (the EC-expanded size of a full /// block), the `resize(need_total_size)` below stays within capacity for every /// `data_len <= block_size` — both shard-size formulas are monotone in /// `data_len` — so this function never reallocates the buffer. #[hotpath::measure(impl_type = "Erasure")] pub fn encode_data_bytes_mut(&self, mut data_buffer: BytesMut, data_len: usize) -> io::Result> { let shard_size_fn = if self.uses_legacy { calc_shard_size_legacy } else { calc_shard_size }; let per_shard_size = shard_size_fn(data_len, self.data_shards); if per_shard_size == 0 { return Ok(vec![Bytes::new(); self.total_shard_count()]); } let need_total_size = per_shard_size * self.total_shard_count(); if data_buffer.len() > data_len { data_buffer.truncate(data_len); } data_buffer.resize(need_total_size, 0u8); { let data_slices: SmallVec<[&mut [u8]; 16]> = data_buffer.chunks_exact_mut(per_shard_size).collect(); if self.parity_shards > 0 { if self.uses_legacy { if let Some(encoder) = self.legacy_encoder.as_ref() { encoder.encode(data_slices)?; } else { warn!("parity_shards > 0, uses_legacy but legacy_encoder is None"); } } else if let Some(encoder) = self.encoder.as_ref() { encoder.encode(data_slices)?; } else { warn!("parity_shards > 0, but encoder is None"); } } } let mut data_buffer = data_buffer.freeze(); let mut shards = Vec::with_capacity(self.total_shard_count()); for _ in 0..self.total_shard_count() { let shard = data_buffer.split_to(per_shard_size); shards.push(shard); } Ok(shards) } /// Decode and reconstruct missing data shards in-place. /// /// # Arguments /// * `shards` - Mutable slice of optional shard data. Missing shards should be `None`. /// /// # Returns /// Ok if reconstruction succeeds, error otherwise. #[hotpath::measure(impl_type = "Erasure")] pub fn decode_data(&self, shards: &mut [Option>]) -> io::Result<()> { if self.parity_shards > 0 { if self.uses_legacy { if let Some(encoder) = self.legacy_encoder.as_ref() { encoder.reconstruct_data(shards)?; } else { warn!("parity_shards > 0, uses_legacy but legacy_encoder is None"); } } else if let Some(encoder) = self.encoder.as_ref() { encoder.reconstruct_data(shards)?; } else { warn!("parity_shards > 0, but encoder is None"); } } Ok(()) } /// Decode and reconstruct missing data shards, then regenerate parity shards. #[hotpath::measure(impl_type = "Erasure")] pub fn decode_data_and_parity(&self, shards: &mut [Option>]) -> io::Result<()> { if self.parity_shards > 0 { if self.uses_legacy { if let Some(encoder) = self.legacy_encoder.as_ref() { encoder.reconstruct(shards)?; } else { warn!("parity_shards > 0, uses_legacy but legacy_encoder is None"); } } else if let Some(encoder) = self.encoder.as_ref() { encoder.reconstruct(shards)?; } else { warn!("parity_shards > 0, but encoder is None"); } } Ok(()) } pub(crate) fn decode_data_with_reconstruction_verification(&self, shards: &mut [Option>]) -> io::Result<()> { let missing_data_source = shards.iter().take(self.data_shards).any(|shard| shard.is_none()); let available_shards = shards.iter().filter(|shard| shard.is_some()).count(); let source_parity = if missing_data_source && available_shards > self.data_shards { shards .iter() .enumerate() .skip(self.data_shards) .filter_map(|(index, shard)| shard.as_ref().map(|shard| (index, shard.clone()))) .collect::>() } else { Vec::new() }; if source_parity.is_empty() { return self.decode_data(shards); } self.decode_data_and_parity(shards)?; for (index, source) in source_parity { let Some(rebuilt) = shards[index].as_ref() else { return Err(io::Error::new( io::ErrorKind::InvalidData, "missing rebuilt parity shard after read verification", )); }; if rebuilt != &source { warn!( shard_index = index, data_shards = self.data_shards, parity_shards = self.parity_shards, "erasure decode rejected inconsistent read source shards" ); return Err(io::Error::new(io::ErrorKind::InvalidData, "inconsistent read source shards")); } } Ok(()) } pub(crate) fn verify_data_and_parity(&self, shards: &[Option>]) -> io::Result { let expected_shards = self.total_shard_count(); if shards.len() != expected_shards { return Err(io::Error::other(format!( "invalid shard count: got {}, expected {}", shards.len(), expected_shards ))); } if self.parity_shards == 0 { return Ok(true); } let mut shard_refs = Vec::with_capacity(expected_shards); let mut shard_len = None; for (index, shard) in shards.iter().enumerate() { let shard = shard .as_deref() .ok_or_else(|| io::Error::other(format!("missing shard {index} for data/parity verification")))?; if let Some(expected_len) = shard_len { if shard.len() != expected_len { return Err(io::Error::other(format!( "inconsistent shard length at index {index}: got {}, expected {}", shard.len(), expected_len ))); } } else { shard_len = Some(shard.len()); } shard_refs.push(shard); } if self.uses_legacy { if let Some(encoder) = self.legacy_encoder.as_ref() { encoder.verify(&shard_refs) } else { Err(io::Error::other("parity_shards > 0, uses_legacy but legacy_encoder is None")) } } else if let Some(encoder) = self.encoder.as_ref() { encoder.verify(&shard_refs) } else { Err(io::Error::other("parity_shards > 0, but encoder is None")) } } /// Get the total number of shards (data + parity). pub fn total_shard_count(&self) -> usize { self.data_shards + self.parity_shards } /// Whether the erasure dimensions are safe for the shard/offset arithmetic. /// /// `block_size` and `data_shards` come straight from on-disk metadata; a /// corrupted or crafted `xl.meta` can carry zero values that would panic the /// `block_size`/`data_shards` divisions in [`Self::shard_size`], /// [`Self::shard_file_size`] and [`Self::shard_file_offset`]. Read paths must /// reject such metadata before performing those divisions. pub fn has_valid_dimensions(&self) -> bool { self.block_size > 0 && self.data_shards > 0 } // /// Calculate the shard size and total size for a given data size. // // Returns (shard_size, total_size) for the given data size // fn need_size(&self, data_size: usize) -> (usize, usize) { // let shard_size = self.shard_size(data_size); // (shard_size, shard_size * (self.total_shard_count())) // } /// Calculate the size of each shard. pub fn shard_size(&self) -> usize { if self.uses_legacy { calc_shard_size_legacy(self.block_size, self.data_shards) } else { calc_shard_size(self.block_size, self.data_shards) } } /// Calculate the total erasure file size for a given original size. // Returns the final erasure size from the original size pub fn shard_file_size(&self, total_length: i64) -> i64 { if total_length == 0 { return 0; } if total_length < 0 { return total_length; } let total_length = total_length as usize; let shard_size_fn = if self.uses_legacy { calc_shard_size_legacy } else { calc_shard_size }; let num_shards = total_length / self.block_size; let last_block_size = total_length % self.block_size; let last_shard_size = shard_size_fn(last_block_size, self.data_shards); (num_shards * self.shard_size() + last_shard_size) as i64 } /// Calculate the offset in the erasure file where reading begins. // Returns the offset in the erasure file where reading begins pub fn shard_file_offset(&self, start_offset: usize, length: usize, total_length: usize) -> usize { let shard_size = self.shard_size(); let shard_file_size = self.shard_file_size(total_length as i64) as usize; let end_shard = (start_offset + length) / self.block_size; let mut till_offset = end_shard * shard_size + shard_size; if till_offset > shard_file_size { till_offset = shard_file_size; } till_offset } /// Encode all data from a reader in blocks, calling an async callback for each encoded block. /// This method is async and returns the total bytes read after all blocks are processed. /// /// # Arguments /// * `reader` - An async reader implementing AsyncRead + Send + Sync + Unpin /// * `mut on_block` - Async callback that receives encoded blocks and returns a Result /// * `F` - Callback type: FnMut(Result, std::io::Error>) -> Future> + Send /// * `Fut` - Future type returned by the callback /// * `E` - Error type returned by the callback /// * `R` - Reader type implementing AsyncRead + Send + Sync + Unpin /// /// # Returns /// Result containing total bytes read, or error from callback /// /// # Errors /// Returns error if reading from reader fails or if callback returns error pub(crate) async fn encode_stream_callback_async( self: std::sync::Arc, reader: &mut R, mut on_block: F, ) -> Result where R: AsyncRead + Send + Sync + Unpin, F: FnMut(io::Result>) -> Fut + Send, Fut: Future> + Send, { if self.block_size == 0 { on_block(Err(io::Error::new(io::ErrorKind::InvalidInput, "erasure block_size must be non-zero"))).await?; return Ok(0); } let block_size = self.block_size; let mut total = 0; let mut buf = vec![0u8; block_size]; loop { match rustfs_utils::read_full_or_eof(&mut *reader, &mut buf).await { Ok(Some(n)) => { debug_assert!(n > 0, "non-zero block_size prevents zero-length reads"); warn!("encode_stream_callback_async read n={}", n); total += n; let erasure = self.clone(); let encode_buf = std::mem::take(&mut buf); let (res, returned_buf) = match tokio::task::spawn_blocking(move || { let res = erasure.encode_data(&encode_buf[..n]); (res, encode_buf) }) .await { Ok(result) => result, Err(err) => { on_block(Err(io::Error::other(format!("EC encode task failed: {err}")))).await?; break; } }; buf = returned_buf; on_block(res).await? } Ok(None) => { warn!("encode_stream_callback_async read unexpected ok"); break; } Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => { warn!("encode_stream_callback_async read unexpected eof"); break; } Err(e) => { warn!("encode_stream_callback_async read error={:?}", e); on_block(Err(e)).await?; break; } } } Ok(total) } } #[cfg(test)] mod tests { use super::*; use proptest::collection::{btree_set, vec}; use proptest::prelude::*; use std::pin::Pin; use std::task::{Context, Poll}; use tokio::io::ReadBuf; fn erasure_with_invalid_dimensions(data_shards: usize, parity_shards: usize, block_size: usize) -> Erasure { Erasure { data_shards, parity_shards, block_size, ..Default::default() } } fn optional_shards(shards: &[Bytes]) -> Vec>> { shards.iter().map(|shard| Some(shard.to_vec())).collect() } fn recover_data(shards: &[Option>], data_shards: usize, original_len: usize) -> Vec { let mut recovered = Vec::new(); for shard in shards.iter().take(data_shards) { recovered.extend_from_slice( shard .as_ref() .expect("reconstructed data shard should be present after decode_data_and_parity"), ); } recovered.truncate(original_len); recovered } fn erasure_recoverability_case_strategy() -> impl Strategy, std::collections::BTreeSet)> { (2usize..=8, 1usize..=4, prop::sample::select(vec![64usize, 256, 1024]), any::()).prop_flat_map( |(data_shards, parity_shards, block_size, uses_legacy)| { let total_shards = data_shards + parity_shards; ( Just(data_shards), Just(parity_shards), Just(block_size), Just(uses_legacy), vec(any::(), 0..=4096), btree_set(0usize..total_shards, 0..=parity_shards), ) }, ) } fn assert_owned_encode_matches_borrowed(erasure: &Erasure, data: Vec) { let borrowed = erasure.encode_data(&data).expect("borrowed encode should succeed"); let owned = erasure.encode_data_owned(data).expect("owned encode should succeed"); assert_eq!(owned, borrowed); } struct ErrorAfterPartialReader { emitted: bool, } impl AsyncRead for ErrorAfterPartialReader { fn poll_read(mut self: Pin<&mut Self>, _cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { if !self.emitted { self.emitted = true; buf.put_slice(&[1]); return Poll::Ready(Ok(())); } Poll::Ready(Err(io::Error::new(io::ErrorKind::BrokenPipe, "partial read failure"))) } } struct ImmediateErrorReader; impl AsyncRead for ImmediateErrorReader { fn poll_read(self: Pin<&mut Self>, _cx: &mut Context<'_>, _buf: &mut ReadBuf<'_>) -> Poll> { Poll::Ready(Err(io::Error::other("immediate read failure"))) } } #[test] fn try_new_rejects_zero_and_overflowing_dimensions_with_typed_errors() { let Err(zero_data) = Erasure::try_new(0, 2, 64) else { panic!("zero data shards must be rejected"); }; assert!(matches!(zero_data, ErasureConstructionError::ZeroDataShards)); let Err(zero_block) = Erasure::try_new(2, 2, 0) else { panic!("zero block size must be rejected"); }; assert!(matches!(zero_block, ErasureConstructionError::ZeroBlockSize)); let Err(overflow) = Erasure::try_new(usize::MAX, 1, 64) else { panic!("overflowing shard counts must be rejected"); }; assert!(matches!( overflow, ErasureConstructionError::ShardCountOverflow { data_shards: usize::MAX, parity_shards: 1, } )); } #[test] fn try_new_rejects_backend_specific_unsupported_dimensions() { let Err(modern) = Erasure::try_new(256, 1, 64) else { panic!("modern dimensions beyond GF(2^8) must be rejected"); }; assert!(matches!( modern, ErasureConstructionError::UnsupportedModernShardCount { data_shards: 256, parity_shards: 1, } )); let Err(legacy) = Erasure::try_new_with_options(60_000, 5_000, 64, true) else { panic!("unsupported legacy SIMD dimensions must be rejected"); }; assert!(matches!( legacy, ErasureConstructionError::UnsupportedLegacyShardCount { data_shards: 60_000, parity_shards: 5_000, } )); } #[test] fn try_new_accepts_exact_backend_shard_limits() { let modern_data_shards = MODERN_MAX_TOTAL_SHARDS - 1; let modern = Erasure::try_new(modern_data_shards, 1, 64).expect("the modern codec's exact field-size boundary must remain valid"); assert_eq!(modern.total_shard_count(), MODERN_MAX_TOTAL_SHARDS); assert!(modern.encoder.is_some()); assert!(modern.legacy_encoder.is_none()); let legacy_data_shards = reed_solomon_simd::engine::GF_ORDER / 2; let legacy_parity_shards = reed_solomon_simd::engine::GF_ORDER - legacy_data_shards; let legacy = Erasure::try_new_with_options(legacy_data_shards, legacy_parity_shards, 64, true) .expect("the legacy codec's exact field-size boundary must remain valid"); assert_eq!(legacy.total_shard_count(), reed_solomon_simd::engine::GF_ORDER); assert!(legacy.encoder.is_none()); assert!(legacy.legacy_encoder.is_some()); } #[test] fn try_new_accepts_zero_parity_without_backend_limits() { let no_parity = Erasure::try_new(2, 0, 64).expect("zero parity is a valid generic codec configuration"); assert_eq!(no_parity.total_shard_count(), 2); assert!(no_parity.encoder.is_none()); assert!(no_parity.legacy_encoder.is_none()); let large_modern = Erasure::try_new(257, 0, 64).expect("zero parity must not inherit the modern backend shard limit"); assert_eq!(large_modern.total_shard_count(), 257); assert!(large_modern.encoder.is_none()); assert!(large_modern.legacy_encoder.is_none()); let legacy_data_shards = reed_solomon_simd::engine::GF_ORDER + 1; let large_legacy = Erasure::try_new_with_options(legacy_data_shards, 0, 64, true) .expect("zero parity must not inherit the legacy backend shard limit"); assert_eq!(large_legacy.total_shard_count(), legacy_data_shards); assert!(large_legacy.encoder.is_none()); assert!(large_legacy.legacy_encoder.is_none()); } #[test] fn try_new_accepts_generic_layouts_above_storage_drive_limits() { let erasure = Erasure::try_new(2, 3, 64).expect("generic 2+3 erasure coding must remain supported"); assert_eq!(erasure.total_shard_count(), 5); assert!(erasure.encoder.is_some()); assert!(erasure.legacy_encoder.is_none()); let modern = Erasure::try_new(9, 8, 64).expect("the codec must not impose the storage layer's drive limit"); assert_eq!(modern.total_shard_count(), 17); assert!(modern.encoder.is_some()); assert!(modern.legacy_encoder.is_none()); let legacy = Erasure::try_new_with_options(9, 8, 64, true) .expect("the legacy codec must not impose the storage layer's drive limit"); assert_eq!(legacy.total_shard_count(), 17); assert!(legacy.encoder.is_none()); assert!(legacy.legacy_encoder.is_some()); } #[test] fn try_new_with_options_initializes_only_the_selected_backend() { let modern = Erasure::try_new_with_options(4, 2, 64, false).expect("modern codec should construct"); assert!(modern.encoder.is_some()); assert!(modern.legacy_encoder.is_none()); let legacy = Erasure::try_new_with_options(4, 2, 64, true).expect("legacy codec should construct"); assert!(legacy.encoder.is_none()); assert!(legacy.legacy_encoder.is_some()); } #[test] fn construction_errors_preserve_encoder_sources() { let modern = ErasureConstructionError::ModernEncoder { source: reed_solomon_erasure::Error::TooManyShards, }; assert!( std::error::Error::source(&modern) .and_then(|source| source.downcast_ref::()) .is_some() ); let legacy = ErasureConstructionError::LegacyEncoder { source: io::Error::other("legacy encoder failure"), }; assert!( std::error::Error::source(&legacy) .and_then(|source| source.downcast_ref::()) .is_some() ); } #[test] #[should_panic(expected = "invalid erasure codec configuration")] fn new_panics_on_invalid_dimensions() { let _ = Erasure::new(0, 1, 64); } #[test] #[should_panic(expected = "invalid erasure codec configuration")] fn new_with_options_panics_on_invalid_dimensions() { let _ = Erasure::new_with_options(1, 1, 0, true); } #[test] fn has_valid_dimensions_rejects_zero_block_size_or_data_shards() { // Well-formed erasure metadata is accepted. assert!(Erasure::new(4, 2, 64).has_valid_dimensions()); assert!(Erasure::new_with_options(6, 4, 1, true).has_valid_dimensions()); // Corrupted on-disk metadata with a zero block_size or zero data_shards // must be rejected before it reaches the shard/offset divisions. assert!(!erasure_with_invalid_dimensions(4, 2, 0).has_valid_dimensions()); assert!(!erasure_with_invalid_dimensions(0, 0, 64).has_valid_dimensions()); assert!(!erasure_with_invalid_dimensions(0, 0, 0).has_valid_dimensions()); } #[tokio::test] async fn encode_stream_callback_async_stops_on_reader_errors() { let erasure = std::sync::Arc::new(Erasure::new(2, 1, 4)); let mut partial_error = ErrorAfterPartialReader { emitted: false }; let mut partial_callbacks = Vec::new(); let total = erasure .clone() .encode_stream_callback_async(&mut partial_error, |result| { partial_callbacks.push(result.map(|blocks| blocks.len()).map_err(|err| err.kind())); async { Ok::<(), io::Error>(()) } }) .await .expect("partial read error should not make callback fail"); assert_eq!(total, 0); assert!( partial_callbacks.is_empty(), "unexpected EOF after a partial read should stop without emitting a block" ); let mut immediate_error = ImmediateErrorReader; let mut immediate_callbacks = Vec::new(); let total = erasure .encode_stream_callback_async(&mut immediate_error, |result| { immediate_callbacks.push(result.map(|blocks| blocks.len()).map_err(|err| err.kind())); async { Ok::<(), io::Error>(()) } }) .await .expect("immediate read error should be delivered to callback"); assert_eq!(total, 0); assert_eq!(immediate_callbacks, vec![Err(io::ErrorKind::Other)]); } #[test] fn default_and_legacy_clone_preserve_safe_zero_state_and_restore_data() { let default = Erasure::default(); assert_eq!(default.total_shard_count(), 0); assert!(!default.has_valid_dimensions()); assert_eq!(default.shard_file_size(0), 0); assert_eq!(default.shard_file_size(-7), -7); let legacy = Erasure::new_with_options(2, 2, 64, true); let cloned = legacy.clone(); assert_eq!(cloned.data_shards, legacy.data_shards); assert_eq!(cloned.parity_shards, legacy.parity_shards); assert_eq!(cloned.block_size, legacy.block_size); assert!(cloned.uses_legacy); let data = b"legacy clone should keep independent SIMD caches"; let encoded = cloned.encode_data(data).expect("legacy clone should encode"); let mut shards = optional_shards(&encoded); shards[0] = None; cloned.decode_data(&mut shards).expect("legacy clone should decode"); assert_eq!(recover_data(&shards, cloned.data_shards, data.len()), data); } #[test] fn legacy_verify_reports_invalid_empty_valid_and_corrupt_parity_sets() { let legacy = LegacyReedSolomonEncoder::new(2, 2).expect("legacy encoder should construct"); let wrong_count: [&[u8]; 1] = [&[]]; let err = legacy.verify(&wrong_count).expect_err("wrong shard count must be rejected"); assert!(err.to_string().contains("invalid shard count")); let empty: [&[u8]; 4] = [&[], &[], &[], &[]]; assert!( legacy .verify(&empty) .expect("all-empty legacy shards are internally consistent") ); let erasure = Erasure::new_with_options(2, 2, 64, true); let encoded = erasure .encode_data(b"legacy verify should compare regenerated parity") .expect("legacy encode should succeed"); let refs = encoded.iter().map(Bytes::as_ref).collect::>(); assert!(legacy.verify(&refs).expect("valid legacy shards should verify")); let mut corrupt = encoded.iter().map(|shard| shard.to_vec()).collect::>(); corrupt[erasure.data_shards][0] ^= 0x80; let corrupt_refs = corrupt.iter().map(Vec::as_slice).collect::>(); assert!(!legacy.verify(&corrupt_refs).expect("corrupt parity should be detected")); } #[test] fn parity_helper_and_empty_payload_recovery_fail_closed_on_malformed_shards() { let mut wrong_count = vec![Some(vec![1])]; let err = encode_parity_shards(&mut wrong_count, 2, 1, |_| panic!("wrong count must fail before encode")) .expect_err("wrong shard count must be rejected"); assert!(err.to_string().contains("invalid shard count")); let mut inconsistent_empty = vec![Some(Vec::new()), Some(vec![1])]; let err = encode_parity_shards(&mut inconsistent_empty, 1, 1, |_| panic!("zero-length mismatch must fail before encode")) .expect_err("mixed empty and non-empty shards must be rejected"); assert!(err.to_string().contains("inconsistent shard length")); let mut inconsistent_non_empty = vec![Some(vec![1]), Some(vec![2, 3])]; let err = encode_parity_shards(&mut inconsistent_non_empty, 1, 1, |_| { panic!("non-empty length mismatch must fail before encode") }) .expect_err("mismatched non-empty shards must be rejected"); assert!(err.to_string().contains("inconsistent shard length")); let mut no_present_empty_payload = vec![None, None, None]; assert!( !recover_empty_payload_data_shards(&mut no_present_empty_payload, 2, 1) .expect("all-missing empty payload marker should not be synthesized") ); } #[test] fn verify_data_and_parity_handles_zero_parity_and_rejects_wrong_count() { let no_parity = Erasure::new(2, 0, 64); assert!( no_parity .verify_data_and_parity(&[Some(vec![1]), Some(vec![2, 3])]) .expect("zero parity requires no parity verification") ); let erasure = Erasure::new(2, 2, 64); let err = erasure .verify_data_and_parity(&[Some(Vec::new())]) .expect_err("wrong shard count must fail verification"); assert!(err.to_string().contains("invalid shard count")); } #[test] fn encode_data_owned_matches_borrowed_path() { for uses_legacy in [false, true] { let erasure = Erasure::new_with_options(4, 2, 64, uses_legacy); assert_owned_encode_matches_borrowed(&erasure, Vec::new()); assert_owned_encode_matches_borrowed(&erasure, b"small payload".to_vec()); assert_owned_encode_matches_borrowed(&erasure, (0_u8..37).collect()); } } #[test] fn encode_data_bytes_mut_matches_borrowed_path() { for uses_legacy in [false, true] { let erasure = Erasure::new_with_options(4, 2, 64, uses_legacy); let block_size = erasure.block_size; let expanded_block_bytes = erasure.shard_size() * erasure.total_shard_count(); let cases: Vec> = vec![ Vec::new(), b"small payload".to_vec(), (0_u8..37).collect(), vec![0xA5; block_size - 1], // last block one byte short of full vec![0x5A; block_size], // exactly one full block ]; for data in cases { let borrowed = erasure.encode_data(&data).expect("borrowed encode should succeed"); let owned = erasure .encode_data_bytes_mut(BytesMut::from(&data[..]), data.len()) .expect("bytesmut encode should succeed"); assert_eq!(owned, borrowed); // Ingest-shaped buffer: spare capacity pre-reserved for the EC-expanded // block, exactly what the HP-10 streaming ingest path hands over. let mut ingest = BytesMut::with_capacity(expanded_block_bytes.max(block_size)); ingest.extend_from_slice(&data); let preallocated = erasure .encode_data_bytes_mut(ingest, data.len()) .expect("preallocated bytesmut encode should succeed"); assert_eq!(preallocated, borrowed); // Buffer longer than data_len (stale bytes past the logical block) // must be truncated before padding. let mut oversized = BytesMut::from(&data[..]); oversized.extend_from_slice(&[0xFF; 8]); let truncated = erasure .encode_data_bytes_mut(oversized, data.len()) .expect("oversized bytesmut encode should succeed"); assert_eq!(truncated, borrowed); } } } /// HP-10 capacity invariant: both shard-size formulas are monotone in `data_len`, /// so pre-reserving `shard_size(block_size) * total_shard_count` covers the /// `need_total_size` of every block-or-smaller payload and the ingest buffer /// never reallocates inside `encode_data_bytes_mut`. #[test] fn shard_size_monotonicity_bounds_expanded_block_capacity() { for shard_fn in [calc_shard_size, calc_shard_size_legacy] { for data_shards in 1usize..=16 { for block_size in [1usize, 2, 63, 64, 65, 1024, 4096] { let full = shard_fn(block_size, data_shards); let mut prev = shard_fn(0, data_shards); for data_len in 1..=block_size { let cur = shard_fn(data_len, data_shards); assert!(cur >= prev, "shard size must be monotone (len {data_len}, shards {data_shards})"); assert!(cur <= full, "per-block shard size must not exceed the full-block bound"); prev = cur; } } // Spot-check the production-scale block size at its boundaries. let block_size = 1usize << 20; let full = shard_fn(block_size, data_shards); for data_len in [0usize, 1, block_size / 2, block_size - 1, block_size] { assert!(shard_fn(data_len, data_shards) <= full); } } } } #[test] fn decode_data_keeps_missing_parity_shard_unreconstructed() { let erasure = Erasure::new(2, 2, 64); let data = b"read decode should not rebuild parity"; let encoded = erasure.encode_data(data).expect("encode should succeed"); let mut shards = optional_shards(&encoded); let missing_parity = erasure.data_shards; shards[missing_parity] = None; erasure.decode_data(&mut shards).expect("decode should succeed"); assert!(shards[missing_parity].is_none(), "read decode should leave parity missing"); for index in 0..erasure.data_shards { assert_eq!( shards[index].as_deref(), Some(encoded[index].as_ref()), "data shard {index} should remain unchanged" ); } } #[test] fn legacy_decode_data_keeps_missing_parity_shard_unreconstructed() { let erasure = Erasure::new_with_options(2, 2, 64, true); let data = b"legacy read decode should not rebuild parity"; let encoded = erasure.encode_data(data).expect("encode should succeed"); let mut shards = optional_shards(&encoded); let missing_parity = erasure.data_shards + 1; shards[missing_parity] = None; erasure.decode_data(&mut shards).expect("decode should succeed"); assert!(shards[missing_parity].is_none(), "legacy read decode should leave parity missing"); for index in 0..erasure.data_shards { assert_eq!( shards[index].as_deref(), Some(encoded[index].as_ref()), "legacy data shard {index} should remain unchanged" ); } } #[test] fn decode_data_rejects_missing_data_above_parity_budget() { for uses_legacy in [false, true] { let erasure = Erasure::new_with_options(3, 2, 64, uses_legacy); let data = b"read restore must fail when too many data shards are gone"; let encoded = erasure.encode_data(data).expect("encode should succeed"); let mut shards = optional_shards(&encoded); shards[0] = None; shards[1] = None; shards[2] = None; let err = erasure .decode_data(&mut shards) .expect_err("decode_data must fail when missing data shards exceed parity"); assert!(!err.to_string().is_empty()); assert!(shards.iter().take(erasure.data_shards).any(Option::is_none)); } } #[test] fn decode_data_and_parity_rejects_wrong_shard_count() { for uses_legacy in [false, true] { let erasure = Erasure::new_with_options(4, 2, 128, uses_legacy); let data = b"wrong shard count should not be accepted as a restored object"; let encoded = erasure.encode_data(data).expect("encode should succeed"); let mut shards = optional_shards(&encoded); let _ = shards.pop(); let err = erasure .decode_data_and_parity(&mut shards) .expect_err("decode_data_and_parity must reject wrong shard count"); assert!(!err.to_string().is_empty()); } } #[test] fn decode_data_with_verification_rejects_corrupt_surplus_parity() { let erasure = Erasure::new(3, 2, 128); let data = b"verified reads must fail closed on stale surplus parity"; let encoded = erasure.encode_data(data).expect("encode should succeed"); let mut shards = optional_shards(&encoded); shards[1] = None; shards[erasure.data_shards].as_mut().expect("parity shard should be present")[0] ^= 0x7d; let err = erasure .decode_data_with_reconstruction_verification(&mut shards) .expect_err("verified decode must reject inconsistent parity"); assert_eq!(err.kind(), io::ErrorKind::InvalidData); } #[test] fn verify_data_and_parity_rejects_missing_and_mismatched_shards() { let erasure = Erasure::new(4, 2, 128); let data = b"verification must reject incomplete or malformed shard sets"; let encoded = erasure.encode_data(data).expect("encode should succeed"); let mut shards = optional_shards(&encoded); shards[0] = None; let missing = erasure .verify_data_and_parity(&shards) .expect_err("verification must reject missing shards"); assert!(missing.to_string().contains("missing shard")); let mut mismatched = optional_shards(&encoded); let _ = mismatched[0].as_mut().expect("data shard should be present").pop(); let mismatch = erasure .verify_data_and_parity(&mismatched) .expect_err("verification must reject mismatched shard lengths"); assert!(mismatch.to_string().contains("inconsistent shard length")); } #[test] fn decode_data_and_parity_leaves_complete_shards_unchanged() { let erasure = Erasure::new(4, 2, 128); let data = b"complete shards should not be changed"; let encoded = erasure.encode_data(data).expect("encode should succeed"); let original = optional_shards(&encoded); let mut shards = original.clone(); erasure .decode_data_and_parity(&mut shards) .expect("decode should succeed without missing shards"); assert_eq!(shards, original); } #[test] fn decode_data_and_parity_reconstructs_missing_parity_shard() { let erasure = Erasure::new(2, 2, 64); let data = b"parity shard must be rebuilt"; let encoded = erasure.encode_data(data).expect("encode should succeed"); let mut shards = optional_shards(&encoded); shards[2] = None; erasure .decode_data_and_parity(&mut shards) .expect("decode should rebuild parity"); for (index, shard) in shards.iter().enumerate() { assert_eq!( shard.as_deref(), Some(encoded[index].as_ref()), "shard {index} should match encoded source" ); } } #[test] fn decode_data_and_parity_reconstructs_missing_data_and_parity_shards() { let erasure = Erasure::new(4, 2, 128); let data = b"data and parity shards should both be reconstructed"; let encoded = erasure.encode_data(data).expect("encode should succeed"); let mut shards = optional_shards(&encoded); shards[1] = None; shards[4] = None; erasure .decode_data_and_parity(&mut shards) .expect("decode should rebuild all missing shards"); for (index, shard) in shards.iter().enumerate() { assert_eq!( shard.as_deref(), Some(encoded[index].as_ref()), "shard {index} should match encoded source" ); } } #[test] fn legacy_decode_data_and_parity_reconstructs_missing_parity_shard() { let erasure = Erasure::new_with_options(2, 2, 64, true); let data = b"legacy parity shard must be rebuilt"; let encoded = erasure.encode_data(data).expect("encode should succeed"); let mut shards = optional_shards(&encoded); shards[3] = None; erasure .decode_data_and_parity(&mut shards) .expect("decode should rebuild parity"); for (index, shard) in shards.iter().enumerate() { assert_eq!( shard.as_deref(), Some(encoded[index].as_ref()), "shard {index} should match encoded source" ); } } #[test] fn legacy_decode_data_and_parity_reconstructs_empty_object_shards() { let erasure = Erasure::new_with_options(3, 3, 64, true); let encoded = erasure.encode_data(&[]).expect("empty encode should succeed"); let mut shards = optional_shards(&encoded); shards[1] = None; shards[4] = None; erasure .decode_data_and_parity(&mut shards) .expect("empty decode should rebuild missing shards without SIMD"); for (index, shard) in shards.iter().enumerate() { assert_eq!( shard.as_deref(), Some(encoded[index].as_ref()), "empty shard {index} should match encoded source" ); } } #[test] fn test_shard_file_size_cases2() { let erasure = Erasure::new(12, 4, 1024 * 1024); assert_eq!(erasure.shard_file_size(1572864), 131073); } proptest! { #[test] fn decode_data_and_parity_round_trips_bounded_recoverability( (data_shards, parity_shards, block_size, uses_legacy, data, missing_indices) in erasure_recoverability_case_strategy(), ) { let erasure = Erasure::new_with_options(data_shards, parity_shards, block_size, uses_legacy); let encoded = erasure.encode_data(&data) .expect("encode_data should succeed for bounded recoverability property cases"); let mut shards = optional_shards(&encoded); for index in &missing_indices { shards[*index] = None; } erasure.decode_data_and_parity(&mut shards) .expect("decode_data_and_parity should recover when missing shard count does not exceed parity"); let recovered = recover_data(&shards, data_shards, data.len()); prop_assert_eq!(recovered, data); for (index, shard) in shards.iter().enumerate() { prop_assert_eq!( shard.as_deref(), Some(encoded[index].as_ref()), "reconstructed shard {} should match the original encoded shard", index ); } } } #[test] fn test_shard_file_size_cases() { let erasure = Erasure::new(4, 2, 8); // Case 1: total_length == 0 assert_eq!(erasure.shard_file_size(0), 0); // Case 2: total_length < block_size assert_eq!(erasure.shard_file_size(5), 2); // 5 div_ceil 4 = 2 // Case 3: total_length == block_size assert_eq!(erasure.shard_file_size(8), 2); // Case 4: total_length > block_size, not aligned assert_eq!(erasure.shard_file_size(13), 4); // 8/8=1, last=5, 5 div_ceil 4=2, 1*2+2=4 // Case 5: total_length > block_size, aligned assert_eq!(erasure.shard_file_size(16), 4); // 16/8=2, last=0, 2*2+0=4 // MinIO-compatible: 1248739/8=156092, last=3, ceil(3/4)=1, 156092*2+1=312185 assert_eq!(erasure.shard_file_size(1248739), 312185); // MinIO-compatible: 43/8=5, last=3, ceil(3/4)=1, 5*2+1=11 assert_eq!(erasure.shard_file_size(43), 11); // 1572864 with block_size=8: 196608 full blocks, last=0, 196608*2+0=393216 assert_eq!(erasure.shard_file_size(1572864), 393216); } #[test] fn test_encode_decode_roundtrip() { let data_shards = 4; let parity_shards = 2; let block_size = 1024; // SIMD mode let erasure = Erasure::new(data_shards, parity_shards, block_size); // Use sufficient test data for SIMD optimization let test_data = b"SIMD mode test data for encoding and decoding roundtrip verification with sufficient length to ensure shard size requirements are met for proper SIMD optimization.".repeat(20); // ~3KB for SIMD let data = &test_data; let encoded_shards = erasure.encode_data(data).expect("operation should succeed"); assert_eq!(encoded_shards.len(), data_shards + parity_shards); // Create decode input with some shards missing, convert to the format expected by decode_data let mut decode_input: Vec>> = vec![None; data_shards + parity_shards]; for i in 0..data_shards { decode_input[i] = Some(encoded_shards[i].to_vec()); } erasure.decode_data(&mut decode_input).expect("operation should succeed"); // Recover original data let mut recovered = Vec::new(); for shard in decode_input.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(data.len()); assert_eq!(&recovered, data); } #[test] fn test_encode_decode_large_1m() { let data_shards = 4; let parity_shards = 2; let block_size = 512 * 3; // SIMD mode let erasure = Erasure::new(data_shards, parity_shards, block_size); // Generate 1MB test data let data: Vec = (0..1048576).map(|i| (i % 256) as u8).collect(); let encoded_shards = erasure.encode_data(&data).expect("operation should succeed"); assert_eq!(encoded_shards.len(), data_shards + parity_shards); // Create decode input with some shards missing, convert to the format expected by decode_data let mut decode_input: Vec>> = vec![None; data_shards + parity_shards]; for i in 0..data_shards { decode_input[i] = Some(encoded_shards[i].to_vec()); } erasure.decode_data(&mut decode_input).expect("operation should succeed"); // Recover original data let mut recovered = Vec::new(); for shard in decode_input.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(data.len()); assert_eq!(recovered, data); } #[test] fn test_encode_all_zero_data() { let data_shards = 3; let parity_shards = 2; let block_size = 6; let erasure = Erasure::new(data_shards, parity_shards, block_size); let data = vec![0u8; block_size]; let shards = erasure.encode_data(&data).expect("operation should succeed"); assert_eq!(shards.len(), data_shards + parity_shards); let total_len: usize = shards.iter().map(|b| b.len()).sum(); assert_eq!(total_len, erasure.shard_size() * (data_shards + parity_shards)); } #[test] fn test_shard_size_and_file_size() { let erasure = Erasure::new(4, 2, 8); assert_eq!(erasure.shard_file_size(33), 9); assert_eq!(erasure.shard_file_size(0), 0); } #[test] fn test_legacy_shard_size_and_file_size() { let erasure = Erasure::new_with_options(4, 2, 8, true); assert_eq!(erasure.shard_size(), 2); assert_eq!(calc_shard_size_legacy(8, 4), 2); assert_eq!(calc_shard_size_legacy(1, 4), 2); assert_eq!(erasure.shard_file_size(33), 10); assert_eq!(erasure.shard_file_size(0), 0); } #[test] fn test_legacy_encode_decode_roundtrip() { let data_shards = 4; let parity_shards = 2; let block_size = 1024; let erasure = Erasure::new_with_options(data_shards, parity_shards, block_size, true); let data = b"Legacy encode/decode roundtrip test data with sufficient length.".repeat(20); let encoded_shards = erasure.encode_data(&data).expect("operation should succeed"); assert_eq!(encoded_shards.len(), data_shards + parity_shards); let mut decode_input: Vec>> = vec![None; data_shards + parity_shards]; for i in 0..data_shards { decode_input[i] = Some(encoded_shards[i].to_vec()); } erasure.decode_data(&mut decode_input).expect("operation should succeed"); let mut recovered = Vec::new(); for shard in decode_input.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(data.len()); assert_eq!(&recovered, &data); } #[test] fn test_legacy_decode_with_missing_shards() { let data_shards = 4; let parity_shards = 2; let block_size = 256; let erasure = Erasure::new_with_options(data_shards, parity_shards, block_size, true); let data = b"Legacy decode with missing shards test.".repeat(10); let encoded_shards = erasure.encode_data(&data).expect("operation should succeed"); let mut shards_opt: Vec>> = encoded_shards.iter().map(|s| Some(s.to_vec())).collect(); shards_opt[1] = None; shards_opt[5] = None; erasure.decode_data(&mut shards_opt).expect("operation should succeed"); let mut recovered = Vec::new(); for shard in shards_opt.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(data.len()); assert_eq!(&recovered, &data); } #[test] fn test_shard_file_offset() { let erasure = Erasure::new(8, 8, 1024 * 1024); let offset = erasure.shard_file_offset(0, 86, 86); println!("offset={offset}"); assert!(offset > 0); let total_length = erasure.shard_file_size(86); println!("total_length={total_length}"); assert!(total_length > 0); } #[tokio::test] async fn test_encode_stream_callback_async_error_propagation() { use std::io::Cursor; use std::sync::Arc; use tokio::sync::mpsc; let data_shards = 4; let parity_shards = 2; let block_size = 1024; // SIMD mode let erasure = Arc::new(Erasure::new(data_shards, parity_shards, block_size)); // Use test data suitable for SIMD mode let data = b"Async error test data with sufficient length to meet requirements for proper testing and validation.".repeat(20); // ~2KB let mut reader = Cursor::new(data); let (tx, mut rx) = mpsc::channel::>(8); let erasure_clone = erasure.clone(); let handle = tokio::spawn(async move { erasure_clone .encode_stream_callback_async::<_, _, (), _>(&mut reader, move |res| { let tx = tx.clone(); async move { let shards = res.expect("operation should succeed"); tx.send(shards).await.expect("operation should succeed"); Ok(()) } }) .await .expect("operation should succeed"); }); let result = handle.await; assert!(result.is_ok()); let collected_shards = rx.recv().await.expect("operation should succeed"); assert_eq!(collected_shards.len(), data_shards + parity_shards); } #[tokio::test] async fn test_encode_stream_callback_async_channel_decode() { use std::io::Cursor; use std::sync::Arc; use tokio::sync::mpsc; let data_shards = 4; let parity_shards = 2; let block_size = 1024; // SIMD mode let erasure = Arc::new(Erasure::new(data_shards, parity_shards, block_size)); // Use test data that fits in exactly one block to avoid multi-block complexity let data = b"Channel async callback test data with sufficient length to ensure proper operation and validation requirements." .repeat(8); // ~1KB let data_clone = data.clone(); // Clone for later comparison let mut reader = Cursor::new(data); let (tx, mut rx) = mpsc::channel::>(8); let erasure_clone = erasure.clone(); let handle = tokio::spawn(async move { erasure_clone .encode_stream_callback_async::<_, _, (), _>(&mut reader, move |res| { let tx = tx.clone(); async move { let shards = res.expect("operation should succeed"); tx.send(shards).await.expect("operation should succeed"); Ok(()) } }) .await .expect("operation should succeed"); }); let result = handle.await; assert!(result.is_ok()); let shards = rx.recv().await.expect("operation should succeed"); assert_eq!(shards.len(), data_shards + parity_shards); // Test decode using the old API that operates in-place let mut decode_input: Vec>> = vec![None; data_shards + parity_shards]; for i in 0..data_shards { decode_input[i] = Some(shards[i].to_vec()); } erasure.decode_data(&mut decode_input).expect("operation should succeed"); // Recover original data let mut recovered = Vec::new(); for shard in decode_input.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(data_clone.len()); assert_eq!(&recovered, &data_clone); } #[tokio::test] async fn test_encode_stream_callback_async_reports_zero_block_size() { use std::io::Cursor; use std::sync::{Arc, Mutex}; let erasure = Arc::new(erasure_with_invalid_dimensions(1, 0, 0)); let mut reader = Cursor::new(b"payload".to_vec()); let observed = Arc::new(Mutex::new(None)); let observed_clone = observed.clone(); let total = erasure .encode_stream_callback_async::<_, _, (), _>(&mut reader, move |res| { let observed = observed_clone.clone(); async move { let err = res.expect_err("zero block size should report an error"); *observed.lock().expect("operation should succeed") = Some((err.kind(), err.to_string())); Ok(()) } }) .await .expect("callback should handle the zero block size error"); assert_eq!(total, 0); let observed = observed.lock().expect("operation should succeed"); let (kind, message) = observed.as_ref().expect("callback should be invoked once"); assert_eq!(*kind, io::ErrorKind::InvalidInput); assert!(message.contains("block_size")); } // SIMD mode specific tests mod simd_tests { use super::*; #[test] fn test_simd_encode_decode_roundtrip() { let data_shards = 4; let parity_shards = 2; let block_size = 1024; // Use larger block size for SIMD mode let erasure = Erasure::new(data_shards, parity_shards, block_size); // Use data that will create shards >= 512 bytes for SIMD optimization let test_data = b"SIMD mode test data for encoding and decoding roundtrip verification with sufficient length to ensure shard size requirements are met for proper SIMD optimization and validation."; let data = test_data.repeat(25); // Create much larger data: ~5KB total, ~1.25KB per shard let encoded_shards = erasure.encode_data(&data).expect("operation should succeed"); assert_eq!(encoded_shards.len(), data_shards + parity_shards); // Create decode input with some shards missing let mut shards_opt: Vec>> = encoded_shards.iter().map(|shard| Some(shard.to_vec())).collect(); // Lose one data shard and one parity shard (should still be recoverable) shards_opt[1] = None; // Lose second data shard shards_opt[5] = None; // Lose second parity shard erasure.decode_data(&mut shards_opt).expect("operation should succeed"); // Verify recovered data let mut recovered = Vec::new(); for shard in shards_opt.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(data.len()); assert_eq!(&recovered, &data); } #[test] fn test_simd_all_zero_data() { let data_shards = 4; let parity_shards = 2; let block_size = 1024; // Use larger block size for SIMD mode let erasure = Erasure::new(data_shards, parity_shards, block_size); // Create all-zero data that ensures adequate shard size for SIMD optimization let data = vec![0u8; 1024]; // 1KB of zeros, each shard will be 256 bytes let encoded_shards = erasure.encode_data(&data).expect("operation should succeed"); assert_eq!(encoded_shards.len(), data_shards + parity_shards); // Verify that all data shards are zeros for (i, shard) in encoded_shards.iter().enumerate().take(data_shards) { assert!(shard.iter().all(|&x| x == 0), "Data shard {i} should be all zeros"); } // Test recovery with some shards missing let mut shards_opt: Vec>> = encoded_shards.iter().map(|shard| Some(shard.to_vec())).collect(); // Lose maximum recoverable shards (equal to parity_shards) shards_opt[0] = None; // Lose first data shard shards_opt[4] = None; // Lose first parity shard erasure.decode_data(&mut shards_opt).expect("operation should succeed"); // Verify recovered data is still all zeros let mut recovered = Vec::new(); for shard in shards_opt.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(data.len()); assert!(recovered.iter().all(|&x| x == 0), "Recovered data should be all zeros"); } #[test] fn test_simd_large_data_1kb() { let data_shards = 8; let parity_shards = 4; let block_size = 1024; // 1KB block size optimal for SIMD let erasure = Erasure::new(data_shards, parity_shards, block_size); // Create 1KB of test data let mut data = Vec::with_capacity(1024); for i in 0..1024 { data.push((i % 256) as u8); } let shards = erasure.encode_data(&data).expect("operation should succeed"); assert_eq!(shards.len(), data_shards + parity_shards); // Simulate the loss of multiple shards let mut shards_opt: Vec>> = shards.iter().map(|b| Some(b.to_vec())).collect(); shards_opt[0] = None; shards_opt[3] = None; shards_opt[9] = None; // Parity shard shards_opt[11] = None; // Parity shard // Decode erasure.decode_data(&mut shards_opt).expect("operation should succeed"); // Recover original data let mut recovered = Vec::new(); for shard in shards_opt.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(data.len()); assert_eq!(&recovered, &data); } #[test] fn test_simd_minimum_shard_size() { let data_shards = 4; let parity_shards = 2; let block_size = 256; // Use 256 bytes to ensure sufficient shard size let erasure = Erasure::new(data_shards, parity_shards, block_size); // Create data that will result in 64+ byte shards let data = vec![0x42u8; 200]; // 200 bytes, should create ~50 byte shards per data shard let shards = erasure.encode_data(&data).expect("minimum shard size data should encode"); println!("SIMD encoding succeeded with shard size: {}", shards[0].len()); // Test decoding let mut shards_opt: Vec>> = shards.iter().map(|b| Some(b.to_vec())).collect(); shards_opt[1] = None; erasure .decode_data(&mut shards_opt) .expect("minimum shard size data should decode"); let mut recovered = Vec::new(); for shard in shards_opt.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(data.len()); assert_eq!(&recovered, &data); } #[test] fn test_simd_maximum_erasures() { let data_shards = 5; let parity_shards = 3; let block_size = 512; let erasure = Erasure::new(data_shards, parity_shards, block_size); let data = b"Testing maximum erasure capacity with SIMD Reed-Solomon implementation for robustness verification!".repeat(3); let shards = erasure.encode_data(&data).expect("operation should succeed"); // Lose exactly the maximum number of shards (equal to parity_shards) let mut shards_opt: Vec>> = shards.iter().map(|b| Some(b.to_vec())).collect(); shards_opt[0] = None; // Data shard shards_opt[2] = None; // Data shard shards_opt[6] = None; // Parity shard // Should succeed with maximum erasures erasure.decode_data(&mut shards_opt).expect("operation should succeed"); let mut recovered = Vec::new(); for shard in shards_opt.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(data.len()); assert_eq!(&recovered, &data); } /// Generates 7557 bytes identical to MinIO generateCompatTestData. fn generate_compat_test_data(size: usize) -> Vec { (0..size).map(|i| ((i * 7 + 13) % 256) as u8).collect() } /// Verifies reed-solomon-simd produces same shards. /// Data shards (0-3) must match for MinIO to read RustFS part files. /// Parity shards (4-5) differ: reed-solomon-simd vs klauspost use different RS encoding. /// Run: cargo test -p rustfs-ecstore test_reed_solomon_compat #[test] fn test_reed_solomon_compat() { let data = generate_compat_test_data(7557); let erasure = Erasure::new(4, 2, 7557); let shards = erasure.encode_data(&data).expect("operation should succeed"); assert_eq!(shards.len(), 6, "expected 6 shards (4 data + 2 parity)"); // Per-shard HighwayHash let expected_hashes: [&str; 6] = [ "fb3db9338e610cec541504ddae4b0bfd54445bcbd45318cf21f35f024240914d", // data 0 "a545269a3196e18e77ef9f5ec6e735a4f4ebe82d342db666b11a5256eb305720", // data 1 "2adbf0058f36c4cbcb5c9c16c38a6530c54198dfe504179a6f92d2349f245318", // data 2 "898e6d060b0cb4f0e830add7e1f936bc8b78442bf582283ee244a3a058602db8", // data 3 "4a20460bca044b3a777b26f2b0bcd371e3eab2f156f84778be3ccd8edd521ef2", // parity 4 "eb8ba4c0db15ca910d58d031f74e4601ba2fed62ad03ec29cadde3367ab0d415", // parity 5 ]; let mut data_shards_match = true; let mut parity_shards_match = true; for (i, shard) in shards.iter().enumerate() { let hash = rustfs_utils::HashAlgorithm::HighwayHash256S.hash_encode(shard); let got = hex_simd::encode_to_string(hash.as_ref(), hex_simd::AsciiCase::Lower); let matches = got == expected_hashes[i]; if i < 4 { data_shards_match &= matches; } else { parity_shards_match &= matches; } if !matches { eprintln!( "Shard {} ({}): got {} want {}", i, if i < 4 { "data" } else { "parity" }, got, expected_hashes[i] ); } } assert!(data_shards_match, "Data shards (0-3) must match"); assert!(parity_shards_match, "Parity shards (4-5): reed-solomon-simd differs"); } #[test] fn test_simd_small_data_handling() { let data_shards = 4; let parity_shards = 2; let block_size = 32; // Small block size for testing edge cases let erasure = Erasure::new(data_shards, parity_shards, block_size); // Use small data to test SIMD handling of small shards let small_data = b"tiny!123".to_vec(); // 8 bytes data // Test encoding with small data let shards = erasure.encode_data(&small_data).expect("small data should encode"); println!("SIMD encoding succeeded: {} bytes into {} shards", small_data.len(), shards.len()); assert_eq!(shards.len(), data_shards + parity_shards); // Test decoding let mut shards_opt: Vec>> = shards.iter().map(|shard| Some(shard.to_vec())).collect(); // Lose some shards to test recovery shards_opt[1] = None; // Lose one data shard shards_opt[4] = None; // Lose one parity shard erasure.decode_data(&mut shards_opt).expect("small data should decode"); let mut recovered = Vec::new(); for shard in shards_opt.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(small_data.len()); println!("recovered: {recovered:?}"); println!("small_data: {small_data:?}"); assert_eq!(&recovered, &small_data); } #[test] fn test_simd_large_block_1mb() { let data_shards = 6; let parity_shards = 3; let block_size = 1024 * 1024; // 1MB block size let erasure = Erasure::new(data_shards, parity_shards, block_size); // Build 2 MB of test data so multiple 1 MB chunks are exercised let mut data = Vec::with_capacity(2 * 1024 * 1024); for i in 0..(2 * 1024 * 1024) { data.push((i % 256) as u8); } println!("🚀 Testing SIMD with 1MB block size and 2MB data"); println!( "📊 Data shards: {}, Parity shards: {}, Total data: {}KB", data_shards, parity_shards, data.len() / 1024 ); // Encode the data let start = std::time::Instant::now(); let shards = erasure.encode_data(&data).expect("operation should succeed"); let encode_duration = start.elapsed(); println!("⏱️ Encoding completed in: {encode_duration:?}"); println!("📦 Generated {} shards, each shard size: {}KB", shards.len(), shards[0].len() / 1024); assert_eq!(shards.len(), data_shards + parity_shards); // Verify that each shard is large enough for SIMD optimization for (i, shard) in shards.iter().enumerate() { println!("🔍 Shard {}: {} bytes ({}KB)", i, shard.len(), shard.len() / 1024); assert!(shard.len() >= 512, "Shard {} is too small for SIMD: {} bytes", i, shard.len()); } // Simulate data loss - lose maximum recoverable number of shards let mut shards_opt: Vec>> = shards.iter().map(|b| Some(b.to_vec())).collect(); shards_opt[0] = None; // Lose 1st data shard shards_opt[2] = None; // Lose 3rd data shard shards_opt[8] = None; // Lose 3rd parity shard (index 6+3-1=8) println!("💥 Simulated loss of 3 shards (max recoverable with 3 parity shards)"); // Decode and recover data let start = std::time::Instant::now(); erasure.decode_data(&mut shards_opt).expect("operation should succeed"); let decode_duration = start.elapsed(); println!("⏱️ Decoding completed in: {decode_duration:?}"); // Verify recovered data integrity let mut recovered = Vec::new(); for shard in shards_opt.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } recovered.truncate(data.len()); assert_eq!(recovered.len(), data.len()); assert_eq!(&recovered, &data, "Data mismatch after recovery!"); println!("✅ Successfully verified data integrity after recovery"); println!("📈 Performance summary:"); println!( " - Encode: {:?} ({:.2} MB/s)", encode_duration, (data.len() as f64 / (1024.0 * 1024.0)) / encode_duration.as_secs_f64() ); println!( " - Decode: {:?} ({:.2} MB/s)", decode_duration, (data.len() as f64 / (1024.0 * 1024.0)) / decode_duration.as_secs_f64() ); } #[tokio::test] async fn test_simd_stream_callback() { use std::io::Cursor; use std::sync::Arc; use tokio::sync::mpsc; let data_shards = 4; let parity_shards = 2; let block_size = 256; // Larger block for SIMD let erasure = Arc::new(Erasure::new(data_shards, parity_shards, block_size)); let test_data = b"SIMD stream processing test with sufficient data length for multiple blocks and proper SIMD optimization verification!"; let data = test_data.repeat(5); // Create owned Vec let data_clone = data.clone(); // Clone for later comparison let mut reader = Cursor::new(data); let (tx, mut rx) = mpsc::channel::>(16); let erasure_clone = erasure.clone(); let handle = tokio::spawn(async move { erasure_clone .encode_stream_callback_async::<_, _, (), _>(&mut reader, move |res| { let tx = tx.clone(); async move { let shards = res.expect("operation should succeed"); tx.send(shards).await.expect("operation should succeed"); Ok(()) } }) .await .expect("operation should succeed"); }); let mut all_blocks = Vec::new(); while let Some(block) = rx.recv().await { all_blocks.push(block); } handle.await.expect("operation should succeed"); // Verify we got multiple blocks assert!(all_blocks.len() > 1, "Should have multiple blocks for stream test"); // Test recovery for each block let mut recovered = Vec::new(); for block in &all_blocks { let mut shards_opt: Vec>> = block.iter().map(|b| Some(b.to_vec())).collect(); // Lose one data shard and one parity shard shards_opt[1] = None; shards_opt[5] = None; erasure.decode_data(&mut shards_opt).expect("operation should succeed"); for shard in shards_opt.iter().take(data_shards) { recovered.extend_from_slice(shard.as_ref().expect("operation should succeed")); } } recovered.truncate(data_clone.len()); assert_eq!(&recovered, &data_clone); } } }