1//! Live Share's codes in Crockford's base32 (0-9 and A-Z without I, L, O and U), shown
2//! `7KQ-4MZ-9XR`: two symbols naming the code's room, its number on a relay; six of secret
3//! (30 bits), which SPAKE2 meets through; and a check symbol, so a mistyped code is refused
4//! here before it spends one of the relay's few tries. Reading ignores case, hyphens and
5//! spaces, and takes I and L for 1 and O for 0, as Crockford's decoding does.
6//!
7//! The check is the symbols' values weighted 1 to 8, summed modulo 31, the prime under 32, so
8//! it stays one of the code's own symbols: it catches any symbol mistyped and any two
9//! neighbours swapped, but for 0 and Z, whose values differ by 31. Crockford's own check
10//! symbol, modulo 37, adds `*~$=U`, which read badly aloud.
11
12use std::io;
13
14const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
15/// The symbols naming a code's room, and how many rooms they name.
16const NAMEPLATE: usize = 2;
17pub const NAMEPLATES: u32 = 1 << (5 * NAMEPLATE);
18/// The symbols of a code's secret.
19pub const SECRET: usize = 6;
20
21/// A new secret, `SECRET` random symbols.
22pub fn secret() -> io::Result<String> {
23 let mut bytes = [0; 4];
24 getrandom::fill(&mut bytes).map_err(|_| io::Error::other("System random source failed"))?;
25 let bits = u32::from_le_bytes(bytes);
26 Ok((0..SECRET)
27 .map(|at| symbol((bits >> (5 * at)) as u8))
28 .collect())
29}
30
31fn symbol(value: u8) -> char {
32 char::from(ALPHABET[usize::from(value & 31)])
33}
34
35/// A symbol's value as typed, reading I and L as 1 and O as 0.
36fn value(typed: char) -> Option<u8> {
37 let typed = match typed.to_ascii_uppercase() {
38 'I' | 'L' => '1',
39 'O' => '0',
40 typed => typed,
41 };
42 ALPHABET
43 .iter()
44 .position(|symbol| char::from(*symbol) == typed)
45 .map(|at| at as u8)
46}
47
48fn check(values: &[u8]) -> u8 {
49 let sum: u32 = (values.iter().enumerate())
50 .map(|(at, value)| (at as u32 + 1) * u32::from(*value))
51 .sum();
52 (sum % 31) as u8
53}
54
55/// The code for room `nameplate` and `secret`, as shown: `7KQ-4MZ-9XR`. A secret that isn't
56/// `SECRET` symbols has no code.
57pub fn format(nameplate: u32, secret: &str) -> Option<String> {
58 let secret: Vec<u8> = secret.chars().map(value).collect::<Option<_>>()?;
59 if nameplate >= NAMEPLATES || secret.len() != SECRET {
60 return None;
61 }
62 let mut values = vec![(nameplate >> 5) as u8, (nameplate & 31) as u8];
63 values.extend(secret);
64 values.push(check(&values));
65 let symbols: Vec<char> = values.into_iter().map(symbol).collect();
66 Some(
67 symbols
68 .chunks(3)
69 .map(|group| group.iter().collect::<String>())
70 .collect::<Vec<_>>()
71 .join("-"),
72 )
73}
74
75/// A code as typed: its room's number and its secret, where it is a code whose check holds.
76pub fn parse(typed: &str) -> Option<(u32, String)> {
77 let values: Vec<u8> = typed
78 .chars()
79 .filter(|c| *c != '-' && !c.is_whitespace())
80 .map(value)
81 .collect::<Option<_>>()?;
82 let (check_value, values) = values.split_last()?;
83 if values.len() != NAMEPLATE + SECRET || check(values) != *check_value {
84 return None;
85 }
86 let nameplate = (u32::from(values[0]) << 5) | u32::from(values[1]);
87 let secret = values[NAMEPLATE..].iter().copied().map(symbol).collect();
88 Some((nameplate, secret))
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn codes_read_back_however_they_are_typed() {
97 let code = format(412, "4MZ9XR").unwrap();
98 assert_eq!(code.len(), 11);
99 assert_eq!(parse(&code), Some((412, "4MZ9XR".into())));
100 let typed = code
101 .to_lowercase()
102 .replace('-', " ")
103 .replace('1', "l")
104 .replace('0', "o");
105 assert_eq!(parse(&typed), Some((412, "4MZ9XR".into())));
106 assert_eq!(parse(&format!(" {code} ")), Some((412, "4MZ9XR".into())));
107 assert!(format(NAMEPLATES, "4MZ9XR").is_none() && format(1, "4MZ9X").is_none());
108 assert!(parse("412-violet-otter").is_none() && parse("").is_none());
109 for _ in 0..100 {
110 let secret = secret().unwrap();
111 assert_eq!(secret.len(), SECRET);
112 assert_eq!(parse(&format(7, &secret).unwrap()), Some((7, secret)));
113 }
114 }
115
116 /// The check refuses any one symbol mistyped, and any two neighbours swapped, but for 0
117 /// and Z.
118 #[test]
119 fn the_check_catches_a_symbol_mistyped_or_two_swapped() {
120 let code: Vec<char> = format(999, "Q4MZ9X")
121 .unwrap()
122 .replace('-', "")
123 .chars()
124 .collect();
125 for at in 0..code.len() {
126 for symbol in ALPHABET.iter().map(|byte| char::from(*byte)) {
127 let mut typed = code.clone();
128 if typed[at] != symbol && !matches!((typed[at], symbol), ('0', 'Z') | ('Z', '0')) {
129 typed[at] = symbol;
130 assert!(
131 parse(&typed.iter().collect::<String>()).is_none(),
132 "{typed:?}"
133 );
134 }
135 }
136 }
137 for at in 0..code.len() - 1 {
138 let mut typed = code.clone();
139 typed.swap(at, at + 1);
140 if typed != code {
141 assert!(
142 parse(&typed.iter().collect::<String>()).is_none(),
143 "{typed:?}"
144 );
145 }
146 }
147 }
148}