Skip to main content

nexo_pairing/
code.rs

1//! Human-friendly pairing code generator.
2//!
3//! 8 chars from `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` (no `0/O/1/I/L`),
4//! retried up to 500 times if the generator collides with an
5//! already-active code in the store.
6
7use rand::Rng;
8use std::collections::HashSet;
9
10pub const ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
11pub const LENGTH: usize = 8;
12const MAX_ATTEMPTS: usize = 500;
13
14pub fn random() -> String {
15    let mut rng = rand::thread_rng();
16    (0..LENGTH)
17        .map(|_| ALPHABET[rng.gen_range(0..ALPHABET.len())] as char)
18        .collect()
19}
20
21/// Generate a code that does not collide with any entry in `existing`.
22/// Returns `Err` after `MAX_ATTEMPTS` collisions — caller should
23/// surface that as a 5xx-equivalent: the keyspace is large
24/// (32^8 ≈ 10^12), so 500 collisions in a row means something else
25/// is wrong (e.g. RNG broken).
26pub fn generate_unique(existing: &HashSet<String>) -> Result<String, &'static str> {
27    for _ in 0..MAX_ATTEMPTS {
28        let candidate = random();
29        if !existing.contains(&candidate) {
30            return Ok(candidate);
31        }
32    }
33    Err("failed to generate unique pairing code after 500 attempts")
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn alphabet_excludes_ambiguous_chars() {
42        // Mirrors OpenClaw's choice.
43        // 0/O/1/I dropped; L stays — empirically distinguishable in
44        // the fixed-width fonts the operator sees.
45        for c in [b'0', b'O', b'1', b'I'] {
46            assert!(
47                !ALPHABET.contains(&c),
48                "ambiguous char {} in alphabet",
49                c as char
50            );
51        }
52    }
53
54    #[test]
55    fn generated_code_uses_only_alphabet() {
56        for _ in 0..50 {
57            let c = random();
58            assert_eq!(c.len(), LENGTH);
59            assert!(c.chars().all(|ch| ALPHABET.contains(&(ch as u8))));
60        }
61    }
62
63    #[test]
64    fn generate_unique_avoids_collision() {
65        let mut existing = HashSet::new();
66        for _ in 0..200 {
67            let c = generate_unique(&existing).unwrap();
68            assert!(!existing.contains(&c));
69            existing.insert(c);
70        }
71    }
72}