Skip to main content

ab_erasure_coding/
lib.rs

1#![expect(incomplete_features, reason = "generic_const_*")]
2#![feature(
3    generic_const_args,
4    generic_const_items,
5    macroless_generic_const_args,
6    min_generic_const_args
7)]
8#![no_std]
9
10use core::fmt;
11use reed_solomon_simd::Error;
12use reed_solomon_simd::engine::DefaultEngine;
13use reed_solomon_simd::rate::{HighRateDecoder, HighRateEncoder, RateDecoder, RateEncoder};
14
15/// Error that occurs when erasure coding data
16#[derive(Debug, Clone, PartialEq, thiserror::Error)]
17pub enum ErasureCodingError {
18    /// Decoder error
19    #[error("Decoder error: {0}")]
20    DecoderError(#[from] Error),
21}
22
23/// Number of `u64` words needed for a bit per shard
24const NUM_WORDS<const NUM_SHARDS: usize>: usize = NUM_SHARDS.div_ceil(u64::BITS as usize);
25
26/// A bit per shard, typically saying whether that shard is present.
27///
28/// Bit `index % 64` of word `index / 64` corresponds to shard `index`, which is the same layout
29/// `reed_solomon_simd::rate::ReceivedShards` uses.
30#[derive(Copy, Clone, Eq, PartialEq)]
31pub struct ShardsBitmap<const NUM_SHARDS: usize> {
32    words: [u64; NUM_WORDS::<NUM_SHARDS>],
33}
34
35impl<const NUM_SHARDS: usize> fmt::Debug for ShardsBitmap<NUM_SHARDS> {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.debug_struct("ShardsBitmap")
38            .field("num_shards", &NUM_SHARDS)
39            .field("num_set", &self.count())
40            .finish()
41    }
42}
43
44impl<const NUM_SHARDS: usize> Default for ShardsBitmap<NUM_SHARDS> {
45    #[inline(always)]
46    fn default() -> Self {
47        Self::none()
48    }
49}
50
51impl<const NUM_SHARDS: usize> ShardsBitmap<NUM_SHARDS> {
52    /// Bitmap with no shards set
53    #[inline(always)]
54    pub const fn none() -> Self {
55        Self {
56            words: [0; NUM_WORDS::<NUM_SHARDS>],
57        }
58    }
59
60    /// Bitmap with all shards set
61    #[inline(always)]
62    pub const fn all() -> Self {
63        let mut this = Self {
64            words: [u64::MAX; NUM_WORDS::<NUM_SHARDS>],
65        };
66
67        // Bits past the last shard must not be set
68        let unused_bits = NUM_WORDS::<NUM_SHARDS> * u64::BITS as usize - NUM_SHARDS;
69        if unused_bits > 0 {
70            this.words[NUM_WORDS::<NUM_SHARDS> - 1] >>= unused_bits;
71        }
72
73        this
74    }
75
76    /// Whether the shard at `index` is set, `false` if `index` is out of bounds
77    #[inline(always)]
78    pub const fn get(&self, index: usize) -> bool {
79        if index >= NUM_SHARDS {
80            return false;
81        }
82
83        (self.words[index / u64::BITS as usize] >> (index % u64::BITS as usize)) & 1 == 1
84    }
85
86    /// Sets the shard at `index`, does nothing if `index` is out of bounds
87    #[inline(always)]
88    pub const fn set(&mut self, index: usize) {
89        if index < NUM_SHARDS {
90            self.words[index / u64::BITS as usize] |= 1 << (index % u64::BITS as usize);
91        }
92    }
93
94    /// Unsets the shard at `index`, does nothing if `index` is out of bounds
95    #[inline(always)]
96    pub const fn unset(&mut self, index: usize) {
97        if index < NUM_SHARDS {
98            self.words[index / u64::BITS as usize] &= !(1 << (index % u64::BITS as usize));
99        }
100    }
101
102    /// Number of shards that are set
103    #[inline]
104    pub const fn count(&self) -> usize {
105        let mut count = 0;
106        let mut word_index = 0;
107        while word_index < NUM_WORDS::<NUM_SHARDS> {
108            count += self.words[word_index].count_ones() as usize;
109            word_index += 1;
110        }
111
112        count
113    }
114}
115
116/// Which source and parity shards are present
117#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
118pub struct ShardsPresent<const NUM_SHARDS: usize> {
119    /// Which source shards are present
120    pub source: ShardsBitmap<NUM_SHARDS>,
121    /// Which parity shards are present
122    pub parity: ShardsBitmap<NUM_SHARDS>,
123}
124
125impl<const NUM_SHARDS: usize> ShardsPresent<NUM_SHARDS> {
126    /// Nothing is present
127    #[inline(always)]
128    pub const fn none() -> Self {
129        Self {
130            source: ShardsBitmap::none(),
131            parity: ShardsBitmap::none(),
132        }
133    }
134
135    /// Everything is present
136    #[inline(always)]
137    pub const fn all() -> Self {
138        Self {
139            source: ShardsBitmap::all(),
140            parity: ShardsBitmap::all(),
141        }
142    }
143}
144
145/// Erasure coding abstraction.
146///
147/// Supports creation of parity shards and recovery of missing data.
148#[derive(Debug, Clone)]
149pub struct ErasureCoding;
150
151impl Default for ErasureCoding {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157impl ErasureCoding {
158    /// Create new erasure coding instance
159    pub fn new() -> Self {
160        Self {}
161    }
162
163    /// Extend contiguously stored source shards with parity shards
164    pub fn extend<const NUM_SHARDS: usize, const SHARD_BYTES: usize>(
165        &self,
166        source: &[[u8; SHARD_BYTES]; NUM_SHARDS],
167        parity: &mut [[u8; SHARD_BYTES]; NUM_SHARDS],
168    ) -> Result<(), ErasureCodingError> {
169        let mut encoder = new_encoder::<NUM_SHARDS, SHARD_BYTES>()?;
170
171        for shard in source {
172            encoder.add_original_shard(shard)?;
173        }
174
175        let result = encoder.encode()?;
176
177        for (input, output) in result.recovery_iter().zip(parity) {
178            output.copy_from_slice(input);
179        }
180
181        Ok(())
182    }
183
184    /// Extend source shards with parity shards, where shards are not stored contiguously
185    pub fn extend_scattered<
186        const NUM_SHARDS: usize,
187        const SHARD_BYTES: usize,
188        SourceShard,
189        ParityShard,
190    >(
191        &self,
192        source: [SourceShard; NUM_SHARDS],
193        parity: [ParityShard; NUM_SHARDS],
194    ) -> Result<(), ErasureCodingError>
195    where
196        SourceShard: AsRef<[u8; SHARD_BYTES]>,
197        ParityShard: AsMut<[u8; SHARD_BYTES]>,
198    {
199        let mut encoder = new_encoder::<NUM_SHARDS, SHARD_BYTES>()?;
200
201        for shard in &source {
202            encoder.add_original_shard(shard.as_ref())?;
203        }
204
205        let result = encoder.encode()?;
206
207        let mut parity = parity;
208        for (input, output) in result.recovery_iter().zip(&mut parity) {
209            output.as_mut().copy_from_slice(input);
210        }
211
212        Ok(())
213    }
214
215    /// Recover missing source shards in place, with everything stored contiguously.
216    ///
217    /// Parity shards are inputs only, missing ones are simply not used. Prefer this over
218    /// [`Self::recover_all()`] when parity shards are not needed.
219    pub fn recover_source<const NUM_SHARDS: usize, const SHARD_BYTES: usize>(
220        &self,
221        source: &mut [[u8; SHARD_BYTES]; NUM_SHARDS],
222        parity: &[[u8; SHARD_BYTES]; NUM_SHARDS],
223        present: &ShardsPresent<NUM_SHARDS>,
224    ) -> Result<(), ErasureCodingError> {
225        let mut decoder = new_decoder::<NUM_SHARDS, SHARD_BYTES>()?;
226
227        for (index, shard) in source.iter().enumerate() {
228            if present.source.get(index) {
229                decoder.add_original_shard(index, shard)?;
230            }
231        }
232        for (index, shard) in parity.iter().enumerate() {
233            if present.parity.get(index) {
234                decoder.add_recovery_shard(index, shard)?;
235            }
236        }
237
238        let result = decoder.decode()?;
239
240        for (index, shard) in source.iter_mut().enumerate() {
241            if !present.source.get(index) {
242                shard.copy_from_slice(restored_original(&result, index));
243            }
244        }
245
246        Ok(())
247    }
248
249    /// Recover missing source shards in place, where shards are not stored contiguously.
250    ///
251    /// Parity shards are inputs only, so missing ones simply have no memory.
252    pub fn recover_source_scattered<
253        const NUM_SHARDS: usize,
254        const SHARD_BYTES: usize,
255        SourceShard,
256        ParityShard,
257    >(
258        &self,
259        source: [SourceShard; NUM_SHARDS],
260        source_present: &ShardsBitmap<NUM_SHARDS>,
261        parity: [Option<ParityShard>; NUM_SHARDS],
262    ) -> Result<(), ErasureCodingError>
263    where
264        SourceShard: AsMut<[u8; SHARD_BYTES]>,
265        ParityShard: AsRef<[u8; SHARD_BYTES]>,
266    {
267        let mut decoder = new_decoder::<NUM_SHARDS, SHARD_BYTES>()?;
268
269        let mut source = source;
270        for (index, shard) in source.iter_mut().enumerate() {
271            if source_present.get(index) {
272                decoder.add_original_shard(index, shard.as_mut().as_slice())?;
273            }
274        }
275        for (index, shard) in parity.iter().enumerate() {
276            if let Some(shard) = shard {
277                decoder.add_recovery_shard(index, shard.as_ref())?;
278            }
279        }
280
281        let result = decoder.decode()?;
282
283        for (index, shard) in source.iter_mut().enumerate() {
284            if !source_present.get(index) {
285                shard
286                    .as_mut()
287                    .copy_from_slice(restored_original(&result, index));
288            }
289        }
290
291        Ok(())
292    }
293
294    /// Recover missing source and parity shards in place, with everything stored contiguously.
295    ///
296    /// Use [`Self::recover_source()`] instead if parity shards are not needed.
297    pub fn recover_all<const NUM_SHARDS: usize, const SHARD_BYTES: usize>(
298        &self,
299        source: &mut [[u8; SHARD_BYTES]; NUM_SHARDS],
300        parity: &mut [[u8; SHARD_BYTES]; NUM_SHARDS],
301        present: &ShardsPresent<NUM_SHARDS>,
302    ) -> Result<(), ErasureCodingError> {
303        self.recover_source(source, parity, present)?;
304
305        if present.parity.count() == NUM_SHARDS {
306            return Ok(());
307        }
308
309        // Source shards are complete at this point, so missing parity shards are simply encoded
310        // again
311        let mut encoder = new_encoder::<NUM_SHARDS, SHARD_BYTES>()?;
312
313        for shard in &*source {
314            encoder.add_original_shard(shard)?;
315        }
316
317        let result = encoder.encode()?;
318
319        for (index, shard) in parity.iter_mut().enumerate() {
320            if !present.parity.get(index) {
321                shard.copy_from_slice(recovery(&result, index));
322            }
323        }
324
325        Ok(())
326    }
327
328    /// Recover missing source and parity shards in place, where shards are not stored contiguously
329    pub fn recover_all_scattered<
330        const NUM_SHARDS: usize,
331        const SHARD_BYTES: usize,
332        SourceShard,
333        ParityShard,
334    >(
335        &self,
336        source: [SourceShard; NUM_SHARDS],
337        parity: [ParityShard; NUM_SHARDS],
338        present: &ShardsPresent<NUM_SHARDS>,
339    ) -> Result<(), ErasureCodingError>
340    where
341        SourceShard: AsMut<[u8; SHARD_BYTES]>,
342        ParityShard: AsMut<[u8; SHARD_BYTES]>,
343    {
344        let mut source = source;
345        let mut parity = parity;
346
347        {
348            let mut decoder = new_decoder::<NUM_SHARDS, SHARD_BYTES>()?;
349
350            for (index, shard) in source.iter_mut().enumerate() {
351                if present.source.get(index) {
352                    decoder.add_original_shard(index, shard.as_mut().as_slice())?;
353                }
354            }
355            for (index, shard) in parity.iter_mut().enumerate() {
356                if present.parity.get(index) {
357                    decoder.add_recovery_shard(index, shard.as_mut().as_slice())?;
358                }
359            }
360
361            let result = decoder.decode()?;
362
363            for (index, shard) in source.iter_mut().enumerate() {
364                if !present.source.get(index) {
365                    shard
366                        .as_mut()
367                        .copy_from_slice(restored_original(&result, index));
368                }
369            }
370        }
371
372        if present.parity.count() == NUM_SHARDS {
373            return Ok(());
374        }
375
376        let mut encoder = new_encoder::<NUM_SHARDS, SHARD_BYTES>()?;
377
378        for shard in &mut source {
379            encoder.add_original_shard(shard.as_mut().as_slice())?;
380        }
381
382        let result = encoder.encode()?;
383
384        for (index, shard) in parity.iter_mut().enumerate() {
385            if !present.parity.get(index) {
386                shard.as_mut().copy_from_slice(recovery(&result, index));
387            }
388        }
389
390        Ok(())
391    }
392}
393
394#[inline(always)]
395fn new_encoder<const NUM_SHARDS: usize, const SHARD_BYTES: usize>()
396-> Result<HighRateEncoder<DefaultEngine>, ErasureCodingError> {
397    Ok(HighRateEncoder::new(
398        NUM_SHARDS,
399        NUM_SHARDS,
400        SHARD_BYTES,
401        DefaultEngine::new(),
402        None,
403    )?)
404}
405
406#[inline(always)]
407fn new_decoder<const NUM_SHARDS: usize, const SHARD_BYTES: usize>()
408-> Result<HighRateDecoder<DefaultEngine>, ErasureCodingError> {
409    Ok(HighRateDecoder::new(
410        NUM_SHARDS,
411        NUM_SHARDS,
412        SHARD_BYTES,
413        DefaultEngine::new(),
414        None,
415    )?)
416}
417
418#[inline(always)]
419fn restored_original<'a>(
420    result: &'a reed_solomon_simd::DecoderResult<'_>,
421    index: usize,
422) -> &'a [u8] {
423    result
424        .restored_original(index)
425        .expect("Always corresponds to a missing source shard; qed")
426}
427
428#[inline(always)]
429fn recovery<'a>(result: &'a reed_solomon_simd::EncoderResult<'_>, index: usize) -> &'a [u8] {
430    result
431        .recovery(index)
432        .expect("Always corresponds to a missing parity shard; qed")
433}