Skip to main content

rusty_erasure_core/
raid.rs

1//! RAID parity: XOR (P-only) and P+Q (RAID-6) generation and checking —
2//! ISA-L's `raid` module semantics with validated, panic-free APIs.
3//!
4//! Q is the classic RAID-6 syndrome `Q = Σ 2^j · D_j` over GF(2^8)/0x11d,
5//! computed Horner-style exactly as ISA-L's `pq_gen_base` (last source first,
6//! `q = s ^ 2·q`), using the same word-parallel multiply-by-2 (eight byte
7//! lanes per u64: shift, isolate bit-7 lanes, expand to a lane mask, fold the
8//! polynomial). Where ISA-L requires 32-byte-multiple lengths and silently
9//! ignores sub-word tails in its base code, this port handles ANY length —
10//! word blocks plus an exact byte tail — and is gated both ways against real
11//! ISA-L (their outputs on aligned lens; their byte-wise checkers accepting
12//! our outputs on odd lens).
13
14use crate::error::CodeError;
15
16const NOTBIT0: u64 = 0xfefe_fefe_fefe_fefe;
17const BIT7: u64 = 0x8080_8080_8080_8080;
18const GF8POLY: u64 = 0x1d1d_1d1d_1d1d_1d1d;
19
20/// Multiply each of the eight GF(2^8) byte lanes of `q` by 2 (ISA-L's
21/// word-parallel trick: `(m << 1) - (m >> 7)` turns each 0x80 lane into 0xff).
22#[inline]
23const fn gf2_mul2_lanes(q: u64) -> u64 {
24    let m = q & BIT7;
25    ((q << 1) & NOTBIT0) ^ (((m << 1).wrapping_sub(m >> 7)) & GF8POLY)
26}
27
28#[inline]
29const fn gf2_mul2_byte(q: u8) -> u8 {
30    (q << 1) ^ (if q & 0x80 != 0 { 0x1d } else { 0 })
31}
32
33fn check_lens(sources: &[&[u8]], len: usize, min_sources: usize) -> Result<(), CodeError> {
34    if sources.len() < min_sources {
35        return Err(CodeError::ShardCount {
36            expected: min_sources,
37            got: sources.len(),
38        });
39    }
40    for (index, s) in sources.iter().enumerate() {
41        if s.len() != len {
42            return Err(CodeError::ShardLength {
43                index,
44                expected: len,
45                got: s.len(),
46            });
47        }
48    }
49    Ok(())
50}
51
52/// XOR parity of ≥2 sources into `parity` — ISA-L `xor_gen`.
53///
54/// Deliberately the multi-pass shape: `copy_from_slice` then one
55/// auto-vectorized XOR pass per source. A single-pass u64×4 fold was tried
56/// (parity written once instead of N times) and MEASURED WORSE (−8%,
57/// LEDGER): the per-source passes are memcpy-class vectorized streams, and
58/// sequential parity stores are nearly free — the classic
59/// redundant-but-cheaper-than-the-fix case.
60pub fn xor_gen(sources: &[&[u8]], parity: &mut [u8]) -> Result<(), CodeError> {
61    check_lens(sources, parity.len(), 2)?;
62    crate::kernel::SCALAR_CENSUS_BYTES.fetch_add(
63        (sources.len() * parity.len()) as u64,
64        core::sync::atomic::Ordering::Relaxed,
65    );
66    let (first, rest) = sources.split_first().expect("count checked");
67    parity.copy_from_slice(first);
68    for src in rest {
69        for (d, &s) in parity.iter_mut().zip(*src) {
70            *d ^= s;
71        }
72    }
73    Ok(())
74}
75
76/// True when the XOR of ALL vectors (parity included) is zero — ISA-L
77/// `xor_check`. Word-wide with per-block early exit.
78pub fn xor_check(vects: &[&[u8]]) -> Result<bool, CodeError> {
79    let len = vects.first().map_or(0, |v| v.len());
80    check_lens(vects, len, 2)?;
81    let words = len / 8;
82    for i in 0..words {
83        let o = i * 8;
84        let mut acc = 0u64;
85        for v in vects {
86            acc ^= u64::from_ne_bytes(v[o..o + 8].try_into().expect("in range"));
87        }
88        if acc != 0 {
89            return Ok(false);
90        }
91    }
92    for i in words * 8..len {
93        let mut acc = 0u8;
94        for v in vects {
95            acc ^= v[i];
96        }
97        if acc != 0 {
98            return Ok(false);
99        }
100    }
101    Ok(true)
102}
103
104/// RAID-6 P+Q of ≥2 sources — ISA-L `pq_gen`, any length.
105pub fn pq_gen(sources: &[&[u8]], p: &mut [u8], q: &mut [u8]) -> Result<(), CodeError> {
106    let len = p.len();
107    if q.len() != len {
108        return Err(CodeError::ShardLength {
109            index: 1,
110            expected: len,
111            got: q.len(),
112        });
113    }
114    check_lens(sources, len, 2)?;
115    crate::kernel::SCALAR_CENSUS_BYTES.fetch_add(
116        (sources.len() * len) as u64,
117        core::sync::atomic::Ordering::Relaxed,
118    );
119
120    // 32-byte blocks: four independent u64 lanes per step (brick) — the ×2
121    // recurrence chains run in parallel across lanes and the shift/mask/poly
122    // trick becomes a 4-wide pattern the compiler can vectorize; per-word
123    // closure bounds checks collapse to one slice per block.
124    let last = sources.len() - 1;
125    let blocks = len / 32;
126    for i in 0..blocks {
127        let o = i * 32;
128        let load = |s: &[u8], w: usize| {
129            u64::from_ne_bytes(s[o + w * 8..o + w * 8 + 8].try_into().expect("in range"))
130        };
131        let mut pw = [0u64; 4];
132        let mut qw = [0u64; 4];
133        for w in 0..4 {
134            pw[w] = load(sources[last], w);
135            qw[w] = pw[w];
136        }
137        for j in (0..last).rev() {
138            for w in 0..4 {
139                let s = load(sources[j], w);
140                pw[w] ^= s;
141                qw[w] = s ^ gf2_mul2_lanes(qw[w]);
142            }
143        }
144        for w in 0..4 {
145            p[o + w * 8..o + w * 8 + 8].copy_from_slice(&pw[w].to_ne_bytes());
146            q[o + w * 8..o + w * 8 + 8].copy_from_slice(&qw[w].to_ne_bytes());
147        }
148    }
149    // Byte tail — the part ISA-L's base quietly ignores; ours is exact.
150    for i in blocks * 32..len {
151        let last = sources.len() - 1;
152        let mut pb = sources[last][i];
153        let mut qb = pb;
154        for j in (0..last).rev() {
155            let s = sources[j][i];
156            pb ^= s;
157            qb = s ^ gf2_mul2_byte(qb);
158        }
159        p[i] = pb;
160        q[i] = qb;
161    }
162    Ok(())
163}
164
165/// Which parity vector a [`pq_check`] mismatch was found in.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum PqParity {
168    /// The XOR parity (P) disagreed.
169    P,
170    /// The RAID-6 syndrome (Q) disagreed.
171    Q,
172}
173
174/// A P/Q consistency failure: the first offending byte offset and which
175/// parity it disagreed with (ISA-L's `i | 1` / `i | 2` return, made typed).
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub struct PqMismatch {
178    /// Byte offset of the first mismatch.
179    pub index: usize,
180    /// Which parity vector disagreed.
181    pub parity: PqParity,
182}
183
184/// Check sources against P and Q — ISA-L `pq_check`. `Ok(None)` means
185/// consistent; `Ok(Some(_))` names the first mismatch.
186pub fn pq_check(sources: &[&[u8]], p: &[u8], q: &[u8]) -> Result<Option<PqMismatch>, CodeError> {
187    let len = p.len();
188    if q.len() != len {
189        return Err(CodeError::ShardLength {
190            index: 1,
191            expected: len,
192            got: q.len(),
193        });
194    }
195    check_lens(sources, len, 2)?;
196    let last = sources.len() - 1;
197
198    // Exact byte-wise check over one range (the reference semantics: first
199    // mismatching byte, P examined before Q at each offset).
200    let byte_scan = |from: usize, to: usize| -> Option<PqMismatch> {
201        for i in from..to {
202            let mut pb = sources[last][i];
203            let mut qb = pb;
204            for j in (0..last).rev() {
205                let s = sources[j][i];
206                pb ^= s;
207                qb = s ^ gf2_mul2_byte(qb);
208            }
209            if p[i] != pb {
210                return Some(PqMismatch {
211                    index: i,
212                    parity: PqParity::P,
213                });
214            }
215            if q[i] != qb {
216                return Some(PqMismatch {
217                    index: i,
218                    parity: PqParity::Q,
219                });
220            }
221        }
222        None
223    };
224
225    // Word-wide fast scan (brick: 8 byte lanes per recurrence step); a block
226    // that disagrees falls back to the byte scan for the exact index and the
227    // exact P-before-Q ordering.
228    let words = len / 8;
229    for i in 0..words {
230        let o = i * 8;
231        let load = |s: &[u8]| u64::from_ne_bytes(s[o..o + 8].try_into().expect("in range"));
232        let mut pw = load(sources[last]);
233        let mut qw = pw;
234        for j in (0..last).rev() {
235            let s = load(sources[j]);
236            pw ^= s;
237            qw = s ^ gf2_mul2_lanes(qw);
238        }
239        if pw != load(p) || qw != load(q) {
240            return Ok(byte_scan(o, o + 8));
241        }
242    }
243    Ok(byte_scan(words * 8, len))
244}