1use std::collections::HashSet;
19
20use crate::username::Username;
21
22pub const MAX_VARIANTS: usize = 64;
24
25const SEPARATORS: [char; 3] = ['_', '-', '.'];
26const LEET: [(char, char); 5] = [('o', '0'), ('i', '1'), ('e', '3'), ('a', '4'), ('s', '5')];
28const SUFFIXES: [&str; 2] = ["1", "123"];
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum PermuteLevel {
33 None,
35 Basic,
37 Aggressive,
39}
40
41#[must_use]
46pub fn permute(username: &Username, level: PermuteLevel) -> Vec<Username> {
47 let base = username.as_str().to_owned();
48 let mut out: Vec<Username> = Vec::new();
49 let mut seen: HashSet<String> = HashSet::new();
50 add(&mut out, &mut seen, base.clone());
51
52 if level == PermuteLevel::None {
53 return out;
54 }
55
56 if base.contains(SEPARATORS) {
58 let stripped: String = base.chars().filter(|c| !SEPARATORS.contains(c)).collect();
59 try_add(&mut out, &mut seen, stripped);
60 for &sep in &SEPARATORS {
61 let swapped: String = base
62 .chars()
63 .map(|c| if SEPARATORS.contains(&c) { sep } else { c })
64 .collect();
65 try_add(&mut out, &mut seen, swapped);
66 }
67 }
68
69 if level == PermuteLevel::Basic {
70 out.truncate(MAX_VARIANTS);
71 return out;
72 }
73
74 let snapshot: Vec<String> = out.iter().map(|u| u.as_str().to_owned()).collect();
76 for variant in &snapshot {
77 for &(from, to) in &LEET {
78 if variant.contains(from) {
79 let leeted: String = variant
80 .chars()
81 .map(|c| if c == from { to } else { c })
82 .collect();
83 try_add(&mut out, &mut seen, leeted);
84 }
85 }
86 }
87 for suffix in SUFFIXES {
89 try_add(&mut out, &mut seen, format!("{base}{suffix}"));
90 }
91
92 out.truncate(MAX_VARIANTS);
93 out
94}
95
96fn add(out: &mut Vec<Username>, seen: &mut HashSet<String>, candidate: String) {
98 if seen.insert(candidate.clone()) {
99 if let Ok(u) = Username::new(candidate) {
100 out.push(u);
101 }
102 }
103}
104
105fn try_add(out: &mut Vec<Username>, seen: &mut HashSet<String>, candidate: String) {
107 if out.len() >= MAX_VARIANTS {
108 return;
109 }
110 add(out, seen, candidate);
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 fn names(v: &[Username]) -> Vec<&str> {
118 v.iter().map(Username::as_str).collect()
119 }
120
121 fn user(s: &str) -> Username {
122 Username::new(s).unwrap()
123 }
124
125 #[test]
126 fn none_returns_only_original() {
127 let v = permute(&user("john_doe"), PermuteLevel::None);
128 assert_eq!(names(&v), ["john_doe"]);
129 }
130
131 #[test]
132 fn original_is_always_first() {
133 for level in [PermuteLevel::Basic, PermuteLevel::Aggressive] {
134 let v = permute(&user("john_doe"), level);
135 assert_eq!(v[0].as_str(), "john_doe");
136 }
137 }
138
139 #[test]
140 fn basic_swaps_separators() {
141 let v = permute(&user("john_doe"), PermuteLevel::Basic);
142 let n = names(&v);
143 for expected in ["john_doe", "johndoe", "john.doe", "john-doe"] {
144 assert!(n.contains(&expected), "missing {expected:?} in {n:?}");
145 }
146 }
147
148 #[test]
149 fn basic_without_separator_is_just_original() {
150 let v = permute(&user("johndoe"), PermuteLevel::Basic);
151 assert_eq!(names(&v), ["johndoe"]);
152 }
153
154 #[test]
155 fn aggressive_adds_leet_and_suffixes() {
156 let v = permute(&user("bob"), PermuteLevel::Aggressive);
157 let n = names(&v);
158 assert!(n.contains(&"bob"));
159 assert!(n.contains(&"b0b"), "leet o→0 missing in {n:?}");
160 assert!(n.contains(&"bob1"));
161 assert!(n.contains(&"bob123"));
162 }
163
164 #[test]
165 fn results_are_deduplicated() {
166 let v = permute(&user("aaa"), PermuteLevel::Aggressive);
167 let n = names(&v);
168 let mut sorted = n.clone();
169 sorted.sort_unstable();
170 sorted.dedup();
171 assert_eq!(sorted.len(), n.len(), "duplicates in {n:?}");
172 }
173
174 #[test]
175 fn all_variants_are_valid_usernames() {
176 let v = permute(&user("john.doe_x"), PermuteLevel::Aggressive);
178 for u in &v {
179 assert!(Username::new(u.as_str()).is_ok());
180 }
181 }
182
183 #[test]
184 fn never_exceeds_cap() {
185 let v = permute(&user("a.b.c-d_e.o.i.e.a.s"), PermuteLevel::Aggressive);
186 assert!(v.len() <= MAX_VARIANTS, "got {}", v.len());
187 }
188
189 proptest::proptest! {
190 #[test]
192 fn permute_invariants(
193 s in "[A-Za-z0-9._-]{1,64}",
194 level_idx in 0usize..3,
195 ) {
196 let level = [
197 PermuteLevel::None,
198 PermuteLevel::Basic,
199 PermuteLevel::Aggressive,
200 ][level_idx];
201 let variants = permute(&user(&s), level);
202
203 proptest::prop_assert!(!variants.is_empty());
204 proptest::prop_assert_eq!(variants[0].as_str(), s.as_str());
206 proptest::prop_assert!(variants.len() <= MAX_VARIANTS);
208 for v in &variants {
210 proptest::prop_assert!(Username::new(v.as_str()).is_ok());
211 }
212 let unique: std::collections::HashSet<&str> =
214 variants.iter().map(Username::as_str).collect();
215 proptest::prop_assert_eq!(unique.len(), variants.len());
216 if level == PermuteLevel::None {
218 proptest::prop_assert_eq!(variants.len(), 1);
219 }
220 }
221 }
222}