stringprep/
lib.rs
1#![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#[derive(Debug)]
20enum ErrorCause {
21 ProhibitedCharacter(char),
23 ProhibitedBidirectionalText,
25}
26
27#[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
46pub fn saslprep<'a>(s: &'a str) -> Result<Cow<'a, str>, Error> {
52 if s.chars()
54 .all(|c| c.is_ascii() && !tables::ascii_control_character(c)) {
55 return Ok(Cow::Borrowed(s));
56 }
57
58 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 let normalized = mapped.nfkc().collect::<String>();
69
70 let prohibited = normalized
72 .chars()
73 .find(|&c| {
74 tables::non_ascii_space_character(c) ||
75 tables::ascii_control_character(c) ||
76 tables::non_ascii_control_character(c) ||
77 tables::private_use(c) ||
78 tables::non_character_code_point(c) ||
79 tables::surrogate_code(c) ||
80 tables::inappropriate_for_plain_text(c) ||
81 tables::inappropriate_for_canonical_representation(c) ||
82 tables::change_display_properties_or_deprecated(c) ||
83 tables::tagging_character(c) });
85 if let Some(c) = prohibited {
86 return Err(Error(ErrorCause::ProhibitedCharacter(c)));
87 }
88
89 if is_prohibited_bidirectional_text(&normalized) {
91 return Err(Error(ErrorCause::ProhibitedBidirectionalText));
92 }
93
94 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
105fn is_prohibited_bidirectional_text(s: &str) -> bool {
107 if s.contains(tables::bidi_r_or_al) {
108 if s.contains(tables::bidi_l) {
111 return true;
112 }
113
114 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
126pub fn nameprep<'a>(s: &'a str) -> Result<Cow<'a, str>, Error> {
132 let mapped = s.chars()
134 .filter(|&c| !tables::commonly_mapped_to_nothing(c))
135 .flat_map(tables::case_fold_for_nfkc);
136
137 let normalized = mapped.nfkc().collect::<String>();
139
140 let prohibited = normalized
142 .chars()
143 .find(|&c| {
144 tables::non_ascii_space_character(c) ||
145 tables::non_ascii_control_character(c) ||
146 tables::private_use(c) ||
147 tables::non_character_code_point(c) ||
148 tables::surrogate_code(c) ||
149 tables::inappropriate_for_plain_text(c) ||
150 tables::inappropriate_for_canonical_representation(c) ||
151 tables::change_display_properties_or_deprecated(c) ||
152 tables::tagging_character(c) });
154 if let Some(c) = prohibited {
155 return Err(Error(ErrorCause::ProhibitedCharacter(c)));
156 }
157
158 if is_prohibited_bidirectional_text(&normalized) {
160 return Err(Error(ErrorCause::ProhibitedBidirectionalText));
161 }
162
163 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
174pub fn nodeprep<'a>(s: &'a str) -> Result<Cow<'a, str>, Error> {
180 let mapped = s.chars()
182 .filter(|&c| !tables::commonly_mapped_to_nothing(c))
183 .flat_map(tables::case_fold_for_nfkc);
184
185 let normalized = mapped.nfkc().collect::<String>();
187
188 let prohibited = normalized
190 .chars()
191 .find(|&c| {
192 tables::ascii_space_character(c) ||
193 tables::non_ascii_space_character(c) ||
194 tables::ascii_control_character(c) ||
195 tables::non_ascii_control_character(c) ||
196 tables::private_use(c) ||
197 tables::non_character_code_point(c) ||
198 tables::surrogate_code(c) ||
199 tables::inappropriate_for_plain_text(c) ||
200 tables::inappropriate_for_canonical_representation(c) ||
201 tables::change_display_properties_or_deprecated(c) ||
202 tables::tagging_character(c) ||
203 prohibited_node_character(c)
204 });
205 if let Some(c) = prohibited {
206 return Err(Error(ErrorCause::ProhibitedCharacter(c)));
207 }
208
209 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
224fn prohibited_node_character(c: char) -> bool {
226 match c {
227 '"' | '&' | '\'' | '/' | ':' | '<' | '>' | '@' => true,
228 _ => false
229 }
230}
231
232pub fn resourceprep<'a>(s: &'a str) -> Result<Cow<'a, str>, Error> {
238 let mapped = s.chars()
240 .filter(|&c| !tables::commonly_mapped_to_nothing(c))
241 .collect::<String>();
242
243 let normalized = mapped.nfkc().collect::<String>();
245
246 let prohibited = normalized
248 .chars()
249 .find(|&c| {
250 tables::non_ascii_space_character(c) ||
251 tables::ascii_control_character(c) ||
252 tables::non_ascii_control_character(c) ||
253 tables::private_use(c) ||
254 tables::non_character_code_point(c) ||
255 tables::surrogate_code(c) ||
256 tables::inappropriate_for_plain_text(c) ||
257 tables::inappropriate_for_canonical_representation(c) ||
258 tables::change_display_properties_or_deprecated(c) ||
259 tables::tagging_character(c) });
261 if let Some(c) = prohibited {
262 return Err(Error(ErrorCause::ProhibitedCharacter(c)));
263 }
264
265 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 #[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}