diff --git a/reed-solomon-novelpoly/src/errors.rs b/reed-solomon-novelpoly/src/errors.rs index 6d3e83a..bc562d9 100644 --- a/reed-solomon-novelpoly/src/errors.rs +++ b/reed-solomon-novelpoly/src/errors.rs @@ -22,6 +22,9 @@ pub enum Error { #[error("Shards do have inconsistent lengths: first = {first}, other = {other})")] InconsistentShardLengths { first: usize, other: usize }, + + #[error("Shard is empty")] + EmptyShard, } /// Result alias to simplify API. diff --git a/reed-solomon-novelpoly/src/novel_poly_basis/mod.rs b/reed-solomon-novelpoly/src/novel_poly_basis/mod.rs index ba980b8..3a80ff2 100644 --- a/reed-solomon-novelpoly/src/novel_poly_basis/mod.rs +++ b/reed-solomon-novelpoly/src/novel_poly_basis/mod.rs @@ -156,7 +156,9 @@ impl ReedSolomon { Ok(shards) } - /// each shard contains one symbol of one run of erasure coding + /// Reconstruct from chunks. + /// + /// The result may be padded with zeros. Truncate the output to the expected byte length. pub fn reconstruct(&self, received_shards: Vec>) -> Result> { let gap = self.n.saturating_sub(received_shards.len()); @@ -190,6 +192,10 @@ impl ReedSolomon { }) .expect("Existential shard count is at least k shards. qed"); + if first_shard_len == 0 { + return Err(Error::EmptyShard); + } + // make sure all shards have the same length as the first one if let Some(other_shard_len) = received_shards[(first_shard_idx + 1)..].iter().find_map(|shard| { shard.as_ref().and_then(|shard| { @@ -231,6 +237,52 @@ impl ReedSolomon { Ok(acc) } + + /// Reconstruct from the set of systematic chunks. + /// Systematic chunks are the first `k` chunks, which contain the initial data. + /// + /// Provide a vector containing chunk data. If too few chunks are provided, recovery is not + /// possible. + /// The result may be padded with zeros. Truncate the output to the expected byte length. + pub fn reconstruct_from_systematic(&self, chunks: Vec) -> Result> { + let Some(first_shard) = chunks.first() else { + return Err(Error::NeedMoreShards { have: 0, min: self.k, all: self.n }); + }; + if chunks.len() < self.k { + return Err(Error::NeedMoreShards { have: chunks.len(), min: self.k, all: self.n }); + } + + let shard_len = AsRef::<[[u8; 2]]>::as_ref(first_shard).len(); + + if shard_len == 0 { + return Err(Error::EmptyShard); + } + + if let Some(length) = chunks.iter().find_map(|c| { + let length = AsRef::<[[u8; 2]]>::as_ref(c).len(); + if length != shard_len { + Some(length) + } else { + None + } + }) { + return Err(Error::InconsistentShardLengths { first: shard_len, other: length }); + } + + let mut systematic_bytes = Vec::with_capacity(shard_len * 2 * self.k); + + for i in 0..shard_len { + for chunk in chunks.iter().take(self.k) { + // No need to check for index out of bounds because i goes up to shard_len and + // we return an error for non uniform chunks. + let chunk = AsRef::<[[u8; 2]]>::as_ref(chunk)[i]; + systematic_bytes.push(chunk[0]); + systematic_bytes.push(chunk[1]); + } + } + + Ok(systematic_bytes) + } } #[cfg(test)] diff --git a/reed-solomon-novelpoly/src/novel_poly_basis/tests.rs b/reed-solomon-novelpoly/src/novel_poly_basis/tests.rs index f7b6bd1..bfb858c 100644 --- a/reed-solomon-novelpoly/src/novel_poly_basis/tests.rs +++ b/reed-solomon-novelpoly/src/novel_poly_basis/tests.rs @@ -479,6 +479,23 @@ impl Arbitrary for ArbitraryData { } } +#[test] +fn round_trip_systematic_quickcheck() { + fn property(available_data: ArbitraryData, n_validators: u16) { + let n_validators = n_validators.max(2); + let rs = CodeParams::derive_parameters(n_validators as usize, (n_validators as usize - 1) / 3 + 1) + .unwrap() + .make_encoder(); + let kpow2 = rs.k; + let chunks = rs.encode::(&available_data.0).unwrap(); + let mut res = rs.reconstruct_from_systematic(chunks.into_iter().take(kpow2).collect()).unwrap(); + res.truncate(available_data.0.len()); + assert_eq!(res, available_data.0); + } + + QuickCheck::new().quickcheck(property as fn(ArbitraryData, u16)) +} + #[test] fn round_trip_quickcheck() { fn property(available_data: ArbitraryData, n_validators: u16) {