reed_solomon_simd/
lib.rs

1#![doc = include_str!(concat!(env!("OUT_DIR"), "/README-rustdocified.md"))]
2#![deny(missing_docs)]
3#![warn(clippy::pedantic, clippy::cargo)]
4// Allow some lints from the pedantic category
5#![allow(
6    clippy::must_use_candidate,
7    clippy::cast_ptr_alignment,
8    clippy::inline_always,
9    clippy::missing_errors_doc,
10    clippy::cast_possible_truncation,
11    clippy::large_stack_arrays,
12    clippy::wildcard_imports
13)]
14#![cfg_attr(not(feature = "std"), no_std)]
15
16extern crate alloc;
17
18use alloc::collections::BTreeMap;
19#[cfg(not(feature = "std"))]
20use alloc::vec::Vec;
21use core::fmt;
22
23pub use crate::{
24    decoder_result::{DecoderResult, RestoredOriginal},
25    encoder_result::{EncoderResult, Recovery},
26    reed_solomon::{ReedSolomonDecoder, ReedSolomonEncoder},
27};
28
29#[cfg(test)]
30#[macro_use]
31mod test_util;
32
33mod decoder_result;
34mod encoder_result;
35mod reed_solomon;
36
37pub mod algorithm {
38    #![doc = include_str!("algorithm.md")]
39}
40pub mod engine;
41pub mod rate;
42
43// ======================================================================
44// Error - PUBLIC
45
46/// Represents all possible errors that can occur in this library.
47#[derive(Clone, Copy, Debug, PartialEq)]
48pub enum Error {
49    /// Given shard has different size than given or inferred shard size.
50    ///
51    /// - Shard size is given explicitly to encoders/decoders
52    ///   and inferred for [`reed_solomon_simd::encode`]
53    ///   and [`reed_solomon_simd::decode`].
54    ///
55    /// [`reed_solomon_simd::encode`]: crate::encode
56    /// [`reed_solomon_simd::decode`]: crate::decode
57    DifferentShardSize {
58        /// Given or inferred shard size.
59        shard_bytes: usize,
60        /// Size of the given shard.
61        got: usize,
62    },
63
64    /// Decoder was given two original shards with same index.
65    DuplicateOriginalShardIndex {
66        /// Given duplicate index.
67        index: usize,
68    },
69
70    /// Decoder was given two recovery shards with same index.
71    DuplicateRecoveryShardIndex {
72        /// Given duplicate index.
73        index: usize,
74    },
75
76    /// Decoder was given original shard with invalid index,
77    /// i.e. `index >= original_count`.
78    InvalidOriginalShardIndex {
79        /// Configured number of original shards.
80        original_count: usize,
81        /// Given invalid index.
82        index: usize,
83    },
84
85    /// Decoder was given recovery shard with invalid index,
86    /// i.e. `index >= recovery_count`.
87    InvalidRecoveryShardIndex {
88        /// Configured number of recovery shards.
89        recovery_count: usize,
90        /// Given invalid index.
91        index: usize,
92    },
93
94    /// Given or inferred shard size is invalid:
95    /// Size must be non-zero and even.
96    ///
97    /// - Shard size is given explicitly to encoders/decoders
98    ///   and inferred for [`reed_solomon_simd::encode`]
99    ///   and [`reed_solomon_simd::decode`].
100    ///
101    /// [`reed_solomon_simd::encode`]: crate::encode
102    /// [`reed_solomon_simd::decode`]: crate::decode
103    InvalidShardSize {
104        /// Given or inferred shard size.
105        shard_bytes: usize,
106    },
107
108    /// Decoder was given too few shards.
109    ///
110    /// Decoding requires as many shards as there were original shards
111    /// in total, in any combination of original shards and recovery shards.
112    NotEnoughShards {
113        /// Configured number of original shards.
114        original_count: usize,
115        /// Number of original shards given to decoder.
116        original_received_count: usize,
117        /// Number of recovery shards given to decoder.
118        recovery_received_count: usize,
119    },
120
121    /// Encoder was given less than `original_count` original shards.
122    TooFewOriginalShards {
123        /// Configured number of original shards.
124        original_count: usize,
125        /// Number of original shards given to encoder.
126        original_received_count: usize,
127    },
128
129    /// Encoder was given more than `original_count` original shards.
130    TooManyOriginalShards {
131        /// Configured number of original shards.
132        original_count: usize,
133    },
134
135    /// Given `original_count` / `recovery_count` combination is not supported.
136    UnsupportedShardCount {
137        /// Given number of original shards.
138        original_count: usize,
139        /// Given number of recovery shards.
140        recovery_count: usize,
141    },
142}
143
144// ======================================================================
145// Error - IMPL DISPLAY
146
147impl fmt::Display for Error {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        match self {
150            Self::DifferentShardSize { shard_bytes, got } => {
151                write!(
152                    f,
153                    "different shard size: expected {shard_bytes} bytes, got {got} bytes"
154                )
155            }
156
157            Self::DuplicateOriginalShardIndex { index } => {
158                write!(f, "duplicate original shard index: {index}")
159            }
160
161            Self::DuplicateRecoveryShardIndex { index } => {
162                write!(f, "duplicate recovery shard index: {index}")
163            }
164
165            Self::InvalidOriginalShardIndex {
166                original_count,
167                index,
168            } => {
169                write!(
170                    f,
171                    "invalid original shard index: {index} >= original_count {original_count}",
172                )
173            }
174
175            Self::InvalidRecoveryShardIndex {
176                recovery_count,
177                index,
178            } => {
179                write!(
180                    f,
181                    "invalid recovery shard index: {index} >= recovery_count {recovery_count}",
182                )
183            }
184
185            Self::InvalidShardSize { shard_bytes } => {
186                write!(
187                    f,
188                    "invalid shard size: {shard_bytes} bytes (must non-zero and multiple of 2)"
189                )
190            }
191
192            Self::NotEnoughShards {
193                original_count,
194                original_received_count,
195                recovery_received_count,
196            } => {
197                write!(
198                    f,
199                    "not enough shards: {original_received_count} original + {recovery_received_count} recovery < {original_count} original_count",
200                )
201            }
202
203            Self::TooFewOriginalShards {
204                original_count,
205                original_received_count,
206            } => {
207                write!(
208                    f,
209                    "too few original shards: got {original_received_count} shards while original_count is {original_count}"
210                )
211            }
212
213            Self::TooManyOriginalShards { original_count } => {
214                write!(
215                    f,
216                    "too many original shards: got more than original_count ({original_count}) shards"
217                )
218            }
219
220            Self::UnsupportedShardCount {
221                original_count,
222                recovery_count,
223            } => {
224                write!(
225                    f,
226                    "unsupported shard count: {original_count} original shards with {recovery_count} recovery shards"
227                )
228            }
229        }
230    }
231}
232
233// ======================================================================
234// Error - IMPL ERROR
235
236impl core::error::Error for Error {}
237
238// ======================================================================
239// FUNCTIONS - PUBLIC
240
241/// Encodes in one go using [`ReedSolomonEncoder`],
242/// returning generated recovery shards.
243///
244/// - Original shards have indexes `0..original_count`
245///   corresponding to the order in which they are given.
246/// - Recovery shards have indexes `0..recovery_count`
247///   corresponding to their position in the returned `Vec`.
248/// - These same indexes must be used when decoding.
249///
250/// See [simple usage](crate#simple-usage) for an example.
251pub fn encode<T>(
252    original_count: usize,
253    recovery_count: usize,
254    original: T,
255) -> Result<Vec<Vec<u8>>, Error>
256where
257    T: IntoIterator,
258    T::Item: AsRef<[u8]>,
259{
260    if !ReedSolomonEncoder::supports(original_count, recovery_count) {
261        return Err(Error::UnsupportedShardCount {
262            original_count,
263            recovery_count,
264        });
265    }
266
267    let mut original = original.into_iter();
268
269    let (shard_bytes, first) = if let Some(first) = original.next() {
270        (first.as_ref().len(), first)
271    } else {
272        return Err(Error::TooFewOriginalShards {
273            original_count,
274            original_received_count: 0,
275        });
276    };
277
278    let mut encoder = ReedSolomonEncoder::new(original_count, recovery_count, shard_bytes)?;
279
280    encoder.add_original_shard(first)?;
281    for original in original {
282        encoder.add_original_shard(original)?;
283    }
284
285    let result = encoder.encode()?;
286
287    Ok(result.recovery_iter().map(<[u8]>::to_vec).collect())
288}
289
290/// Decodes in one go using [`ReedSolomonDecoder`],
291/// returning restored original shards with their indexes.
292///
293/// - Given shard indexes must be the same that were used in encoding.
294///
295/// See [simple usage](crate#simple-usage) for an example and more details.
296pub fn decode<O, R, OT, RT>(
297    original_count: usize,
298    recovery_count: usize,
299    original: O,
300    recovery: R,
301) -> Result<BTreeMap<usize, Vec<u8>>, Error>
302where
303    O: IntoIterator<Item = (usize, OT)>,
304    R: IntoIterator<Item = (usize, RT)>,
305    OT: AsRef<[u8]>,
306    RT: AsRef<[u8]>,
307{
308    if !ReedSolomonDecoder::supports(original_count, recovery_count) {
309        return Err(Error::UnsupportedShardCount {
310            original_count,
311            recovery_count,
312        });
313    }
314
315    let original = original.into_iter();
316    let mut recovery = recovery.into_iter();
317
318    let (shard_bytes, first_recovery) = if let Some(first_recovery) = recovery.next() {
319        (first_recovery.1.as_ref().len(), first_recovery)
320    } else {
321        // NO RECOVERY SHARDS
322
323        let original_received_count = original.count();
324        if original_received_count == original_count {
325            // Nothing to do, original data is complete.
326            return Ok(BTreeMap::new());
327        }
328
329        return Err(Error::NotEnoughShards {
330            original_count,
331            original_received_count,
332            recovery_received_count: 0,
333        });
334    };
335
336    let mut decoder = ReedSolomonDecoder::new(original_count, recovery_count, shard_bytes)?;
337
338    for (index, original) in original {
339        decoder.add_original_shard(index, original)?;
340    }
341
342    decoder.add_recovery_shard(first_recovery.0, first_recovery.1)?;
343    for (index, recovery) in recovery {
344        decoder.add_recovery_shard(index, recovery)?;
345    }
346
347    let mut result = BTreeMap::new();
348    for (index, original) in decoder.decode()?.restored_original_iter() {
349        result.insert(index, original.to_vec());
350    }
351
352    Ok(result)
353}
354
355// ======================================================================
356// TESTS
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use crate::{engine::DefaultEngine, rate::DefaultRate};
362
363    // ============================================================
364    // ROUNDTRIP
365
366    #[test]
367    fn roundtrip() {
368        let original = test_util::generate_original(2, 1024, 123);
369
370        let recovery = encode(2, 3, &original).unwrap();
371
372        test_util::assert_hash(&recovery, test_util::LOW_2_3);
373
374        let restored = decode(2, 3, [(0, ""); 0], [(0, &recovery[0]), (1, &recovery[1])]).unwrap();
375
376        assert_eq!(restored.len(), 2);
377        assert_eq!(restored[&0], original[0]);
378        assert_eq!(restored[&1], original[1]);
379    }
380
381    // ==================================================
382    // trait Send
383
384    #[test]
385    fn test_send() {
386        fn assert_send<T: Send>() {}
387        assert_send::<ReedSolomonEncoder>();
388        assert_send::<ReedSolomonDecoder>();
389        assert_send::<DefaultEngine>();
390        assert_send::<DefaultRate<DefaultEngine>>();
391        assert_send::<DecoderResult>();
392        assert_send::<EncoderResult>();
393        assert_send::<Error>();
394    }
395
396    // ==================================================
397    // trait Sync
398
399    #[test]
400    fn test_sync() {
401        fn assert_sync<T: Sync>() {}
402        assert_sync::<ReedSolomonEncoder>();
403        assert_sync::<ReedSolomonDecoder>();
404        assert_sync::<DefaultEngine>();
405        assert_sync::<DefaultRate<DefaultEngine>>();
406        assert_sync::<DecoderResult>();
407        assert_sync::<EncoderResult>();
408        assert_sync::<Error>();
409    }
410
411    // ============================================================
412    // encode
413
414    mod encode {
415        use super::super::*;
416
417        // ==================================================
418        // ERRORS
419
420        #[test]
421        fn different_shard_size_with_different_original_shard_sizes() {
422            assert_eq!(
423                encode(2, 1, &[&[0u8; 64] as &[u8], &[0u8; 128]]),
424                Err(Error::DifferentShardSize {
425                    shard_bytes: 64,
426                    got: 128
427                })
428            );
429        }
430
431        #[test]
432        fn invalid_shard_size_with_empty_shard() {
433            assert_eq!(
434                encode(1, 1, &[&[0u8; 0]]),
435                Err(Error::InvalidShardSize { shard_bytes: 0 })
436            );
437        }
438
439        #[test]
440        fn too_few_original_shards_with_zero_shards_given() {
441            assert_eq!(
442                encode(1, 1, &[] as &[&[u8]]),
443                Err(Error::TooFewOriginalShards {
444                    original_count: 1,
445                    original_received_count: 0,
446                })
447            );
448        }
449
450        #[test]
451        fn too_many_original_shards() {
452            assert_eq!(
453                encode(1, 1, &[[0u8; 64], [0u8; 64]]),
454                Err(Error::TooManyOriginalShards { original_count: 1 })
455            );
456        }
457
458        #[test]
459        fn unsupported_shard_count_with_zero_original_count() {
460            assert_eq!(
461                encode(0, 1, &[] as &[&[u8]]),
462                Err(Error::UnsupportedShardCount {
463                    original_count: 0,
464                    recovery_count: 1,
465                })
466            );
467        }
468
469        #[test]
470        fn unsupported_shard_count_with_zero_recovery_count() {
471            assert_eq!(
472                encode(1, 0, &[[0u8; 64]]),
473                Err(Error::UnsupportedShardCount {
474                    original_count: 1,
475                    recovery_count: 0,
476                })
477            );
478        }
479    }
480
481    // ============================================================
482    // decode
483
484    mod decode {
485        use super::super::*;
486
487        #[test]
488        fn no_original_missing_with_no_recovery_given() {
489            let restored = decode(1, 1, [(0, &[0u8; 64])], [(0, ""); 0]).unwrap();
490            assert!(restored.is_empty());
491        }
492
493        // ==================================================
494        // ERRORS
495
496        #[test]
497        fn different_shard_size_with_different_original_shard_sizes() {
498            assert_eq!(
499                decode(
500                    2,
501                    1,
502                    [(0, &[0u8; 64] as &[u8]), (1, &[0u8; 128])],
503                    [(0, &[0u8; 64])],
504                ),
505                Err(Error::DifferentShardSize {
506                    shard_bytes: 64,
507                    got: 128
508                })
509            );
510        }
511
512        #[test]
513        fn different_shard_size_with_different_recovery_shard_sizes() {
514            assert_eq!(
515                decode(
516                    1,
517                    2,
518                    [(0, &[0u8; 64])],
519                    [(0, &[0u8; 64] as &[u8]), (1, &[0u8; 128])],
520                ),
521                Err(Error::DifferentShardSize {
522                    shard_bytes: 64,
523                    got: 128
524                })
525            );
526        }
527
528        #[test]
529        fn different_shard_size_with_empty_original_shard() {
530            assert_eq!(
531                decode(1, 1, [(0, &[0u8; 0])], [(0, &[0u8; 64])]),
532                Err(Error::DifferentShardSize {
533                    shard_bytes: 64,
534                    got: 0
535                })
536            );
537        }
538
539        #[test]
540        fn duplicate_original_shard_index() {
541            assert_eq!(
542                decode(2, 1, [(0, &[0u8; 64]), (0, &[0u8; 64])], [(0, &[0u8; 64])]),
543                Err(Error::DuplicateOriginalShardIndex { index: 0 })
544            );
545        }
546
547        #[test]
548        fn duplicate_recovery_shard_index() {
549            assert_eq!(
550                decode(1, 2, [(0, &[0u8; 64])], [(0, &[0u8; 64]), (0, &[0u8; 64])]),
551                Err(Error::DuplicateRecoveryShardIndex { index: 0 })
552            );
553        }
554
555        #[test]
556        fn invalid_original_shard_index() {
557            assert_eq!(
558                decode(1, 1, [(1, &[0u8; 64])], [(0, &[0u8; 64])]),
559                Err(Error::InvalidOriginalShardIndex {
560                    original_count: 1,
561                    index: 1,
562                })
563            );
564        }
565
566        #[test]
567        fn invalid_recovery_shard_index() {
568            assert_eq!(
569                decode(1, 1, [(0, &[0u8; 64])], [(1, &[0u8; 64])]),
570                Err(Error::InvalidRecoveryShardIndex {
571                    recovery_count: 1,
572                    index: 1,
573                })
574            );
575        }
576
577        #[test]
578        fn invalid_shard_size_with_empty_recovery_shard() {
579            assert_eq!(
580                decode(1, 1, [(0, &[0u8; 64])], [(0, &[0u8; 0])]),
581                Err(Error::InvalidShardSize { shard_bytes: 0 })
582            );
583        }
584
585        #[test]
586        fn not_enough_shards() {
587            assert_eq!(
588                decode(1, 1, [(0, ""); 0], [(0, ""); 0]),
589                Err(Error::NotEnoughShards {
590                    original_count: 1,
591                    original_received_count: 0,
592                    recovery_received_count: 0,
593                })
594            );
595        }
596
597        #[test]
598        fn unsupported_shard_count_with_zero_original_count() {
599            assert_eq!(
600                decode(0, 1, [(0, ""); 0], [(0, ""); 0]),
601                Err(Error::UnsupportedShardCount {
602                    original_count: 0,
603                    recovery_count: 1,
604                })
605            );
606        }
607
608        #[test]
609        fn unsupported_shard_count_with_zero_recovery_count() {
610            assert_eq!(
611                decode(1, 0, [(0, ""); 0], [(0, ""); 0]),
612                Err(Error::UnsupportedShardCount {
613                    original_count: 1,
614                    recovery_count: 0,
615                })
616            );
617        }
618    }
619}