Skip to main content

leodos_protocols/coding/fec/
reed_solomon.rs

1//! CCSDS Reed-Solomon (255,223) Forward Error Correction
2//!
3//! Implements RS(255,223) over GF(2^8) as specified in
4//! CCSDS 131.0-B-5 (TM Synchronization and Channel Coding).
5//!
6//! # Parameters
7//!
8//! - Field polynomial: p(x) = x^8 + x^7 + x^2 + x + 1 (0x187)
9//! - Primitive element α = 0x02 (the polynomial x)
10//! - First consecutive root: α^112
11//! - Code generator: g(x) = ∏(x - α^(112+i)) for i = 0..31
12//! - 32 parity symbols per codeword
13//! - Corrects up to 16 symbol errors per codeword
14//!
15//! # Interleaving
16//!
17//! Supports interleaving depths I = 1..5 as per CCSDS spec.
18//! With interleaving depth I, the total codeblock is I×255 symbols
19//! and can correct up to I×16 symbol errors.
20
21/// Number of symbols in a full RS codeword.
22pub const N: usize = 255;
23/// Number of data symbols per codeword.
24pub const K: usize = 223;
25/// Number of parity symbols (2T where T=16).
26pub const PARITY: usize = N - K; // 32
27/// Maximum correctable symbol errors per codeword.
28pub const T: usize = PARITY / 2; // 16
29/// First consecutive root exponent.
30const FCR: u8 = 112;
31
32/// GF(2^8) arithmetic with CCSDS polynomial 0x187.
33mod gf {
34    /// Field polynomial: x^8 + x^7 + x^2 + x + 1.
35    const POLY: u16 = 0x187;
36
37    /// Exponential table: exp[i] = α^i mod p(x).
38    /// Doubled to 512 entries so exp[a + b] works without mod.
39    pub(super) static EXP: [u8; 512] = {
40        let mut t = [0u8; 512];
41        let mut val: u16 = 1;
42        let mut i = 0;
43        while i < 255 {
44            t[i] = val as u8;
45            t[i + 255] = val as u8;
46            // val *= α (left-shift + reduce)
47            val <<= 1;
48            if val & 0x100 != 0 {
49                val ^= POLY;
50            }
51            i += 1;
52        }
53        t[255] = t[0];
54        t
55    };
56
57    /// Logarithm table: log[α^i] = i. log[0] is undefined.
58    pub(super) static LOG: [u8; 256] = {
59        let mut t = [0u8; 256];
60        let mut i = 0u16;
61        while i < 255 {
62            t[EXP[i as usize] as usize] = i as u8;
63            i += 1;
64        }
65        t
66    };
67
68    /// α^n.
69    #[inline]
70    pub fn exp(n: usize) -> u8 {
71        EXP[n % 255]
72    }
73
74    /// log_α(x).
75    #[inline]
76    pub fn log(x: u8) -> u8 {
77        LOG[x as usize]
78    }
79
80    /// a × b in GF(2^8).
81    #[inline]
82    pub fn mul(a: u8, b: u8) -> u8 {
83        if a == 0 || b == 0 {
84            return 0;
85        }
86        exp(log(a) as usize + log(b) as usize)
87    }
88
89    /// a + b in GF(2^8) (XOR).
90    #[inline]
91    pub fn add(a: u8, b: u8) -> u8 {
92        a ^ b
93    }
94
95    /// a^(-1) in GF(2^8).
96    #[inline]
97    pub fn inv(a: u8) -> u8 {
98        if a == 0 {
99            return 0;
100        }
101        EXP[255 - LOG[a as usize] as usize]
102    }
103
104    /// a / b in GF(2^8).
105    #[inline]
106    pub fn div(a: u8, b: u8) -> u8 {
107        if a == 0 {
108            return 0;
109        }
110        mul(a, inv(b))
111    }
112
113    /// Evaluates polynomial at x using Horner's method.
114    /// Descending order: poly[0] is the highest-degree coefficient.
115    pub fn poly_eval(poly: &[u8], x: u8) -> u8 {
116        if poly.is_empty() {
117            return 0;
118        }
119        let mut result = poly[0];
120        for &coef in &poly[1..] {
121            result = add(mul(result, x), coef);
122        }
123        result
124    }
125
126    /// Evaluates polynomial at x using Horner's method.
127    /// Ascending order: poly[0] is the constant term.
128    pub fn poly_eval_asc(poly: &[u8], x: u8) -> u8 {
129        if poly.is_empty() {
130            return 0;
131        }
132        let mut result = poly[poly.len() - 1];
133        for i in (0..poly.len() - 1).rev() {
134            result = add(mul(result, x), poly[i]);
135        }
136        result
137    }
138}
139
140/// Errors from Reed-Solomon operations.
141#[derive(Debug, Copy, Clone, Eq, PartialEq)]
142pub enum RsError {
143    /// Buffer too short.
144    BufferTooShort {
145        /// Required size.
146        required: usize,
147        /// Provided size.
148        provided: usize,
149    },
150    /// Too many errors to correct.
151    TooManyErrors,
152    /// Invalid interleave depth (must be 1..=5).
153    InvalidInterleaveDepth(u8),
154}
155
156/// The RS(255,223) generator polynomial coefficients.
157///
158/// g(x) = ∏(x - α^(FCR+i)) for i=0..31
159/// Stored as 33 coefficients, highest degree first.
160static GENERATOR: [u8; PARITY + 1] = {
161    // Start with g(x) = 1
162    let mut g = [0u8; PARITY + 1];
163    g[0] = 1;
164    let mut len = 1;
165
166    let mut i = 0u16;
167    while i < PARITY as u16 {
168        // Multiply g by (x - α^(FCR+i))
169        let root = gf::EXP[(FCR as u16 + i) as usize % 255];
170        let mut j = len;
171        while j > 0 {
172            if g[j - 1] != 0 {
173                g[j] = g[j] ^ gf::EXP
174                    [((gf::LOG[g[j - 1] as usize] as u16
175                        + gf::LOG[root as usize] as u16)
176                        % 255) as usize];
177            }
178            j -= 1;
179        }
180        len += 1;
181        i += 1;
182    }
183    g
184};
185
186/// Encodes `data` (up to 223 bytes) into `output` (255 bytes).
187///
188/// The output contains the original data followed by 32 parity bytes.
189/// Returns the number of bytes written (always 255 for a full block).
190pub fn encode(data: &[u8], output: &mut [u8]) -> Result<usize, RsError> {
191    let data_len = data.len();
192    if data_len > K {
193        return Err(RsError::BufferTooShort {
194            required: K,
195            provided: data_len,
196        });
197    }
198    if output.len() < N {
199        return Err(RsError::BufferTooShort {
200            required: N,
201            provided: output.len(),
202        });
203    }
204
205    let mut codeword = [0u8; N];
206    codeword[..data_len].copy_from_slice(data);
207
208    // Systematic encoding: divide data polynomial by g(x),
209    // remainder becomes the 32 parity symbols.
210    let mut remainder = [0u8; PARITY];
211    for i in 0..K {
212        let feedback = gf::add(codeword[i], remainder[0]);
213        if feedback != 0 {
214            for j in 0..PARITY - 1 {
215                remainder[j] = gf::add(
216                    remainder[j + 1],
217                    gf::mul(feedback, GENERATOR[j + 1]),
218                );
219            }
220            remainder[PARITY - 1] =
221                gf::mul(feedback, GENERATOR[PARITY]);
222        } else {
223            remainder.copy_within(1..PARITY, 0);
224            remainder[PARITY - 1] = 0;
225        }
226    }
227
228    output[..K].copy_from_slice(&codeword[..K]);
229    output[K..N].copy_from_slice(&remainder);
230
231    Ok(N)
232}
233
234/// S_j = c(α^(FCR+j)) for j = 0..2T-1.
235fn syndromes(codeword: &[u8; N]) -> [u8; PARITY] {
236    let mut s = [0u8; PARITY];
237    for j in 0..PARITY {
238        let root = gf::exp(FCR as usize + j);
239        s[j] = gf::poly_eval(codeword, root);
240    }
241    s
242}
243
244/// Berlekamp-Massey: finds error locator σ(x) from syndromes.
245///
246/// σ(x) = 1 + σ_1·x + σ_2·x² + ... stored ascending (σ[0] = 1).
247/// Returns (sigma, num_errors).
248fn berlekamp_massey(
249    syn: &[u8; PARITY],
250) -> Result<([u8; PARITY + 1], usize), RsError> {
251    let mut sigma = [0u8; PARITY + 1];
252    sigma[0] = 1;
253    let mut old_sigma = [0u8; PARITY + 1];
254    old_sigma[0] = 1;
255    let mut l = 0usize;
256
257    for n in 0..PARITY {
258        // Compute discrepancy Δ_n
259        let mut delta = syn[n];
260        for i in 1..=l {
261            delta = gf::add(delta, gf::mul(sigma[i], syn[n - i]));
262        }
263
264        // Shift old_sigma (multiply by x)
265        let mut shifted = [0u8; PARITY + 1];
266        for i in 1..=PARITY {
267            shifted[i] = old_sigma[i - 1];
268        }
269        old_sigma = shifted;
270
271        if delta != 0 {
272            let mut scaled = [0u8; PARITY + 1];
273            for i in 0..=PARITY {
274                scaled[i] = gf::mul(delta, old_sigma[i]);
275            }
276
277            // Update connection length when 2L ≤ n
278            if 2 * l <= n {
279                let inv_delta = gf::inv(delta);
280                for i in 0..=PARITY {
281                    old_sigma[i] =
282                        gf::mul(sigma[i], inv_delta);
283                }
284                l = n + 1 - l;
285            }
286
287            // σ ← σ + Δ·old_sigma
288            for i in 0..=PARITY {
289                sigma[i] = gf::add(sigma[i], scaled[i]);
290            }
291        }
292    }
293
294    if l > T {
295        return Err(RsError::TooManyErrors);
296    }
297
298    Ok((sigma, l))
299}
300
301/// Chien search: finds error positions from σ(x).
302///
303/// σ(α^m) = 0 means there is an error at codeword position
304/// (m + 254) % 255, because the error locator X_k = α^{254-pos}.
305fn chien_search(
306    sigma: &[u8; PARITY + 1],
307    num_errors: usize,
308) -> Result<[u8; T], RsError> {
309    let mut positions = [0u8; T];
310    let mut found = 0;
311
312    for m in 0..N {
313        let x = gf::exp(m);
314        if gf::poly_eval_asc(&sigma[..=num_errors], x) == 0 {
315            positions[found] = ((m + N - 1) % N) as u8;
316            found += 1;
317            if found == num_errors {
318                break;
319            }
320        }
321    }
322
323    if found != num_errors {
324        return Err(RsError::TooManyErrors);
325    }
326
327    Ok(positions)
328}
329
330/// Forney algorithm: computes error magnitudes.
331///
332/// e_k = X_k^{1-FCR} · Ω(X_k⁻¹) / σ'(X_k⁻¹)
333fn forney(
334    syn: &[u8; PARITY],
335    sigma: &[u8; PARITY + 1],
336    positions: &[u8],
337    num_errors: usize,
338) -> [u8; T] {
339    // Error evaluator Ω(x) = S(x)·σ(x) mod x^{2T}
340    let mut omega = [0u8; PARITY];
341    for i in 0..PARITY {
342        let mut val = 0u8;
343        for j in 0..=i.min(num_errors) {
344            val = gf::add(val, gf::mul(sigma[j], syn[i - j]));
345        }
346        omega[i] = val;
347    }
348
349    // Formal derivative σ'(x) in GF(2): only odd-index terms
350    let mut sigma_deriv = [0u8; PARITY];
351    for i in (1..=num_errors).step_by(2) {
352        sigma_deriv[i - 1] = sigma[i];
353    }
354
355    let mut magnitudes = [0u8; T];
356    for k in 0..num_errors {
357        let pos = positions[k] as usize;
358        // X_k = α^{254-pos}, X_k⁻¹ = α^{pos+1}
359        let x_k_log = (N - 1 - pos) % N;
360        let x_k_inv = gf::exp((N - x_k_log) % N);
361
362        let omega_val =
363            gf::poly_eval_asc(&omega[..PARITY], x_k_inv);
364        let deriv_val =
365            gf::poly_eval_asc(&sigma_deriv[..num_errors], x_k_inv);
366
367        let power_log =
368            (x_k_log * (1 + N - FCR as usize)) % N;
369        let power = gf::exp(power_log);
370
371        magnitudes[k] =
372            gf::mul(power, gf::div(omega_val, deriv_val));
373    }
374
375    magnitudes
376}
377
378/// Decodes and corrects a 255-byte RS codeword in-place.
379///
380/// Returns the number of corrected symbol errors, or an error
381/// if there are too many to correct (>16).
382pub fn decode(codeword: &mut [u8; N]) -> Result<usize, RsError> {
383    let syn = syndromes(codeword);
384
385    if syn.iter().all(|&s| s == 0) {
386        return Ok(0);
387    }
388
389    let (sigma, num_errors) = berlekamp_massey(&syn)?;
390    let positions = chien_search(&sigma, num_errors)?;
391    let magnitudes = forney(&syn, &sigma, &positions, num_errors);
392
393    for i in 0..num_errors {
394        let pos = positions[i] as usize;
395        codeword[pos] = gf::add(codeword[pos], magnitudes[i]);
396    }
397
398    // Verify correction
399    let check = syndromes(codeword);
400    if check.iter().all(|&s| s == 0) {
401        Ok(num_errors)
402    } else {
403        Err(RsError::TooManyErrors)
404    }
405}
406
407/// Encodes with interleaving depth I (1..=5).
408///
409/// Input: `data` of length I×223 bytes.
410/// Output: `output` of length I×255 bytes.
411pub fn encode_interleaved(
412    data: &[u8],
413    depth: u8,
414    output: &mut [u8],
415) -> Result<usize, RsError> {
416    if depth == 0 || depth > 5 {
417        return Err(RsError::InvalidInterleaveDepth(depth));
418    }
419    let d = depth as usize;
420    let total_data = d * K;
421    let total_code = d * N;
422
423    if data.len() < total_data {
424        return Err(RsError::BufferTooShort {
425            required: total_data,
426            provided: data.len(),
427        });
428    }
429    if output.len() < total_code {
430        return Err(RsError::BufferTooShort {
431            required: total_code,
432            provided: output.len(),
433        });
434    }
435
436    for i in 0..d {
437        // De-interleave: pick every d-th symbol starting at i
438        let mut block = [0u8; K];
439        for j in 0..K {
440            block[j] = data[j * d + i];
441        }
442
443        let mut codeword = [0u8; N];
444        encode(&block, &mut codeword)?;
445
446        // Re-interleave output
447        for j in 0..N {
448            output[j * d + i] = codeword[j];
449        }
450    }
451
452    Ok(total_code)
453}
454
455/// Decodes with interleaving depth I (1..=5).
456///
457/// Operates in-place on a buffer of I×255 bytes.
458/// Returns total number of corrected symbol errors.
459pub fn decode_interleaved(
460    data: &mut [u8],
461    depth: u8,
462) -> Result<usize, RsError> {
463    if depth == 0 || depth > 5 {
464        return Err(RsError::InvalidInterleaveDepth(depth));
465    }
466    let d = depth as usize;
467    let total_code = d * N;
468
469    if data.len() < total_code {
470        return Err(RsError::BufferTooShort {
471            required: total_code,
472            provided: data.len(),
473        });
474    }
475
476    let mut total_corrected = 0;
477
478    for i in 0..d {
479        // De-interleave
480        let mut codeword = [0u8; N];
481        for j in 0..N {
482            codeword[j] = data[j * d + i];
483        }
484
485        let corrected = decode(&mut codeword)?;
486        total_corrected += corrected;
487
488        // Re-interleave corrected data back
489        for j in 0..N {
490            data[j * d + i] = codeword[j];
491        }
492    }
493
494    Ok(total_corrected)
495}
496
497/// RS(255,223) encoder implementing [`FecEncoder`](crate::coding::FecEncoder).
498pub struct ReedSolomonEncoder {
499    interleave_depth: u8,
500}
501
502impl ReedSolomonEncoder {
503    /// Creates an encoder with the given interleave depth (1..=5).
504    pub fn new(interleave_depth: u8) -> Self {
505        Self { interleave_depth }
506    }
507}
508
509impl crate::coding::FecEncoder for ReedSolomonEncoder {
510    type Error = RsError;
511
512    fn encode(&self, data: &[u8], output: &mut [u8]) -> Result<usize, Self::Error> {
513        if self.interleave_depth <= 1 {
514            encode(data, output)
515        } else {
516            encode_interleaved(data, self.interleave_depth, output)
517        }
518    }
519}
520
521/// RS(255,223) decoder implementing [`FecDecoder`](crate::coding::FecDecoder).
522pub struct ReedSolomonDecoder {
523    interleave_depth: u8,
524}
525
526impl ReedSolomonDecoder {
527    /// Creates a decoder with the given interleave depth (1..=5).
528    pub fn new(interleave_depth: u8) -> Self {
529        Self { interleave_depth }
530    }
531}
532
533impl crate::coding::FecDecoder for ReedSolomonDecoder {
534    type Error = RsError;
535
536    fn decode(&self, data: &mut [u8]) -> Result<usize, Self::Error> {
537        if self.interleave_depth <= 1 {
538            if data.len() < N {
539                return Err(RsError::BufferTooShort { required: N, provided: data.len() });
540            }
541            let codeword: &mut [u8; N] = (&mut data[..N]).try_into().unwrap();
542            decode(codeword)
543        } else {
544            decode_interleaved(data, self.interleave_depth)
545        }
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    #[test]
554    fn gf_basic_arithmetic() {
555        // α^0 = 1
556        assert_eq!(gf::exp(0), 1);
557        // α^1 = 2
558        assert_eq!(gf::exp(1), 2);
559        // α^255 = α^0 = 1 (field wraps)
560        assert_eq!(gf::exp(255), 1);
561        // a * 1 = a
562        assert_eq!(gf::mul(42, 1), 42);
563        // a * 0 = 0
564        assert_eq!(gf::mul(42, 0), 0);
565        // a * inv(a) = 1
566        for a in 1..=255u8 {
567            assert_eq!(gf::mul(a, gf::inv(a)), 1);
568        }
569    }
570
571    #[test]
572    fn gf_log_exp_inverse() {
573        for i in 0..255usize {
574            let a = gf::exp(i);
575            assert_eq!(gf::log(a) as usize, i);
576        }
577    }
578
579    #[test]
580    fn encode_no_error_decode() {
581        let data = [0xAB; K];
582        let mut codeword = [0u8; N];
583        encode(&data, &mut codeword).unwrap();
584
585        // Data portion should match
586        assert_eq!(&codeword[..K], &data);
587
588        // Parity should be non-trivial
589        assert!(codeword[K..].iter().any(|&b| b != 0));
590
591        // Decode should find 0 errors
592        let corrected = decode(&mut codeword).unwrap();
593        assert_eq!(corrected, 0);
594    }
595
596    #[test]
597    fn encode_decode_single_error() {
598        let data = [0x42; K];
599        let mut codeword = [0u8; N];
600        encode(&data, &mut codeword).unwrap();
601
602        // Introduce 1 error
603        codeword[50] ^= 0xFF;
604
605        let corrected = decode(&mut codeword).unwrap();
606        assert_eq!(corrected, 1);
607        assert_eq!(&codeword[..K], &[0x42; K]);
608    }
609
610    #[test]
611    fn encode_decode_max_errors() {
612        let data = [0x13; K];
613        let mut codeword = [0u8; N];
614        encode(&data, &mut codeword).unwrap();
615        let original = codeword;
616
617        // Introduce T (16) errors at different positions
618        for i in 0..T {
619            codeword[i * 15] ^= ((i as u8) + 1).wrapping_mul(0x37);
620        }
621
622        let corrected = decode(&mut codeword).unwrap();
623        assert_eq!(corrected, T);
624        assert_eq!(codeword, original);
625    }
626
627    #[test]
628    fn too_many_errors() {
629        let data = [0x00; K];
630        let mut codeword = [0u8; N];
631        encode(&data, &mut codeword).unwrap();
632
633        // Introduce T+1 errors — should fail
634        for i in 0..=T {
635            codeword[i] ^= 0xFF;
636        }
637
638        let result = decode(&mut codeword);
639        assert!(result.is_err());
640    }
641
642    #[test]
643    fn generator_polynomial_degree() {
644        // Generator should have degree PARITY (32)
645        assert_eq!(GENERATOR[0], 1); // monic
646        // At least some non-zero coefficients
647        assert!(GENERATOR[1..].iter().any(|&c| c != 0));
648    }
649
650    #[test]
651    fn generator_roots() {
652        // Each α^(FCR+i) for i=0..31 should be a root of g(x)
653        for i in 0..PARITY {
654            let root = gf::exp(FCR as usize + i);
655            let val = gf::poly_eval(&GENERATOR, root);
656            assert_eq!(val, 0, "α^{} should be a root", FCR as usize + i);
657        }
658    }
659
660    #[test]
661    fn encode_zeros() {
662        let data = [0u8; K];
663        let mut codeword = [0u8; N];
664        encode(&data, &mut codeword).unwrap();
665        // All-zero data should produce all-zero codeword
666        assert!(codeword.iter().all(|&b| b == 0));
667    }
668
669    #[test]
670    fn interleaved_encode_decode_depth_1() {
671        let data = [0x55; K];
672        let mut output = [0u8; N];
673        let len = encode_interleaved(&data, 1, &mut output).unwrap();
674        assert_eq!(len, N);
675
676        let corrected =
677            decode_interleaved(&mut output, 1).unwrap();
678        assert_eq!(corrected, 0);
679        assert_eq!(&output[..K], &data);
680    }
681
682    #[test]
683    fn interleaved_depth_2_with_errors() {
684        let data = [0xAA; 2 * K];
685        let mut output = [0u8; 2 * N];
686        encode_interleaved(&data, 2, &mut output).unwrap();
687
688        // Corrupt some symbols in each interleaved codeword
689        output[0] ^= 0x11; // affects codeword 0
690        output[1] ^= 0x22; // affects codeword 1
691
692        let corrected =
693            decode_interleaved(&mut output, 2).unwrap();
694        assert_eq!(corrected, 2);
695    }
696
697    #[test]
698    fn invalid_interleave_depth() {
699        let data = [0u8; K];
700        let mut output = [0u8; N];
701        assert!(matches!(
702            encode_interleaved(&data, 0, &mut output),
703            Err(RsError::InvalidInterleaveDepth(0))
704        ));
705        assert!(matches!(
706            encode_interleaved(&data, 6, &mut output),
707            Err(RsError::InvalidInterleaveDepth(6))
708        ));
709    }
710
711    #[test]
712    fn parity_error_correction() {
713        let data = [0x77; K];
714        let mut codeword = [0u8; N];
715        encode(&data, &mut codeword).unwrap();
716        let original = codeword;
717
718        // Corrupt only parity bytes
719        codeword[K] ^= 0xFF;
720        codeword[K + 5] ^= 0xAA;
721
722        let corrected = decode(&mut codeword).unwrap();
723        assert_eq!(corrected, 2);
724        assert_eq!(codeword, original);
725    }
726
727    #[test]
728    fn end_to_end_example() {
729        // "HELLO" padded with zeros
730        let mut data = [0u8; K];
731        data[0] = 72;  // H
732        data[1] = 69;  // E
733        data[2] = 76;  // L
734        data[3] = 76;  // L
735        data[4] = 79;  // O
736
737        // Encode: 223 data bytes -> 255 byte codeword
738        let mut codeword = [0u8; N];
739        encode(&data, &mut codeword).unwrap();
740
741        // Data unchanged at front
742        assert_eq!(&codeword[..5], &[72, 69, 76, 76, 79]);
743        // Parity appended at back
744        assert_eq!(
745            &codeword[K..K + 8],
746            &[243, 147, 197, 58, 154, 156, 250, 218]
747        );
748
749        // Corrupt byte 2: 'L' (76) -> 255
750        codeword[2] = 255;
751
752        // Decode: finds and fixes the error
753        let corrected = decode(&mut codeword).unwrap();
754        assert_eq!(corrected, 1);
755        assert_eq!(codeword[2], 76); // 'L' restored
756    }
757}