| 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 | |
| 12 | use std::io; |
| 13 | |
| 14 | const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; |
| 15 | /// The symbols naming a code's room, and how many rooms they name. |
| 16 | const NAMEPLATE: usize = 2; |
| 17 | pub const NAMEPLATES: u32 = 1 << (5 * NAMEPLATE); |
| 18 | /// The symbols of a code's secret. |
| 19 | pub const SECRET: usize = 6; |
| 20 | |
| 21 | /// A new secret, `SECRET` random symbols. |
| 22 | pub 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 | |
| 31 | fn 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. |
| 36 | fn 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 | |
| 48 | fn 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. |
| 57 | pub 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. |
| 76 | pub 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)] |
| 92 | mod 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 | } |