rusty_erasure_core/matrix.rs
1//! Coding matrices: generation (Vandermonde / Cauchy), inversion, and row
2//! selection for recovery.
3//!
4//! Constructions are behavior-identical to ISA-L's `gf_gen_rs_matrix`,
5//! `gf_gen_cauchy1_matrix`, and `gf_invert_matrix` — with the contract ISA-L
6//! leaves to the caller enforced here as `Result`s: dimension checks, the
7//! documented Vandermonde safe region, and singularity as a typed error
8//! instead of a `-1`.
9
10use alloc::vec;
11use alloc::vec::Vec;
12
13use crate::error::MatrixError;
14use crate::gf;
15
16/// A row-major `rows x cols` matrix over GF(2^8).
17///
18/// For an encode matrix, `rows = k + p` (sources + parity, ISA-L's `m`) and
19/// `cols = k`; the top `k x k` block is the identity, so source shards pass
20/// through unchanged and the bottom `p` rows generate parity.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Matrix {
23 rows: usize,
24 cols: usize,
25 data: Vec<u8>,
26}
27
28/// ISA-L's documented safe region for Vandermonde matrices (`k` sources,
29/// `m` total rows): inside it every recovery submatrix is invertible; outside
30/// it singular decode matrices exist, so [`Matrix::reed_solomon`] refuses.
31const fn vandermonde_safe(k: usize, m: usize) -> bool {
32 k <= 3 || (k == 4 && m <= 25) || (k == 5 && m <= 10) || (k <= 21 && m - k == 4) || m - k <= 3
33}
34
35fn check_dims(k: usize, p: usize) -> Result<(), MatrixError> {
36 // checked_add: k + p must not overflow for adversarial dimensions — the
37 // no-panic sweep feeds usize::MAX here on purpose.
38 if k == 0 || p == 0 || !k.checked_add(p).is_some_and(|m| m <= 255) {
39 return Err(MatrixError::Dimensions { k, p });
40 }
41 Ok(())
42}
43
44impl Matrix {
45 /// Vandermonde-style encode matrix for `k` sources and `p` parity rows —
46 /// ISA-L's `gf_gen_rs_matrix`, refusing the configurations ISA-L's own
47 /// documentation marks unsafe (where some decode submatrices are
48 /// singular). Outside the safe region use [`Matrix::cauchy`].
49 pub fn reed_solomon(k: usize, p: usize) -> Result<Self, MatrixError> {
50 check_dims(k, p)?;
51 let m = k + p;
52 if !vandermonde_safe(k, m) {
53 return Err(MatrixError::VandermondeUnsafe { k, p });
54 }
55 let mut data = vec![0u8; m * k];
56 for i in 0..k {
57 data[k * i + i] = 1;
58 }
59 let mut row_gen: u8 = 1;
60 for i in k..m {
61 let mut coeff: u8 = 1;
62 for j in 0..k {
63 data[k * i + j] = coeff;
64 coeff = gf::mul(coeff, row_gen);
65 }
66 row_gen = gf::mul(row_gen, 2);
67 }
68 Ok(Self {
69 rows: m,
70 cols: k,
71 data,
72 })
73 }
74
75 /// Cauchy encode matrix for `k` sources and `p` parity rows — ISA-L's
76 /// `gf_gen_cauchy1_matrix`. Every square submatrix is invertible, so any
77 /// `(k, p)` within the field limit is a valid configuration; this is the
78 /// recommended general-purpose construction.
79 pub fn cauchy(k: usize, p: usize) -> Result<Self, MatrixError> {
80 check_dims(k, p)?;
81 let m = k + p;
82 let mut data = vec![0u8; m * k];
83 for i in 0..k {
84 data[k * i + i] = 1;
85 }
86 for i in k..m {
87 for j in 0..k {
88 // i >= k > j, so i ^ j is never zero and inv() never sees 0.
89 data[k * i + j] = gf::inv((i ^ j) as u8);
90 }
91 }
92 Ok(Self {
93 rows: m,
94 cols: k,
95 data,
96 })
97 }
98
99 /// Build a matrix from raw row-major bytes. `data.len()` must equal
100 /// `rows * cols`, and both dimensions must be in `1..=256` (the GF(2^8)
101 /// shard-index limit; the field-based constructors are stricter because
102 /// their constructions need ≤ 255 distinct nonzero elements).
103 pub fn from_bytes(rows: usize, cols: usize, data: Vec<u8>) -> Result<Self, MatrixError> {
104 if rows == 0 || cols == 0 || rows > 256 || cols > 256 || data.len() != rows * cols {
105 return Err(MatrixError::Dimensions {
106 k: cols,
107 p: rows.saturating_sub(cols),
108 });
109 }
110 Ok(Self { rows, cols, data })
111 }
112
113 /// Number of rows (`k + p` for an encode matrix — ISA-L's `m`).
114 pub fn rows(&self) -> usize {
115 self.rows
116 }
117
118 /// Number of columns (`k`, the source count, for an encode matrix).
119 pub fn cols(&self) -> usize {
120 self.cols
121 }
122
123 /// The coefficient at (`row`, `col`), or `None` out of bounds.
124 pub fn get(&self, row: usize, col: usize) -> Option<u8> {
125 if row < self.rows && col < self.cols {
126 Some(self.data[self.cols * row + col])
127 } else {
128 None
129 }
130 }
131
132 /// The raw row-major coefficient bytes.
133 pub fn as_bytes(&self) -> &[u8] {
134 &self.data
135 }
136
137 /// The bottom `p` rows — the parity-generating block, in exactly the
138 /// layout `tables::init_tables` expects.
139 pub fn parity_bytes(&self) -> &[u8] {
140 &self.data[self.cols * self.cols..]
141 }
142
143 /// Select `indices.len()` rows (in order) into a new matrix — the step
144 /// that builds a decode matrix from the surviving shards' rows. Indices
145 /// must be in range; duplicates are allowed here and will simply produce
146 /// a singular matrix at inversion.
147 pub fn select_rows(&self, indices: &[usize]) -> Result<Self, MatrixError> {
148 if indices.is_empty() || indices.len() > 256 {
149 return Err(MatrixError::Dimensions { k: self.cols, p: 0 });
150 }
151 let mut data = Vec::with_capacity(indices.len() * self.cols);
152 for &r in indices {
153 if r >= self.rows {
154 return Err(MatrixError::Dimensions { k: self.cols, p: 0 });
155 }
156 data.extend_from_slice(&self.data[self.cols * r..self.cols * (r + 1)]);
157 }
158 Ok(Self {
159 rows: indices.len(),
160 cols: self.cols,
161 data,
162 })
163 }
164
165 /// Invert a square matrix — ISA-L's `gf_invert_matrix` (Gauss-Jordan with
166 /// row-swap pivoting), except non-destructive and with singularity as a
167 /// typed error. Non-square input is a dimension error.
168 pub fn invert(&self) -> Result<Self, MatrixError> {
169 if self.rows != self.cols {
170 return Err(MatrixError::Dimensions {
171 k: self.cols,
172 p: self.rows.saturating_sub(self.cols),
173 });
174 }
175 let n = self.rows;
176 let mut a = self.data.clone();
177 let mut out = vec![0u8; n * n];
178 invert_gauss_jordan(&mut a, &mut out, n)?;
179 Ok(Self {
180 rows: n,
181 cols: n,
182 data: out,
183 })
184 }
185
186 /// Matrix product `self * rhs` (used by tests and the recovery path).
187 /// Dimension mismatch is an error, never a panic.
188 pub fn multiply(&self, rhs: &Self) -> Result<Self, MatrixError> {
189 if self.cols != rhs.rows {
190 return Err(MatrixError::Dimensions { k: rhs.rows, p: 0 });
191 }
192 let mut data = vec![0u8; self.rows * rhs.cols];
193 for i in 0..self.rows {
194 for j in 0..rhs.cols {
195 let mut s = 0u8;
196 for t in 0..self.cols {
197 s ^= gf::mul(self.data[self.cols * i + t], rhs.data[rhs.cols * t + j]);
198 }
199 data[rhs.cols * i + j] = s;
200 }
201 }
202 Ok(Self {
203 rows: self.rows,
204 cols: rhs.cols,
205 data,
206 })
207 }
208
209 /// True if this is the identity matrix.
210 pub fn is_identity(&self) -> bool {
211 self.rows == self.cols
212 && self
213 .data
214 .iter()
215 .enumerate()
216 .all(|(idx, &v)| v == u8::from(idx / self.cols == idx % self.cols))
217 }
218}
219
220/// The raw Gauss-Jordan inversion under [`Matrix::invert`] and the compat
221/// layer's `gf_invert_matrix` — ISA-L semantics: `a` (row-major `n x n`) is
222/// DESTROYED (reduced to the identity on success), `out` receives the inverse,
223/// and a singular input is a typed error. Slice lengths must be `n * n`.
224pub fn invert_gauss_jordan(a: &mut [u8], out: &mut [u8], n: usize) -> Result<(), MatrixError> {
225 if n == 0
226 || !n
227 .checked_mul(n)
228 .is_some_and(|nn| a.len() == nn && out.len() == nn)
229 {
230 return Err(MatrixError::Dimensions { k: n, p: 0 });
231 }
232 out.fill(0);
233 for i in 0..n {
234 out[n * i + i] = 1;
235 }
236
237 for i in 0..n {
238 if a[n * i + i] == 0 {
239 // Find a lower row with a non-zero in this column and swap.
240 let mut pivot = None;
241 for j in i + 1..n {
242 if a[n * j + i] != 0 {
243 pivot = Some(j);
244 break;
245 }
246 }
247 let Some(j) = pivot else {
248 return Err(MatrixError::Singular);
249 };
250 for c in 0..n {
251 a.swap(n * i + c, n * j + c);
252 out.swap(n * i + c, n * j + c);
253 }
254 }
255
256 let scale = gf::inv(a[n * i + i]);
257 for c in 0..n {
258 a[n * i + c] = gf::mul(a[n * i + c], scale);
259 out[n * i + c] = gf::mul(out[n * i + c], scale);
260 }
261
262 for j in 0..n {
263 if j == i {
264 continue;
265 }
266 let f = a[n * j + i];
267 if f == 0 {
268 continue;
269 }
270 for c in 0..n {
271 out[n * j + c] ^= gf::mul(f, out[n * i + c]);
272 a[n * j + c] ^= gf::mul(f, a[n * i + c]);
273 }
274 }
275 }
276 Ok(())
277}