product_os_urlencoding/
lib.rs

1#![no_std]
2extern crate no_std_compat as std;
3extern crate alloc;
4
5use std::prelude::v1::*;
6
7use std::string::String;
8use std::string::FromUtf8Error;
9use std::fmt::{self, Display};
10
11pub fn encode(data: &str) -> String {
12    let escaped = encode_into(data);
13    let string = String::from_utf8_lossy(escaped.as_slice());
14    string.to_string()
15}
16
17#[inline]
18fn encode_into(data: &str) -> Vec<u8> {
19    let mut escaped = vec![];
20
21    for byte in data.as_bytes().iter() {
22        match byte.to_owned() {
23            b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' |  b'-' | b'.' | b'_' | b'~' => {
24                escaped.push(byte.to_owned());
25            },
26            other => {
27                escaped.push(b'%');
28                escaped.push(to_hex_digit(other.to_owned() >> 4));
29                escaped.push(to_hex_digit(other & 15));
30            },
31        }
32    }
33
34    escaped
35}
36
37#[inline]
38fn from_hex_digit(digit: u8) -> Option<u8> {
39    match digit {
40        b'0'..=b'9' => Some(digit - b'0'),
41        b'A'..=b'F' => Some(digit - b'A' + 10),
42        b'a'..=b'f' => Some(digit - b'a' + 10),
43        _ => None,
44    }
45}
46
47#[inline]
48fn to_hex_digit(digit: u8) -> u8 {
49    match digit {
50        0..=9 => b'0' + digit,
51        10..=255 => b'A' - 10 + digit
52    }
53}
54
55pub fn decode(string: &str) -> Result<String, FromUrlEncodingError> {
56    let mut out: Vec<u8> = vec![];
57    let mut bytes = string.as_bytes().iter().copied();
58    while let Some(b) = bytes.next() {
59        match b {
60            b'%' => {
61                match bytes.next() {
62                    Some(first) => match from_hex_digit(first.to_owned()) {
63                        Some(first_val) => match bytes.next() {
64                            Some(second) => match from_hex_digit(second.to_owned()) {
65                                Some(second_val) => {
66                                    out.push((first_val << 4) | second_val);
67                                },
68                                None => {
69                                    out.push(b'%');
70                                    out.push(first);
71                                    out.push(second);
72                                },
73                            },
74                            None => {
75                                out.push(b'%');
76                                out.push(first);
77                            },
78                        },
79                        None => {
80                            out.push(b'%');
81                            out.push(first);
82                        },
83                    },
84                    None => out.push(b'%'),
85                };
86            },
87            other => out.push(other),
88        }
89    }
90    String::from_utf8(out).map_err(|error| FromUrlEncodingError::Utf8CharacterError {error})
91}
92
93#[derive(Debug)]
94pub enum FromUrlEncodingError {
95    UriCharacterError { character: char, index: usize },
96    Utf8CharacterError { error: FromUtf8Error },
97}
98
99
100impl Display for FromUrlEncodingError {
101    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
102        match self {
103            &FromUrlEncodingError::UriCharacterError {character, index} =>
104                write!(f, "invalid URI char [{}] at [{}]", character, index),
105            &FromUrlEncodingError::Utf8CharacterError {ref error} =>
106                write!(f, "invalid utf8 char: {}", error)
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use alloc::string::String;
114    use super::encode;
115    use super::decode;
116    use super::from_hex_digit;
117
118    #[test]
119    fn it_encodes_successfully() {
120        let expected = "this%20that";
121        assert_eq!(expected, encode("this that"));
122    }
123
124    #[test]
125    fn it_encodes_successfully_emoji() {
126        let emoji_string = "👾 Exterminate!";
127        let expected = "%F0%9F%91%BE%20Exterminate%21";
128        assert_eq!(expected, encode(emoji_string));
129    }
130
131    #[test]
132    fn it_decodes_successfully() {
133        let expected = String::from("this that");
134        let encoded = "this%20that";
135        assert_eq!(expected, decode(encoded).unwrap());
136    }
137
138    #[test]
139    fn it_decodes_successfully_emoji() {
140        let expected = String::from("👾 Exterminate!");
141        let encoded = "%F0%9F%91%BE%20Exterminate%21";
142        assert_eq!(expected, decode(encoded).unwrap());
143    }
144
145    #[test]
146    fn it_decodes_unsuccessfully_emoji() {
147        let bad_encoded_string = "👾 Exterminate!";
148
149        assert_eq!(bad_encoded_string, decode(bad_encoded_string).unwrap());
150    }
151
152
153    #[test]
154    fn misc() {
155        assert_eq!(3, from_hex_digit(b'3').unwrap());
156        assert_eq!(10, from_hex_digit(b'a').unwrap());
157        assert_eq!(15, from_hex_digit(b'F').unwrap());
158        assert_eq!(None, from_hex_digit(b'G'));
159        assert_eq!(None, from_hex_digit(9));
160
161        assert_eq!("pureascii", encode("pureascii"));
162        assert_eq!("pureascii", decode("pureascii").unwrap());
163        assert_eq!("", encode(""));
164        assert_eq!("", decode("").unwrap());
165        assert_eq!("%00", encode("\0"));
166        assert_eq!("\0", decode("\0").unwrap());
167        assert!(decode("%F0%0F%91%BE%20Hello%21").is_err());
168        assert_eq!("this%2that", decode("this%2that").unwrap());
169        assert_eq!("this that", decode("this%20that").unwrap());
170        assert_eq!("this that%", decode("this%20that%").unwrap());
171        assert_eq!("this that%2", decode("this%20that%2").unwrap());
172    }
173}