#[must_use]
pub fn parity(bits: &[bool]) -> bool {
bits.iter().filter(|&&b| b).count() % 2 == 1
}
#[must_use]
pub fn parity_u64(x: u64) -> bool {
x.count_ones() % 2 == 1
}
#[must_use]
pub fn checksum_fletcher16(data: &[u8]) -> u16 {
let (mut lo, mut hi) = (0u16, 0u16);
for &b in data {
lo = (lo + u16::from(b)) % 255;
hi = (hi + lo) % 255;
}
(hi << 8) | lo
}
#[must_use]
pub fn checksum_fletcher32(data: &[u8]) -> u32 {
let (mut lo, mut hi) = (0u32, 0u32);
for pair in data.chunks(2) {
let w = u32::from(pair[0]) | (u32::from(*pair.get(1).unwrap_or(&0)) << 8);
lo = (lo + w) % 65535;
hi = (hi + lo) % 65535;
}
(hi << 16) | lo
}
#[must_use]
pub fn adler32(data: &[u8]) -> u32 {
const MOD: u32 = 65521;
let (mut a, mut b) = (1u32, 0u32);
for &x in data {
a = (a + u32::from(x)) % MOD;
b = (b + a) % MOD;
}
(b << 16) | a
}
fn reflect(x: u64, width: u32) -> u64 {
let mut out = 0u64;
for i in 0..width {
if x & (1 << i) != 0 {
out |= 1 << (width - 1 - i);
}
}
out
}
#[must_use]
pub fn crc(data: &[u8], poly: u64, width: u32, init: u64, xor_out: u64, reflect_io: bool) -> u64 {
assert!((8..=64).contains(&width), "CRC width must be between 8 and 64");
let mask = if width == 64 { u64::MAX } else { (1u64 << width) - 1 };
let top = 1u64 << (width - 1);
let mut reg = init & mask;
for &byte in data {
let b = if reflect_io { reflect(u64::from(byte), 8) } else { u64::from(byte) };
reg ^= b << (width - 8);
for _ in 0..8 {
reg = if reg & top != 0 { ((reg << 1) ^ poly) & mask } else { (reg << 1) & mask };
}
}
if reflect_io {
reg = reflect(reg, width);
}
(reg ^ xor_out) & mask
}
#[must_use]
pub fn crc32_ieee(data: &[u8]) -> u32 {
crc(data, 0x04C1_1DB7, 32, 0xFFFF_FFFF, 0xFFFF_FFFF, true) as u32
}
#[must_use]
pub fn crc16_ccitt(data: &[u8]) -> u16 {
crc(data, 0x1021, 16, 0xFFFF, 0x0000, false) as u16
}
#[must_use]
pub fn crc8(data: &[u8]) -> u8 {
crc(data, 0x07, 8, 0x00, 0x00, false) as u8
}
#[must_use]
pub fn crc_table(poly: u32) -> [u32; 256] {
let mut table = [0u32; 256];
for (i, entry) in table.iter_mut().enumerate() {
let mut c = i as u32;
for _ in 0..8 {
c = if c & 1 != 0 { (c >> 1) ^ poly } else { c >> 1 };
}
*entry = c;
}
table
}
#[must_use]
pub fn crc32_with_table(data: &[u8], table: &[u32; 256]) -> u32 {
let mut c = 0xFFFF_FFFFu32;
for &b in data {
c = table[((c ^ u32::from(b)) & 0xFF) as usize] ^ (c >> 8);
}
c ^ 0xFFFF_FFFF
}
#[must_use]
pub fn luhn_check(digits: &[u8]) -> bool {
assert!(digits.iter().all(|&d| d <= 9), "decimal digits only");
if digits.is_empty() {
return false;
}
luhn_sum(digits).is_multiple_of(10)
}
fn luhn_sum(digits: &[u8]) -> u32 {
digits
.iter()
.rev()
.enumerate()
.map(|(i, &d)| {
let mut v = u32::from(d);
if i % 2 == 1 {
v *= 2;
if v > 9 {
v -= 9;
}
}
v
})
.sum()
}
#[must_use]
pub fn luhn_generate(payload: &[u8]) -> u8 {
assert!(payload.iter().all(|&d| d <= 9), "decimal digits only");
let mut with_slot = payload.to_vec();
with_slot.push(0);
((10 - luhn_sum(&with_slot) % 10) % 10) as u8
}
#[must_use]
pub fn isbn10_check(digits: &[u8]) -> bool {
assert_eq!(digits.len(), 10, "an ISBN-10 has ten digits");
assert!(digits[..9].iter().all(|&d| d <= 9), "only the check digit may be X");
assert!(digits[9] <= 10, "the check digit is 0 to 9 or X");
let sum: u32 =
digits.iter().enumerate().map(|(i, &d)| (10 - i as u32) * u32::from(d)).sum();
sum.is_multiple_of(11)
}
#[must_use]
pub fn isbn13_check(digits: &[u8]) -> bool {
assert_eq!(digits.len(), 13, "an ISBN-13 has thirteen digits");
assert!(digits.iter().all(|&d| d <= 9), "decimal digits only");
let sum: u32 = digits
.iter()
.enumerate()
.map(|(i, &d)| if i % 2 == 0 { u32::from(d) } else { 3 * u32::from(d) })
.sum();
sum.is_multiple_of(10)
}
const VERHOEFF_D: [[u8; 10]; 10] = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
];
const VERHOEFF_P: [[u8; 10]; 8] = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
];
const VERHOEFF_INV: [u8; 10] = [0, 4, 3, 2, 1, 5, 6, 7, 8, 9];
#[must_use]
pub fn verhoeff_check(digits: &[u8]) -> bool {
assert!(digits.iter().all(|&d| d <= 9), "decimal digits only");
let mut c = 0usize;
for (i, &d) in digits.iter().rev().enumerate() {
c = VERHOEFF_D[c][VERHOEFF_P[i % 8][d as usize] as usize] as usize;
}
c == 0
}
#[must_use]
pub fn verhoeff_generate(payload: &[u8]) -> u8 {
assert!(payload.iter().all(|&d| d <= 9), "decimal digits only");
let mut c = 0usize;
for (i, &d) in payload.iter().rev().enumerate() {
c = VERHOEFF_D[c][VERHOEFF_P[(i + 1) % 8][d as usize] as usize] as usize;
}
VERHOEFF_INV[c]
}
const DAMM: [[u8; 10]; 10] = [
[0, 3, 1, 7, 5, 9, 8, 6, 4, 2],
[7, 0, 9, 2, 1, 5, 4, 8, 6, 3],
[4, 2, 0, 6, 8, 7, 1, 3, 5, 9],
[1, 7, 5, 0, 9, 8, 3, 4, 2, 6],
[6, 1, 2, 3, 0, 4, 5, 9, 7, 8],
[3, 6, 7, 4, 2, 0, 9, 5, 8, 1],
[5, 8, 6, 9, 7, 2, 0, 1, 3, 4],
[8, 9, 4, 5, 3, 6, 2, 0, 1, 7],
[9, 4, 3, 8, 6, 1, 7, 2, 0, 5],
[2, 5, 8, 1, 4, 3, 6, 7, 9, 0],
];
#[must_use]
pub fn damm_check(digits: &[u8]) -> bool {
assert!(digits.iter().all(|&d| d <= 9), "decimal digits only");
let mut interim = 0usize;
for &d in digits {
interim = DAMM[interim][d as usize] as usize;
}
interim == 0
}
#[must_use]
pub fn damm_generate(payload: &[u8]) -> u8 {
assert!(payload.iter().all(|&d| d <= 9), "decimal digits only");
let mut interim = 0usize;
for &d in payload {
interim = DAMM[interim][d as usize] as usize;
}
interim as u8
}
#[must_use]
pub fn hamming_distance_bits(a: u64, b: u64) -> u32 {
(a ^ b).count_ones()
}
#[must_use]
pub fn hamming_distance_bytes(a: &[u8], b: &[u8]) -> Option<u32> {
if a.len() != b.len() {
return None;
}
Some(a.iter().zip(b).map(|(&x, &y)| u32::from((x ^ y).count_ones() as u8)).sum())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monte_carlo::Rng;
fn pick(rng: &mut Rng, n: usize) -> usize {
((u128::from(rng.next_u64()) * n as u128) >> 64) as usize
}
const CHECK: &[u8] = b"123456789";
#[test]
fn crcs_match_their_published_check_values() {
let cases: [(&str, u64, u32, u64, u64, bool, u64); 8] = [
("CRC-8/SMBUS", 0x07, 8, 0x00, 0x00, false, 0xF4),
("CRC-8/MAXIM-DOW", 0x31, 8, 0x00, 0x00, true, 0xA1),
("CRC-16/ARC", 0x8005, 16, 0x0000, 0x0000, true, 0xBB3D),
("CRC-16/CCITT-FALSE", 0x1021, 16, 0xFFFF, 0x0000, false, 0x29B1),
("CRC-16/XMODEM", 0x1021, 16, 0x0000, 0x0000, false, 0x31C3),
("CRC-32/ISO-HDLC", 0x04C1_1DB7, 32, 0xFFFF_FFFF, 0xFFFF_FFFF, true, 0xCBF4_3926),
("CRC-32/BZIP2", 0x04C1_1DB7, 32, 0xFFFF_FFFF, 0xFFFF_FFFF, false, 0xFC89_1918),
(
"CRC-64/XZ",
0x42F0_E1EB_A9EA_3693,
64,
0xFFFF_FFFF_FFFF_FFFF,
0xFFFF_FFFF_FFFF_FFFF,
true,
0x995D_C9BB_DF19_39FA,
),
];
for (name, poly, width, init, xorout, reflect_io, want) in cases {
assert_eq!(crc(CHECK, poly, width, init, xorout, reflect_io), want, "{name}");
}
assert_eq!(crc32_ieee(CHECK), 0xCBF4_3926);
assert_eq!(crc16_ccitt(CHECK), 0x29B1);
assert_eq!(crc8(CHECK), 0xF4);
let table = crc_table(0xEDB8_8320);
let mut rng = Rng::new(0x_C2C0);
for _ in 0..500 {
let n = pick(&mut rng, 64);
let data: Vec<u8> = (0..n).map(|_| (rng.next_u64() & 0xFF) as u8).collect();
assert_eq!(crc32_with_table(&data, &table), crc32_ieee(&data));
}
}
#[test]
fn a_crc_detects_every_burst_no_longer_than_its_width() {
let mut rng = Rng::new(0x_B025);
for (width, reflected, f) in [
(8u32, false, (|d: &[u8]| u64::from(crc8(d))) as fn(&[u8]) -> u64),
(16, false, |d: &[u8]| u64::from(crc16_ccitt(d))),
(16, true, |d: &[u8]| crc(d, 0x8005, 16, 0, 0, true)),
(32, true, |d: &[u8]| u64::from(crc32_ieee(d))),
(32, false, |d: &[u8]| crc(d, 0x04C1_1DB7, 32, 0xFFFF_FFFF, 0xFFFF_FFFF, false)),
] {
for _ in 0..400 {
let bytes = 8 + pick(&mut rng, 24);
let data: Vec<u8> = (0..bytes).map(|_| (rng.next_u64() & 0xFF) as u8).collect();
let clean = f(&data);
let total_bits = bytes * 8;
let start = pick(&mut rng, total_bits - width as usize);
let mut pattern = rng.next_u64() & ((1u64 << (width - 1)) - 1);
pattern = (pattern << 1) | 1;
let mut bad = data.clone();
for k in 0..width as usize {
if pattern & (1 << k) != 0 {
let bit = start + k;
let within = if reflected { bit % 8 } else { 7 - bit % 8 };
bad[bit / 8] ^= 1 << within;
}
}
assert_ne!(
f(&bad),
clean,
"a {width}-bit burst went undetected (reflected: {reflected})"
);
}
}
}
#[test]
fn a_zero_seeded_crc_is_linear() {
let mut rng = Rng::new(0x_11EA);
for _ in 0..400 {
let n = 1 + pick(&mut rng, 32);
let a: Vec<u8> = (0..n).map(|_| (rng.next_u64() & 0xFF) as u8).collect();
let b: Vec<u8> = (0..n).map(|_| (rng.next_u64() & 0xFF) as u8).collect();
let x: Vec<u8> = a.iter().zip(&b).map(|(&p, &q)| p ^ q).collect();
for (poly, width, reflect_io) in
[(0x07u64, 8u32, false), (0x1021, 16, false), (0x8005, 16, true), (0x04C1_1DB7, 32, true)]
{
let ca = crc(&a, poly, width, 0, 0, reflect_io);
let cb = crc(&b, poly, width, 0, 0, reflect_io);
let cx = crc(&x, poly, width, 0, 0, reflect_io);
assert_eq!(cx, ca ^ cb, "not linear for polynomial {poly:#x}");
}
}
}
#[test]
fn an_even_term_generator_detects_every_odd_error_count() {
assert_eq!(0x1021u64.count_ones() + 1, 4, "CRC-16/CCITT has an even term count");
assert_eq!(0x07u64.count_ones() + 1, 4, "CRC-8/SMBUS has an even term count");
assert_eq!(0x04C1_1DB7u64.count_ones() + 1, 15, "CRC-32 has an odd term count");
let mut rng = Rng::new(0x_0DD1);
for (name, f) in [
("CRC-16/CCITT", (|d: &[u8]| u64::from(crc16_ccitt(d))) as fn(&[u8]) -> u64),
("CRC-8/SMBUS", |d: &[u8]| u64::from(crc8(d))),
] {
for _ in 0..1500 {
let bytes = 4 + pick(&mut rng, 28);
let data: Vec<u8> = (0..bytes).map(|_| (rng.next_u64() & 0xFF) as u8).collect();
let clean = f(&data);
let flips = 1 + 2 * pick(&mut rng, 5);
let mut bad = data.clone();
let mut chosen = std::collections::BTreeSet::new();
while chosen.len() < flips {
chosen.insert(pick(&mut rng, bytes * 8));
}
for bit in chosen {
bad[bit / 8] ^= 1 << (bit % 8);
}
assert_ne!(f(&bad), clean, "{name} missed {flips} bit errors");
}
}
}
#[test]
fn position_weighted_sums_notice_reordering() {
assert_eq!(checksum_fletcher16(b"abcde"), 0xC8F0);
assert_eq!(checksum_fletcher32(b"abcde"), 0xF04F_C729);
assert_eq!(adler32(b"Wikipedia"), 0x11E6_0398);
assert_eq!(adler32(b""), 1, "the leading one distinguishes empty from zeros");
assert_ne!(adler32(b""), adler32(&[0u8]));
let mut rng = Rng::new(0x_F1E7);
let mut swaps = 0;
let mut fletcher_blind = 0;
for _ in 0..2000 {
let n = 2 + pick(&mut rng, 30);
let mut data: Vec<u8> = (0..n).map(|_| (rng.next_u64() & 0xFF) as u8).collect();
let (i, j) = (pick(&mut rng, n), pick(&mut rng, n));
if i == j || data[i] == data[j] {
continue;
}
let plain: u32 = data.iter().map(|&b| u32::from(b)).sum();
let before = (checksum_fletcher16(&data), adler32(&data));
data.swap(i, j);
let after = (checksum_fletcher16(&data), adler32(&data));
swaps += 1;
assert_eq!(plain, data.iter().map(|&b| u32::from(b)).sum::<u32>());
let shift = (data[j] as i64 - data[i] as i64) * (j as i64 - i as i64);
assert_eq!(
before.0 != after.0,
shift.rem_euclid(255) != 0,
"Fletcher-16's blind spot is not where the modulus puts it"
);
assert!(shift.abs() < 65521);
assert_ne!(before.1, after.1, "Adler-32 missed a transposition");
if shift.rem_euclid(255) == 0 {
fletcher_blind += 1;
}
}
assert!(swaps > 1000, "only {swaps} genuine transpositions were drawn");
assert!(fletcher_blind > 0, "Fletcher-16's blind spot was never exercised");
}
#[test]
fn parity_detects_exactly_the_odd_error_counts() {
let bits: Vec<bool> = (0..10).map(|i| i % 3 == 0).collect();
let p = parity(&bits);
for pattern in 0u32..1024 {
let flipped: Vec<bool> =
bits.iter().enumerate().map(|(i, &b)| b ^ (pattern & (1 << i) != 0)).collect();
let detected = parity(&flipped) != p;
assert_eq!(detected, pattern.count_ones() % 2 == 1, "pattern {pattern:#b}");
}
assert!(!parity(&[]));
for x in [0u64, 1, 3, 7, 0xFF, u64::MAX] {
assert_eq!(parity_u64(x), x.count_ones() % 2 == 1);
}
}
#[test]
fn luhn_catches_all_but_its_one_known_blind_spot() {
assert!(luhn_check(&[7, 9, 9, 2, 7, 3, 9, 8, 7, 1, 3]));
assert_eq!(luhn_generate(&[7, 9, 9, 2, 7, 3, 9, 8, 7, 1]), 3);
assert!(!luhn_check(&[7, 9, 9, 2, 7, 3, 9, 8, 7, 1, 4]));
let mut rng = Rng::new(0x_1A4A);
let mut blind = 0;
let mut caught = 0;
for _ in 0..600 {
let n = 4 + pick(&mut rng, 12);
let payload: Vec<u8> = (0..n).map(|_| pick(&mut rng, 10) as u8).collect();
let mut full = payload.clone();
full.push(luhn_generate(&payload));
assert!(luhn_check(&full), "generated check digit does not validate");
for i in 0..full.len() {
for d in 0..10u8 {
if d == full[i] {
continue;
}
let mut bad = full.clone();
bad[i] = d;
assert!(!luhn_check(&bad), "a single-digit error went undetected");
}
}
for i in 0..full.len() - 1 {
if full[i] == full[i + 1] {
continue;
}
let mut bad = full.clone();
bad.swap(i, i + 1);
let pair = (full[i].min(full[i + 1]), full[i].max(full[i + 1]));
if luhn_check(&bad) {
assert_eq!(pair, (0, 9), "an unexpected transposition went undetected");
blind += 1;
} else {
caught += 1;
}
}
}
assert!(caught > 1000, "only {caught} transpositions were tested");
assert!(blind > 0, "the 09-against-90 blind spot was never exercised");
}
#[test]
fn verhoeff_and_damm_catch_every_single_error_and_transposition() {
assert!(verhoeff_check(&[2, 3, 6, 3]));
assert_eq!(verhoeff_generate(&[2, 3, 6]), 3);
assert!(damm_check(&[5, 7, 2, 4]));
assert_eq!(damm_generate(&[5, 7, 2]), 4);
let mut rng = Rng::new(0x_5E1F);
let mut transpositions = 0;
for _ in 0..400 {
let n = 3 + pick(&mut rng, 12);
let payload: Vec<u8> = (0..n).map(|_| pick(&mut rng, 10) as u8).collect();
for (name, generate, check) in [
(
"Verhoeff",
verhoeff_generate as fn(&[u8]) -> u8,
verhoeff_check as fn(&[u8]) -> bool,
),
("Damm", damm_generate, damm_check),
] {
let mut full = payload.clone();
full.push(generate(&payload));
assert!(check(&full), "{name} rejects its own check digit");
for i in 0..full.len() {
for d in 0..10u8 {
if d == full[i] {
continue;
}
let mut bad = full.clone();
bad[i] = d;
assert!(!check(&bad), "{name} missed a single-digit error");
}
}
for i in 0..full.len() - 1 {
if full[i] == full[i + 1] {
continue;
}
let mut bad = full.clone();
bad.swap(i, i + 1);
assert!(!check(&bad), "{name} missed a transposition");
transpositions += 1;
}
}
}
assert!(transpositions > 2000, "only {transpositions} transpositions were tested");
}
#[test]
fn isbn_checks_and_the_price_of_a_composite_modulus() {
assert!(isbn10_check(&[0, 3, 0, 6, 4, 0, 6, 1, 5, 2]));
assert!(isbn10_check(&[0, 8, 0, 4, 4, 2, 9, 5, 7, 10]), "a check digit of X");
assert!(!isbn10_check(&[0, 3, 0, 6, 4, 0, 6, 1, 5, 3]));
assert!(isbn13_check(&[9, 7, 8, 0, 3, 0, 6, 4, 0, 6, 1, 5, 7]));
assert!(!isbn13_check(&[9, 7, 8, 0, 3, 0, 6, 4, 0, 6, 1, 5, 8]));
let mut rng = Rng::new(0x_15B4);
let mut ten_missed = 0;
let mut thirteen_missed_by_five = 0;
let mut thirteen_missed_otherwise = 0;
for _ in 0..2000 {
let body: Vec<u8> = (0..9).map(|_| pick(&mut rng, 10) as u8).collect();
let weighted: u32 =
body.iter().enumerate().map(|(i, &d)| (10 - i as u32) * u32::from(d)).sum();
let mut ten = body.clone();
ten.push(((11 - weighted % 11) % 11) as u8);
assert!(isbn10_check(&ten));
for i in 0..9 {
if ten[i] == ten[i + 1] || ten[i + 1] > 9 {
continue;
}
let mut bad = ten.clone();
bad.swap(i, i + 1);
if isbn10_check(&bad) {
ten_missed += 1;
}
}
let body: Vec<u8> = (0..12).map(|_| pick(&mut rng, 10) as u8).collect();
let weighted: u32 = body
.iter()
.enumerate()
.map(|(i, &d)| if i % 2 == 0 { u32::from(d) } else { 3 * u32::from(d) })
.sum();
let mut thirteen = body.clone();
thirteen.push(((10 - weighted % 10) % 10) as u8);
assert!(isbn13_check(&thirteen));
for i in 0..12 {
if thirteen[i] == thirteen[i + 1] {
continue;
}
let mut bad = thirteen.clone();
bad.swap(i, i + 1);
if isbn13_check(&bad) {
let gap = thirteen[i].abs_diff(thirteen[i + 1]);
if gap == 5 {
thirteen_missed_by_five += 1;
} else {
thirteen_missed_otherwise += 1;
}
}
}
}
assert_eq!(ten_missed, 0, "ISBN-10 missed {ten_missed} transpositions");
assert!(thirteen_missed_by_five > 0, "the ISBN-13 blind spot was never exercised");
assert_eq!(
thirteen_missed_otherwise, 0,
"ISBN-13 missed a transposition of digits not differing by five"
);
}
#[test]
fn hamming_distance_is_a_metric() {
let mut rng = Rng::new(0x_4A33);
for _ in 0..3000 {
let (a, b, c) = (rng.next_u64(), rng.next_u64(), rng.next_u64());
assert_eq!(hamming_distance_bits(a, a), 0);
assert_eq!(hamming_distance_bits(a, b) == 0, a == b);
assert_eq!(hamming_distance_bits(a, b), hamming_distance_bits(b, a));
assert!(
hamming_distance_bits(a, c)
<= hamming_distance_bits(a, b) + hamming_distance_bits(b, c),
"the triangle inequality fails"
);
let t = rng.next_u64();
assert_eq!(hamming_distance_bits(a ^ t, b ^ t), hamming_distance_bits(a, b));
}
assert_eq!(hamming_distance_bytes(b"karolin", b"kathrin"), Some(9));
assert_eq!(hamming_distance_bytes(b"abc", b"abcd"), None);
for _ in 0..500 {
let x = rng.next_u64();
let y = rng.next_u64();
assert_eq!(
hamming_distance_bytes(&x.to_le_bytes(), &y.to_le_bytes()),
Some(hamming_distance_bits(x, y))
);
}
}
}