pub(crate) const ALPHABET: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const MODULUS: u32 = 36;
const RADIX: u32 = 37;
pub(crate) fn char_value(c: char) -> Option<u32> {
match c {
'0'..='9' => Some(c as u32 - '0' as u32),
'A'..='Z' => Some(c as u32 - 'A' as u32 + 10),
_ => None,
}
}
pub(crate) fn value_char(v: u32) -> Option<char> {
ALPHABET.get(v as usize).map(|b| *b as char)
}
fn accumulate(text: &str) -> Option<u32> {
let mut p = MODULUS;
for c in text.chars() {
let a = char_value(c)?;
let mut s = (p + a) % MODULUS;
if s == 0 {
s = MODULUS;
}
p = (2 * s) % RADIX;
}
Some(p)
}
pub(crate) fn compute(data: &str) -> Option<char> {
let p = accumulate(data)?;
for v in 0..MODULUS {
let mut s = (p + v) % MODULUS;
if s == 0 {
s = MODULUS;
}
if (2 * s) % RADIX == 1 {
return value_char(v);
}
}
None
}
pub(crate) fn verify(text: &str) -> bool {
accumulate(text) == Some(1)
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::format;
use alloc::string::String;
#[test]
fn computed_check_character_verifies() {
for data in ["A", "0", "Z", "9ED9285C", "PKG9ED9285C", "000000000000"] {
let c = compute(data).expect("alphabet is valid");
assert!(verify(&format!("{data}{c}")), "failed for {data}");
}
}
#[test]
fn rejects_characters_outside_the_alphabet() {
assert!(compute("abc").is_none());
assert!(compute("PKG-1234").is_none());
assert!(!verify("PKG-1234"));
}
#[test]
fn detects_every_single_character_substitution() {
let data = "9ED9285C";
let check = compute(data).unwrap();
let good: String = format!("{data}{check}");
for pos in 0..good.len() {
let original = good.as_bytes()[pos];
for &sub in ALPHABET.iter() {
if sub == original {
continue;
}
let mut bytes = good.as_bytes().to_vec();
bytes[pos] = sub;
let corrupted = String::from_utf8(bytes).expect("alphabet is ascii");
assert!(!verify(&corrupted), "missed substitution in {corrupted}");
}
}
}
#[test]
fn detects_every_adjacent_transposition() {
let data = "9ED9285C";
let check = compute(data).unwrap();
let good = format!("{data}{check}");
let bytes = good.as_bytes();
for i in 0..bytes.len() - 1 {
if bytes[i] == bytes[i + 1] {
continue; }
let mut swapped = bytes.to_vec();
swapped.swap(i, i + 1);
let swapped = String::from_utf8(swapped).unwrap();
assert!(!verify(&swapped), "missed transposition in {swapped}");
}
}
}