precis_profiles/
usernames.rs

1include!(concat!(env!("OUT_DIR"), "/width_mapping.rs"));
2
3use crate::bidi;
4use crate::common;
5use lazy_static::lazy_static;
6use precis_core::profile::{PrecisFastInvocation, Profile, Rules};
7use precis_core::Codepoints;
8use precis_core::{Error, UnexpectedError};
9use precis_core::{IdentifierClass, StringClass};
10use std::borrow::Cow;
11
12fn get_decomposition_mapping(cp: u32) -> Option<u32> {
13    WIDE_NARROW_MAPPING
14        .binary_search_by(|cps| cps.0.partial_cmp(&cp).unwrap())
15        .map(|x| WIDE_NARROW_MAPPING[x].1)
16        .ok()
17}
18
19fn has_width_mapping(c: char) -> bool {
20    get_decomposition_mapping(c as u32).is_some()
21}
22
23fn width_mapping_rule<'a, T>(s: T) -> Result<Cow<'a, str>, Error>
24where
25    T: Into<Cow<'a, str>>,
26{
27    let s = s.into();
28    match s.find(has_width_mapping) {
29        None => Ok(s),
30        Some(pos) => {
31            let mut res = String::from(&s[..pos]);
32            res.reserve(s.len() - res.len());
33            for c in s[pos..].chars() {
34                res.push(match get_decomposition_mapping(c as u32) {
35                    Some(d) => {
36                        char::from_u32(d).ok_or(Error::Unexpected(UnexpectedError::Undefined))?
37                    }
38                    None => c,
39                });
40            }
41            Ok(res.into())
42        }
43    }
44}
45
46fn directionality_rule<'a, T>(s: T) -> Result<Cow<'a, str>, Error>
47where
48    T: Into<Cow<'a, str>>,
49{
50    let s = s.into();
51    if bidi::has_rtl(&s) {
52        bidi::satisfy_bidi_rule(&s)
53            .then_some(s)
54            .ok_or(Error::Invalid)
55    } else {
56        Ok(s)
57    }
58}
59
60/// [`UsernameCaseMapped`](https://datatracker.ietf.org/doc/html/rfc8265#section-3.3).
61/// Profile designed to deal with `usernames` in security and application protocols.
62/// It replaces the `SASLprep` profile of `Stringprep`. Look at the
63/// [`IANA` Considerations](https://datatracker.ietf.org/doc/html/rfc8265#section-7.1)
64/// section for more details.
65/// # Example
66/// ```rust
67/// # use precis_core::{CodepointInfo, DerivedPropertyValue, Error};
68/// # use precis_core::profile::Profile;
69/// # use precis_profiles::UsernameCaseMapped;
70/// # use std::borrow::Cow;
71/// // create UsernameCaseMapped profile
72/// let profile = UsernameCaseMapped::new();
73///
74/// // prepare string
75/// assert_eq!(profile.prepare("Guybrush"), Ok(Cow::from("Guybrush")));
76///
77/// // UsernameCaseMapped does not accept spaces. Unicode code point 0x0020
78/// assert_eq!(profile.prepare("Guybrush Threepwood"),
79///    Err(Error::BadCodepoint(CodepointInfo { cp: 0x0020, position: 8, property: DerivedPropertyValue::SpecClassDis })));
80///
81/// // enforce string
82/// assert_eq!(profile.enforce("Guybrush"), Ok(Cow::from("guybrush")));
83///
84/// // compare strings
85/// assert_eq!(profile.compare("Guybrush", "guybrush"), Ok(true));
86/// ```
87#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
88pub struct UsernameCaseMapped(IdentifierClass);
89
90impl UsernameCaseMapped {
91    /// Creates a [`UsernameCaseMapped`] profile.
92    pub fn new() -> Self {
93        Self(IdentifierClass::default())
94    }
95}
96
97impl Profile for UsernameCaseMapped {
98    fn prepare<'a, S>(&self, s: S) -> Result<Cow<'a, str>, Error>
99    where
100        S: Into<Cow<'a, str>>,
101    {
102        let s = self.width_mapping_rule(s)?;
103        let s = (!s.is_empty()).then_some(s).ok_or(Error::Invalid)?;
104        self.0.allows(&s)?;
105        Ok(s)
106    }
107
108    fn enforce<'a, S>(&self, s: S) -> Result<Cow<'a, str>, Error>
109    where
110        S: Into<Cow<'a, str>>,
111    {
112        let s = self.prepare(s)?;
113        let s = self.case_mapping_rule(s)?;
114        let s = self.normalization_rule(s)?;
115        let s = (!s.is_empty()).then_some(s).ok_or(Error::Invalid)?;
116        self.directionality_rule(s)
117    }
118
119    fn compare<A, B>(&self, s1: A, s2: B) -> Result<bool, Error>
120    where
121        A: AsRef<str>,
122        B: AsRef<str>,
123    {
124        Ok(self.enforce(s1.as_ref())? == self.enforce(s2.as_ref())?)
125    }
126}
127
128impl Rules for UsernameCaseMapped {
129    fn width_mapping_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
130    where
131        T: Into<Cow<'a, str>>,
132    {
133        width_mapping_rule(s)
134    }
135
136    fn case_mapping_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
137    where
138        T: Into<Cow<'a, str>>,
139    {
140        common::case_mapping_rule(s)
141    }
142
143    fn normalization_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
144    where
145        T: Into<Cow<'a, str>>,
146    {
147        common::normalization_form_nfc(s)
148    }
149
150    fn directionality_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
151    where
152        T: Into<Cow<'a, str>>,
153    {
154        directionality_rule(s)
155    }
156}
157
158fn get_username_case_mapped_profile() -> &'static UsernameCaseMapped {
159    lazy_static! {
160        static ref USERNAME_CASE_MAPPED: UsernameCaseMapped = UsernameCaseMapped::default();
161    }
162    &USERNAME_CASE_MAPPED
163}
164
165impl PrecisFastInvocation for UsernameCaseMapped {
166    fn prepare<'a, S>(s: S) -> Result<Cow<'a, str>, Error>
167    where
168        S: Into<Cow<'a, str>>,
169    {
170        get_username_case_mapped_profile().prepare(s)
171    }
172
173    fn enforce<'a, S>(s: S) -> Result<Cow<'a, str>, Error>
174    where
175        S: Into<Cow<'a, str>>,
176    {
177        get_username_case_mapped_profile().enforce(s)
178    }
179
180    fn compare<A, B>(s1: A, s2: B) -> Result<bool, Error>
181    where
182        A: AsRef<str>,
183        B: AsRef<str>,
184    {
185        get_username_case_mapped_profile().compare(s1, s2)
186    }
187}
188
189/// [`UsernameCasePreserved`](https://datatracker.ietf.org/doc/html/rfc8265#section-3.4).
190/// Profile designed to deal with `usernames` in security and application protocols.
191/// It replaces the `SASLprep` profile of `Stringprep`. Look at the
192/// [`IANA` Considerations](https://datatracker.ietf.org/doc/html/rfc8265#section-7.2)
193/// section for more details.
194/// # Example
195/// ```rust
196/// # use precis_core::{CodepointInfo, DerivedPropertyValue, Error};
197/// # use precis_core::profile::Profile;
198/// # use precis_profiles::UsernameCasePreserved;
199/// # use std::borrow::Cow;
200/// // create UsernameCasePreserved profile
201/// let profile = UsernameCasePreserved::new();
202///
203/// // prepare string
204/// assert_eq!(profile.prepare("Guybrush"), Ok(Cow::from("Guybrush")));
205///
206/// // UsernameCaseMapped does not accept spaces. Unicode code point 0x0020
207/// assert_eq!(profile.prepare("Guybrush Threepwood"),
208///    Err(Error::BadCodepoint(CodepointInfo { cp: 0x0020, position: 8, property: DerivedPropertyValue::SpecClassDis })));
209///
210/// // enforce string
211/// assert_eq!(profile.enforce("Guybrush"), Ok(Cow::from("Guybrush")));
212///
213/// // compare strings
214/// assert_eq!(profile.compare("Guybrush", "Guybrush"), Ok(true));
215/// ```
216#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
217pub struct UsernameCasePreserved(IdentifierClass);
218
219impl UsernameCasePreserved {
220    /// Creates a [`UsernameCasePreserved`] profile.
221    pub fn new() -> Self {
222        Self(IdentifierClass::default())
223    }
224}
225
226impl Profile for UsernameCasePreserved {
227    fn prepare<'a, S>(&self, s: S) -> Result<Cow<'a, str>, Error>
228    where
229        S: Into<Cow<'a, str>>,
230    {
231        let s = self.width_mapping_rule(s)?;
232        let s = (!s.is_empty()).then_some(s).ok_or(Error::Invalid)?;
233        self.0.allows(&s)?;
234        Ok(s)
235    }
236
237    fn enforce<'a, S>(&self, s: S) -> Result<Cow<'a, str>, Error>
238    where
239        S: Into<Cow<'a, str>>,
240    {
241        let s = self.prepare(s)?;
242        let s = self.normalization_rule(s)?;
243        let s = (!s.is_empty()).then_some(s).ok_or(Error::Invalid)?;
244        self.directionality_rule(s)
245    }
246
247    fn compare<A, B>(&self, s1: A, s2: B) -> Result<bool, Error>
248    where
249        A: AsRef<str>,
250        B: AsRef<str>,
251    {
252        Ok(self.enforce(s1.as_ref())? == self.enforce(s2.as_ref())?)
253    }
254}
255
256impl Rules for UsernameCasePreserved {
257    fn width_mapping_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
258    where
259        T: Into<Cow<'a, str>>,
260    {
261        width_mapping_rule(s)
262    }
263
264    fn normalization_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
265    where
266        T: Into<Cow<'a, str>>,
267    {
268        common::normalization_form_nfc(s)
269    }
270
271    fn directionality_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
272    where
273        T: Into<Cow<'a, str>>,
274    {
275        directionality_rule(s)
276    }
277}
278
279fn get_username_case_preserved_profile() -> &'static UsernameCasePreserved {
280    lazy_static! {
281        static ref USERNAME_CASE_PRESERVED: UsernameCasePreserved =
282            UsernameCasePreserved::default();
283    }
284    &USERNAME_CASE_PRESERVED
285}
286
287impl PrecisFastInvocation for UsernameCasePreserved {
288    fn prepare<'a, S>(s: S) -> Result<Cow<'a, str>, Error>
289    where
290        S: Into<Cow<'a, str>>,
291    {
292        get_username_case_preserved_profile().prepare(s)
293    }
294
295    fn enforce<'a, S>(s: S) -> Result<Cow<'a, str>, Error>
296    where
297        S: Into<Cow<'a, str>>,
298    {
299        get_username_case_preserved_profile().enforce(s)
300    }
301
302    fn compare<A, B>(s1: A, s2: B) -> Result<bool, Error>
303    where
304        A: AsRef<str>,
305        B: AsRef<str>,
306    {
307        get_username_case_preserved_profile().compare(s1, s2)
308    }
309}
310
311#[cfg(test)]
312mod profile_rules {
313    use crate::usernames::*;
314
315    #[test]
316    fn test_width_mapping_rule() {
317        let res = width_mapping_rule("");
318        assert_eq!(res, Ok(Cow::from("")));
319
320        // Valid username with no modifications
321        let res = width_mapping_rule("TestName");
322        assert_eq!(res, Ok(Cow::from("TestName")));
323
324        // Mapping code point `U+FF03` (`#`) to `U+0023` (`#`)
325        let res = width_mapping_rule("\u{ff03}");
326        assert_eq!(res, Ok(Cow::from("\u{0023}")));
327
328        let res = width_mapping_rule("a\u{ff03}");
329        assert_eq!(res, Ok(Cow::from("a\u{0023}")));
330
331        let res = width_mapping_rule("\u{ff03}a");
332        assert_eq!(res, Ok(Cow::from("\u{0023}a")));
333
334        let res = width_mapping_rule("\u{ff03}\u{ff03}\u{ff03}");
335        assert_eq!(res, Ok(Cow::from("\u{0023}\u{0023}\u{0023}")));
336    }
337
338    #[test]
339    fn test_directionality_rule() {
340        let res = directionality_rule("");
341        assert_eq!(res, Ok(Cow::from("")));
342
343        // No `RTL` label
344        let res = directionality_rule("Hello");
345        assert_eq!(res, Ok(Cow::from("Hello")));
346
347        // `RTL` label
348        let res = directionality_rule("\u{05be}");
349        assert_eq!(res, Ok(Cow::from("\u{05be}")));
350
351        // `LTR` label
352        let res = directionality_rule("\u{00aa}");
353        assert_eq!(res, Ok(Cow::from("\u{00aa}")));
354
355        // Invalid label
356        let res = directionality_rule("\u{05be}Hello");
357        assert_eq!(res, Err(Error::Invalid));
358    }
359
360    #[test]
361    fn username_name_case_mapped_profile() {
362        let profile = UsernameCaseMapped::new();
363
364        let res = profile.prepare("XxXxX");
365        assert_eq!(res, Ok(Cow::from("XxXxX")));
366
367        let res = profile.enforce("XxXxX");
368        assert_eq!(res, Ok(Cow::from("xxxxx")));
369
370        let res = profile.compare("heLLo", "Hello");
371        assert_eq!(res, Ok(true));
372    }
373
374    #[test]
375    fn username_name_case_preserved_profile() {
376        let profile = UsernameCasePreserved::new();
377
378        let res = profile.prepare("XxXxX");
379        assert_eq!(res, Ok(Cow::from("XxXxX")));
380
381        let res = profile.enforce("XxXxX");
382        assert_eq!(res, Ok(Cow::from("XxXxX")));
383
384        let res = profile.compare("Hello", "Hello");
385        assert_eq!(res, Ok(true));
386    }
387}