Skip to main content

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}