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}