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}