Skip to main content

commonware_cryptography/reed_solomon/
decoder_result.rs

1use crate::reed_solomon::rate::DecoderWork;
2
3/// The restored original shards from a decode that ran (an original was missing).
4///
5/// [`Decoder::decode`] returns `None` instead when every original shard was already provided, since
6/// there is nothing to reconstruct.
7///
8/// [`Decoder::decode`]: crate::reed_solomon::Decoder::decode
9pub struct DecoderResult<'a> {
10    work: &'a mut DecoderWork,
11}
12
13impl DecoderResult<'_> {
14    /// Returns the restored original shard with the given `index`, or `None` if `index` does not
15    /// correspond to a missing original shard.
16    pub fn original(&self, index: usize) -> Option<&[u8]> {
17        self.work.original(index)
18    }
19
20    /// Returns an iterator over all restored original shards and their indexes, ordered by index.
21    pub const fn original_iter(&self) -> Originals<'_> {
22        Originals::new(self.work)
23    }
24}
25
26impl<'a> DecoderResult<'a> {
27    /// Wraps `work` after a decode that restored at least one missing original.
28    ///
29    /// Dropping the result clears the received-shard state in `work`.
30    pub(crate) const fn new(work: &'a mut DecoderWork) -> Self {
31        Self { work }
32    }
33}
34
35impl Drop for DecoderResult<'_> {
36    fn drop(&mut self) {
37        self.work.reset_received();
38    }
39}
40
41/// The restored shards from a successful [`Decoder::decode_with_recovery`] or
42/// [`Decoder::decode_with_recovery_plan`], exposing both the restored original shards (like
43/// [`DecoderResult`]) and the reconstructed recovery shards.
44///
45/// [`Decoder::decode_with_recovery`]: crate::reed_solomon::Decoder::decode_with_recovery
46/// [`Decoder::decode_with_recovery_plan`]: crate::reed_solomon::Decoder::decode_with_recovery_plan
47pub struct RecoveryDecoderResult<'a> {
48    inner: DecoderResult<'a>,
49}
50
51impl RecoveryDecoderResult<'_> {
52    /// Returns the restored original shard with the given `index`, or `None` if it was provided or
53    /// `index` is out of range. See [`DecoderResult::original`].
54    pub fn original(&self, index: usize) -> Option<&[u8]> {
55        self.inner.original(index)
56    }
57
58    /// Returns an iterator over the restored original shards and their indexes, ordered by index.
59    /// See [`DecoderResult::original_iter`].
60    pub const fn original_iter(&self) -> Originals<'_> {
61        self.inner.original_iter()
62    }
63
64    /// Returns the reconstructed recovery shard with the given `index`, or `None` if it was
65    /// provided or `index` is out of range.
66    pub fn recovery(&self, index: usize) -> Option<&[u8]> {
67        self.inner.work.recovery(index)
68    }
69
70    /// Returns an iterator over the reconstructed recovery shards and their indexes, ordered by
71    /// index.
72    pub const fn recovery_iter(&self) -> Recoveries<'_> {
73        Recoveries::new(self.inner.work)
74    }
75}
76
77impl<'a> RecoveryDecoderResult<'a> {
78    /// Wraps `inner`, which must come from a decode run with `compute_recovery` set.
79    ///
80    /// Otherwise the missing recovery positions hold intermediate transform values, and
81    /// [`Self::recovery`] returns them as if they were shards.
82    pub(crate) const fn new(inner: DecoderResult<'a>) -> Self {
83        Self { inner }
84    }
85}
86
87/// Iterator over restored original shards and their indexes.
88///
89/// This struct is created by [`DecoderResult::original_iter`].
90pub struct Originals<'a> {
91    /// Number of restored originals not yet yielded.
92    remaining: usize,
93    /// Original index at which the next scan starts.
94    next_index: usize,
95    work: &'a DecoderWork,
96}
97
98impl<'a> Iterator for Originals<'a> {
99    type Item = (usize, &'a [u8]);
100    fn next(&mut self) -> Option<(usize, &'a [u8])> {
101        if self.remaining == 0 {
102            return None;
103        }
104
105        let mut index = self.next_index;
106        while index < self.work.original_count() {
107            if let Some(original) = self.work.original(index) {
108                self.next_index = index + 1;
109                self.remaining -= 1;
110                return Some((index, original));
111            }
112            index += 1;
113        }
114
115        unreachable!("Inconsistency in internal data structures. Please report.");
116    }
117
118    fn size_hint(&self) -> (usize, Option<usize>) {
119        (self.remaining, Some(self.remaining))
120    }
121}
122
123impl ExactSizeIterator for Originals<'_> {}
124
125impl<'a> Originals<'a> {
126    /// Creates an iterator over the missing originals in `work`, starting at index 0.
127    pub(crate) const fn new(work: &'a DecoderWork) -> Self {
128        Self {
129            remaining: work.missing_original_count(),
130            next_index: 0,
131            work,
132        }
133    }
134}
135
136/// Iterator over restored recovery shards and their indexes.
137///
138/// This struct is created by [`RecoveryDecoderResult::recovery_iter`].
139pub struct Recoveries<'a> {
140    /// Number of restored recovery shards not yet yielded.
141    remaining: usize,
142    /// Recovery index at which the next scan starts.
143    next_index: usize,
144    work: &'a DecoderWork,
145}
146
147impl<'a> Iterator for Recoveries<'a> {
148    type Item = (usize, &'a [u8]);
149    fn next(&mut self) -> Option<(usize, &'a [u8])> {
150        if self.remaining == 0 {
151            return None;
152        }
153
154        let mut index = self.next_index;
155        while index < self.work.recovery_count() {
156            if let Some(recovery) = self.work.recovery(index) {
157                self.next_index = index + 1;
158                self.remaining -= 1;
159                return Some((index, recovery));
160            }
161            index += 1;
162        }
163
164        unreachable!("Inconsistency in internal data structures. Please report.");
165    }
166
167    fn size_hint(&self) -> (usize, Option<usize>) {
168        (self.remaining, Some(self.remaining))
169    }
170}
171
172impl ExactSizeIterator for Recoveries<'_> {}
173
174impl<'a> Recoveries<'a> {
175    /// Creates an iterator over the missing recovery shards in `work`, starting at index 0.
176    ///
177    /// `work` must hold a decode that restored at least one original. Otherwise `next` panics
178    /// if any recovery shard is missing.
179    pub(crate) const fn new(work: &'a DecoderWork) -> Self {
180        Self {
181            remaining: work.missing_recovery_count(),
182            next_index: 0,
183            work,
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::reed_solomon::{
192        Decoder, Encoder, SHARD_CHUNK_BYTES, rate::rate_low::DIRECT_EVALUATION_LIMIT, test_util,
193    };
194    #[cfg(not(feature = "std"))]
195    use alloc::vec::Vec;
196    use commonware_utils::test_rng;
197    use rand::seq::SliceRandom as _;
198
199    /// Encodes three originals into two recovery shards, decodes from original 1 and both
200    /// recovery shards, and checks the restored originals 0 and 2.
201    fn simple_roundtrip(shard_size: usize) {
202        let original = test_util::generate_original(3, shard_size, 0);
203
204        let mut encoder = Encoder::new(3, 2, shard_size).unwrap();
205        let mut decoder = Decoder::new(3, 2, shard_size).unwrap();
206
207        for original in &original {
208            encoder.add_original_shard(original).unwrap();
209        }
210
211        let result = encoder.encode().unwrap();
212        let recovery: Vec<_> = result.recovery_iter().collect();
213
214        assert!(recovery.iter().all(|slice| slice.len() == shard_size));
215
216        decoder.add_original_shard(1, &original[1]).unwrap();
217        decoder.add_recovery_shard(0, recovery[0]).unwrap();
218        decoder.add_recovery_shard(1, recovery[1]).unwrap();
219
220        let result = decoder.decode().unwrap().unwrap();
221
222        assert_eq!(result.original(0).unwrap(), original[0]);
223        assert!(result.original(1).is_none());
224        assert_eq!(result.original(2).unwrap(), original[2]);
225        assert!(result.original(3).is_none());
226
227        let mut iter: Originals<'_> = result.original_iter();
228        assert_eq!(iter.next(), Some((0, original[0].as_slice())));
229        assert_eq!(iter.next(), Some((2, original[2].as_slice())));
230        assert_eq!(iter.next(), None);
231        assert_eq!(iter.next(), None);
232    }
233
234    /// Covers `DecoderResult::original`, `DecoderResult::original_iter`, and `Originals`.
235    #[test]
236    fn decoder_result() {
237        simple_roundtrip(1024);
238    }
239
240    #[test]
241    fn shard_size_not_divisible_by_chunk_size() {
242        for shard_size in [
243            2,
244            4,
245            6,
246            30,
247            32,
248            34,
249            62,
250            SHARD_CHUNK_BYTES,
251            66,
252            126,
253            128,
254            130,
255        ] {
256            simple_roundtrip(shard_size);
257        }
258    }
259
260    #[test]
261    fn decoder_result_size_hint() {
262        let shard_size = SHARD_CHUNK_BYTES;
263        let original = test_util::generate_original(3, shard_size, 0);
264
265        let mut encoder = Encoder::new(3, 2, shard_size).unwrap();
266        let mut decoder = Decoder::new(3, 2, shard_size).unwrap();
267
268        for original in &original {
269            encoder.add_original_shard(original).unwrap();
270        }
271
272        let result = encoder.encode().unwrap();
273        let recovery: Vec<_> = result.recovery_iter().collect();
274
275        decoder.add_original_shard(1, &original[1]).unwrap();
276        decoder.add_recovery_shard(0, recovery[0]).unwrap();
277        decoder.add_recovery_shard(1, recovery[1]).unwrap();
278
279        let result = decoder.decode().unwrap().unwrap();
280
281        let mut iter: Originals<'_> = result.original_iter();
282
283        assert_eq!(iter.len(), 2);
284
285        assert!(iter.next().is_some());
286        assert_eq!(iter.len(), 1);
287
288        assert!(iter.next().is_some());
289        assert_eq!(iter.len(), 0);
290
291        assert!(iter.next().is_none());
292        assert_eq!(iter.len(), 0);
293    }
294
295    /// Decodes from exactly `original_count` shards (dropping original 0 and every recovery
296    /// except index 1) and asserts the reconstructed recovery shards are byte-identical to the
297    /// encoder's output. This is the load-bearing check for `recovery`. The reveal and
298    /// last-chunk undo on the recovery positions must reproduce `encoding.recovery(i)` exactly,
299    /// including the partial-final-chunk path (shard sizes not divisible by `SHARD_CHUNK_BYTES`).
300    fn recovery_roundtrip(original_count: usize, recovery_count: usize, shard_size: usize) {
301        let original = test_util::generate_original(original_count, shard_size, 0);
302
303        let mut encoder = Encoder::new(original_count, recovery_count, shard_size).unwrap();
304        for original in &original {
305            encoder.add_original_shard(original).unwrap();
306        }
307        let encoding = encoder.encode().unwrap();
308        let recovery: Vec<Vec<u8>> = encoding.recovery_iter().map(<[u8]>::to_vec).collect();
309
310        // Provide originals `1..original_count` plus recovery 1 (exactly `original_count`
311        // shards), so original 0 and every recovery except index 1 are reconstructed.
312        let mut decoder = Decoder::new(original_count, recovery_count, shard_size).unwrap();
313        for (i, original) in original.iter().enumerate().skip(1) {
314            decoder.add_original_shard(i, original).unwrap();
315        }
316        decoder.add_recovery_shard(1, &recovery[1]).unwrap();
317        let decoding = decoder.decode_with_recovery().unwrap().unwrap();
318
319        assert_eq!(decoding.original(0).unwrap(), original[0].as_slice());
320        for (i, recovery) in recovery.iter().enumerate() {
321            let label = format!("oc={original_count} rc={recovery_count} ss={shard_size} rec={i}");
322            if i == 1 {
323                assert!(decoding.recovery(i).is_none(), "provided recovery: {label}");
324            } else {
325                assert_eq!(
326                    decoding.recovery(i).unwrap(),
327                    recovery.as_slice(),
328                    "restored recovery mismatch: {label}"
329                );
330            }
331        }
332
333        let via_iter: Vec<(usize, Vec<u8>)> = decoding
334            .recovery_iter()
335            .map(|(i, s)| (i, s.to_vec()))
336            .collect();
337        let expected: Vec<(usize, Vec<u8>)> = (0..recovery_count)
338            .filter(|&i| i != 1)
339            .map(|i| (i, recovery[i].clone()))
340            .collect();
341        assert_eq!(via_iter, expected);
342    }
343
344    #[test]
345    fn recovery_matches_encoder() {
346        // Shard sizes span the partial-final-chunk boundary (`SHARD_CHUNK_BYTES` = 64).
347        for shard_size in [2, 34, 62, SHARD_CHUNK_BYTES, 66, 130, 1024] {
348            // HighRate selections (original_count_pow2 >= recovery_count_pow2).
349            recovery_roundtrip(3, 2, shard_size);
350            recovery_roundtrip(16, 4, shard_size);
351
352            // LowRate selections (original_count_pow2 < recovery_count_pow2), including the
353            // 250-shard / k=83 / m=167 shape used by the coding crate.
354            recovery_roundtrip(4, 8, shard_size);
355            recovery_roundtrip(83, 167, shard_size);
356        }
357    }
358
359    #[test]
360    fn direct_recovery_matches_encoder() {
361        // `quarter` originals pad to a chunk of `quarter.next_power_of_two()`, so the last two
362        // `(k, m)` cases end exactly at and one past DIRECT_EVALUATION_LIMIT.
363        let mut rng = test_rng();
364        let quarter = DIRECT_EVALUATION_LIMIT / 4;
365        let recovery = DIRECT_EVALUATION_LIMIT - quarter.next_power_of_two();
366        for (k, m) in [
367            (7, 13),
368            (84, 166),
369            (quarter, recovery),
370            (quarter, recovery + 1),
371        ] {
372            for shard_size in [2, 66, 1024] {
373                let originals = test_util::generate_original(k, shard_size, 0);
374                let mut encoder = Encoder::new(k, m, shard_size).unwrap();
375                for shard in &originals {
376                    encoder.add_original_shard(shard).unwrap();
377                }
378                let encoding = encoder.encode().unwrap();
379                let recoveries: Vec<_> = encoding.recovery_iter().collect();
380                let mut decoder = Decoder::new(k, m, shard_size).unwrap();
381
382                // Always omit original 0 so recovery runs, even with surplus shards.
383                let mut indices: Vec<_> = (1..k + m).collect();
384                for provided in [k, k + 3, k + m - 1] {
385                    indices.shuffle(&mut rng);
386                    let selected = &indices[..provided];
387                    for &i in selected {
388                        if i < k {
389                            decoder.add_original_shard(i, &originals[i]).unwrap();
390                        } else {
391                            decoder
392                                .add_recovery_shard(i - k, recoveries[i - k])
393                                .unwrap();
394                        }
395                    }
396                    let result = decoder.decode_with_recovery().unwrap().unwrap();
397                    for (i, shard) in originals.iter().enumerate() {
398                        if !selected.contains(&i) {
399                            assert_eq!(result.original(i).unwrap(), shard.as_slice());
400                        }
401                    }
402                    for (i, shard) in recoveries.iter().enumerate() {
403                        if !selected.contains(&(k + i)) {
404                            assert_eq!(result.recovery(i).unwrap(), *shard);
405                        }
406                    }
407                }
408            }
409        }
410    }
411
412    /// Provides every original, so there is nothing to reconstruct, and asserts both decode entry
413    /// points return `None`.
414    fn assert_decode_none(original_count: usize, recovery_count: usize) {
415        let shard_size = SHARD_CHUNK_BYTES;
416        let original = test_util::generate_original(original_count, shard_size, 0);
417
418        let mut decoder = Decoder::new(original_count, recovery_count, shard_size).unwrap();
419        for (i, shard) in original.iter().enumerate() {
420            decoder.add_original_shard(i, shard).unwrap();
421        }
422        assert!(decoder.decode().unwrap().is_none());
423
424        let mut decoder = Decoder::new(original_count, recovery_count, shard_size).unwrap();
425        for (i, shard) in original.iter().enumerate() {
426            decoder.add_original_shard(i, shard).unwrap();
427        }
428        assert!(decoder.decode_with_recovery().unwrap().is_none());
429    }
430
431    #[test]
432    fn decode_none_when_all_originals_present() {
433        assert_decode_none(3, 2); // HighRate (original_count_pow2 >= recovery_count_pow2)
434        assert_decode_none(4, 8); // LowRate (original_count_pow2 < recovery_count_pow2)
435    }
436}