diff --git a/crates/xtce-gs-link/src/rs.rs b/crates/xtce-gs-link/src/rs.rs index 0ed6533..68d3b69 100644 --- a/crates/xtce-gs-link/src/rs.rs +++ b/crates/xtce-gs-link/src/rs.rs @@ -1450,4 +1450,67 @@ mod tests { } } } + /// Reed-Solomon cannot tell you that you forgot to derandomise. + /// + /// The pipeline is `synchronise โ†’ derandomise โ†’ Reed-Solomon`, and the temptation is to + /// put the error-correcting code first, on the theory that a check this strong would + /// notice a block that had not been derandomised yet. It does not, and the reason is + /// worth stating once where somebody reordering the pipeline will read it. + /// + /// The randomiser sequence of CCSDS 131.0-B-5 ยง10.4 is 255 octets. An RS(255,223) + /// codeword is 255 symbols over GF(2^8), and this code is linear: the sum of two + /// codewords is a codeword, and over GF(2) that sum is XOR. The sequence happens to be a + /// codeword itself โ€” so a randomised frame, which is `codeword XOR sequence`, is also a + /// codeword. Exactly. Zero syndromes, zero corrections, and a block that disagrees with + /// the frame it came from in every single symbol. + /// + /// So the only thing standing between a misordered pipeline and 255 bytes of confident + /// nonsense is the order itself. + #[test] + fn reed_solomon_cannot_see_that_the_derandomiser_was_skipped() { + // Both codes CCSDS 131.0-B-5 defines, because the first question anybody asks is + // whether the shorter one escapes it. It does not: E=16 and E=32 take consecutive + // roots from the same generator, so RS(255,223) is a subcode of RS(255,239) and a + // codeword of the longer code is a codeword of the shorter one. + for parity in [16, 32] { + check_the_randomiser_is_invisible(parity); + } + } + + fn check_the_randomiser_is_invisible(parity: usize) { + let rs = ReedSolomon::ccsds(parity, 1).expect("a CCSDS code"); + + // The premise: the randomiser sequence is a codeword. + let mut sequence = crate::derand::sequence(); + assert_eq!(rs.decode(&mut sequence), Ok(0)); + + // And arbitrary bytes of the same length are not, so that is not a vacuous check. + let mut arbitrary = [0u8; CODEWORD_SYMBOLS]; + let mut state = 1u32; + for byte in &mut arbitrary { + state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345); + *byte = (state >> 16) as u8; + } + assert_eq!(rs.decode(&mut arbitrary), Err(RsError::Uncorrectable)); + + // The consequence: a real frame, encoded and then randomised, decodes clean. + let frame: Vec = (0..rs.data_length() as u32) + .map(|i| (i.wrapping_mul(37) ^ 0xA5) as u8) + .collect(); + let mut encoded = Vec::new(); + rs.encode(&frame, &mut encoded) + .expect("the frame is exactly the code's data length"); + + let mut skipped_the_derandomiser = encoded.clone(); + crate::derand::randomize(&mut skipped_the_derandomiser); + assert_eq!(rs.decode(&mut skipped_the_derandomiser), Ok(0)); + + // Clean, and wrong in every symbol. + let differing = encoded + .iter() + .zip(&skipped_the_derandomiser) + .filter(|(sent, got)| sent != got) + .count(); + assert_eq!(differing, CODEWORD_SYMBOLS); + } }