Skip to main content

commonware_cryptography/reed_solomon/
decoder_result.rs

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