Skip to main content

rusty_erasure_core/
encode.rs

1//! The `Coder`: encode, incremental update, verify, and recover — ISA-L's
2//! `ec_encode_data` / `ec_encode_data_update` semantics plus the recovery flow
3//! ISA-L leaves as an exercise, all behind validated, panic-free APIs.
4//!
5//! Conformance: `encode` and `update` are byte-identical to ISA-L's `_base`
6//! implementations (golden-vector gated); a completed `update` sequence is
7//! byte-identical to one-shot `encode`; `recover` is gated against ground
8//! truth (the original shards ARE the expected output).
9
10use alloc::vec;
11use alloc::vec::Vec;
12
13use crate::error::{CodeError, MatrixError, RecoverError};
14use crate::kernel::Kernels;
15use crate::matrix::Matrix;
16
17/// A prepared decode: the survivor selection, inverted submatrix (composed
18/// into per-target coefficient rows), and expanded tables for ONE loss
19/// pattern — reusable across every stripe sharing that pattern. Built by
20/// [`Coder::decode_plan`], consumed by [`Coder::recover_with`].
21#[derive(Debug, Clone)]
22pub struct DecodePlan {
23    gftbls: Vec<u8>,
24    survivors: Vec<usize>,
25    rebuild: Vec<usize>,
26    n: usize,
27}
28
29/// An erasure coder for one `(matrix)` configuration: `k` source shards in,
30/// `p` parity shards out, recovery from any `k` survivors.
31///
32/// Construction expands the parity coefficients into ISA-L-layout tables once;
33/// encode/update/recover then run with zero allocations on the data path
34/// (recover allocates only its small decode-matrix scratch).
35#[derive(Debug, Clone)]
36pub struct Coder {
37    matrix: Matrix,
38    /// Expanded tables for the parity rows: `p * k * 32` bytes, row-major —
39    /// table for (parity row `l`, source `j`) at `(l*k + j) * 32`.
40    gftbls: Vec<u8>,
41    /// The kernel set chosen ONCE at construction (dispatch at the surface,
42    /// never in the loop).
43    kernels: Kernels,
44}
45
46impl Coder {
47    /// Build a coder from an encode matrix (`rows = k + p`, `cols = k`, top
48    /// block identity — what [`Matrix::reed_solomon`] / [`Matrix::cauchy`]
49    /// produce). A matrix with no parity rows is a dimension error.
50    ///
51    /// This constructor uses the **scalar** kernel set — core carries no
52    /// detection machinery. The `rusty_erasure` facade's `coder()` picks the
53    /// best SIMD set for the running CPU via [`Coder::with_kernels`]; prefer
54    /// it in applications.
55    pub fn new(matrix: Matrix) -> Result<Self, MatrixError> {
56        Self::with_kernels(matrix, Kernels::scalar())
57    }
58
59    /// Build a coder driving an explicit kernel set (see [`Kernels`]).
60    pub fn with_kernels(matrix: Matrix, kernels: Kernels) -> Result<Self, MatrixError> {
61        if matrix.rows() <= matrix.cols() {
62            return Err(MatrixError::Dimensions {
63                k: matrix.cols(),
64                p: matrix.rows().saturating_sub(matrix.cols()),
65            });
66        }
67        let gftbls = (kernels.init)(matrix.parity_bytes());
68        Ok(Self {
69            matrix,
70            gftbls,
71            kernels,
72        })
73    }
74
75    /// The kernel set this coder drives (name is useful for reporting).
76    pub fn kernels(&self) -> &Kernels {
77        &self.kernels
78    }
79
80    /// Source-shard count.
81    pub fn k(&self) -> usize {
82        self.matrix.cols()
83    }
84
85    /// Parity-shard count.
86    pub fn p(&self) -> usize {
87        self.matrix.rows() - self.matrix.cols()
88    }
89
90    /// The encode matrix this coder was built from.
91    pub fn matrix(&self) -> &Matrix {
92        &self.matrix
93    }
94
95    /// The expanded parity tables, in THIS coder's kernel-set format
96    /// (`kernels().table_bytes` per coefficient — ISA-L nibble layout for the
97    /// scalar/PSHUFB sets, affine matrices for GFNI). Exposed for the compat
98    /// layer and the conformance tests.
99    pub fn gftbls(&self) -> &[u8] {
100        &self.gftbls
101    }
102
103    fn check_data(&self, data: &[&[u8]], len: usize) -> Result<(), CodeError> {
104        if data.len() != self.k() {
105            return Err(CodeError::ShardCount {
106                expected: self.k(),
107                got: data.len(),
108            });
109        }
110        for (index, d) in data.iter().enumerate() {
111            if d.len() != len {
112                return Err(CodeError::ShardLength {
113                    index,
114                    expected: len,
115                    got: d.len(),
116                });
117            }
118        }
119        Ok(())
120    }
121
122    /// Encode: `k` equal-length source shards in, `p` parity shards out
123    /// (overwritten). Byte-identical to ISA-L `ec_encode_data`.
124    pub fn encode(&self, data: &[&[u8]], parity: &mut [&mut [u8]]) -> Result<(), CodeError> {
125        if parity.len() != self.p() {
126            return Err(CodeError::ShardCount {
127                expected: self.p(),
128                got: parity.len(),
129            });
130        }
131        let len = parity.first().map_or(0, |b| b.len());
132        for (index, b) in parity.iter().enumerate() {
133            if b.len() != len {
134                return Err(CodeError::ShardLength {
135                    index: self.k() + index,
136                    expected: len,
137                    got: b.len(),
138                });
139            }
140        }
141        self.check_data(data, len)?;
142        (self.kernels.encode)(&self.gftbls, data, parity);
143        Ok(())
144    }
145
146    /// Incremental encode: fold ONE source shard (index `shard_index`) into
147    /// all parity shards. Starting from zeroed parity buffers and calling this
148    /// once per source (any order) yields byte-identical output to
149    /// [`Coder::encode`] — ISA-L `ec_encode_data_update` semantics.
150    pub fn update(
151        &self,
152        shard_index: usize,
153        data: &[u8],
154        parity: &mut [&mut [u8]],
155    ) -> Result<(), CodeError> {
156        let k = self.k();
157        if shard_index >= k {
158            return Err(CodeError::ShardIndex {
159                index: shard_index,
160                k,
161            });
162        }
163        if parity.len() != self.p() {
164            return Err(CodeError::ShardCount {
165                expected: self.p(),
166                got: parity.len(),
167            });
168        }
169        for (index, b) in parity.iter().enumerate() {
170            if b.len() != data.len() {
171                return Err(CodeError::ShardLength {
172                    index: k + index,
173                    expected: data.len(),
174                    got: b.len(),
175                });
176            }
177        }
178        (self.kernels.update)(&self.gftbls, k, shard_index, data, parity);
179        Ok(())
180    }
181
182    /// Check that `parity` is consistent with `data`. `Ok(true)` means every
183    /// parity shard matches a fresh encode.
184    pub fn verify(&self, data: &[&[u8]], parity: &[&[u8]]) -> Result<bool, CodeError> {
185        if parity.len() != self.p() {
186            return Err(CodeError::ShardCount {
187                expected: self.p(),
188                got: parity.len(),
189            });
190        }
191        let len = parity.first().map_or(0, |b| b.len());
192        for (index, b) in parity.iter().enumerate() {
193            if b.len() != len {
194                return Err(CodeError::ShardLength {
195                    index: self.k() + index,
196                    expected: len,
197                    got: b.len(),
198                });
199            }
200        }
201        self.check_data(data, len)?;
202        // One fused kernel call re-encodes every parity row in a single walk
203        // of the sources (brick: data was read p times, once per row — the
204        // census counter shows k*len counted bytes per verify instead of
205        // p*k*len). Memory cost: a p*len scratch instead of len.
206        let p = self.p();
207        if len == 0 {
208            return Ok(true);
209        }
210        let mut scratch = vec![0u8; p * len];
211        {
212            let mut rows: Vec<&mut [u8]> = scratch.chunks_mut(len).collect();
213            (self.kernels.encode)(&self.gftbls, data, &mut rows);
214        }
215        for (l, expect) in parity.iter().enumerate() {
216            if &scratch[l * len..(l + 1) * len] != *expect {
217                return Ok(false);
218            }
219        }
220        Ok(true)
221    }
222
223    /// Prepare a reusable decode plan for one loss pattern: which shards are
224    /// present (`present[i]`), and which indices to rebuild. The expensive,
225    /// data-independent work — survivor selection, submatrix inversion,
226    /// coefficient composition, table expansion — happens ONCE here;
227    /// [`Coder::recover_with`] then rebuilds any number of stripes with that
228    /// pattern at pure kernel cost (repair jobs and steady-state degraded
229    /// reads reuse one plan across every stripe).
230    pub fn decode_plan(
231        &self,
232        present: &[bool],
233        rebuild: &[usize],
234    ) -> Result<DecodePlan, RecoverError> {
235        let k = self.k();
236        let n = self.matrix.rows();
237        if present.len() != n {
238            return Err(CodeError::ShardCount {
239                expected: n,
240                got: present.len(),
241            }
242            .into());
243        }
244        for &x in rebuild {
245            if x >= n {
246                return Err(CodeError::ShardIndex { index: x, k: n }.into());
247            }
248        }
249        let mut survivors: Vec<usize> = Vec::with_capacity(k);
250        let mut have = 0usize;
251        for (i, &ok) in present.iter().enumerate() {
252            if ok {
253                have += 1;
254                if survivors.len() < k {
255                    survivors.push(i);
256                }
257            }
258        }
259        if survivors.len() < k {
260            return Err(RecoverError::TooManyMissing {
261                missing: n - have,
262                p: self.p(),
263            });
264        }
265        let b = self.matrix.select_rows(&survivors)?;
266        let d = b.invert()?;
267        let mut coeffs = vec![0u8; rebuild.len() * k];
268        for (r, &x) in rebuild.iter().enumerate() {
269            let row = &mut coeffs[r * k..(r + 1) * k];
270            if x < k {
271                for (t, c) in row.iter_mut().enumerate() {
272                    *c = d.get(x, t).expect("in range");
273                }
274            } else {
275                for (t, c) in row.iter_mut().enumerate() {
276                    let mut s = 0u8;
277                    for j in 0..k {
278                        s ^= crate::gf::mul(
279                            self.matrix.get(x, j).expect("in range"),
280                            d.get(j, t).expect("in range"),
281                        );
282                    }
283                    *c = s;
284                }
285            }
286        }
287        Ok(DecodePlan {
288            gftbls: (self.kernels.init)(&coeffs),
289            survivors,
290            rebuild: rebuild.to_vec(),
291            n,
292        })
293    }
294
295    /// Rebuild one stripe with a prepared [`DecodePlan`] — pure kernel cost,
296    /// no matrix work, no table expansion, one small scratch collection.
297    pub fn recover_with(
298        &self,
299        plan: &DecodePlan,
300        shards: &[Option<&[u8]>],
301        out: &mut [&mut [u8]],
302    ) -> Result<(), RecoverError> {
303        if shards.len() != plan.n {
304            return Err(CodeError::ShardCount {
305                expected: plan.n,
306                got: shards.len(),
307            }
308            .into());
309        }
310        if out.len() != plan.rebuild.len() {
311            return Err(CodeError::ShardCount {
312                expected: plan.rebuild.len(),
313                got: out.len(),
314            }
315            .into());
316        }
317        let mut src: Vec<&[u8]> = Vec::with_capacity(plan.survivors.len());
318        let len = out.first().map_or(0, |b| b.len());
319        for &i in &plan.survivors {
320            let s = shards[i].ok_or(RecoverError::TooManyMissing {
321                missing: 1,
322                p: self.p(),
323            })?;
324            if s.len() != len {
325                return Err(CodeError::ShardLength {
326                    index: i,
327                    expected: len,
328                    got: s.len(),
329                }
330                .into());
331            }
332            src.push(s);
333        }
334        for b in out.iter() {
335            if b.len() != len {
336                return Err(CodeError::ShardLength {
337                    index: 0,
338                    expected: len,
339                    got: b.len(),
340                }
341                .into());
342            }
343        }
344        (self.kernels.encode)(&plan.gftbls, &src, out);
345        Ok(())
346    }
347
348    /// Rebuild shards from survivors.
349    ///
350    /// `shards` is the full stripe in index order — `k` sources then `p`
351    /// parity — with `None` for anything lost. `rebuild` names the shard
352    /// indices to reconstruct (source or parity), and `out` supplies one
353    /// equal-length buffer per rebuild target. Any `k` present shards suffice;
354    /// fewer is [`RecoverError::TooManyMissing`].
355    ///
356    /// For many stripes with one loss pattern, build a [`Coder::decode_plan`]
357    /// once and use [`Coder::recover_with`] — this one-shot form re-derives
358    /// the decode matrix every call.
359    pub fn recover(
360        &self,
361        shards: &[Option<&[u8]>],
362        rebuild: &[usize],
363        out: &mut [&mut [u8]],
364    ) -> Result<(), RecoverError> {
365        let k = self.k();
366        let n = self.matrix.rows();
367        if shards.len() != n {
368            return Err(CodeError::ShardCount {
369                expected: n,
370                got: shards.len(),
371            }
372            .into());
373        }
374        if rebuild.len() != out.len() {
375            return Err(CodeError::ShardCount {
376                expected: rebuild.len(),
377                got: out.len(),
378            }
379            .into());
380        }
381        for &x in rebuild {
382            if x >= n {
383                return Err(CodeError::ShardIndex { index: x, k: n }.into());
384            }
385        }
386
387        // Survivors: the first k present shards, in index order.
388        let mut survivors: Vec<usize> = Vec::with_capacity(k);
389        let mut present = 0usize;
390        for (i, s) in shards.iter().enumerate() {
391            if s.is_some() {
392                present += 1;
393                if survivors.len() < k {
394                    survivors.push(i);
395                }
396            }
397        }
398        if survivors.len() < k {
399            return Err(RecoverError::TooManyMissing {
400                missing: n - present,
401                p: self.p(),
402            });
403        }
404
405        // Shard length agreement across every present shard and output buffer.
406        let len = shards[survivors[0]].expect("survivor is present").len();
407        for (index, s) in shards.iter().enumerate() {
408            if let Some(s) = s
409                && s.len() != len
410            {
411                return Err(CodeError::ShardLength {
412                    index,
413                    expected: len,
414                    got: s.len(),
415                }
416                .into());
417            }
418        }
419        for (i, b) in out.iter().enumerate() {
420            if b.len() != len {
421                return Err(CodeError::ShardLength {
422                    index: rebuild[i],
423                    expected: len,
424                    got: b.len(),
425                }
426                .into());
427            }
428        }
429
430        // Decode matrix: invert the survivors' rows. d maps survivor shard
431        // values back to the original sources.
432        let b = self.matrix.select_rows(&survivors)?;
433        let d = b.invert()?;
434
435        let src: Vec<&[u8]> = survivors
436            .iter()
437            .map(|&i| shards[i].expect("survivor is present"))
438            .collect();
439
440        // One decode-coefficient row per rebuild target, expanded together so
441        // a SINGLE kernels.encode call (with its row fusion) rebuilds all of
442        // them in one walk over the survivors.
443        let mut coeffs = vec![0u8; rebuild.len() * k];
444        for (r, &x) in rebuild.iter().enumerate() {
445            let row = &mut coeffs[r * k..(r + 1) * k];
446            if x < k {
447                // Missing source: row x of the inverse maps survivors -> source x.
448                for (t, c) in row.iter_mut().enumerate() {
449                    *c = d.get(x, t).expect("in range");
450                }
451            } else {
452                // Missing parity: compose the parity row with the inverse so a
453                // single pass over the survivors rebuilds it directly.
454                for (t, c) in row.iter_mut().enumerate() {
455                    let mut s = 0u8;
456                    for j in 0..k {
457                        s ^= crate::gf::mul(
458                            self.matrix.get(x, j).expect("in range"),
459                            d.get(j, t).expect("in range"),
460                        );
461                    }
462                    *c = s;
463                }
464            }
465        }
466        let gftbls = (self.kernels.init)(&coeffs);
467        (self.kernels.encode)(&gftbls, &src, out);
468        Ok(())
469    }
470}