Skip to main content

ps_ecc/reed_solomon/methods/
correct_in_place.rs

1use crate::{RSDecodeError, ReedSolomon};
2
3impl ReedSolomon {
4    /// Corrects a received codeword in-place.
5    /// # Errors
6    /// - [`RSDecodeError::InsufficientLength`] is returned if `received` holds
7    ///   fewer bytes than [`ReedSolomon::parity_bytes`].
8    /// - [`std::num::TryFromIntError`] is returned if `received` holds more
9    ///   than 255 bytes.
10    /// - [`RSComputeErrorsError`](crate::RSComputeErrorsError) is propagated
11    ///   from [`ReedSolomon::compute_errors`].
12    /// - [`RSDecodeError::TooManyErrors`] is returned if the data is unrecoverable.
13    pub fn correct_in_place(&self, received: &mut [u8]) -> Result<(), RSDecodeError> {
14        let parity_bytes = self.parity_bytes();
15
16        if received.len() < usize::from(parity_bytes) {
17            return Err(RSDecodeError::InsufficientLength {
18                parity_bytes,
19                received: received.len(),
20            });
21        }
22
23        let received_len = u8::try_from(received.len())?;
24        let syndromes = Self::compute_syndromes(parity_bytes, received);
25
26        let Some(errors) = self.compute_errors(received_len, &syndromes)? else {
27            return Ok(());
28        };
29
30        Self::apply_corrections(received, errors.first_n_coefficients(received_len.into()));
31
32        match self.validate(received) {
33            None => Ok(()),
34            Some(_) => Err(RSDecodeError::TooManyErrors),
35        }
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use ps_buffer::ToBuffer;
42
43    use crate::{RSComputeErrorsError, RSDecodeError, ReedSolomon};
44
45    type TestError = Box<dyn std::error::Error>;
46
47    #[test]
48    fn test_correct_in_place_no_errors() -> Result<(), TestError> {
49        let rs = ReedSolomon::new(2)?;
50        let message = b"InPlace".to_buffer()?;
51        let mut encoded = rs.encode(&message)?;
52
53        rs.correct_in_place(&mut encoded)?;
54
55        assert_eq!(encoded.slice(4..), message.as_slice());
56
57        Ok(())
58    }
59
60    #[test]
61    fn test_correct_in_place_one_error() -> Result<(), TestError> {
62        let rs = ReedSolomon::new(3)?;
63        let message = b"InPlace1".to_buffer()?;
64        let mut encoded = rs.encode(&message)?;
65
66        encoded[4] ^= 16;
67
68        rs.correct_in_place(&mut encoded)?;
69
70        assert_eq!(encoded.slice(6..), message.as_slice());
71
72        Ok(())
73    }
74
75    #[test]
76    fn test_correct_in_place_too_many_errors() -> Result<(), TestError> {
77        let rs = ReedSolomon::new(1)?;
78        let message = b"TooMany".to_buffer()?;
79        let mut encoded = rs.encode(&message)?;
80
81        encoded[0] ^= 1;
82        encoded[1] ^= 2;
83
84        assert_eq!(
85            rs.correct_in_place(&mut encoded),
86            Err(RSDecodeError::RSComputeErrorsError(
87                RSComputeErrorsError::TooManyErrors
88            ))
89        );
90
91        Ok(())
92    }
93
94    #[test]
95    fn test_correct_in_place_rejects_input_shorter_than_parity() -> Result<(), TestError> {
96        // An all-zero truncated slice yields zero syndromes; without the
97        // length check it was accepted unchanged as a pristine codeword.
98        let rs = ReedSolomon::new(4)?;
99
100        let mut received = [0u8; 7];
101
102        assert_eq!(
103            rs.correct_in_place(&mut received),
104            Err(RSDecodeError::InsufficientLength {
105                parity_bytes: 8,
106                received: 7,
107            })
108        );
109
110        Ok(())
111    }
112
113    #[test]
114    fn test_correct_in_place_accepts_parity_only_codeword() -> Result<(), TestError> {
115        // The encoding of the empty message is all-zero parity; a slice of
116        // exactly parity_bytes zeros is that codeword and passes the guard.
117        let rs = ReedSolomon::new(4)?;
118
119        let mut received = [0u8; 8];
120
121        rs.correct_in_place(&mut received)?;
122
123        assert_eq!(received, [0u8; 8]);
124
125        Ok(())
126    }
127
128    #[test]
129    fn test_correct_in_place_multiple_errors_at_boundaries() -> Result<(), TestError> {
130        let rs = ReedSolomon::new(4)?;
131        let message = b"Boundary".to_buffer()?;
132        let mut encoded = rs.encode(&message)?;
133        let len = encoded.len();
134
135        encoded[0] ^= 1; // Error at start
136        encoded[len - 1] ^= 2; // Error at end
137
138        rs.correct_in_place(&mut encoded)?;
139
140        assert_eq!(encoded.slice(8..), message.as_slice());
141
142        Ok(())
143    }
144
145    /// Tests that `correct_in_place` handles syndromes with trailing zeros.
146    ///
147    /// Although `coefficients()` trims trailing zeros, this is harmless because
148    /// the euclidean algorithm converts syndromes to a `Polynomial`, which normalizes
149    /// by trimming trailing zeros anyway.
150    #[test]
151    fn test_correct_in_place_with_trailing_zero_syndrome() -> Result<(), TestError> {
152        let rs = ReedSolomon::new(2)?;
153        let message = b"TestMessage!".to_buffer()?;
154        let encoded = rs.encode(&message)?;
155
156        // Find a double corruption where the last syndrome is zero but others are not.
157        // A single error e at position j gives syndrome S_i = e * α^(i*j), which is
158        // never zero for e ≠ 0. But two errors can cancel: e1*α^(i*j1) + e2*α^(i*j2) = 0.
159        'search: for pos1 in 0..encoded.len() {
160            for pos2 in (pos1 + 1)..encoded.len() {
161                for xor1 in 1u8..=255 {
162                    for xor2 in 1u8..=255 {
163                        let mut corrupted = encoded.clone()?;
164
165                        corrupted[pos1] ^= xor1;
166                        corrupted[pos2] ^= xor2;
167
168                        let syndromes =
169                            ReedSolomon::compute_syndromes(rs.parity_bytes(), &corrupted);
170                        let coeffs = syndromes.first_n_coefficients(rs.parity_bytes().into());
171
172                        // We want: last syndrome is 0, but not all are 0
173                        if coeffs.last() == Some(&0) && coeffs.iter().any(|&s| s != 0) {
174                            // Found a case where trailing syndrome is zero.
175                            // This should still be correctable (two errors, t=2).
176                            let mut to_correct = corrupted.clone()?;
177
178                            rs.correct_in_place(&mut to_correct)?;
179                            assert_eq!(to_correct.as_slice(), encoded.as_slice());
180                            break 'search;
181                        }
182                    }
183                }
184            }
185        }
186
187        Ok(())
188    }
189
190    /// Tests correction when syndrome polynomial has trailing zeros trimmed.
191    ///
192    /// Verifies that `coefficients()` being shorter than `parity_bytes` does not
193    /// affect correctness, since the euclidean algorithm normalizes internally.
194    #[test]
195    fn test_correct_in_place_trailing_zero_syndrome_trimmed() -> Result<(), TestError> {
196        let rs = ReedSolomon::new(2)?;
197        let message = b"TestMessage!".to_buffer()?;
198        let encoded = rs.encode(&message)?;
199
200        // Search for a corruption pattern with trailing zero syndrome
201        for pos1 in 0..encoded.len() {
202            for pos2 in (pos1 + 1)..encoded.len() {
203                for xor1 in 1u8..=255 {
204                    for xor2 in 1u8..=255 {
205                        let mut corrupted = encoded.clone()?;
206
207                        corrupted[pos1] ^= xor1;
208                        corrupted[pos2] ^= xor2;
209
210                        let syndromes =
211                            ReedSolomon::compute_syndromes(rs.parity_bytes(), &corrupted);
212                        let full = syndromes.first_n_coefficients(rs.parity_bytes().into());
213
214                        if full.last() == Some(&0) && full.iter().any(|&s| s != 0) {
215                            // Found pattern with trailing zero syndrome.
216                            // coefficients() is shorter than parity_bytes due to trimming.
217                            assert!(syndromes.coefficients().len() < rs.parity_bytes().into());
218
219                            // Correction succeeds despite trimmed syndromes.
220                            let mut to_correct = corrupted.clone()?;
221
222                            rs.correct_in_place(&mut to_correct)?;
223                            assert_eq!(to_correct.as_slice(), encoded.as_slice());
224
225                            return Ok(());
226                        }
227                    }
228                }
229            }
230        }
231
232        panic!("No trailing zero syndrome pattern found");
233    }
234
235    #[test]
236    fn test_correct_in_place_all_zeros() -> Result<(), TestError> {
237        let rs = ReedSolomon::new(2)?;
238        let message = vec![0; 10].to_buffer()?;
239        let mut encoded = rs.encode(&message)?;
240
241        encoded[2] ^= 1;
242
243        rs.correct_in_place(&mut encoded)?;
244
245        assert_eq!(encoded.slice(4..), message.as_slice());
246
247        Ok(())
248    }
249}