Skip to main content

ps_ecc/
euclidean.rs

1//! Optimized Extended Euclidean Algorithm for Reed-Solomon decoding.
2//!
3//! This implementation minimizes memory allocations by using stack-allocated
4//! polynomials and ring buffer index swapping instead of data movement.
5
6use crate::{EuclideanError, Polynomial};
7
8/// Extended Euclidean algorithm for Reed-Solomon decoding.
9///
10/// Given a syndrome polynomial and the error-correction capability `t`,
11/// computes the error locator polynomial (sigma) and the error evaluator
12/// polynomial (omega). The syndrome polynomial has `2 * t` coefficients.
13///
14/// # Arguments
15///
16/// * `syndromes` - Syndrome polynomial
17/// * `t` - Error-correction capability, the number of correctable errors;
18///   must satisfy `2 * t <= 254` so that `x^(2t)` is representable
19///
20/// # Returns
21///
22/// A tuple `(sigma, omega)` where:
23/// * `sigma` is the error locator polynomial
24/// * `omega` is the error evaluator polynomial
25///
26/// # Errors
27///
28/// - [`EuclideanError::CapabilityTooHigh`] is returned if `2 * t` exceeds
29///   [`Polynomial::MAX_DEGREE`].
30/// - An error is returned if division by zero occurs (indicates invalid
31///   input), or if an intermediate product would exceed the maximum
32///   polynomial degree (not expected to occur).
33pub fn euclidean(
34    syndromes: &Polynomial,
35    t: u8,
36) -> Result<(Polynomial, Polynomial), EuclideanError> {
37    let two_t = usize::from(t) * 2;
38
39    if two_t > usize::from(Polynomial::MAX_DEGREE) {
40        return Err(EuclideanError::CapabilityTooHigh { t });
41    }
42
43    // Ring buffers: r[idx] is current, r[idx ^ 1] is previous
44    let mut r: [Polynomial; 2] = [Polynomial::default(), Polynomial::default()];
45    let mut t_poly: [Polynomial; 2] = [Polynomial::default(), Polynomial::default()];
46
47    // r[0] = x^(2t)
48    #[allow(clippy::cast_possible_truncation)]
49    r[0].set(two_t as u8, 1);
50
51    // r[1] = syndrome polynomial
52    r[1] = *syndromes;
53
54    // t[0] = 0 (already default)
55    // t[1] = 1
56    t_poly[1].set(0, 1);
57
58    // Reusable quotient polynomial
59    let mut q = Polynomial::default();
60
61    // idx points to current r and t
62    let mut idx: usize = 1;
63
64    while r[idx].degree() >= t {
65        // Split arrays to satisfy borrow checker
66        // idx is always 0 or 1, so one of (r0, r1) is (prev, curr)
67        let (r_left, r_right) = r.split_at_mut(1);
68        let (r_prev, r_curr) = if idx == 1 {
69            (&mut r_left[0], &r_right[0])
70        } else {
71            (&mut r_right[0], &r_left[0])
72        };
73
74        let (t_left, t_right) = t_poly.split_at_mut(1);
75        let (t_prev, t_curr) = if idx == 1 {
76            (&mut t_left[0], &t_right[0])
77        } else {
78            (&mut t_right[0], &t_left[0])
79        };
80
81        // r[prev] <- r[prev] mod r[curr], q <- quotient
82        r_prev.div_rem_inplace(r_curr, &mut q)?;
83
84        // t[prev] <- t[prev] ^ (q * t[curr])
85        t_prev.mul_xor_assign(&q, t_curr)?;
86
87        // Flip index: what was prev becomes curr
88        idx ^= 1;
89    }
90
91    // Return (sigma, omega) = (t[idx], r[idx])
92    Ok((t_poly[idx], r[idx]))
93}
94
95#[cfg(test)]
96#[allow(clippy::expect_used)]
97mod tests {
98    use super::*;
99    use crate::finite_field::{inv, mul, ANTILOG_TABLE};
100
101    #[test]
102    fn euclidean_zero_syndromes() {
103        // All-zero syndromes indicate no errors
104        let syndromes: Polynomial = [0u8; 4].into_iter().collect();
105        let t = 2;
106
107        let (sigma, omega) = euclidean(&syndromes, t).expect("should succeed");
108
109        // With no errors, sigma should be constant (degree 0)
110        assert_eq!(sigma.degree(), 0);
111        // omega should be zero polynomial
112        assert_eq!(omega.coefficients(), &[0]);
113    }
114
115    #[test]
116    fn euclidean_single_error_sigma_has_one_root() {
117        // For a single error at position j, sigma(x) = 1 - alpha^j * x
118        // The syndrome for a single error e at position j is: S_i = e * alpha^(i*j)
119        // Let's use error value e=1 at position j=0, so S_i = alpha^0 = 1 for all i
120        let syndromes: Polynomial = [1u8; 4].into_iter().collect(); // Single error at position 0 with value 1
121        let t = 2;
122
123        let (sigma, _omega) = euclidean(&syndromes, t).expect("should succeed");
124
125        // sigma should have degree 1 (one error)
126        assert!(sigma.degree() <= 1);
127
128        // Verify sigma(alpha^0) = 0 (error at position 0)
129        // alpha^(255-0) mod 255 = alpha^0 = 1
130        let x = ANTILOG_TABLE[255 % 255].get(); // alpha^0 = 1
131
132        let sigma_at_x = Polynomial::eval_coefficients_at(sigma.coefficients(), x);
133
134        // After normalization, sigma(x) should be zero at the error position
135        // Note: euclidean returns unnormalized sigma, so we normalize first
136        let scale = inv(sigma[0u8]).expect("sigma[0] should be non-zero").get();
137        let _normalized_eval = mul(sigma_at_x, scale);
138
139        // Due to how the algorithm works, the root check is on sigma directly
140        // sigma(alpha^(-j)) = 0 for error at position j
141        assert_eq!(
142            Polynomial::eval_coefficients_at(sigma.coefficients(), 1),
143            0,
144            "sigma should have a root at alpha^0 for error at position 0"
145        );
146    }
147
148    #[test]
149    fn euclidean_verifies_degree_bounds() {
150        // The extended Euclidean algorithm guarantees:
151        // - deg(sigma) <= t (error locator has at most t roots)
152        // - deg(omega) < t (loop exits when remainder degree < t)
153        let syndromes: Polynomial = [3u8, 5, 7, 11].into_iter().collect(); // Arbitrary non-zero syndromes
154        let t = 2;
155
156        let (sigma, omega) = euclidean(&syndromes, t).expect("should succeed");
157
158        // Verify deg(sigma) <= t (can correct at most t errors)
159        assert!(
160            sigma.degree() <= t,
161            "sigma degree {} exceeds t={}",
162            sigma.degree(),
163            t
164        );
165
166        // Verify deg(omega) < t (algorithm invariant)
167        assert!(
168            omega.degree() < t || omega.is_zero(),
169            "omega degree {} should be < t={}",
170            omega.degree(),
171            t
172        );
173    }
174
175    #[test]
176    fn euclidean_with_t_equals_one() {
177        // Minimum parity: can correct 1 error
178        let syndromes: Polynomial = [42u8, 0].into_iter().collect(); // 2t = 2 syndromes
179        let t = 1;
180
181        let (sigma, omega) = euclidean(&syndromes, t).expect("should succeed");
182
183        assert!(sigma.degree() <= t);
184        assert!(omega.degree() < t || omega.is_zero());
185    }
186
187    #[test]
188    fn euclidean_with_large_t() {
189        // Larger parity count
190        let syndromes: Polynomial = [1u8; 20].into_iter().collect(); // t = 10
191        let t = 10;
192
193        let (sigma, omega) = euclidean(&syndromes, t).expect("should succeed");
194
195        assert!(sigma.degree() <= t);
196        assert!(omega.degree() < t || omega.is_zero());
197    }
198
199    #[test]
200    fn euclidean_minimal_valid_case() {
201        // Minimal meaningful case: t=1, 2 syndromes
202        let syndromes: Polynomial = [42u8, 17].into_iter().collect();
203        let t = 1;
204
205        let (sigma, omega) = euclidean(&syndromes, t).expect("should succeed");
206
207        assert!(sigma.degree() <= t);
208        assert!(omega.degree() < t || omega.is_zero());
209    }
210
211    #[test]
212    fn euclidean_syndrome_with_trailing_zeros() {
213        // Syndromes with trailing zeros (leading zeros in polynomial sense)
214        let syndromes: Polynomial = [1u8, 2, 0, 0].into_iter().collect(); // Effective degree 1, but length 4
215        let t = 2;
216
217        let (sigma, omega) = euclidean(&syndromes, t).expect("should succeed");
218
219        // sigma degree bounded by t
220        assert!(sigma.degree() <= t);
221
222        // omega degree bounded by t (loop exits when deg(r) < t)
223        assert!(
224            omega.degree() < t || omega.is_zero(),
225            "omega degree {} should be < t={}",
226            omega.degree(),
227            t
228        );
229    }
230
231    #[test]
232    fn euclidean_rejects_capability_above_127() {
233        // For t = 128, 2 * t = 256 previously wrapped to x^0 in the u8
234        // cast, silently returning garbage (sigma, omega).
235        let syndromes: Polynomial = [1u8; 4].into_iter().collect();
236
237        for t in [128, 200, 255] {
238            assert_eq!(
239                euclidean(&syndromes, t),
240                Err(EuclideanError::CapabilityTooHigh { t })
241            );
242        }
243    }
244
245    #[test]
246    fn euclidean_max_valid_length() {
247        // 255 coefficients is the maximum for Polynomial
248        let syndromes: Polynomial = [0u8; 255].into_iter().collect();
249        let t = 127;
250
251        let result = euclidean(&syndromes, t);
252
253        assert!(result.is_ok());
254    }
255
256    #[test]
257    fn euclidean_output_is_deterministic() {
258        // Same input should always produce same output
259        let syndromes: Polynomial = [7u8, 13, 19, 23, 29, 31].into_iter().collect();
260        let t = 3;
261
262        let (sigma1, omega1) = euclidean(&syndromes, t).expect("should succeed");
263        let (sigma2, omega2) = euclidean(&syndromes, t).expect("should succeed");
264
265        assert_eq!(sigma1, sigma2);
266        assert_eq!(omega1, omega2);
267    }
268
269    #[test]
270    fn euclidean_integration_with_rs_syndromes() {
271        // Use actual RS syndrome computation for a realistic test
272        // Create a simple codeword with a known error pattern
273        use crate::ReedSolomon;
274
275        let rs = ReedSolomon::new(2).expect("valid RS");
276
277        // Encode a simple message
278        let message = b"test";
279        let mut encoded = rs.encode(message).expect("encoding succeeds");
280
281        // Introduce a single error
282        encoded[0] ^= 0x42; // Flip some bits in first byte
283
284        // Compute syndromes
285        let syndromes = ReedSolomon::compute_syndromes(rs.parity_bytes(), &encoded);
286
287        // Run euclidean algorithm
288        let t = rs.parity_bytes() / 2;
289        let (sigma, omega) = euclidean(&syndromes, t).expect("euclidean succeeds");
290
291        // Verify sigma has at most t roots
292        assert!(sigma.degree() <= t, "sigma degree should be <= t");
293
294        // Verify omega degree constraint (deg(omega) < t)
295        assert!(
296            omega.degree() < t || omega.is_zero(),
297            "omega degree {} should be < t={}",
298            omega.degree(),
299            t
300        );
301    }
302}