precis_profiles/
nicknames.rs

1use crate::common;
2use lazy_static::lazy_static;
3use precis_core::profile::stabilize;
4use precis_core::profile::{PrecisFastInvocation, Profile, Rules};
5use precis_core::Error;
6use precis_core::{FreeformClass, StringClass};
7use std::borrow::Cow;
8
9// This function is used to check whether the input label will require any
10// modifications to apply the additional mapping rule for Nickname profile or not.
11// It makes a quick check to see if we can avoid making a copy of the input label,
12// for such purpose, it processes characters starting from the beginning of
13// the label looking for spaces characters. It stops processing characters
14// as soon as a non-ASCII character is found and returns its index. If it is a
15// ASCII character, it processes the next character and if it is a space separator
16// stops processing more characters returning the position of the next separator,
17// otherwise it continues iterating over the label. If not modifications will be
18// required, then the function will return None
19fn find_disallowed_space(label: &str) -> Option<usize> {
20    let mut begin = true;
21    let mut prev_space = false;
22    let mut last_c: Option<char> = None;
23    let mut offset = 0;
24
25    for (index, c) in label.chars().enumerate() {
26        offset = index;
27        if !common::is_space_separator(c) {
28            last_c = Some(c);
29            prev_space = false;
30            begin = false;
31            continue;
32        }
33
34        if begin {
35            // Starts with space
36            return Some(index);
37        }
38
39        if prev_space {
40            // More than one separator
41            return Some(index);
42        }
43
44        if c == common::SPACE {
45            prev_space = true;
46            last_c = Some(c);
47        } else {
48            // non-ASCII space
49            return Some(index);
50        }
51    }
52
53    if let Some(common::SPACE) = last_c {
54        // last character is a space
55        Some(offset)
56    } else {
57        // The string might have ASCII separators, but it does not contain
58        // more than one spaces in a row and it does not ends with a space
59        None
60    }
61}
62
63// Additional Mapping Rule: The additional mapping rule consists of
64// the following sub-rules.
65//  a. Map any instances of non-ASCII space to SPACE (`U+0020`); a
66//     non-ASCII space is any Unicode code point having a general
67//     category of "Zs", naturally with the exception of SPACE
68//     (`U+0020`).  (The inclusion of only ASCII space prevents
69//     confusion with various non-ASCII space code points, many of
70//     which are difficult to reproduce across different input
71//     methods.)
72//
73//  b. Remove any instances of the ASCII space character at the
74//     beginning or end of a nickname.
75//
76//  c. Map interior sequences of more than one ASCII space character
77//     to a single ASCII space character.
78fn trim_spaces<'a, T>(s: T) -> Result<Cow<'a, str>, Error>
79where
80    T: Into<Cow<'a, str>>,
81{
82    let s = s.into();
83    match find_disallowed_space(&s) {
84        None => Ok(s),
85        Some(pos) => {
86            let mut res = String::from(&s[..pos]);
87            res.reserve(s.len() - res.len());
88            let mut begin = true;
89            let mut prev_space = false;
90            for c in s[pos..].chars() {
91                if !common::is_space_separator(c) {
92                    res.push(c);
93                    prev_space = false;
94                    begin = false;
95                    continue;
96                }
97
98                if begin {
99                    // skip spaces at the beginning
100                    continue;
101                }
102
103                if !prev_space {
104                    res.push(common::SPACE);
105                }
106
107                prev_space = true;
108            }
109            // Skip last space character
110            if let Some(c) = res.pop() {
111                if c != common::SPACE {
112                    res.push(c);
113                }
114            }
115            Ok(res.into())
116        }
117    }
118}
119
120/// [`Nickname`](https://datatracker.ietf.org/doc/html/rfc8266#section-2).
121/// Nicknames or display names in messaging and text conferencing technologies;
122/// pet names for devices, accounts, and people; and other uses of nicknames,
123/// display names, or pet names. Look at the
124/// [`IANA` Considerations](https://datatracker.ietf.org/doc/html/rfc8266#section-5)
125/// section for more details.
126/// # Example
127/// ```rust
128/// # use precis_core::profile::Profile;
129/// # use precis_profiles::Nickname;
130/// # use std::borrow::Cow;
131/// // create Nickname profile
132/// let profile = Nickname::new();
133///
134/// // prepare string
135/// assert_eq!(profile.prepare("Guybrush Threepwood"),
136///     Ok(Cow::from("Guybrush Threepwood")));
137///
138/// // enforce string
139/// assert_eq!(profile.enforce("   Guybrush     Threepwood  "),
140///     Ok(Cow::from("Guybrush Threepwood")));
141///
142/// // compare strings
143/// assert_eq!(profile.compare("Guybrush   Threepwood  ",
144///     "guybrush threepwood"), Ok(true));
145/// ```
146#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
147pub struct Nickname(FreeformClass);
148
149impl Nickname {
150    /// Creates a [`Nickname`] profile.
151    pub fn new() -> Self {
152        Self(FreeformClass::default())
153    }
154
155    fn apply_prepare_rules<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
156    where
157        T: Into<Cow<'a, str>>,
158    {
159        let s = s.into();
160        let s = (!s.is_empty()).then_some(s).ok_or(Error::Invalid)?;
161        self.0.allows(&s)?;
162        Ok(s)
163    }
164
165    fn apply_enforce_rules<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
166    where
167        T: Into<Cow<'a, str>>,
168    {
169        let s = self.apply_prepare_rules(s)?;
170        let s = self.additional_mapping_rule(s)?;
171        let s = self.normalization_rule(s)?;
172        (!s.is_empty()).then_some(s).ok_or(Error::Invalid)
173    }
174
175    fn apply_compare_rules<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
176    where
177        T: Into<Cow<'a, str>>,
178    {
179        let s = self.apply_prepare_rules(s)?;
180        let s = self.additional_mapping_rule(s)?;
181        let s = self.case_mapping_rule(s)?;
182        self.normalization_rule(s)
183    }
184}
185
186impl Profile for Nickname {
187    fn prepare<'a, S>(&self, s: S) -> Result<Cow<'a, str>, Error>
188    where
189        S: Into<Cow<'a, str>>,
190    {
191        self.apply_prepare_rules(s)
192    }
193
194    fn enforce<'a, S>(&self, s: S) -> Result<Cow<'a, str>, Error>
195    where
196        S: Into<Cow<'a, str>>,
197    {
198        stabilize(s, |s| self.apply_enforce_rules(s))
199    }
200
201    fn compare<A, B>(&self, s1: A, s2: B) -> Result<bool, Error>
202    where
203        A: AsRef<str>,
204        B: AsRef<str>,
205    {
206        Ok(stabilize(s1.as_ref(), |s| self.apply_compare_rules(s))?
207            == stabilize(s2.as_ref(), |s| self.apply_compare_rules(s))?)
208    }
209}
210
211impl Rules for Nickname {
212    fn additional_mapping_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
213    where
214        T: Into<Cow<'a, str>>,
215    {
216        trim_spaces(s)
217    }
218
219    fn case_mapping_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
220    where
221        T: Into<Cow<'a, str>>,
222    {
223        common::case_mapping_rule(s)
224    }
225
226    fn normalization_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
227    where
228        T: Into<Cow<'a, str>>,
229    {
230        common::normalization_form_nfkc(s)
231    }
232}
233
234fn get_nickname_profile() -> &'static Nickname {
235    lazy_static! {
236        static ref NICKNAME: Nickname = Nickname::default();
237    }
238    &NICKNAME
239}
240
241impl PrecisFastInvocation for Nickname {
242    fn prepare<'a, S>(s: S) -> Result<Cow<'a, str>, Error>
243    where
244        S: Into<Cow<'a, str>>,
245    {
246        get_nickname_profile().prepare(s)
247    }
248
249    fn enforce<'a, S>(s: S) -> Result<Cow<'a, str>, Error>
250    where
251        S: Into<Cow<'a, str>>,
252    {
253        get_nickname_profile().enforce(s)
254    }
255
256    fn compare<A, B>(s1: A, s2: B) -> Result<bool, Error>
257    where
258        A: AsRef<str>,
259        B: AsRef<str>,
260    {
261        get_nickname_profile().compare(s1, s2)
262    }
263}
264
265#[cfg(test)]
266mod test_nicknames {
267    use crate::nicknames::*;
268
269    #[test]
270    fn test_find_disallowed_space() {
271        assert_eq!(find_disallowed_space(""), None);
272        assert_eq!(find_disallowed_space("test"), None);
273        assert_eq!(find_disallowed_space("test "), Some(4));
274        assert_eq!(find_disallowed_space("test good"), None);
275
276        // Two ASCII spaces in a row
277        assert_eq!(find_disallowed_space("  test"), Some(0));
278        assert_eq!(find_disallowed_space("t  est"), Some(2));
279
280        // Starts with ASCII space
281        assert_eq!(find_disallowed_space(" test"), Some(0));
282
283        // Non ASCII separator
284        assert_eq!(find_disallowed_space("\u{00a0}test"), Some(0));
285        assert_eq!(find_disallowed_space("te\u{00a0}st"), Some(2));
286        assert_eq!(find_disallowed_space("test\u{00a0}"), Some(4));
287    }
288
289    #[test]
290    fn test_trim_spaces() {
291        // Check ASCII spaces
292        assert_eq!(trim_spaces("  "), Ok(Cow::from("")));
293        assert_eq!(trim_spaces(" test"), Ok(Cow::from("test")));
294        assert_eq!(trim_spaces("test "), Ok(Cow::from("test")));
295
296        assert_eq!(trim_spaces("hello  world"), Ok(Cow::from("hello world")));
297
298        assert_eq!(trim_spaces(""), Ok(Cow::from("")));
299        assert_eq!(trim_spaces(" test"), Ok(Cow::from("test")));
300        assert_eq!(trim_spaces("test "), Ok(Cow::from("test")));
301        assert_eq!(
302            trim_spaces("   hello  world   "),
303            Ok(Cow::from("hello world"))
304        );
305
306        // Check non-ASCII spaces
307        assert_eq!(trim_spaces("\u{205f}test\u{205f}"), Ok(Cow::from("test")));
308        assert_eq!(
309            trim_spaces("\u{205f}\u{205f}hello\u{205f}\u{205f}world\u{205f}\u{205f}"),
310            Ok(Cow::from("hello world"))
311        );
312
313        // Mix ASCII and non-ASCII spaces
314        assert_eq!(trim_spaces(" \u{205f}test\u{205f} "), Ok(Cow::from("test")));
315        assert_eq!(
316            trim_spaces("\u{205f} hello \u{205f} world \u{205f} "),
317            Ok(Cow::from("hello world"))
318        );
319    }
320
321    #[test]
322    fn nick_name_profile() {
323        let profile = Nickname::new();
324
325        let res = profile.prepare("Foo Bar");
326        assert_eq!(res, Ok(Cow::from("Foo Bar")));
327
328        let res = profile.enforce("Foo Bar");
329        assert_eq!(res, Ok(Cow::from("Foo Bar")));
330
331        let res = profile.compare("Foo Bar", "foo bar");
332        assert_eq!(res, Ok(true));
333    }
334}