1#![doc = include_str!(concat!(env!("OUT_DIR"), "/README-rustdocified.md"))]
2#![deny(missing_docs)]
3#![warn(clippy::pedantic, clippy::cargo)]
4#![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#[derive(Clone, Copy, Debug, PartialEq)]
48pub enum Error {
49 DifferentShardSize {
58 shard_bytes: usize,
60 got: usize,
62 },
63
64 DuplicateOriginalShardIndex {
66 index: usize,
68 },
69
70 DuplicateRecoveryShardIndex {
72 index: usize,
74 },
75
76 InvalidOriginalShardIndex {
79 original_count: usize,
81 index: usize,
83 },
84
85 InvalidRecoveryShardIndex {
88 recovery_count: usize,
90 index: usize,
92 },
93
94 InvalidShardSize {
104 shard_bytes: usize,
106 },
107
108 NotEnoughShards {
113 original_count: usize,
115 original_received_count: usize,
117 recovery_received_count: usize,
119 },
120
121 TooFewOriginalShards {
123 original_count: usize,
125 original_received_count: usize,
127 },
128
129 TooManyOriginalShards {
131 original_count: usize,
133 },
134
135 UnsupportedShardCount {
137 original_count: usize,
139 recovery_count: usize,
141 },
142}
143
144impl 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
233impl core::error::Error for Error {}
237
238pub 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
290pub 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 let original_received_count = original.count();
324 if original_received_count == original_count {
325 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#[cfg(test)]
359mod tests {
360 use super::*;
361 use crate::{engine::DefaultEngine, rate::DefaultRate};
362
363 #[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 #[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 #[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 mod encode {
415 use super::super::*;
416
417 #[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 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 #[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}