Skip to main content

commonware_cryptography/reed_solomon/engine/
shards.rs

1//! Owned shard storage and mutable shard views, laid out as flat arrays of chunks.
2
3use crate::reed_solomon::engine::SHARD_CHUNK_BYTES;
4#[cfg(not(feature = "std"))]
5use alloc::vec::Vec;
6use core::ops::{Bound, Index, IndexMut, Range, RangeBounds};
7
8/// Owned work buffer of `shard_count` shards, each `shard_chunk_count` chunks long.
9///
10/// Shard `i` occupies chunks `i * shard_chunk_count..(i + 1) * shard_chunk_count` of `data`.
11pub(crate) struct Shards {
12    shard_count: usize,
13    /// Shard length in `SHARD_CHUNK_BYTES` chunks.
14    shard_chunk_count: usize,
15
16    /// Flat Vec of `shard_count * shard_chunk_count * SHARD_CHUNK_BYTES` bytes.
17    data: Vec<[u8; SHARD_CHUNK_BYTES]>,
18}
19
20impl Shards {
21    /// Returns a [`ShardsRefMut`] view of all shards.
22    pub(crate) fn as_ref_mut(&mut self) -> ShardsRefMut<'_> {
23        ShardsRefMut::new(self.shard_count, self.shard_chunk_count, self.data.as_mut())
24    }
25
26    /// Creates an empty buffer with zero shards and no allocation.
27    pub(crate) const fn new() -> Self {
28        Self {
29            shard_count: 0,
30            shard_chunk_count: 0,
31            data: Vec::new(),
32        }
33    }
34
35    /// Sets the dimensions to `shard_count` shards of `shard_chunk_count` chunks each.
36    ///
37    /// Existing chunks keep their bytes at the same flat positions and chunks added by growth
38    /// are zeroed. A change in `shard_chunk_count` moves shard boundaries across old data.
39    pub(crate) fn resize(&mut self, shard_count: usize, shard_chunk_count: usize) {
40        self.shard_count = shard_count;
41        self.shard_chunk_count = shard_chunk_count;
42
43        self.data.resize(
44            self.shard_count * self.shard_chunk_count,
45            [0; SHARD_CHUNK_BYTES],
46        );
47    }
48
49    /// Copies `shard` into shard `index` using the last-chunk encoding.
50    ///
51    /// Whole chunks are copied verbatim. A partial tail of `2 * n` bytes places `n` low bytes at
52    /// offset 0 and `n` high bytes at offset `SHARD_CHUNK_BYTES / 2` of the final chunk.
53    /// Bytes of shard `index` past the copied data keep their previous values.
54    ///
55    /// `shard` must fit in shard `index`.
56    ///
57    /// # Panics
58    ///
59    /// If `shard.len()` is odd.
60    pub(crate) fn insert(&mut self, index: usize, shard: &[u8]) {
61        assert_eq!(shard.len() % 2, 0);
62
63        let whole_chunk_count = shard.len() / SHARD_CHUNK_BYTES;
64        let tail_len = shard.len() % SHARD_CHUNK_BYTES;
65
66        let (src_chunks, src_tail) = shard.split_at(shard.len() - tail_len);
67
68        let dst = &mut self[index];
69        dst[..whole_chunk_count]
70            .as_flattened_mut()
71            .copy_from_slice(src_chunks);
72
73        // A partial final chunk holds a tail of `2 * n` bytes: `n` low bytes at offset 0 and
74        // `n` high bytes at offset `SHARD_CHUNK_BYTES / 2`. See `reed_solomon/algorithm.md`.
75        if tail_len > 0 {
76            let (src_lo, src_hi) = src_tail.split_at(tail_len / 2);
77            let (dst_lo, dst_hi) = dst[whole_chunk_count].split_at_mut(SHARD_CHUNK_BYTES / 2);
78            dst_lo[..src_lo.len()].copy_from_slice(src_lo);
79            dst_hi[..src_hi.len()].copy_from_slice(src_hi);
80        }
81    }
82
83    /// Undoes the last-chunk encoding for the shards in `range`.
84    ///
85    /// Moves the high bytes of a partial final chunk to directly follow its low bytes.
86    pub(crate) fn undo_last_chunk_encoding(&mut self, shard_bytes: usize, range: Range<usize>) {
87        let whole_chunk_count = shard_bytes / SHARD_CHUNK_BYTES;
88        let tail_len = shard_bytes % SHARD_CHUNK_BYTES;
89
90        if tail_len == 0 {
91            return;
92        }
93
94        for idx in range {
95            let last_chunk = &mut self[idx][whole_chunk_count];
96            last_chunk.copy_within(
97                SHARD_CHUNK_BYTES / 2..SHARD_CHUNK_BYTES / 2 + tail_len / 2,
98                tail_len / 2,
99            );
100        }
101    }
102}
103
104impl Index<usize> for Shards {
105    type Output = [[u8; SHARD_CHUNK_BYTES]];
106    fn index(&self, index: usize) -> &Self::Output {
107        &self.data[index * self.shard_chunk_count..(index + 1) * self.shard_chunk_count]
108    }
109}
110
111impl IndexMut<usize> for Shards {
112    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
113        &mut self.data[index * self.shard_chunk_count..(index + 1) * self.shard_chunk_count]
114    }
115}
116
117/// Mutable reference to a shard array.
118pub struct ShardsRefMut<'a> {
119    shard_count: usize,
120    /// Shard length in `SHARD_CHUNK_BYTES` chunks.
121    shard_chunk_count: usize,
122
123    /// Exactly `shard_count * shard_chunk_count` chunks. Shard `i` starts at chunk
124    /// `i * shard_chunk_count`.
125    data: &'a mut [[u8; SHARD_CHUNK_BYTES]],
126}
127
128/// Four mutable shards returned by [`ShardsRefMut::dist4_mut`].
129type FourShardsMut<'a> = (
130    &'a mut [[u8; SHARD_CHUNK_BYTES]],
131    &'a mut [[u8; SHARD_CHUNK_BYTES]],
132    &'a mut [[u8; SHARD_CHUNK_BYTES]],
133    &'a mut [[u8; SHARD_CHUNK_BYTES]],
134);
135
136impl<'a> ShardsRefMut<'a> {
137    /// Returns mutable references to shards at `pos` and `pos + dist`.
138    ///
139    /// See source code of [`Naive::fft`] for an example.
140    ///
141    /// # Panics
142    ///
143    /// If `dist` is `0` or either shard index is out of bounds.
144    ///
145    /// [`Naive::fft`]: crate::reed_solomon::engine::Naive#method.fft
146    pub fn dist2_mut(
147        &mut self,
148        mut pos: usize,
149        mut dist: usize,
150    ) -> (
151        &mut [[u8; SHARD_CHUNK_BYTES]],
152        &mut [[u8; SHARD_CHUNK_BYTES]],
153    ) {
154        assert!(pos < self.shard_count && dist > 0 && dist < self.shard_count - pos);
155        pos *= self.shard_chunk_count;
156        dist *= self.shard_chunk_count;
157
158        let (a, b) = self.data[pos..].split_at_mut(dist);
159        (
160            &mut a[..self.shard_chunk_count],
161            &mut b[..self.shard_chunk_count],
162        )
163    }
164
165    /// Returns mutable references to shards at
166    /// `pos`, `pos + dist`, `pos + dist * 2` and `pos + dist * 3`.
167    ///
168    /// See source code of [`Scalar::fft`] for an example
169    /// (specifically the private method `fft_butterfly_two_layers`).
170    ///
171    /// # Panics
172    ///
173    /// If `dist` is `0` or any shard index is out of bounds.
174    ///
175    /// [`Scalar::fft`]: crate::reed_solomon::engine::Scalar#method.fft
176    pub fn dist4_mut(&mut self, mut pos: usize, mut dist: usize) -> FourShardsMut<'_> {
177        assert!(pos < self.shard_count && dist > 0 && dist <= (self.shard_count - 1 - pos) / 3);
178        pos *= self.shard_chunk_count;
179        dist *= self.shard_chunk_count;
180
181        let (ab, cd) = self.data[pos..].split_at_mut(dist * 2);
182        let (a, b) = ab.split_at_mut(dist);
183        let (c, d) = cd.split_at_mut(dist);
184
185        (
186            &mut a[..self.shard_chunk_count],
187            &mut b[..self.shard_chunk_count],
188            &mut c[..self.shard_chunk_count],
189            &mut d[..self.shard_chunk_count],
190        )
191    }
192
193    /// Returns `true` if this contains no shards.
194    pub const fn is_empty(&self) -> bool {
195        self.shard_count == 0
196    }
197
198    /// Returns number of shards.
199    pub const fn len(&self) -> usize {
200        self.shard_count
201    }
202
203    /// Creates new [`ShardsRefMut`] that references given `data`.
204    ///
205    /// Each shard contains `shard_chunk_count` chunks. Zero-length shards are supported.
206    ///
207    /// # Panics
208    ///
209    /// If `shard_count * shard_chunk_count` overflows or exceeds `data.len()`.
210    pub fn new(
211        shard_count: usize,
212        shard_chunk_count: usize,
213        data: &'a mut [[u8; SHARD_CHUNK_BYTES]],
214    ) -> Self {
215        let chunk_count = shard_count
216            .checked_mul(shard_chunk_count)
217            .expect("shard dimensions overflow");
218        assert!(data.len() >= chunk_count);
219
220        Self {
221            shard_count,
222            shard_chunk_count,
223            data: &mut data[..chunk_count],
224        }
225    }
226
227    /// Splits this [`ShardsRefMut`] into two so that
228    /// first includes shards `0..mid` and second includes shards `mid..`.
229    ///
230    /// # Panics
231    ///
232    /// If `mid > self.len()`.
233    pub fn split_at_mut(&mut self, mid: usize) -> (ShardsRefMut<'_>, ShardsRefMut<'_>) {
234        assert!(mid <= self.shard_count);
235        let (a, b) = self.data.split_at_mut(mid * self.shard_chunk_count);
236
237        (
238            ShardsRefMut::new(mid, self.shard_chunk_count, a),
239            ShardsRefMut::new(self.shard_count - mid, self.shard_chunk_count, b),
240        )
241    }
242
243    /// Fills the shards in `range` with zero bytes.
244    ///
245    /// # Panics
246    ///
247    /// If the range is reversed or extends beyond `self.len()`.
248    pub fn zero<R: RangeBounds<usize>>(&mut self, range: R) {
249        let start = match range.start_bound() {
250            Bound::Included(start) => *start,
251            Bound::Excluded(start) => start.checked_add(1).expect("shard range start overflow"),
252            Bound::Unbounded => 0,
253        };
254
255        let end = match range.end_bound() {
256            Bound::Included(end) => end.checked_add(1).expect("shard range end overflow"),
257            Bound::Excluded(end) => *end,
258            Bound::Unbounded => self.shard_count,
259        };
260
261        assert!(start <= end && end <= self.shard_count);
262        self.data[start * self.shard_chunk_count..end * self.shard_chunk_count]
263            .fill([0; SHARD_CHUNK_BYTES]);
264    }
265}
266
267impl Index<usize> for ShardsRefMut<'_> {
268    type Output = [[u8; SHARD_CHUNK_BYTES]];
269    fn index(&self, index: usize) -> &Self::Output {
270        assert!(index < self.shard_count);
271        &self.data[index * self.shard_chunk_count..(index + 1) * self.shard_chunk_count]
272    }
273}
274
275impl IndexMut<usize> for ShardsRefMut<'_> {
276    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
277        assert!(index < self.shard_count);
278        &mut self.data[index * self.shard_chunk_count..(index + 1) * self.shard_chunk_count]
279    }
280}
281
282impl ShardsRefMut<'_> {
283    /// Copies shards `src..src + count` to `dest..dest + count`.
284    ///
285    /// The ranges may overlap. Both must lie within `0..self.len()`.
286    pub(crate) fn copy_within(&mut self, mut src: usize, mut dest: usize, mut count: usize) {
287        src *= self.shard_chunk_count;
288        dest *= self.shard_chunk_count;
289        count *= self.shard_chunk_count;
290
291        self.data.copy_within(src..src + count, dest);
292    }
293
294    /// Returns mutable flat slices of shard ranges `x..x + count` and `y..y + count`.
295    ///
296    /// # Panics
297    ///
298    /// If either range extends beyond `self.len()` or the ranges overlap.
299    pub(crate) fn flat2_mut(
300        &mut self,
301        mut x: usize,
302        mut y: usize,
303        mut count: usize,
304    ) -> (
305        &mut [[u8; SHARD_CHUNK_BYTES]],
306        &mut [[u8; SHARD_CHUNK_BYTES]],
307    ) {
308        assert!(x <= self.shard_count && count <= self.shard_count - x);
309        assert!(y <= self.shard_count && count <= self.shard_count - y);
310        assert!(x.abs_diff(y) >= count);
311        x *= self.shard_chunk_count;
312        y *= self.shard_chunk_count;
313        count *= self.shard_chunk_count;
314
315        if x < y {
316            let (head, tail) = self.data.split_at_mut(y);
317            (&mut head[x..x + count], &mut tail[..count])
318        } else {
319            let (head, tail) = self.data.split_at_mut(x);
320            (&mut tail[..count], &mut head[y..y + count])
321        }
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::reed_solomon::engine::utils::xor_within;
329
330    #[test]
331    #[should_panic]
332    fn dimensions_overflow() {
333        ShardsRefMut::new(usize::MAX / 2 + 1, 2, &mut []);
334    }
335
336    #[test]
337    #[should_panic]
338    fn empty_shard_index_out_of_bounds() {
339        let shards = ShardsRefMut::new(2, 0, &mut []);
340        let _ = &shards[2];
341    }
342
343    #[test]
344    #[should_panic]
345    fn empty_shard_mutable_index_out_of_bounds() {
346        let mut shards = ShardsRefMut::new(2, 0, &mut []);
347        let _ = &mut shards[2];
348    }
349
350    #[test]
351    #[should_panic]
352    fn empty_shard_split_out_of_bounds() {
353        ShardsRefMut::new(2, 0, &mut []).split_at_mut(3);
354    }
355
356    #[test]
357    #[should_panic]
358    fn empty_shard_range_out_of_bounds() {
359        ShardsRefMut::new(2, 0, &mut []).zero(0..3);
360    }
361
362    #[test]
363    #[should_panic]
364    fn empty_shard_range_reversed() {
365        ShardsRefMut::new(2, 0, &mut []).zero((Bound::Included(2), Bound::Excluded(1)));
366    }
367
368    #[test]
369    #[should_panic]
370    fn empty_shard_range_start_overflow() {
371        ShardsRefMut::new(2, 0, &mut []).zero((Bound::Excluded(usize::MAX), Bound::Unbounded));
372    }
373
374    #[test]
375    #[should_panic]
376    fn empty_shard_range_end_overflow() {
377        ShardsRefMut::new(2, 0, &mut []).zero(..=usize::MAX);
378    }
379
380    #[test]
381    #[should_panic]
382    fn empty_shard_xor_out_of_bounds() {
383        xor_within(&mut ShardsRefMut::new(4, 0, &mut []), 0, 3, 2);
384    }
385
386    #[test]
387    #[should_panic]
388    fn empty_shard_xor_overlapping() {
389        xor_within(&mut ShardsRefMut::new(4, 0, &mut []), 0, 1, 2);
390    }
391
392    #[test]
393    #[should_panic]
394    fn empty_shard_dist2_zero() {
395        ShardsRefMut::new(2, 0, &mut []).dist2_mut(0, 0);
396    }
397
398    #[test]
399    #[should_panic]
400    fn empty_shard_dist4_zero() {
401        ShardsRefMut::new(4, 0, &mut []).dist4_mut(0, 0);
402    }
403
404    #[test]
405    #[should_panic]
406    fn empty_shard_dist2_out_of_bounds() {
407        ShardsRefMut::new(2, 0, &mut []).dist2_mut(1, 1);
408    }
409
410    #[test]
411    #[should_panic]
412    fn empty_shard_dist4_out_of_bounds() {
413        ShardsRefMut::new(4, 0, &mut []).dist4_mut(1, 1);
414    }
415
416    #[test]
417    fn empty_shards() {
418        let mut shards = ShardsRefMut::new(4, 0, &mut []);
419        assert_eq!(shards.len(), 4);
420        assert!(!shards.is_empty());
421        assert!(shards[3].is_empty());
422        shards.dist2_mut(1, 2);
423        shards.dist4_mut(0, 1);
424        shards.zero(1..=3);
425        let (left, right) = shards.split_at_mut(4);
426        assert_eq!(left.len(), 4);
427        assert!(right.is_empty());
428    }
429
430    #[test]
431    fn views_preserve_shard_boundaries() {
432        let mut data = [[9; SHARD_CHUNK_BYTES]; 14];
433        let mut shards = ShardsRefMut::new(6, 2, &mut data);
434        shards.zero(2..4);
435        assert_eq!(shards[2], [[0; SHARD_CHUNK_BYTES]; 2]);
436        assert_eq!(shards[3], [[0; SHARD_CHUNK_BYTES]; 2]);
437        let (first, last) = shards.dist2_mut(0, 5);
438        first.fill([1; SHARD_CHUNK_BYTES]);
439        last.fill([6; SHARD_CHUNK_BYTES]);
440        let (a, b, c, d) = shards.dist4_mut(0, 1);
441        for (index, shard) in [a, b, c, d].into_iter().enumerate() {
442            shard.fill([index as u8 + 1; SHARD_CHUNK_BYTES]);
443        }
444        let (_, mut right) = shards.split_at_mut(3);
445        right[1].fill([5; SHARD_CHUNK_BYTES]);
446        for (index, shard) in data.as_chunks::<2>().0.iter().enumerate() {
447            let expected = if index < 6 { index as u8 + 1 } else { 9 };
448            assert_eq!(shard, &[[expected; SHARD_CHUNK_BYTES]; 2]);
449        }
450    }
451}