mail4agent_messenger_shell/
nick.rs1use crate::ShellError;
9
10pub fn nick_from_display_name(display_name: &str) -> Result<String, ShellError> {
22 let mut out = String::new();
23 let mut pending_dash = false;
24 for ch in display_name.chars() {
25 match latin_piece(ch) {
26 Some("") => {}
27 Some(piece) => {
28 if pending_dash && !out.is_empty() {
29 out.push('-');
30 }
31 pending_dash = false;
32 out.push_str(piece);
33 }
34 None => pending_dash = true,
35 }
36 }
37 let nick: String = out.chars().take(32).collect();
38 let nick = nick.trim_end_matches('-').to_string();
39 if !is_nick_token(&nick) {
40 return Err(ShellError::Nick);
41 }
42 Ok(nick)
43}
44
45pub fn routine_folder_id(name: &str) -> Option<String> {
61 let mut slug = String::new();
62 let mut pending_dash = false;
63 for ch in name.to_lowercase().chars() {
64 if ch.is_ascii_lowercase() || ch.is_ascii_digit() {
65 if pending_dash && !slug.is_empty() {
66 slug.push('-');
67 }
68 pending_dash = false;
69 slug.push(ch);
70 } else {
71 pending_dash = true;
72 }
73 }
74 let slug: String = slug.chars().take(48).collect();
76 if slug.is_empty() {
77 None
78 } else {
79 Some(slug)
80 }
81}
82
83pub fn lookup_nick(raw: &str) -> Result<String, ShellError> {
88 let trimmed = raw.trim();
89 if is_nick_token(trimmed) {
90 return Ok(trimmed.to_string());
91 }
92 nick_from_display_name(trimmed)
93}
94
95pub(crate) fn is_node_nick(nick: &str) -> bool {
97 let parts: Vec<&str> = nick.split('.').collect();
98 nick.len() <= 64 && parts.len() == 3 && parts.iter().all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'))
99}
100
101fn is_nick_token(nick: &str) -> bool {
102 !nick.is_empty()
103 && nick.len() <= 64
104 && nick
105 .chars()
106 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '.')
107}
108
109fn latin_piece(ch: char) -> Option<&'static str> {
110 if ch.is_ascii_alphanumeric() {
111 return Some(ascii_lower(ch));
112 }
113 let lower = ch.to_lowercase().next()?;
114 Some(match lower {
115 'а' => "a",
116 'б' => "b",
117 'в' => "v",
118 'г' => "g",
119 'д' => "d",
120 'е' => "e",
121 'ё' => "yo",
122 'ж' => "zh",
123 'з' => "z",
124 'и' => "i",
125 'й' => "i",
126 'к' => "k",
127 'л' => "l",
128 'м' => "m",
129 'н' => "n",
130 'о' => "o",
131 'п' => "p",
132 'р' => "r",
133 'с' => "s",
134 'т' => "t",
135 'у' => "u",
136 'ф' => "f",
137 'х' => "kh",
138 'ц' => "ts",
139 'ч' => "ch",
140 'ш' => "sh",
141 'щ' => "shch",
142 'ъ' | 'ь' => "",
143 'ы' => "y",
144 'э' => "e",
145 'ю' => "yu",
146 'я' => "ya",
147 'є' => "ye",
149 'і' => "i",
150 'ї' => "yi",
151 'ґ' => "g",
152 'ў' => "u",
153 _ => return None,
154 })
155}
156
157fn ascii_lower(ch: char) -> &'static str {
158 match ch {
159 'A' | 'a' => "a",
160 'B' | 'b' => "b",
161 'C' | 'c' => "c",
162 'D' | 'd' => "d",
163 'E' | 'e' => "e",
164 'F' | 'f' => "f",
165 'G' | 'g' => "g",
166 'H' | 'h' => "h",
167 'I' | 'i' => "i",
168 'J' | 'j' => "j",
169 'K' | 'k' => "k",
170 'L' | 'l' => "l",
171 'M' | 'm' => "m",
172 'N' | 'n' => "n",
173 'O' | 'o' => "o",
174 'P' | 'p' => "p",
175 'Q' | 'q' => "q",
176 'R' | 'r' => "r",
177 'S' | 's' => "s",
178 'T' | 't' => "t",
179 'U' | 'u' => "u",
180 'V' | 'v' => "v",
181 'W' | 'w' => "w",
182 'X' | 'x' => "x",
183 'Y' | 'y' => "y",
184 'Z' | 'z' => "z",
185 '0' => "0",
186 '1' => "1",
187 '2' => "2",
188 '3' => "3",
189 '4' => "4",
190 '5' => "5",
191 '6' => "6",
192 '7' => "7",
193 '8' => "8",
194 '9' => "9",
195 _ => "",
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn display_names_become_nicks() {
205 assert_eq!(nick_from_display_name("Alice").unwrap(), "alice");
206 assert_eq!(nick_from_display_name(" Alice ").unwrap(), "alice");
207 assert_eq!(
208 nick_from_display_name("Привет мир").unwrap(),
209 "privet-mir"
210 );
211 assert_eq!(
212 nick_from_display_name("Свой браузер").unwrap(),
213 "svoi-brauzer"
214 );
215 assert_eq!(
216 nick_from_display_name("foo+bar").unwrap(),
217 "foo-bar"
218 );
219 assert_eq!(nick_from_display_name("Sample Bot").unwrap(), "sample-bot");
220 assert_eq!(
221 nick_from_display_name("Объём, данные").unwrap(),
222 "obyom-dannye"
223 );
224 assert_eq!(
225 nick_from_display_name(&("x".repeat(31) + " y")).unwrap(),
226 "x".repeat(31)
227 );
228 assert_ne!(
229 nick_from_display_name("Привет мир").unwrap(),
230 "nachshtab"
231 );
232 assert_eq!(nick_from_display_name("Foo Bar!!").unwrap(), "foo-bar");
233 assert_eq!(nick_from_display_name("__a__b__").unwrap(), "a-b");
234 assert_eq!(
235 nick_from_display_name(&"A".repeat(40)).unwrap(),
236 "a".repeat(32)
237 );
238 assert!(nick_from_display_name("...").is_err());
239 assert_eq!(lookup_nick("Привет мир").unwrap(), "privet-mir");
240 assert_eq!(lookup_nick("chief_bob").unwrap(), "chief_bob");
241 assert_eq!(lookup_nick("Alice").unwrap(), "Alice");
242 }
243
244 #[test]
245 fn routine_folder_matches_the_host_slug() {
246 assert_eq!(routine_folder_id("alice").as_deref(), Some("alice"));
248 assert_eq!(
249 routine_folder_id("chief_bob").as_deref(),
250 Some("chief-bob")
251 );
252 assert_eq!(
253 routine_folder_id("sample_bot").as_deref(),
254 Some("sample-bot")
255 );
256 assert_eq!(
257 routine_folder_id("foobar").as_deref(),
258 Some("foobar")
259 );
260 assert_eq!(
261 routine_folder_id("foo+bar").as_deref(),
262 Some("foo-bar")
263 );
264 assert_eq!(routine_folder_id("Alice").as_deref(), Some("alice"));
265 assert_eq!(routine_folder_id("__a__b__").as_deref(), Some("a-b"));
266 assert_eq!(routine_folder_id("Привет"), None);
267 assert_eq!(routine_folder_id("___"), None);
268 assert_eq!(
269 routine_folder_id(&"x".repeat(60)).map(|s| s.len()),
270 Some(48)
271 );
272 assert_eq!(
273 routine_folder_id(&format!("{}_b", "a".repeat(47))).as_deref(),
274 Some(format!("{}-", "a".repeat(47)).as_str())
275 );
276 let chain = |name: &str| routine_folder_id(&nick_from_display_name(name).unwrap());
278 for name in [
279 "Alice",
280 "Привет мир",
281 "foo+bar",
282 "Sample Bot",
283 "carol",
284 ] {
285 let nick = nick_from_display_name(name).unwrap();
286 assert_eq!(routine_folder_id(&nick).as_deref(), Some(nick.as_str()));
287 }
288 assert_eq!(
289 chain("Привет мир").as_deref(),
290 Some("privet-mir")
291 );
292 assert_eq!(chain("foo+bar").as_deref(), Some("foo-bar"));
293 assert_eq!(chain("Sample Bot").as_deref(), Some("sample-bot"));
294 assert_eq!(chain("carol").as_deref(), Some("carol"));
295 }
296}