1use crate::{
2 tokens::{
3 CollapsedEnsNameToken, EnsNameToken, TokenDisallowed, TokenEmoji, TokenIgnored,
4 TokenMapped, TokenNfc, TokenStop, TokenValid,
5 },
6 utils, CodePoint, CodePointsSpecs, ProcessError,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct TokenizedName {
12 pub input: String,
13 pub tokens: Vec<EnsNameToken>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct TokenizedLabel<'a> {
19 pub tokens: &'a [EnsNameToken],
20}
21
22impl TokenizedName {
23 pub fn empty() -> Self {
24 Self {
25 input: "".to_string(),
26 tokens: vec![],
27 }
28 }
29
30 pub fn from_input(
32 input: impl AsRef<str>,
33 specs: &CodePointsSpecs,
34 apply_nfc: bool,
35 ) -> Result<Self, ProcessError> {
36 tokenize_name(input, specs, apply_nfc)
37 }
38
39 pub fn is_empty(&self) -> bool {
40 self.tokens.is_empty()
41 }
42
43 pub fn iter_tokens(&self) -> impl Iterator<Item = &EnsNameToken> {
45 self.tokens.iter()
46 }
47
48 pub fn iter_labels(&self) -> impl Iterator<Item = TokenizedLabel<'_>> {
51 self.tokens
52 .split(|t| matches!(t, EnsNameToken::Stop(_)))
53 .map(TokenizedLabel::from)
54 }
55
56 pub fn labels(&self) -> Vec<TokenizedLabel<'_>> {
57 self.iter_labels().collect()
58 }
59}
60
61impl TokenizedLabel<'_> {
62 pub fn is_fully_emoji(&self) -> bool {
64 self.tokens
65 .iter()
66 .all(|t| matches!(t, EnsNameToken::Emoji(_)))
67 }
68
69 pub fn is_fully_ascii(&self) -> bool {
71 self.tokens
72 .iter()
73 .all(|token| token.cps().into_iter().all(utils::is_ascii))
74 }
75
76 pub fn iter_cps(&self) -> impl DoubleEndedIterator<Item = CodePoint> + '_ {
78 self.tokens.iter().flat_map(|token| token.cps())
79 }
80
81 pub fn collapse_into_text_or_emoji(&self) -> Vec<CollapsedEnsNameToken> {
84 let mut current_text_cps = vec![];
85 let mut collapsed = vec![];
86 for token in self.tokens.iter() {
87 match token {
88 EnsNameToken::Valid(_) | EnsNameToken::Mapped(_) | EnsNameToken::Nfc(_) => {
89 current_text_cps.extend(token.cps().iter());
90 }
91 EnsNameToken::Emoji(token) => {
92 if !current_text_cps.is_empty() {
93 collapsed.push(CollapsedEnsNameToken::Text(TokenValid {
94 cps: current_text_cps,
95 }));
96 current_text_cps = vec![];
97 }
98 collapsed.push(CollapsedEnsNameToken::Emoji(token.clone()));
99 }
100 EnsNameToken::Ignored(_) | EnsNameToken::Disallowed(_) | EnsNameToken::Stop(_) => {}
101 }
102 }
103 if !current_text_cps.is_empty() {
104 collapsed.push(CollapsedEnsNameToken::Text(TokenValid {
105 cps: current_text_cps,
106 }));
107 }
108 collapsed
109 }
110
111 pub fn get_cps_of_not_ignored_text(&self) -> Vec<CodePoint> {
113 self.collapse_into_text_or_emoji()
114 .into_iter()
115 .filter_map(|token| {
116 if let CollapsedEnsNameToken::Text(token) = token {
117 Some(token.cps)
118 } else {
119 None
120 }
121 })
122 .flatten()
123 .collect()
124 }
125}
126
127impl<'a, T> From<&'a T> for TokenizedLabel<'a>
128where
129 T: AsRef<[EnsNameToken]> + ?Sized,
130{
131 fn from(tokens: &'a T) -> Self {
132 TokenizedLabel {
133 tokens: tokens.as_ref(),
134 }
135 }
136}
137
138fn tokenize_name(
139 name: impl AsRef<str>,
140 specs: &CodePointsSpecs,
141 apply_nfc: bool,
142) -> Result<TokenizedName, ProcessError> {
143 let name = name.as_ref();
144 if name.is_empty() {
145 return Ok(TokenizedName::empty());
146 }
147 let tokens = tokenize_input(name, specs, apply_nfc)?;
148 Ok(TokenizedName {
149 input: name.to_string(),
150 tokens,
151 })
152}
153
154fn tokenize_input(
155 input: impl AsRef<str>,
156 specs: &CodePointsSpecs,
157 apply_nfc: bool,
158) -> Result<Vec<EnsNameToken>, ProcessError> {
159 let input = input.as_ref();
160
161 let mut tokens = Vec::new();
162 let mut input_cur = 0;
163
164 while input_cur < input.len() {
165 if let Some(emoji) = maybe_starts_with_emoji(input_cur, input, specs) {
166 let cursor_offset = emoji.input.len();
167 tokens.push(EnsNameToken::Emoji(emoji));
168 input_cur += cursor_offset;
169 } else {
170 let char = input[input_cur..]
171 .chars()
172 .next()
173 .expect("input_cur is in bounds");
174 let cursor_offset = char.len_utf8();
175 let cp = char as CodePoint;
176 let token = process_one_cp(cp, specs);
177 tokens.push(token);
178 input_cur += cursor_offset;
179 }
180 }
181
182 if apply_nfc {
183 perform_nfc_transform(&mut tokens, specs);
184 }
185 collapse_valid_tokens(&mut tokens);
186 Ok(tokens)
187}
188
189fn perform_nfc_transform(tokens: &mut Vec<EnsNameToken>, specs: &CodePointsSpecs) {
190 let mut i = 0;
191 let mut start = -1i32;
192
193 while i < tokens.len() {
194 let token = &tokens[i];
195 match token {
196 EnsNameToken::Valid(_) | EnsNameToken::Mapped(_) => {
197 let cps = token.cps();
198 if specs.cps_requires_check(&cps) {
199 let mut end = i + 1;
200 for (pos, token) in tokens.iter().enumerate().skip(end) {
201 match token {
202 EnsNameToken::Valid(_) | EnsNameToken::Mapped(_) => {
203 if !specs.cps_requires_check(&cps) {
204 break;
205 }
206 end = pos + 1;
207 }
208 EnsNameToken::Ignored(_) => {}
209 _ => break,
210 }
211 }
212
213 if start < 0 {
214 start = i as i32;
215 }
216
217 let slice = &tokens[start as usize..end];
218 let mut cps = Vec::new();
219 for tok in slice {
220 match tok {
221 EnsNameToken::Valid(_) | EnsNameToken::Mapped(_) => {
222 cps.extend(&tok.cps());
223 }
224 _ => {}
225 }
226 }
227
228 let str0 = utils::cps2str(&cps);
229 let str = utils::nfc(&str0);
230
231 if str0 == str {
232 i = end - 1;
233 } else {
234 let new_token = EnsNameToken::Nfc(TokenNfc {
235 input: cps,
236 cps: utils::str2cps(&str),
237 });
238 tokens.splice(start as usize..end, vec![new_token]);
239 i = start as usize;
240 }
241 start = -1;
242 } else {
243 start = i as i32;
244 }
245 }
246 EnsNameToken::Ignored(_) => {}
247 _ => {
248 start = -1;
249 }
250 }
251 i += 1;
252 }
253}
254
255fn maybe_starts_with_emoji(i: usize, label: &str, specs: &CodePointsSpecs) -> Option<TokenEmoji> {
258 let (end, _) = specs.longest_emoji_at(label, i)?;
259 let input_cps = utils::str2cps(&label[i..end]);
260 let cps_no_fe0f = utils::filter_fe0f(&input_cps);
261 let emoji = specs
262 .cps_emoji_no_fe0f_to_pretty(&cps_no_fe0f)
263 .expect("emoji should be found")
264 .clone();
265 Some(TokenEmoji {
266 input: label[i..end].to_string(),
267 cps_input: input_cps,
268 emoji,
269 cps_no_fe0f,
270 })
271}
272
273fn process_one_cp(cp: CodePoint, specs: &CodePointsSpecs) -> EnsNameToken {
274 if specs.is_stop(cp) {
275 EnsNameToken::Stop(TokenStop { cp })
276 } else if specs.is_valid(cp) {
277 EnsNameToken::Valid(TokenValid { cps: vec![cp] })
278 } else if specs.is_ignored(cp) {
279 EnsNameToken::Ignored(TokenIgnored { cp })
280 } else if let Some(normalized) = specs.maybe_normalize(cp) {
281 EnsNameToken::Mapped(TokenMapped {
282 cp,
283 cps: normalized.clone(),
284 })
285 } else {
286 EnsNameToken::Disallowed(TokenDisallowed { cp })
287 }
288}
289
290fn collapse_valid_tokens(tokens: &mut Vec<EnsNameToken>) {
291 let mut i = 0;
292 while i < tokens.len() {
293 if let EnsNameToken::Valid(token) = &tokens[i] {
294 let mut j = i + 1;
295 let mut cps = token.cps.clone();
296 while j < tokens.len() {
297 if let EnsNameToken::Valid(next_token) = &tokens[j] {
298 cps.extend(next_token.cps.iter());
299 j += 1;
300 } else {
301 break;
302 }
303 }
304 let new_token = EnsNameToken::Valid(TokenValid { cps });
305 tokens.splice(i..j, vec![new_token]);
306 }
307 i += 1;
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use pretty_assertions::assert_eq;
315 use rstest::{fixture, rstest};
316
317 #[fixture]
318 #[once]
319 fn specs() -> CodePointsSpecs {
320 CodePointsSpecs::default()
321 }
322
323 #[rstest]
324 #[case::empty(vec![], vec![])]
325 #[case::single(
326 vec![EnsNameToken::Valid(TokenValid { cps: vec![1, 2, 3] })],
327 vec![EnsNameToken::Valid(TokenValid { cps: vec![1, 2, 3] })],
328 )]
329 #[case::two(
330 vec![
331 EnsNameToken::Valid(TokenValid { cps: vec![1, 2, 3] }),
332 EnsNameToken::Valid(TokenValid { cps: vec![4, 5, 6] }),
333 ],
334 vec![EnsNameToken::Valid(TokenValid { cps: vec![1, 2, 3, 4, 5, 6] })],
335 )]
336 #[case::full(
337 vec![
338 EnsNameToken::Valid(TokenValid { cps: vec![1, 2, 3] }),
339 EnsNameToken::Disallowed(TokenDisallowed { cp: 0 }),
340 EnsNameToken::Valid(TokenValid { cps: vec![4, 5, 6] }),
341 EnsNameToken::Valid(TokenValid { cps: vec![7, 8, 9] }),
342 EnsNameToken::Valid(TokenValid { cps: vec![10, 11, 12] }),
343 EnsNameToken::Disallowed(TokenDisallowed { cp: 10 }),
344 EnsNameToken::Stop(TokenStop { cp: 11 }),
345 EnsNameToken::Valid(TokenValid { cps: vec![12] }),
346 EnsNameToken::Ignored(TokenIgnored { cp: 13 }),
347 ],
348 vec![
349 EnsNameToken::Valid(TokenValid { cps: vec![1, 2, 3] }),
350 EnsNameToken::Disallowed(TokenDisallowed { cp: 0 }),
351 EnsNameToken::Valid(TokenValid { cps: vec![4, 5, 6, 7, 8, 9, 10, 11, 12] }),
352 EnsNameToken::Disallowed(TokenDisallowed { cp: 10 }),
353 EnsNameToken::Stop(TokenStop { cp: 11 }),
354 EnsNameToken::Valid(TokenValid { cps: vec![12] }),
355 EnsNameToken::Ignored(TokenIgnored { cp: 13 }),
356 ],
357 )]
358 fn test_collapse_valid_tokens(
359 #[case] input: Vec<EnsNameToken>,
360 #[case] expected: Vec<EnsNameToken>,
361 ) {
362 let mut tokens = input;
363 collapse_valid_tokens(&mut tokens);
364 assert_eq!(tokens, expected);
365 }
366
367 #[rstest]
368 #[case::xyz(
369 "xyz👨🏻/",
370 true,
371 vec![
372 EnsNameToken::Valid(TokenValid { cps: vec![120, 121, 122] }),
373 EnsNameToken::Emoji(TokenEmoji { input: "👨🏻".to_string(), cps_input: vec![128104, 127995], emoji: vec![128104, 127995], cps_no_fe0f: vec![128104, 127995] }),
374 EnsNameToken::Disallowed(TokenDisallowed { cp: 47 }),
375 ]
376 )]
377 #[case::a_poop_b(
378 "A💩︎︎b",
379 true,
380 vec![
381 EnsNameToken::Mapped(TokenMapped { cp: 65, cps: vec![97] }),
382 EnsNameToken::Emoji(TokenEmoji { input: "💩".to_string(), cps_input: vec![128169], emoji: vec![128169, 65039], cps_no_fe0f: vec![128169] }),
383 EnsNameToken::Ignored(TokenIgnored { cp: 65038 }),
384 EnsNameToken::Ignored(TokenIgnored { cp: 65038 }),
385 EnsNameToken::Valid(TokenValid { cps: vec![98] }),
386 ]
387 )]
388 #[case::atm(
389 "a™️",
390 true,
391 vec![
392 EnsNameToken::Valid(TokenValid { cps: vec![97] }),
393 EnsNameToken::Mapped(TokenMapped { cp: 8482, cps: vec![116, 109] }),
394 EnsNameToken::Ignored(TokenIgnored { cp: 65039 }),
395 ]
396 )]
397 #[case::no_nfc(
398 "_R💩\u{FE0F}a\u{FE0F}\u{304}\u{AD}.",
399 false,
400 vec![
401 EnsNameToken::Valid(TokenValid { cps: vec![95] }),
402 EnsNameToken::Mapped(TokenMapped { cp: 82, cps: vec![114] }),
403 EnsNameToken::Emoji(TokenEmoji { input: "💩️".to_string(), cps_input: vec![128169, 65039], emoji: vec![128169, 65039], cps_no_fe0f: vec![128169] }),
404 EnsNameToken::Valid(TokenValid { cps: vec![97] }),
405 EnsNameToken::Ignored(TokenIgnored { cp: 65039 }),
406 EnsNameToken::Valid(TokenValid { cps: vec![772] }),
407 EnsNameToken::Ignored(TokenIgnored { cp: 173 }),
408 EnsNameToken::Stop(TokenStop { cp: 46 }),
409 ]
410 )]
411 #[case::with_nfc(
412 "_R💩\u{FE0F}a\u{FE0F}\u{304}\u{AD}.",
413 true,
414 vec![
415 EnsNameToken::Valid(TokenValid { cps: vec![95] }),
416 EnsNameToken::Mapped(TokenMapped { cp: 82, cps: vec![114] }),
417 EnsNameToken::Emoji(TokenEmoji { input: "💩️".to_string(), cps_input: vec![128169, 65039], emoji: vec![128169, 65039], cps_no_fe0f: vec![128169] }),
418 EnsNameToken::Nfc(TokenNfc { input: vec![97, 772], cps: vec![257] }),
419 EnsNameToken::Ignored(TokenIgnored { cp: 173 }),
420 EnsNameToken::Stop(TokenStop { cp: 46 }),
421 ]
422 )]
423 #[case::raffy(
424 "RaFFY🚴♂️.eTh",
425 true,
426 vec![
427 EnsNameToken::Mapped(TokenMapped { cp: 82, cps: vec![114] }),
428 EnsNameToken::Valid(TokenValid { cps: vec![97] }),
429 EnsNameToken::Mapped(TokenMapped { cp: 70, cps: vec![102] }),
430 EnsNameToken::Mapped(TokenMapped { cp: 70, cps: vec![102] }),
431 EnsNameToken::Mapped(TokenMapped { cp: 89, cps: vec![121] }),
432 EnsNameToken::Emoji(TokenEmoji { input: "🚴\u{200d}♂\u{fe0f}".to_string(), cps_input: vec![128692, 8205, 9794, 65039], emoji: vec![128692, 8205, 9794, 65039], cps_no_fe0f: vec![128692, 8205, 9794] }),
433 EnsNameToken::Stop(TokenStop { cp: 46 }),
434 EnsNameToken::Valid(TokenValid { cps: vec![101] }),
435 EnsNameToken::Mapped(TokenMapped { cp: 84, cps: vec![116] }),
436 EnsNameToken::Valid(TokenValid { cps: vec![104] }),
437 ]
438 )]
439 #[case::emojis(
440 "⛹️♀",
441 true,
442 vec![
443 EnsNameToken::Emoji(TokenEmoji { input: "⛹️♀".to_string(), cps_input: vec![9977, 65039, 8205, 9792], emoji: vec![9977, 65039, 8205, 9792, 65039], cps_no_fe0f: vec![9977, 8205, 9792] }),
444 ]
445 )]
446 fn test_ens_tokenize(
447 #[case] input: &str,
448 #[case] apply_nfc: bool,
449 #[case] expected: Vec<EnsNameToken>,
450 specs: &CodePointsSpecs,
451 ) {
452 let tokens = tokenize_input(input, specs, apply_nfc).expect("tokenize");
453 assert_eq!(tokens, expected);
454 }
455
456 #[rstest]
457 #[case::leading_cm(
458 "󠅑𑆻👱🏿♀️xyz",
459 vec![
460 CollapsedEnsNameToken::Text(TokenValid { cps: vec![70075] }),
461 CollapsedEnsNameToken::Emoji(TokenEmoji { input: "👱🏿♀️".to_string(), cps_input: vec![128113, 127999, 8205, 9792, 65039], emoji: vec![128113, 127999, 8205, 9792, 65039], cps_no_fe0f: vec![128113, 127999, 8205, 9792] }),
462 CollapsedEnsNameToken::Text(TokenValid { cps: vec![120, 121, 122] }),
463 ]
464 )]
465 #[case::atm(
466 "a™️",
467 vec![
468 CollapsedEnsNameToken::Text(TokenValid { cps: vec![97, 116, 109] }),
469 ]
470 )]
471 fn test_collapse(
472 #[case] input: &str,
473 #[case] expected: Vec<CollapsedEnsNameToken>,
474 specs: &CodePointsSpecs,
475 ) {
476 let tokens = tokenize_input(input, specs, true).expect("tokenize");
477 let label = TokenizedLabel::from(&tokens);
478 let result = label.collapse_into_text_or_emoji();
479 assert_eq!(result, expected);
480 }
481}