#![expect(
clippy::indexing_slicing,
reason = "all indexing in this module is statically bounded by the codec / table geometry"
)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum SecdedOutcome {
Clean,
Corrected,
Uncorrectable,
}
pub trait SecdedCodec {
const DATA_WORD_BYTES: usize;
const PARITY_BYTES: usize;
fn encode_word(data: &[u8], parity_out: &mut [u8]);
fn decode_word(data: &mut [u8], parity: &[u8]) -> SecdedOutcome;
}
pub struct Hsiao7264;
const DATA_BITS: usize = 64;
const DATA_COLS: [u8; DATA_BITS] = build_data_cols();
const fn build_data_cols() -> [u8; DATA_BITS] {
let mut cols = [0u8; DATA_BITS];
let mut filled = 0usize;
let mut byte: u8 = 0;
loop {
let ones = byte.count_ones();
if ones % 2 == 1 && ones >= 3 {
cols[filled] = byte;
filled += 1;
}
if byte == u8::MAX || filled == DATA_BITS {
break;
}
byte += 1;
}
cols
}
const TABLES: [[u8; 256]; 8] = build_tables();
const fn build_tables() -> [[u8; 256]; 8] {
let mut tables = [[0u8; 256]; 8];
let mut lane = 0usize;
while lane < 8 {
let mut b = 0usize;
while b < 256 {
let mut acc = 0u8;
let mut bit = 0usize;
while bit < 8 {
if (b >> bit) & 1 == 1 {
acc ^= DATA_COLS[lane * 8 + bit];
}
bit += 1;
}
tables[lane][b] = acc;
b += 1;
}
lane += 1;
}
tables
}
#[inline]
fn check_byte(word: u64) -> u8 {
let b = word.to_le_bytes();
TABLES[0][b[0] as usize]
^ TABLES[1][b[1] as usize]
^ TABLES[2][b[2] as usize]
^ TABLES[3][b[3] as usize]
^ TABLES[4][b[4] as usize]
^ TABLES[5][b[5] as usize]
^ TABLES[6][b[6] as usize]
^ TABLES[7][b[7] as usize]
}
impl SecdedCodec for Hsiao7264 {
const DATA_WORD_BYTES: usize = 8;
const PARITY_BYTES: usize = 1;
fn encode_word(data: &[u8], parity_out: &mut [u8]) {
assert!(
data.len() == Self::DATA_WORD_BYTES,
"Hsiao7264 word must be 8 bytes"
);
assert!(
parity_out.len() >= Self::PARITY_BYTES,
"Hsiao7264 needs 1 parity byte",
);
let mut word = [0u8; 8];
word.copy_from_slice(data);
parity_out[0] = check_byte(u64::from_le_bytes(word));
}
fn decode_word(data: &mut [u8], parity: &[u8]) -> SecdedOutcome {
assert!(
data.len() == Self::DATA_WORD_BYTES,
"Hsiao7264 word must be 8 bytes"
);
assert!(
parity.len() >= Self::PARITY_BYTES,
"Hsiao7264 needs 1 parity byte",
);
let mut word_bytes = [0u8; 8];
word_bytes.copy_from_slice(data);
let word = u64::from_le_bytes(word_bytes);
let syndrome = parity[0] ^ check_byte(word);
if syndrome == 0 {
return SecdedOutcome::Clean;
}
if syndrome.is_power_of_two() {
return SecdedOutcome::Corrected;
}
if syndrome.count_ones() % 2 == 1 {
let mut bit = 0usize;
while bit < DATA_BITS {
if DATA_COLS[bit] == syndrome {
let corrected = word ^ (1u64 << bit);
data.copy_from_slice(&corrected.to_le_bytes());
return SecdedOutcome::Corrected;
}
bit += 1;
}
}
SecdedOutcome::Uncorrectable
}
}
#[must_use]
pub fn block_parity_len(payload_len: usize) -> usize {
payload_len.div_ceil(Hsiao7264::DATA_WORD_BYTES) * Hsiao7264::PARITY_BYTES
}
#[must_use]
pub fn encode_block_parity(payload: &[u8]) -> alloc::vec::Vec<u8> {
let mut out = alloc::vec::Vec::with_capacity(block_parity_len(payload.len()));
let mut word = [0u8; Hsiao7264::DATA_WORD_BYTES];
for chunk in payload.chunks(Hsiao7264::DATA_WORD_BYTES) {
word.fill(0);
word[..chunk.len()].copy_from_slice(chunk);
let mut check = [0u8; Hsiao7264::PARITY_BYTES];
Hsiao7264::encode_word(&word, &mut check);
out.extend_from_slice(&check);
}
out
}
#[must_use]
pub fn try_correct_block(payload: &mut [u8], parity: &[u8]) -> SecdedOutcome {
if parity.len() < block_parity_len(payload.len()) {
return SecdedOutcome::Uncorrectable;
}
let mut any_corrected = false;
let mut word = [0u8; Hsiao7264::DATA_WORD_BYTES];
for (i, chunk) in payload.chunks_mut(Hsiao7264::DATA_WORD_BYTES).enumerate() {
let len = chunk.len();
word.fill(0);
word[..len].copy_from_slice(chunk);
let p = &parity[i * Hsiao7264::PARITY_BYTES..(i + 1) * Hsiao7264::PARITY_BYTES];
match Hsiao7264::decode_word(&mut word, p) {
SecdedOutcome::Clean => {}
SecdedOutcome::Corrected => {
chunk.copy_from_slice(&word[..len]);
any_corrected = true;
}
SecdedOutcome::Uncorrectable => return SecdedOutcome::Uncorrectable,
}
}
if any_corrected {
SecdedOutcome::Corrected
} else {
SecdedOutcome::Clean
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn data_cols_are_distinct_odd_weight_at_least_three() {
for (i, &col) in DATA_COLS.iter().enumerate() {
assert!(col.count_ones() % 2 == 1, "col {i} must be odd weight");
assert!(
col.count_ones() >= 3,
"col {i} must avoid the weight-1 check columns"
);
for (j, &other) in DATA_COLS.iter().enumerate() {
if i != j {
assert_ne!(col, other, "cols {i} and {j} collide");
}
}
}
}
#[test]
fn clean_word_round_trips() {
let data = 0x0123_4567_89ab_cdefu64.to_le_bytes();
let mut parity = [0u8; 1];
Hsiao7264::encode_word(&data, &mut parity);
let mut recv = data;
assert_eq!(
Hsiao7264::decode_word(&mut recv, &parity),
SecdedOutcome::Clean,
);
assert_eq!(recv, data, "clean decode must not alter the word");
}
#[test]
fn corrects_every_single_data_bit_flip() {
for seed in [0u64, u64::MAX, 0x0123_4567_89ab_cdef, 0xdead_beef_cafe_babe] {
let data = seed.to_le_bytes();
let mut parity = [0u8; 1];
Hsiao7264::encode_word(&data, &mut parity);
for bit in 0..64 {
let mut recv = (seed ^ (1u64 << bit)).to_le_bytes();
let outcome = Hsiao7264::decode_word(&mut recv, &parity);
assert_eq!(
outcome,
SecdedOutcome::Corrected,
"single flip at bit {bit} (seed {seed:#x}) must be corrected",
);
assert_eq!(
recv, data,
"corrected word must equal the original (bit {bit}, seed {seed:#x})",
);
}
}
}
#[test]
fn corrects_single_check_bit_flip() {
let data = 0x0123_4567_89ab_cdefu64.to_le_bytes();
let mut parity = [0u8; 1];
Hsiao7264::encode_word(&data, &mut parity);
for bit in 0..8 {
let mut bad_parity = parity;
bad_parity[0] ^= 1 << bit;
let mut recv = data;
let outcome = Hsiao7264::decode_word(&mut recv, &bad_parity);
assert_eq!(
outcome,
SecdedOutcome::Corrected,
"check-bit flip {bit} must be reported corrected",
);
assert_eq!(recv, data, "data must be intact on a check-bit flip");
}
}
#[test]
fn detects_every_double_data_bit_flip() {
let seed = 0x0123_4567_89ab_cdefu64;
let data = seed.to_le_bytes();
let mut parity = [0u8; 1];
Hsiao7264::encode_word(&data, &mut parity);
for a in 0..64 {
for b in (a + 1)..64 {
let corrupted = seed ^ (1u64 << a) ^ (1u64 << b);
let mut recv = corrupted.to_le_bytes();
let outcome = Hsiao7264::decode_word(&mut recv, &parity);
assert_eq!(
outcome,
SecdedOutcome::Uncorrectable,
"double flip at bits {a},{b} must be detected, not miscorrected",
);
assert_eq!(
recv.as_slice(),
corrupted.to_le_bytes().as_slice(),
"uncorrectable decode must leave the word unchanged",
);
}
}
}
#[test]
fn overhead_is_one_eighth() {
assert_eq!(Hsiao7264::PARITY_BYTES * 8, Hsiao7264::DATA_WORD_BYTES);
}
#[test]
fn block_parity_len_is_one_byte_per_eight() {
assert_eq!(block_parity_len(0), 0);
assert_eq!(block_parity_len(1), 1);
assert_eq!(block_parity_len(8), 1);
assert_eq!(block_parity_len(9), 2);
assert_eq!(block_parity_len(64), 8);
assert_eq!(block_parity_len(65), 9);
}
#[test]
fn block_round_trip_is_clean() {
let payload: alloc::vec::Vec<u8> = (0..100u8).map(|i| i.wrapping_mul(7)).collect();
let parity = encode_block_parity(&payload);
assert_eq!(parity.len(), block_parity_len(payload.len()));
let mut recv = payload.clone();
assert_eq!(try_correct_block(&mut recv, &parity), SecdedOutcome::Clean);
assert_eq!(recv, payload, "clean block must be unchanged");
}
#[test]
fn block_corrects_single_bit_flip_at_every_byte() {
let payload: alloc::vec::Vec<u8> = (0..100u8).map(|i| i.wrapping_mul(7)).collect();
let parity = encode_block_parity(&payload);
for byte_idx in 0..payload.len() {
for bit in 0..8 {
let mut recv = payload.clone();
recv[byte_idx] ^= 1 << bit;
let outcome = try_correct_block(&mut recv, &parity);
assert_eq!(
outcome,
SecdedOutcome::Corrected,
"single flip at byte {byte_idx} bit {bit} must be corrected",
);
assert_eq!(
recv, payload,
"block must be repaired to the original (byte {byte_idx}, bit {bit})",
);
}
}
}
#[test]
fn block_detects_double_bit_flip_within_one_word() {
let payload: alloc::vec::Vec<u8> = (0..32u8).collect();
let parity = encode_block_parity(&payload);
let mut recv = payload;
recv[0] ^= 0b0000_0001;
recv[1] ^= 0b0000_0010;
assert_eq!(
try_correct_block(&mut recv, &parity),
SecdedOutcome::Uncorrectable,
"two bit errors in one word must be detected",
);
}
#[test]
fn block_truncated_parity_is_uncorrectable() {
let payload: alloc::vec::Vec<u8> = (0..64u8).collect();
let parity = encode_block_parity(&payload);
let short = &parity[..parity.len() - 1];
let mut recv = payload;
assert_eq!(
try_correct_block(&mut recv, short),
SecdedOutcome::Uncorrectable,
"a truncated parity trailer cannot verify the block",
);
}
}