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