Skip to main content

commonware_cryptography/reed_solomon/
mod.rs

1//! Encode and reconstruct shards using Reed-Solomon erasure coding over GF(2^16).
2//!
3//! [`Encoder`] and [`Decoder`] select a rate and CPU engine automatically. Every shard
4//! must have the same nonzero, even byte length. [`Encoder::supports`] checks whether
5//! a pair of original and recovery shard counts is supported.
6//!
7//! Decoding requires at least as many distinct shards as there were original shards.
8//! All supplied shards must belong to the same codeword: this module reconstructs
9//! missing shards and does not authenticate their contents.
10//!
11//! [`Plan`] prepares erasure coefficients once for decoders that share shard
12//! counts and received indices, even when their shard byte lengths differ.
13//!
14//! # Basic Usage
15//!
16//! ```
17//! use commonware_cryptography::reed_solomon::{Decoder, Encoder};
18//!
19//! let originals = [[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]];
20//! let mut encoder = Encoder::new(2, 1, 6)?;
21//! for shard in &originals {
22//!     encoder.add_original_shard(shard)?;
23//! }
24//! let encoded = encoder.encode()?;
25//!
26//! let mut decoder = Decoder::new(2, 1, 6)?;
27//! decoder.add_original_shard(0, originals[0])?;
28//! decoder.add_recovery_shard(0, encoded.recovery(0).unwrap())?;
29//! let decoded = decoder.decode()?.unwrap();
30//! assert_eq!(decoded.original(1), Some(originals[1].as_slice()));
31//! # Ok::<(), commonware_cryptography::reed_solomon::Error>(())
32//! ```
33//!
34//! Result objects borrow the working buffers. Dropping a result resets its encoder
35//! or decoder for another round with the same configuration.
36//!
37//! # Working Space
38//!
39//! A coder sizes its working space when it is created or reset: up to
40//! `2 * (original_count + recovery_count).next_power_of_two()` shards of `shard_bytes` bytes,
41//! each rounded up to whole [`SHARD_CHUNK_BYTES`] blocks. The space is one allocation that a
42//! reset can grow but never shrinks. Creating or resetting a coder returns an error for
43//! unsupported shard counts, a zero or odd `shard_bytes`, or a working space larger than
44//! `isize::MAX` bytes, the largest allocation a `Vec` can hold.
45//!
46//! Callers that take shard counts or widths from an untrusted source must bound them against
47//! their own memory budget before creating or resetting a coder. Wide shards can be coded as
48//! independent byte ranges so that the range width, not the shard width, sets the working space.
49//! The ranges produce the same bytes as coding each shard whole when every range except the last
50//! is a whole number of [`SHARD_CHUNK_BYTES`] blocks, as [algorithm > Shard] explains.
51//!
52//! # Changes vs. Upstream
53//!
54//! This module vendors [`reed_solomon_simd`].
55//!
56//! - Moved the crate into `commonware_cryptography::reed_solomon` and rewrote internal
57//!   `crate::` paths accordingly.
58//! - Retained the upstream `LICENSE` file, including the BSD-3-Clause notice for Leopard-RS.
59//! - Removed the build script and README rustdoc generation.
60//! - Removed upstream examples. Kept upstream-style benchmarks under Commonware's benchmark layout
61//!   and naming rules.
62//! - Uses workspace dependencies and [`commonware_formatting`] in the test harness.
63//! - Uses [`thiserror`] for error display formatting.
64//! - Renamed upstream `ReedSolomonEncoder` and `ReedSolomonDecoder` to [`Encoder`] and [`Decoder`],
65//!   and the `NoSimd` engine to [`Scalar`](engine::Scalar).
66//! - Uses plain code references for cfg-gated SIMD engine docs so rustdoc works on all targets.
67//! - Validates transform domains, shard ranges, and working-space sizes at their public boundaries.
68//! - Supports AVX-512 with GFNI multiplication and runtime CPU feature checks.
69//! - Removed the SSSE3 engine.
70//! - Adds [`Plan`] and plan-based decoding to reuse erasure coefficients across decoders.
71//! - Sizes decoder Walsh transforms to the decoding domain.
72//! - Fuses AVX-512 butterfly layers.
73//! - Fuses formal-derivative leaves in blocks of four shards for every engine. When the `Avx512`
74//!   engine's CPU features are present, blocks of 16 shards use an AVX-512 leaf for suitable shard
75//!   counts and sizes, regardless of the selected engine.
76//! - Builds the 128-bit multiplication tables in native byte order, since the Neon and Avx2
77//!   engines read each table as a vector of its in-memory bytes.
78//! - Includes independent field-arithmetic checks, lifecycle regressions, and differential fuzzing.
79//! - Rewrote comments in Commonware style: removed section banners and uppercase step headers,
80//!   and attached floating comments to the code they describe.
81//!
82//! [`reed_solomon_simd`]: https://crates.io/crates/reed-solomon-simd
83//! [`thiserror`]: https://docs.rs/thiserror
84//! [algorithm > Shard]: crate::reed_solomon::algorithm#shard
85
86#![allow(
87    clippy::multiple_inherent_impl,
88    reason = "Keep the vendored implementation aligned with its upstream layout."
89)]
90
91pub use self::{
92    decoder_result::{DecoderResult, Originals, Recoveries, RecoveryDecoderResult},
93    encoder_result::{EncoderResult, Recovery},
94    engine::SHARD_CHUNK_BYTES,
95    plan::Plan,
96    wrappers::{Decoder, Encoder},
97};
98use thiserror::Error;
99
100#[cfg(test)]
101#[macro_use]
102mod test_util;
103
104#[cfg(any(test, feature = "fuzz"))]
105pub mod fuzz;
106
107mod decoder_result;
108mod encoder_result;
109mod plan;
110mod wrappers;
111
112pub mod algorithm {
113    #![doc = include_str!("algorithm.md")]
114}
115pub mod engine;
116pub mod rate;
117
118/// Represents all possible errors that can occur in this library.
119#[derive(Clone, Copy, Debug, Error, PartialEq)]
120pub enum Error {
121    /// Given shard has different size than the configured shard size.
122    #[error("different shard size: expected {shard_bytes} bytes, got {got} bytes")]
123    DifferentShardSize {
124        /// Configured shard size.
125        shard_bytes: usize,
126        /// Size of the given shard.
127        got: usize,
128    },
129
130    /// Decoder or [`Plan`] was given the same original shard index twice.
131    #[error("duplicate original shard index: {index}")]
132    DuplicateOriginalShardIndex {
133        /// Given duplicate index.
134        index: usize,
135    },
136
137    /// Decoder or [`Plan`] was given the same recovery shard index twice.
138    #[error("duplicate recovery shard index: {index}")]
139    DuplicateRecoveryShardIndex {
140        /// Given duplicate index.
141        index: usize,
142    },
143
144    /// Decoder or [`Plan`] was given an invalid original shard index,
145    /// i.e. `index >= original_count`.
146    #[error("invalid original shard index: {index} >= original_count {original_count}")]
147    InvalidOriginalShardIndex {
148        /// Configured number of original shards.
149        original_count: usize,
150        /// Given invalid index.
151        index: usize,
152    },
153
154    /// Decoder or [`Plan`] was given an invalid recovery shard index,
155    /// i.e. `index >= recovery_count`.
156    #[error("invalid recovery shard index: {index} >= recovery_count {recovery_count}")]
157    InvalidRecoveryShardIndex {
158        /// Configured number of recovery shards.
159        recovery_count: usize,
160        /// Given invalid index.
161        index: usize,
162    },
163
164    /// Configured shard size is zero, odd, or requires more working space than a single
165    /// allocation can hold.
166    #[error("invalid shard size: {shard_bytes} bytes (zero, odd, or working space too large)")]
167    InvalidShardSize {
168        /// Configured shard size.
169        shard_bytes: usize,
170    },
171
172    /// Decoder or [`Plan`] was given too few shards.
173    ///
174    /// Decoding requires as many shards as there were original shards
175    /// in total, in any combination of original shards and recovery shards.
176    #[error(
177        "not enough shards: {original_received_count} original + {recovery_received_count} recovery < {original_count} original_count"
178    )]
179    NotEnoughShards {
180        /// Configured number of original shards.
181        original_count: usize,
182        /// Number of original shards given.
183        original_received_count: usize,
184        /// Number of recovery shards given.
185        recovery_received_count: usize,
186    },
187
188    /// Decoder was given a [`Plan`] built for different shard counts or received
189    /// shard indices.
190    #[error("plan does not match decoder")]
191    PlanMismatch,
192
193    /// Encoder was given less than `original_count` original shards.
194    #[error(
195        "too few original shards: got {original_received_count} shards while original_count is {original_count}"
196    )]
197    TooFewOriginalShards {
198        /// Configured number of original shards.
199        original_count: usize,
200        /// Number of original shards given to encoder.
201        original_received_count: usize,
202    },
203
204    /// Encoder was given more than `original_count` original shards.
205    #[error("too many original shards: got more than original_count ({original_count}) shards")]
206    TooManyOriginalShards {
207        /// Configured number of original shards.
208        original_count: usize,
209    },
210
211    /// Given `original_count` / `recovery_count` combination is not supported.
212    #[error(
213        "unsupported shard count: {original_count} original shards with {recovery_count} recovery shards"
214    )]
215    UnsupportedShardCount {
216        /// Given number of original shards.
217        original_count: usize,
218        /// Given number of recovery shards.
219        recovery_count: usize,
220    },
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::reed_solomon::{engine::DefaultEngine, rate::DefaultRate};
227
228    #[test]
229    fn roundtrip() {
230        let original = test_util::generate_original(2, 1024, 123);
231
232        let mut encoder = Encoder::new(2, 3, 1024).unwrap();
233        for shard in &original {
234            encoder.add_original_shard(shard).unwrap();
235        }
236        let encoding = encoder.encode().unwrap();
237        let recovery: Vec<_> = encoding.recovery_iter().map(<[u8]>::to_vec).collect();
238
239        test_util::assert_hash(&recovery, test_util::LOW_2_3);
240
241        let mut decoder = Decoder::new(2, 3, 1024).unwrap();
242        decoder.add_recovery_shard(0, &recovery[0]).unwrap();
243        decoder.add_recovery_shard(1, &recovery[1]).unwrap();
244        let decoding = decoder.decode().unwrap().unwrap();
245        let mut restored = decoding.original_iter();
246
247        assert_eq!(restored.next(), Some((0, original[0].as_slice())));
248        assert_eq!(restored.next(), Some((1, original[1].as_slice())));
249        assert_eq!(restored.next(), None);
250    }
251
252    #[test]
253    fn test_send() {
254        fn assert_send<T: Send>() {}
255        assert_send::<Encoder>();
256        assert_send::<Decoder>();
257        assert_send::<Plan>();
258        assert_send::<DefaultEngine>();
259        assert_send::<DefaultRate<DefaultEngine>>();
260        assert_send::<DecoderResult<'_>>();
261        assert_send::<RecoveryDecoderResult<'_>>();
262        assert_send::<EncoderResult<'_>>();
263        assert_send::<Error>();
264    }
265
266    #[test]
267    fn test_sync() {
268        fn assert_sync<T: Sync>() {}
269        assert_sync::<Encoder>();
270        assert_sync::<Decoder>();
271        assert_sync::<Plan>();
272        assert_sync::<DefaultEngine>();
273        assert_sync::<DefaultRate<DefaultEngine>>();
274        assert_sync::<DecoderResult<'_>>();
275        assert_sync::<RecoveryDecoderResult<'_>>();
276        assert_sync::<EncoderResult<'_>>();
277        assert_sync::<Error>();
278    }
279}