|
1 | | -//! Layer 4 — Reed-Solomon error correction. |
| 1 | +//! Layer 4 — Reed-Solomon error correction over GF(256). |
2 | 2 | //! |
3 | | -//! This module is a thin wrapper over the [`reed_solomon_erasure`] crate. |
4 | | -//! The default geometry is RS(255, 223) over GF(256), matching the Python |
5 | | -//! reference (which uses `reedsolo`). |
6 | | -//! |
7 | | -//! v0.1 reference implementation: skeleton only — full RS encoding will be |
8 | | -//! wired in v0.1.1. For now, the `encode_with_ecc` function passes data |
9 | | -//! through untouched, with the L4 tag value set to `none` by the encoder. |
10 | | -//! |
11 | | -//! This compromise lets us ship a Rust v0.1 skeleton that compiles and |
12 | | -//! round-trips L1+L5, while keeping the RS integration as a clean follow-up |
13 | | -//! PR. The Python reference is the canonical L4 implementation for v0.1. |
| 3 | +//! Thin wrapper over the [`reed_solomon`] crate. The default geometry is |
| 4 | +//! RS(255, 223) matching the Python reference (which uses `reedsolo`). |
| 5 | +//! Conformant implementations MUST produce byte-identical codewords given |
| 6 | +//! identical inputs. |
| 7 | +
|
| 8 | +use reed_solomon::{Decoder, Encoder}; |
14 | 9 |
|
15 | 10 | /// Errors produced by the L4 codec. |
16 | 11 | #[derive(Debug, thiserror::Error)] |
17 | 12 | #[allow(missing_docs)] |
18 | 13 | pub enum EccError { |
19 | | - #[error("Reed-Solomon decoding failed: {0}")] |
| 14 | + #[error("Reed-Solomon decoding failed (too many errors): {0}")] |
20 | 15 | DecodeFailed(String), |
21 | | - #[error("Invalid block size {0} (must be ≤ 255 with parity)")] |
| 16 | + #[error("Invalid block size {0} (must be <= 255 with parity)")] |
22 | 17 | InvalidBlockSize(usize), |
| 18 | + #[error("Encoded data length {0} not a multiple of full block size {1}")] |
| 19 | + UnalignedData(usize, usize), |
23 | 20 | } |
24 | 21 |
|
25 | 22 | /// Default number of parity bytes per RS block. Per SPEC §3.1. |
26 | 23 | pub const DEFAULT_NSYM: usize = 32; |
27 | | -/// Default data bytes per RS block (n=255 − nsym). |
| 24 | +/// Default data bytes per RS block (n = 255 − nsym). |
28 | 25 | pub const DEFAULT_BLOCK: usize = 223; |
29 | 26 |
|
30 | | -/// Encode bytes with Reed-Solomon RS(255, 223). |
31 | | -/// |
32 | | -/// **v0.1 skeleton**: passes data through unchanged. The full implementation |
33 | | -/// will use [`reed_solomon_erasure`] in v0.1.1. |
34 | | -pub fn encode_with_ecc(data: &[u8], _nsym: usize, _block_size: usize) -> Vec<u8> { |
35 | | - // TODO(v0.1.1): real RS encoding via reed-solomon-erasure |
36 | | - data.to_vec() |
| 27 | +/// Encode a single RS block: `data.len() + nsym` bytes (data followed by parity). |
| 28 | +pub fn rs_encode_msg(data: &[u8], nsym: usize) -> Result<Vec<u8>, EccError> { |
| 29 | + if data.len() + nsym > 255 { |
| 30 | + return Err(EccError::InvalidBlockSize(data.len() + nsym)); |
| 31 | + } |
| 32 | + let encoder = Encoder::new(nsym); |
| 33 | + let encoded = encoder.encode(data); |
| 34 | + Ok(encoded.iter().copied().collect()) |
| 35 | +} |
| 36 | + |
| 37 | +/// Decode a single RS codeword. Returns (decoded_message_bytes, errors_corrected). |
| 38 | +pub fn rs_decode_msg(codeword: &[u8], nsym: usize) -> Result<(Vec<u8>, usize), EccError> { |
| 39 | + let decoder = Decoder::new(nsym); |
| 40 | + let result = decoder |
| 41 | + .correct(codeword, None) |
| 42 | + .map_err(|e| EccError::DecodeFailed(format!("{:?}", e)))?; |
| 43 | + let n_errs = result.errors_count(); |
| 44 | + let msg: Vec<u8> = result.data().to_vec(); |
| 45 | + Ok((msg, n_errs)) |
| 46 | +} |
| 47 | + |
| 48 | +/// Split `data` into RS blocks of size `block_size` (zero-padding the last) |
| 49 | +/// and encode each. Output length = N × (block_size + nsym). |
| 50 | +pub fn encode_with_ecc(data: &[u8], nsym: usize, block_size: usize) -> Result<Vec<u8>, EccError> { |
| 51 | + let mut out = Vec::new(); |
| 52 | + for chunk in data.chunks(block_size) { |
| 53 | + let mut padded = chunk.to_vec(); |
| 54 | + if padded.len() < block_size { |
| 55 | + padded.resize(block_size, 0); |
| 56 | + } |
| 57 | + out.extend(rs_encode_msg(&padded, nsym)?); |
| 58 | + } |
| 59 | + Ok(out) |
37 | 60 | } |
38 | 61 |
|
39 | | -/// Decode RS-encoded bytes, returning (payload, total_errors_corrected). |
40 | | -/// |
41 | | -/// **v0.1 skeleton**: passes data through unchanged. |
| 62 | +/// Decode RS-encoded data, returning (concatenated_payload_bytes, total_errors_corrected). |
| 63 | +/// Caller (decoder.rs) trims the payload based on the L5 LEN field. |
42 | 64 | pub fn decode_with_ecc( |
43 | 65 | data: &[u8], |
44 | | - _nsym: usize, |
45 | | - _block_size: usize, |
| 66 | + nsym: usize, |
| 67 | + block_size: usize, |
46 | 68 | ) -> Result<(Vec<u8>, usize), EccError> { |
47 | | - Ok((data.to_vec(), 0)) |
| 69 | + let full_block = block_size + nsym; |
| 70 | + if data.len() % full_block != 0 { |
| 71 | + return Err(EccError::UnalignedData(data.len(), full_block)); |
| 72 | + } |
| 73 | + let mut out = Vec::with_capacity(data.len() - (data.len() / full_block) * nsym); |
| 74 | + let mut total_errors = 0usize; |
| 75 | + for chunk in data.chunks(full_block) { |
| 76 | + let (msg, n_errs) = rs_decode_msg(chunk, nsym)?; |
| 77 | + out.extend(msg); |
| 78 | + total_errors += n_errs; |
| 79 | + } |
| 80 | + Ok((out, total_errors)) |
48 | 81 | } |
49 | 82 |
|
50 | 83 | #[cfg(test)] |
51 | 84 | mod tests { |
52 | 85 | use super::*; |
53 | 86 |
|
54 | 87 | #[test] |
55 | | - fn passthrough_round_trip_v01_skeleton() { |
56 | | - let data = b"some payload bytes"; |
57 | | - let encoded = encode_with_ecc(data, DEFAULT_NSYM, DEFAULT_BLOCK); |
58 | | - let (decoded, n_err) = decode_with_ecc(&encoded, DEFAULT_NSYM, DEFAULT_BLOCK).unwrap(); |
59 | | - assert_eq!(decoded.as_slice(), data); |
60 | | - assert_eq!(n_err, 0); |
| 88 | + fn round_trip_no_errors() { |
| 89 | + let data: Vec<u8> = (0..223).collect(); |
| 90 | + let encoded = encode_with_ecc(&data, DEFAULT_NSYM, DEFAULT_BLOCK).unwrap(); |
| 91 | + assert_eq!(encoded.len(), 255); |
| 92 | + let (decoded, n_errs) = decode_with_ecc(&encoded, DEFAULT_NSYM, DEFAULT_BLOCK).unwrap(); |
| 93 | + assert_eq!(decoded, data); |
| 94 | + assert_eq!(n_errs, 0); |
| 95 | + } |
| 96 | + |
| 97 | + #[test] |
| 98 | + fn corrects_up_to_16_byte_errors() { |
| 99 | + let data: Vec<u8> = (0..223).collect(); |
| 100 | + let mut encoded = encode_with_ecc(&data, DEFAULT_NSYM, DEFAULT_BLOCK).unwrap(); |
| 101 | + |
| 102 | + // Flip 10 bytes (well within RS(255,223) 16-error capacity) |
| 103 | + let positions = [3usize, 17, 42, 88, 100, 130, 175, 200, 220, 250]; |
| 104 | + for &p in &positions { |
| 105 | + encoded[p] ^= 0xAA; |
| 106 | + } |
| 107 | + let (decoded, n_errs) = decode_with_ecc(&encoded, DEFAULT_NSYM, DEFAULT_BLOCK).unwrap(); |
| 108 | + assert_eq!(decoded, data); |
| 109 | + assert_eq!(n_errs, 10); |
| 110 | + } |
| 111 | + |
| 112 | + #[test] |
| 113 | + fn fails_on_too_many_errors() { |
| 114 | + let data: Vec<u8> = (0..223).collect(); |
| 115 | + let mut encoded = encode_with_ecc(&data, DEFAULT_NSYM, DEFAULT_BLOCK).unwrap(); |
| 116 | + // Flip 20 bytes (exceeds 16-error capacity) |
| 117 | + for p in 0..20 { |
| 118 | + encoded[p * 10] ^= 0xFF; |
| 119 | + } |
| 120 | + let result = decode_with_ecc(&encoded, DEFAULT_NSYM, DEFAULT_BLOCK); |
| 121 | + assert!(matches!(result, Err(EccError::DecodeFailed(_)))); |
61 | 122 | } |
62 | 123 | } |
0 commit comments