x520_stringprep/
lib.rs

1#![doc = include_str!("../README.md")]
2#![no_std]
3// Some things in this file were copied from `stringprep` crate.
4// License at the time of copying:
5// Copyright (c) 2017 The rust-stringprep Developers
6
7// Permission is hereby granted, free of charge, to any person obtaining a copy
8// of this software and associated documentation files (the "Software"), to deal
9// in the Software without restriction, including without limitation the rights
10// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11// copies of the Software, and to permit persons to whom the Software is
12// furnished to do so, subject to the following conditions:
13
14// The above copyright notice and this permission notice shall be included in all
15// copies or substantial portions of the Software.
16
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23// SOFTWARE.
24
25mod rfc3454;
26
27#[cfg(feature = "alloc")]
28extern crate alloc;
29#[cfg(feature = "alloc")]
30use alloc::string::String;
31
32use core::iter::{Filter, FlatMap, FusedIterator, Iterator, Map};
33use core::slice::Iter;
34use core::str::Chars;
35use unicode_normalization::{Recompositions, UnicodeNormalization};
36
37const REPLACEMENT_CHARACTER: char = '\u{FFFD}';
38
39/// Determine if a character is an unassigned code point
40///
41/// This is not 100% correct: there are many more unassigned ranges; however,
42/// this covers a huge range of them. This sloppy approximation is not done
43/// out of laziness, but rather, in the interest of performance. Also, many
44/// unassigned code points may become assigned in the future, so this less
45/// strict check is a little more future-proof.
46///
47/// No new code points are even proposed for planes 4 to 13. Plane 3 is
48/// entirely unassigned.
49#[inline]
50const fn is_unassigned(c: char) -> bool {
51    c >= '\u{30000}' && c <= '\u{DFFFF}'
52}
53
54/// Determine if a character is a private use code point
55#[inline]
56const fn is_private_use(c: char) -> bool {
57    matches!(c, '\u{E000}'..='\u{F8FF}' | '\u{F0000}'..='\u{FFFFD}' | '\u{100000}'..='\u{10FFFD}')
58}
59
60/// Determine if a character is a non-character code point
61#[inline]
62const fn is_non_char(c: char) -> bool {
63    let bottom_nybble = c as u32 & 0xFFFF;
64    if bottom_nybble >= 0xFFFE && bottom_nybble <= 0xFFFF {
65        return true;
66    }
67    matches!(c, '\u{FDD0}'..='\u{FDEF}')
68}
69
70/// Determines if `c` is to be removed according to section 7.2 of
71/// [ITU-T Recommendation X.520 (2019)](https://www.itu.int/rec/T-REC-X.520-201910-I/en).
72#[inline]
73fn x520_mapped_to_something(c: &char) -> bool {
74    match *c {
75        '\u{00AD}'
76        | '\u{1806}'
77        | '\u{034F}'
78        | '\u{180B}'..='\u{180D}'
79        | '\u{FE00}'..='\u{FE0F}'
80        | '\u{FFFC}'
81        | '\u{200B}' => false,
82        // Technically control characters, but mapped to whitespace in X.520.
83        '\u{09}' | '\u{0A}'..='\u{0D}' | '\u{85}' => true,
84        _ => !c.is_control(),
85    }
86}
87
88#[inline]
89fn is_separator(c: char) -> bool {
90    match c {
91        | '\u{20}' // SPACE
92        | '\u{a0}' // NO-BREAK SPACE
93        | '\u{2028}' // LINE SEPARATOR
94        | '\u{2029}' // PARAGRAPH SEPARATOR
95        | '\u{1680}' // OGHAM SPACE MARK
96        | '\u{2000}'..='\u{200a}' // Width-specific spaces
97        | '\u{202f}' // NARROW NO-BREAK SPACE
98        | '\u{205f}' // MEDIUM MATHEMATICAL SPACE
99        | '\u{3000}' // IDEOGRAPHIC SPACE
100        => true,
101        _ => false,
102    }
103}
104
105#[inline]
106fn x520_map(c: char) -> char {
107    match c {
108        '\u{09}' | '\u{0A}'..='\u{0D}' | '\u{85}' => ' ',
109        c => {
110            if is_separator(c) {
111                ' '
112            } else {
113                c
114            }
115        }
116    }
117}
118
119/// B.2 Mapping for case-folding used with NFKC.
120#[inline]
121fn case_fold_for_nfkc(c: char) -> CaseFoldForNfkc {
122    let inner = match rfc3454::B_2.binary_search_by_key(&c, |e| e.0) {
123        Ok(idx) => FoldInner::Chars(rfc3454::B_2[idx].1.chars()),
124        Err(_) => FoldInner::Char(Some(c)),
125    };
126    CaseFoldForNfkc(inner)
127}
128
129enum FoldInner {
130    Chars(Chars<'static>),
131    Char(Option<char>),
132}
133
134/// The iterator returned by `case_fold_for_nfkc`.
135struct CaseFoldForNfkc(FoldInner);
136
137impl Iterator for CaseFoldForNfkc {
138    type Item = char;
139
140    fn next(&mut self) -> Option<char> {
141        match self.0 {
142            FoldInner::Chars(ref mut it) => it.next(),
143            FoldInner::Char(ref mut ch) => ch.take(),
144        }
145    }
146}
147
148impl FusedIterator for CaseFoldForNfkc {}
149
150pub struct X520CaseExactStringPrepChars<I>
151    where I: Iterator<Item = char> {
152    s: Recompositions<Map<Filter<I, fn(&char) -> bool>, fn(char) -> char>>,
153    previous_was_space: bool,
154}
155
156impl<I> X520CaseExactStringPrepChars<I>
157    where I: Iterator<Item = char> {
158    pub fn new(s: I) -> Self {
159        X520CaseExactStringPrepChars {
160            previous_was_space: false,
161            s: s
162                .filter(x520_mapped_to_something as fn(&char) -> bool)
163                .map(x520_map as fn(_) -> _)
164                .nfkc(),
165        }
166    }
167}
168
169impl<I> Iterator for X520CaseExactStringPrepChars<I>
170    where I: Iterator<Item = char> {
171    /// The error is returned if a prohibited character is encountered.
172    type Item = Result<char, char>;
173
174    fn next(&mut self) -> Option<Self::Item> {
175        while let Some(c) = self.s.next() {
176            if c == ' ' {
177                if self.previous_was_space == true {
178                    continue;
179                } else {
180                    self.previous_was_space = true;
181                    return Some(Ok(' '));
182                }
183            }
184            self.previous_was_space = false;
185            // You don't have to check for Surrogate codes: it is not possible
186            // in UTF-8 strings in Rust.
187            if is_unassigned(c) || is_private_use(c) || is_non_char(c) || c == '\u{FFFD}' {
188                // Prohibited character: matching MUST return UNDEFINED.
189                return Some(Err(c));
190            }
191            return Some(Ok(c));
192        }
193        None
194    }
195}
196
197impl<I> FusedIterator for X520CaseExactStringPrepChars<I>
198    where I: Iterator<Item = char> {
199}
200
201pub struct X520CaseIgnoreStringPrepChars<I>
202    where I: Iterator<Item = char> {
203    s: Recompositions<
204        FlatMap<
205            Map<Filter<I, fn(&char) -> bool>, fn(char) -> char>,
206            CaseFoldForNfkc,
207            fn(char) -> CaseFoldForNfkc,
208        >,
209    >,
210    previous_was_space: bool,
211}
212
213impl<I> FusedIterator for X520CaseIgnoreStringPrepChars<I>
214    where I: Iterator<Item = char> {
215}
216
217impl<I> X520CaseIgnoreStringPrepChars<I>
218    where I: Iterator<Item = char> {
219    pub fn new(s: I) -> Self {
220        X520CaseIgnoreStringPrepChars {
221            previous_was_space: false,
222            s: s
223                .filter(x520_mapped_to_something as fn(&char) -> bool)
224                .map(x520_map as fn(_) -> _)
225                .flat_map(case_fold_for_nfkc as fn(_) -> _)
226                .nfkc(),
227        }
228    }
229}
230
231impl<I> Iterator for X520CaseIgnoreStringPrepChars<I>
232    where I: Iterator<Item = char> {
233    /// The error is returned if a prohibited character is encountered.
234    type Item = Result<char, char>;
235
236    fn next(&mut self) -> Option<Self::Item> {
237        while let Some(c) = self.s.next() {
238            if c == ' ' {
239                if self.previous_was_space == true {
240                    continue;
241                } else {
242                    self.previous_was_space = true;
243                    return Some(Ok(' '));
244                }
245            }
246            self.previous_was_space = false;
247            // You don't have to check for Surrogate codes: it is not possible
248            // in UTF-8 strings in Rust.
249            if is_unassigned(c) || is_private_use(c) || is_non_char(c) || c == '\u{FFFD}' {
250                // Prohibited character: matching MUST return UNDEFINED.
251                return Some(Err(c));
252            }
253            return Some(Ok(c));
254        }
255        None
256    }
257}
258
259/// Iterate over characters string-prepped per ITU-T Recommendation X.520 (case-sensitive)
260/// 
261/// This does NOT trim leading or trailing whitespace: that is left to the caller.
262#[inline]
263pub fn x520_stringprep_case_exact_str<'a>(s: &'a str) -> X520CaseExactStringPrepChars<Chars<'a>> {
264    X520CaseExactStringPrepChars::new(s.chars())
265}
266
267/// Iterate over characters string-prepped per ITU-T Recommendation X.520 (case-insensitive)
268/// 
269/// This does NOT trim leading or trailing whitespace: that is left to the caller.
270#[inline]
271pub fn x520_stringprep_case_ignore_str<'a>(s: &'a str) -> X520CaseIgnoreStringPrepChars<Chars<'a>> {
272    X520CaseIgnoreStringPrepChars::new(s.chars())
273}
274
275/// Iterate over characters of a string-prepped `BMPString` per ITU-T Recommendation X.520 (case-sensitive)
276/// 
277/// This does NOT trim leading or trailing whitespace: that is left to the caller.
278#[inline]
279pub fn x520_stringprep_case_exact_bmp<'a>(s: &'a [u16]) -> X520CaseExactStringPrepChars<Map<Iter<'a, u16>, fn(&u16) -> char>> {
280    let it: Map<Iter<'a, u16>, fn(&u16) -> char> = s
281        .iter()
282        .map(|c| char::from_u32(*c as u32).unwrap_or(REPLACEMENT_CHARACTER));
283    X520CaseExactStringPrepChars::new(it)
284}
285
286/// Iterate over characters of a string-prepped `BMPString` per ITU-T Recommendation X.520 (case-insensitive)
287/// 
288/// This does NOT trim leading or trailing whitespace: that is left to the caller.
289#[inline]
290pub fn x520_stringprep_case_ignore_bmp<'a>(s: &'a [u16]) -> X520CaseIgnoreStringPrepChars<Map<Iter<'a, u16>, fn(&u16) -> char>> {
291    let it: Map<Iter<'a, u16>, fn(&u16) -> char> = s
292        .iter()
293        .map(|c| char::from_u32(*c as u32).unwrap_or(REPLACEMENT_CHARACTER));
294    X520CaseIgnoreStringPrepChars::new(it)
295}
296
297/// Iterate over characters of a string-prepped `UniversalString` per ITU-T Recommendation X.520 (case-sensitive)
298/// 
299/// This does NOT trim leading or trailing whitespace: that is left to the caller.
300#[inline]
301pub fn x520_stringprep_case_exact_univ_str<'a>(s: &'a [u32]) -> X520CaseExactStringPrepChars<Map<Iter<'a, u32>, fn(&u32) -> char>> {
302    let it: Map<Iter<'a, u32>, fn(&u32) -> char> = s
303        .iter()
304        .map(|c| char::from_u32(*c as u32).unwrap_or(REPLACEMENT_CHARACTER));
305    X520CaseExactStringPrepChars::new(it)
306}
307
308/// Iterate over characters of a string-prepped `UniversalString` per ITU-T Recommendation X.520 (case-insensitive)
309/// 
310/// This does NOT trim leading or trailing whitespace: that is left to the caller.
311#[inline]
312pub fn x520_stringprep_case_ignore_univ_str<'a>(s: &'a [u32]) -> X520CaseIgnoreStringPrepChars<Map<Iter<'a, u32>, fn(&u32) -> char>> {
313    let it: Map<Iter<'a, u32>, fn(&u32) -> char> = s
314        .iter()
315        .map(|c| char::from_u32(*c).unwrap_or(REPLACEMENT_CHARACTER));
316    X520CaseIgnoreStringPrepChars::new(it)
317}
318
319/// Check if a string is already string-prepped per ITU-T Recommendation X.520 (case-sensitive)
320/// 
321/// This does NOT trim leading or trailing whitespace: that is left to the caller.
322pub fn is_x520_stringprepped_case_exact_str(s: &str) -> bool {
323    let mut chars = s.chars();
324    let mut it = x520_stringprep_case_exact_str(s);
325    while let Some(c) = it.next() {
326        if c.is_err() {
327            return false;
328        }
329        if chars.next() != Some(c.unwrap()) {
330            return false;
331        }
332    }
333    true
334}
335
336/// Check if a string is already string-prepped per ITU-T Recommendation X.520 (case-insensitive)
337/// 
338/// This does NOT trim leading or trailing whitespace: that is left to the caller.
339pub fn is_x520_stringprepped_case_ignore_str(s: &str) -> bool {
340    let mut chars = s.chars();
341    let mut it = x520_stringprep_case_ignore_str(s);
342    while let Some(c) = it.next() {
343        if c.is_err() {
344            return false;
345        }
346        if chars.next() != Some(c.unwrap()) {
347            return false;
348        }
349    }
350    true
351}
352
353/// Normalize a string to case-exact form.
354/// 
355/// Returns an error if the string contains prohibited characters. The error
356/// itself contains the prohibited character.
357#[cfg(feature = "alloc")]
358#[inline]
359pub fn x520_stringprep_to_case_exact_string(s: &str) -> Result<String, char> {
360    x520_stringprep_case_exact_str(s).collect()
361}
362
363/// Normalize a string to case-ignore form.
364/// 
365/// Returns an error if the string contains prohibited characters. The error
366/// itself contains the prohibited character.
367#[cfg(feature = "alloc")]
368#[inline]
369pub fn x520_stringprep_to_case_ignore_string(s: &str) -> Result<String, char> {
370    x520_stringprep_case_ignore_str(s).collect()
371}
372
373/// Compare two strings for equality, string-prepped per ITU-T Recommendation X.520 (case-sensitive)
374/// 
375/// This does NOT trim leading or trailing whitespace: that is left to the caller.
376#[inline]
377pub fn x520_stringprep_case_exact_compare(s1: &str, s2: &str) -> bool {
378    x520_stringprep_case_exact_str(s1).eq(x520_stringprep_case_exact_str(s2))
379}
380
381/// Compare two strings for equality, string-prepped per ITU-T Recommendation X.520 (case-insensitive)
382/// 
383/// This does NOT trim leading or trailing whitespace: that is left to the caller.
384#[inline]
385pub fn x520_stringprep_case_ignore_compare(s1: &str, s2: &str) -> bool {
386    x520_stringprep_case_ignore_str(s1).eq(x520_stringprep_case_ignore_str(s2))
387}
388
389#[cfg(test)]
390mod tests {
391    use super::{
392        x520_stringprep_case_exact_str,
393        x520_stringprep_case_ignore_str,
394        x520_stringprep_case_exact_bmp,
395        x520_stringprep_case_exact_univ_str,
396    };
397    extern crate alloc;
398    use alloc::string::String;
399    use alloc::vec::Vec;
400
401    #[test]
402    fn test_case_exact_stringprep_1() {
403        let input = "Jonathan   Wilbur";
404        let output: String = x520_stringprep_case_exact_str(input)
405            .map(|maybe_c| maybe_c.unwrap())
406            .collect();
407        assert_eq!(output.as_str(), "Jonathan Wilbur");
408    }
409
410    #[test]
411    fn test_nfkc_normalization() {
412        // Test NFKC normalization - combining characters should be normalized
413        let input = "e\u{0301}"; // e + combining acute accent
414        let output: String = x520_stringprep_case_exact_str(input)
415            .map(|maybe_c| maybe_c.unwrap())
416            .collect();
417        assert_eq!(output, "é"); // Should be normalized to precomposed é
418
419        // Test with multiple combining characters
420        let input = "e\u{0301}\u{0300}"; // e + acute + grave
421        let output: String = x520_stringprep_case_exact_str(input)
422            .map(|maybe_c| maybe_c.unwrap())
423            .collect();
424        assert_eq!(output, "é\u{0300}"); // Should be normalized but may not combine further
425    }
426
427    #[test]
428    fn test_whitespace_mapping() {
429        // Test ASCII whitespace characters
430        let input = "Hello\tWorld\nTest\r\nSpace";
431        let output: String = x520_stringprep_case_exact_str(input)
432            .map(|maybe_c| maybe_c.unwrap())
433            .collect();
434        assert_eq!(output, "Hello World Test Space");
435
436        // Test Unicode separator characters
437        let input = "Hello\u{2000}World\u{2001}Test\u{2002}Space"; // Various Unicode spaces
438        let output: String = x520_stringprep_case_exact_str(input)
439            .map(|maybe_c| maybe_c.unwrap())
440            .collect();
441        assert_eq!(output, "Hello World Test Space");
442
443        // Test mixed whitespace
444        let input = "Hello\t\u{2000}World\n\u{2001}Test\r\u{2002}Space";
445        let output: String = x520_stringprep_case_exact_str(input)
446            .map(|maybe_c| maybe_c.unwrap())
447            .collect();
448        assert_eq!(output, "Hello World Test Space");
449    }
450
451    #[test]
452    fn test_space_consolidation() {
453        // Test multiple consecutive spaces
454        let input = "Hello    World";
455        let output: String = x520_stringprep_case_exact_str(input)
456            .map(|maybe_c| maybe_c.unwrap())
457            .collect();
458        assert_eq!(output, "Hello World");
459
460        // Test mixed whitespace consolidation
461        let input = "Hello\t\t\n\n\r\rWorld";
462        let output: String = x520_stringprep_case_exact_str(input)
463            .map(|maybe_c| maybe_c.unwrap())
464            .collect();
465        assert_eq!(output, "Hello World");
466
467        // Test Unicode spaces consolidation
468        let input = "Hello\u{2000}\u{2001}\u{2002}World";
469        let output: String = x520_stringprep_case_exact_str(input)
470            .map(|maybe_c| maybe_c.unwrap())
471            .collect();
472        assert_eq!(output, "Hello World");
473
474        // Test mixed ASCII and Unicode spaces
475        let input = "Hello \t\u{2000}\nWorld";
476        let output: String = x520_stringprep_case_exact_str(input)
477            .map(|maybe_c| maybe_c.unwrap())
478            .collect();
479        assert_eq!(output, "Hello World");
480    }
481
482    #[test]
483    fn test_leading_trailing_spaces() {
484        // Test leading spaces - function preserves one leading space
485        let input = "   Hello World";
486        let output: String = x520_stringprep_case_exact_str(input)
487            .map(|maybe_c| maybe_c.unwrap())
488            .collect();
489        assert_eq!(output, " Hello World");
490
491        // Test trailing spaces - function preserves one trailing space
492        let input = "Hello World   ";
493        let output: String = x520_stringprep_case_exact_str(input)
494            .map(|maybe_c| maybe_c.unwrap())
495            .collect();
496        assert_eq!(output, "Hello World ");
497
498        // Test both leading and trailing spaces
499        let input = "   Hello World   ";
500        let output: String = x520_stringprep_case_exact_str(input)
501            .map(|maybe_c| maybe_c.unwrap())
502            .collect();
503        assert_eq!(output, " Hello World ");
504    }
505
506    #[test]
507    fn test_prohibited_characters() {
508        // // Test unassigned characters (should return Err)
509        // let input = "Hello\u{0378}World"; // U+0378 is unassigned
510        // let result: Result<String, char> = x520_stringprep_case_exact_str(input)
511        //     .collect();
512        // assert!(result.is_err());
513        // assert_eq!(result.unwrap_err(), '\u{0378}');
514
515        // Test private use characters (should return Err)
516        let input = "Hello\u{E000}World"; // U+E000 is private use
517        let result: Result<String, char> = x520_stringprep_case_exact_str(input).collect();
518        assert!(result.is_err());
519        assert_eq!(result.unwrap_err(), '\u{E000}');
520
521        // Test non-character code points (should return Err)
522        let input = "Hello\u{FDD0}World"; // U+FDD0 is non-character
523        let result: Result<String, char> = x520_stringprep_case_exact_str(input).collect();
524        assert!(result.is_err());
525        assert_eq!(result.unwrap_err(), '\u{FDD0}');
526
527        // Test replacement character (should return Err)
528        let input = "Hello\u{FFFD}World";
529        let result: Result<String, char> = x520_stringprep_case_exact_str(input).collect();
530        assert!(result.is_err());
531        assert_eq!(result.unwrap_err(), '\u{FFFD}');
532    }
533
534    #[test]
535    fn test_control_characters() {
536        // Test that control characters are mapped to spaces
537        let input = "Hello\u{0009}World"; // Horizontal tab
538        let output: String = x520_stringprep_case_exact_str(input)
539            .map(|maybe_c| maybe_c.unwrap())
540            .collect();
541        assert_eq!(output, "Hello World");
542
543        // Test line feed
544        let input = "Hello\u{000A}World";
545        let output: String = x520_stringprep_case_exact_str(input)
546            .map(|maybe_c| maybe_c.unwrap())
547            .collect();
548        assert_eq!(output, "Hello World");
549
550        // Test carriage return
551        let input = "Hello\u{000D}World";
552        let output: String = x520_stringprep_case_exact_str(input)
553            .map(|maybe_c| maybe_c.unwrap())
554            .collect();
555        assert_eq!(output, "Hello World");
556
557        // Test next line
558        let input = "Hello\u{0085}World";
559        let output: String = x520_stringprep_case_exact_str(input)
560            .map(|maybe_c| maybe_c.unwrap())
561            .collect();
562        assert_eq!(output, "Hello World");
563    }
564
565    #[test]
566    fn test_filtered_characters() {
567        // Test characters that should be filtered out (not mapped to space)
568        let input = "Hello\u{00AD}World"; // Soft hyphen - should be filtered out
569        let output: String = x520_stringprep_case_exact_str(input)
570            .map(|maybe_c| maybe_c.unwrap())
571            .collect();
572        assert_eq!(output, "HelloWorld");
573
574        // Test zero-width space
575        let input = "Hello\u{200B}World";
576        let output: String = x520_stringprep_case_exact_str(input)
577            .map(|maybe_c| maybe_c.unwrap())
578            .collect();
579        assert_eq!(output, "HelloWorld");
580
581        // Test object replacement character
582        let input = "Hello\u{FFFC}World";
583        let output: String = x520_stringprep_case_exact_str(input)
584            .map(|maybe_c| maybe_c.unwrap())
585            .collect();
586        assert_eq!(output, "HelloWorld");
587    }
588
589    #[test]
590    fn test_complex_normalization() {
591        // Test a complex case with multiple transformations
592        let input = "  Hello\te\u{0301}\u{2000}Ä\u{FB03}n  ";
593        let output: String = x520_stringprep_case_exact_str(input)
594            .map(|maybe_c| maybe_c.unwrap())
595            .collect();
596        assert_eq!(output, " Hello é Äffin ");
597
598        let output: String = x520_stringprep_case_ignore_str(input)
599            .map(|maybe_c| maybe_c.unwrap())
600            .collect();
601        assert_eq!(output, " hello é äffin ");
602    }
603
604    #[test]
605    fn test_empty_string() {
606        let input = "";
607        let output: String = x520_stringprep_case_exact_str(input)
608            .map(|maybe_c| maybe_c.unwrap())
609            .collect();
610        assert_eq!(output, "");
611
612        let output: String = x520_stringprep_case_ignore_str(input)
613            .map(|maybe_c| maybe_c.unwrap())
614            .collect();
615        assert_eq!(output, "");
616    }
617
618    #[test]
619    fn test_only_spaces() {
620        let input = "   \t\n\r   ";
621        let output: String = x520_stringprep_case_exact_str(input)
622            .map(|maybe_c| maybe_c.unwrap())
623            .collect();
624        assert_eq!(output, " ");
625
626        let output: String = x520_stringprep_case_ignore_str(input)
627            .map(|maybe_c| maybe_c.unwrap())
628            .collect();
629        assert_eq!(output, " ");
630    }
631
632    #[test]
633    fn test_case_ignore_stringprep_1() {
634        let input = "Jonathan   Wilbur";
635        let output: String = x520_stringprep_case_ignore_str(input)
636            .map(|maybe_c| maybe_c.unwrap())
637            .collect();
638        assert_eq!(output.as_str(), "jonathan wilbur");
639    }
640
641    // This test is just to make sure we can use this for `BMPString`.
642    #[test]
643    fn test_bmp_string_1() {
644        let input: Vec<u16> = "Jonathan   Wilbur".encode_utf16().collect();
645        let output = x520_stringprep_case_exact_bmp(input.as_slice()).collect::<Result<String, char>>().unwrap();
646        assert_eq!(output.as_str(), "Jonathan Wilbur");
647    }
648
649    // This test is just to make sure we can use this for `UniversalString`.
650    #[test]
651    fn test_univ_string_1() {
652        let input: Vec<u32> = "Jonathan   Wilbur".chars().map(|c| c as u32).collect();
653        let output = x520_stringprep_case_exact_univ_str(input.as_slice()).collect::<Result<String, char>>().unwrap();
654        assert_eq!(output.as_str(), "Jonathan Wilbur");
655    }
656
657}