1 use std::str;
2 use std::string::FromUtf8Error;
3
encode(data: &str) -> String4 pub fn encode(data: &str) -> String {
5 let mut escaped = String::new();
6 for b in data.as_bytes().iter() {
7 match *b as char {
8 // Accepted characters
9 'A'...'Z' | 'a'...'z' | '0'...'9' | '-' | '_' | '.' | '~' => escaped.push(*b as char),
10
11 // Everything else is percent-encoded
12 b => escaped.push_str(format!("%{:02X}", b as u32).as_str()),
13 };
14 }
15 return escaped;
16 }
17
decode(data: &str) -> Result<String, FromUrlEncodingError>18 pub fn decode(data: &str) -> Result<String, FromUrlEncodingError> {
19 validate_urlencoded_str(data)?;
20 let mut unescaped_bytes: Vec<u8> = Vec::new();
21 let mut bytes = data.bytes();
22 // If validate_urlencoded_str returned Ok, then we know
23 // every '%' is followed by 2 hex characters
24 while let Some(b) = bytes.next() {
25 match b as char {
26 '%' => {
27 let bytes_to_decode = &[bytes.next().unwrap(), bytes.next().unwrap()];
28 let hex_str = str::from_utf8(bytes_to_decode).unwrap();
29 unescaped_bytes.push(u8::from_str_radix(hex_str, 16).unwrap());
30 },
31 _ => {
32 // Assume whoever did the encoding intended what we got
33 unescaped_bytes.push(b);
34 }
35 }
36 }
37 String::from_utf8(unescaped_bytes).or_else(|e| Err(FromUrlEncodingError::Utf8CharacterError {
38 error: e,
39 }))
40 }
41
42 // Validates every '%' character is followed by exactly 2 hex
43 // digits.
validate_urlencoded_str(data: &str) -> Result<(), FromUrlEncodingError>44 fn validate_urlencoded_str(data: &str) -> Result<(), FromUrlEncodingError> {
45 let mut iter = data.char_indices();
46 while let Some((idx, chr)) = iter.next() {
47 match chr {
48 '%' => {
49 validate_percent_encoding(&mut iter, idx)?;
50 },
51 _ => continue,
52 }
53 }
54 Ok(())
55 }
56
57 // Validates the next two characters returned by the provided iterator are
58 // hexadecimal digits.
validate_percent_encoding(iter: &mut str::CharIndices, current_idx: usize) -> Result<(), FromUrlEncodingError>59 fn validate_percent_encoding(iter: &mut str::CharIndices, current_idx: usize) -> Result<(), FromUrlEncodingError> {
60 for _ in 0..2 {
61 match iter.next() {
62 // Only hex digits are valid
63 Some((_, c)) if c.is_digit(16) => {
64 continue
65 },
66 Some((i, c)) => return Err(FromUrlEncodingError::UriCharacterError {
67 character: c,
68 index: i,
69 }),
70 // We got a '%' without 2 characters after it, so mark the '%' as bad
71 None => return Err(FromUrlEncodingError::UriCharacterError {
72 character: '%',
73 index: current_idx,
74 }),
75 }
76 }
77 Ok(())
78 }
79
80 #[derive(Debug)]
81 pub enum FromUrlEncodingError {
82 UriCharacterError { character: char, index: usize },
83 Utf8CharacterError { error: FromUtf8Error },
84 }
85
86 #[cfg(test)]
87 mod tests {
88 use super::encode;
89 use super::decode;
90 use super::FromUrlEncodingError;
91
92 #[test]
it_encodes_successfully()93 fn it_encodes_successfully() {
94 let expected = "this%20that";
95 assert_eq!(expected, encode("this that"));
96 }
97
98 #[test]
it_encodes_successfully_emoji()99 fn it_encodes_successfully_emoji() {
100 let emoji_string = " Exterminate!";
101 let expected = "%F0%9F%91%BE%20Exterminate%21";
102 assert_eq!(expected, encode(emoji_string));
103 }
104
105 #[test]
it_decodes_successfully()106 fn it_decodes_successfully() {
107 let expected = String::from("this that");
108 let encoded = "this%20that";
109 assert_eq!(expected, decode(encoded).unwrap());
110 }
111
112 #[test]
it_decodes_successfully_emoji()113 fn it_decodes_successfully_emoji() {
114 let expected = String::from(" Exterminate!");
115 let encoded = "%F0%9F%91%BE%20Exterminate%21";
116 assert_eq!(expected, decode(encoded).unwrap());
117 }
118
119 #[test]
it_decodes_unsuccessfully_emoji()120 fn it_decodes_unsuccessfully_emoji() {
121 let bad_encoded_string = " Exterminate!";
122
123 assert_eq!(bad_encoded_string, decode(bad_encoded_string).unwrap());
124 }
125
126 #[test]
it_decodes_unsuccessfuly_bad_percent_01()127 fn it_decodes_unsuccessfuly_bad_percent_01() {
128 let bad_encoded_string = "this%2that";
129 let expected_idx = 6;
130 let expected_char = 't';
131
132 match decode(bad_encoded_string).unwrap_err() {
133 FromUrlEncodingError::UriCharacterError { index: i, character: c } => {
134 assert_eq!(expected_idx, i);
135 assert_eq!(expected_char, c)
136 },
137 _ => panic!()
138 }
139 }
140
141 #[test]
it_decodes_unsuccessfuly_bad_percent_02()142 fn it_decodes_unsuccessfuly_bad_percent_02() {
143 let bad_encoded_string = "this%20that%";
144 let expected_idx = 11;
145 let expected_char = '%';
146
147 match decode(bad_encoded_string).unwrap_err() {
148 FromUrlEncodingError::UriCharacterError { index: i, character: c } => {
149 assert_eq!(expected_idx, i);
150 assert_eq!(expected_char, c)
151 },
152 _ => panic!()
153 }
154 }
155
156 #[test]
it_decodes_unsuccessfuly_bad_percent_03()157 fn it_decodes_unsuccessfuly_bad_percent_03() {
158 let bad_encoded_string = "this%20that%2";
159 let expected_idx = 11;
160 let expected_char = '%';
161
162 match decode(bad_encoded_string).unwrap_err() {
163 FromUrlEncodingError::UriCharacterError { index: i, character: c } => {
164 assert_eq!(expected_idx, i);
165 assert_eq!(expected_char, c)
166 },
167 _ => panic!()
168 }
169 }
170 }
171