stringprep/
lib.rs

1//! An implementation of the "stringprep" algorithm defined in [RFC 3454][].
2//!
3//! [RFC 3454]: https://tools.ietf.org/html/rfc3454
4#![doc(html_root_url="https://docs.rs/stringprep/0.1.2")]
5#![warn(missing_docs)]
6extern crate unicode_bidi;
7extern crate unicode_normalization;
8
9use std::ascii::AsciiExt;
10use std::borrow::Cow;
11use std::error;
12use std::fmt;
13use unicode_normalization::UnicodeNormalization;
14
15mod rfc3454;
16pub mod tables;
17
18/// Describes why a string failed stringprep normalization.
19#[derive(Debug)]
20enum ErrorCause {
21    /// Contains stringprep prohibited characters.
22    ProhibitedCharacter(char),
23    /// Violates stringprep rules for bidirectional text.
24    ProhibitedBidirectionalText,
25}
26
27/// An error performing the stringprep algorithm.
28#[derive(Debug)]
29pub struct Error(ErrorCause);
30
31impl fmt::Display for Error {
32    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
33        match self.0 {
34            ErrorCause::ProhibitedCharacter(c) => write!(fmt, "prohibited character `{}`", c),
35            ErrorCause::ProhibitedBidirectionalText => write!(fmt, "prohibited bidirectional text"),
36        }
37    }
38}
39
40impl error::Error for Error {
41    fn description(&self) -> &str {
42        "error performing stringprep algorithm"
43    }
44}
45
46/// Prepares a string with the SASLprep profile of the stringprep algorithm.
47///
48/// SASLprep is defined in [RFC 4013][].
49///
50/// [RFC 4013]: https://tools.ietf.org/html/rfc4013
51pub fn saslprep<'a>(s: &'a str) -> Result<Cow<'a, str>, Error> {
52    // fast path for ascii text
53    if s.chars()
54           .all(|c| c.is_ascii() && !tables::ascii_control_character(c)) {
55        return Ok(Cow::Borrowed(s));
56    }
57
58    // 2.1 Mapping
59    let mapped = s.chars()
60        .map(|c| if tables::non_ascii_space_character(c) {
61                 ' '
62             } else {
63                 c
64             })
65        .filter(|&c| !tables::commonly_mapped_to_nothing(c));
66
67    // 2.2 Normalization
68    let normalized = mapped.nfkc().collect::<String>();
69
70    // 2.3 Prohibited Output
71    let prohibited = normalized
72        .chars()
73        .find(|&c| {
74            tables::non_ascii_space_character(c) /* C.1.2 */ ||
75            tables::ascii_control_character(c) /* C.2.1 */ ||
76            tables::non_ascii_control_character(c) /* C.2.2 */ ||
77            tables::private_use(c) /* C.3 */ ||
78            tables::non_character_code_point(c) /* C.4 */ ||
79            tables::surrogate_code(c) /* C.5 */ ||
80            tables::inappropriate_for_plain_text(c) /* C.6 */ ||
81            tables::inappropriate_for_canonical_representation(c) /* C.7 */ ||
82            tables::change_display_properties_or_deprecated(c) /* C.8 */ ||
83            tables::tagging_character(c) /* C.9 */
84        });
85    if let Some(c) = prohibited {
86        return Err(Error(ErrorCause::ProhibitedCharacter(c)));
87    }
88
89    // 2.4. Bidirectional Characters
90    if is_prohibited_bidirectional_text(&normalized) {
91        return Err(Error(ErrorCause::ProhibitedBidirectionalText));
92    }
93
94    // 2.5 Unassigned Code Points
95    let unassigned = normalized
96        .chars()
97        .find(|&c| tables::unassigned_code_point(c));
98    if let Some(c) = unassigned {
99        return Err(Error(ErrorCause::ProhibitedCharacter(c)));
100    }
101
102    Ok(Cow::Owned(normalized))
103}
104
105// RFC3454, 6. Bidirectional Characters
106fn is_prohibited_bidirectional_text(s: &str) -> bool {
107    if s.contains(tables::bidi_r_or_al) {
108        // 2) If a string contains any RandALCat character, the string
109        // MUST NOT contain any LCat character.
110        if s.contains(tables::bidi_l) {
111            return true;
112        }
113
114        // 3) If a string contains any RandALCat character, a RandALCat
115        // character MUST be the first character of the string, and a
116        // RandALCat character MUST be the last character of the string.
117        if !tables::bidi_r_or_al(s.chars().next().unwrap()) ||
118           !tables::bidi_r_or_al(s.chars().next_back().unwrap()) {
119            return true;
120        }
121    }
122
123    false
124}
125
126/// Prepares a string with the Nameprep profile of the stringprep algorithm.
127///
128/// Nameprep is defined in [RFC 3491][].
129///
130/// [RFC 3491]: https://tools.ietf.org/html/rfc3491
131pub fn nameprep<'a>(s: &'a str) -> Result<Cow<'a, str>, Error> {
132    // 3. Mapping
133    let mapped = s.chars()
134        .filter(|&c| !tables::commonly_mapped_to_nothing(c))
135        .flat_map(tables::case_fold_for_nfkc);
136
137    // 4. Normalization
138    let normalized = mapped.nfkc().collect::<String>();
139
140    // 5. Prohibited Output
141    let prohibited = normalized
142        .chars()
143        .find(|&c| {
144            tables::non_ascii_space_character(c) /* C.1.2 */ ||
145            tables::non_ascii_control_character(c) /* C.2.2 */ ||
146            tables::private_use(c) /* C.3 */ ||
147            tables::non_character_code_point(c) /* C.4 */ ||
148            tables::surrogate_code(c) /* C.5 */ ||
149            tables::inappropriate_for_plain_text(c) /* C.6 */ ||
150            tables::inappropriate_for_canonical_representation(c) /* C.7 */ ||
151            tables::change_display_properties_or_deprecated(c) /* C.9 */ ||
152            tables::tagging_character(c) /* C.9 */
153        });
154    if let Some(c) = prohibited {
155        return Err(Error(ErrorCause::ProhibitedCharacter(c)));
156    }
157
158    // 6. Bidirectional Characters
159    if is_prohibited_bidirectional_text(&normalized) {
160        return Err(Error(ErrorCause::ProhibitedBidirectionalText));
161    }
162
163    // 7 Unassigned Code Points
164    let unassigned = normalized
165        .chars()
166        .find(|&c| tables::unassigned_code_point(c));
167    if let Some(c) = unassigned {
168        return Err(Error(ErrorCause::ProhibitedCharacter(c)));
169    }
170
171    Ok(Cow::Owned(normalized))
172}
173
174/// Prepares a string with the Nodeprep profile of the stringprep algorithm.
175///
176/// Nameprep is defined in [RFC 3920, Appendix A][].
177///
178/// [RFC 3920, Appendix A]: https://tools.ietf.org/html/rfc3920#appendix-A
179pub fn nodeprep<'a>(s: &'a str) -> Result<Cow<'a, str>, Error> {
180    // A.3. Mapping
181    let mapped = s.chars()
182        .filter(|&c| !tables::commonly_mapped_to_nothing(c))
183        .flat_map(tables::case_fold_for_nfkc);
184
185    // A.4. Normalization
186    let normalized = mapped.nfkc().collect::<String>();
187
188    // A.5. Prohibited Output
189    let prohibited = normalized
190        .chars()
191        .find(|&c| {
192            tables::ascii_space_character(c) /* C.1.1 */ ||
193            tables::non_ascii_space_character(c) /* C.1.2 */ ||
194            tables::ascii_control_character(c) /* C.2.1 */ ||
195            tables::non_ascii_control_character(c) /* C.2.2 */ ||
196            tables::private_use(c) /* C.3 */ ||
197            tables::non_character_code_point(c) /* C.4 */ ||
198            tables::surrogate_code(c) /* C.5 */ ||
199            tables::inappropriate_for_plain_text(c) /* C.6 */ ||
200            tables::inappropriate_for_canonical_representation(c) /* C.7 */ ||
201            tables::change_display_properties_or_deprecated(c) /* C.9 */ ||
202            tables::tagging_character(c) /* C.9 */ ||
203            prohibited_node_character(c)
204        });
205    if let Some(c) = prohibited {
206        return Err(Error(ErrorCause::ProhibitedCharacter(c)));
207    }
208
209    // A.6. Bidirectional Characters
210    if is_prohibited_bidirectional_text(&normalized) {
211        return Err(Error(ErrorCause::ProhibitedBidirectionalText));
212    }
213
214    let unassigned = normalized
215        .chars()
216        .find(|&c| tables::unassigned_code_point(c));
217    if let Some(c) = unassigned {
218        return Err(Error(ErrorCause::ProhibitedCharacter(c)));
219    }
220
221    Ok(Cow::Owned(normalized))
222}
223
224// Additional characters not allowed in JID nodes, by RFC3920.
225fn prohibited_node_character(c: char) -> bool {
226    match c {
227        '"' | '&' | '\'' | '/' | ':' | '<' | '>' | '@' => true,
228        _ => false
229    }
230}
231
232/// Prepares a string with the Resourceprep profile of the stringprep algorithm.
233///
234/// Nameprep is defined in [RFC 3920, Appendix B][].
235///
236/// [RFC 3920, Appendix B]: https://tools.ietf.org/html/rfc3920#appendix-B
237pub fn resourceprep<'a>(s: &'a str) -> Result<Cow<'a, str>, Error> {
238    // B.3. Mapping
239    let mapped = s.chars()
240        .filter(|&c| !tables::commonly_mapped_to_nothing(c))
241        .collect::<String>();
242
243    // B.4. Normalization
244    let normalized = mapped.nfkc().collect::<String>();
245
246    // B.5. Prohibited Output
247    let prohibited = normalized
248        .chars()
249        .find(|&c| {
250            tables::non_ascii_space_character(c) /* C.1.2 */ ||
251            tables::ascii_control_character(c) /* C.2.1 */ ||
252            tables::non_ascii_control_character(c) /* C.2.2 */ ||
253            tables::private_use(c) /* C.3 */ ||
254            tables::non_character_code_point(c) /* C.4 */ ||
255            tables::surrogate_code(c) /* C.5 */ ||
256            tables::inappropriate_for_plain_text(c) /* C.6 */ ||
257            tables::inappropriate_for_canonical_representation(c) /* C.7 */ ||
258            tables::change_display_properties_or_deprecated(c) /* C.9 */ ||
259            tables::tagging_character(c) /* C.9 */
260        });
261    if let Some(c) = prohibited {
262        return Err(Error(ErrorCause::ProhibitedCharacter(c)));
263    }
264
265    // B.6. Bidirectional Characters
266    if is_prohibited_bidirectional_text(&normalized) {
267        return Err(Error(ErrorCause::ProhibitedBidirectionalText));
268    }
269
270    let unassigned = normalized
271        .chars()
272        .find(|&c| tables::unassigned_code_point(c));
273    if let Some(c) = unassigned {
274        return Err(Error(ErrorCause::ProhibitedCharacter(c)));
275    }
276
277    Ok(Cow::Owned(normalized))
278}
279
280#[cfg(test)]
281mod test {
282    use super::*;
283
284	fn assert_prohibited_character<T>(result: Result<T, Error>) {
285		match result {
286			Err(Error(ErrorCause::ProhibitedCharacter(_))) => (),
287			_ => assert!(false)
288		}
289	}
290
291    // RFC4013, 3. Examples
292    #[test]
293    fn saslprep_examples() {
294		assert_prohibited_character(saslprep("\u{0007}"));
295    }
296
297	#[test]
298	fn nodeprep_examples() {
299        assert_prohibited_character(nodeprep(" "));
300        assert_prohibited_character(nodeprep("\u{00a0}"));
301        assert_prohibited_character(nodeprep("foo@bar"));
302	}
303
304    #[test]
305    fn resourceprep_examples() {
306        assert_eq!("foo@bar", resourceprep("foo@bar").unwrap());
307    }
308}