rs_urlencoding/
dec.rs

1use std::borrow::Cow;
2use std::string::FromUtf8Error;
3
4#[inline]
5pub(crate) fn from_hex_digit(digit: u8) -> Option<u8> {
6    match digit {
7        b'0'..=b'9' => Some(digit - b'0'),
8        b'A'..=b'F' => Some(digit - b'A' + 10),
9        b'a'..=b'f' => Some(digit - b'a' + 10),
10        _ => None,
11    }
12}
13
14/// Decode percent-encoded string assuming UTF-8 encoding.
15///
16/// If you need a `String`, call `.into_owned()` (not `.to_owned()`).
17///
18/// Unencoded `+` is preserved literally, and _not_ changed to a space.
19pub fn decode(data: &str) -> Result<Cow<str>, FromUtf8Error> {
20    match decode_binary(data.as_bytes()) {
21        Cow::Borrowed(_) => Ok(Cow::Borrowed(data)),
22        Cow::Owned(s) => Ok(Cow::Owned(String::from_utf8(s)?)),
23    }
24}
25
26/// Decode percent-encoded string as binary data, in any encoding.
27///
28/// Unencoded `+` is preserved literally, and _not_ changed to a space.
29pub fn decode_binary(mut data: &[u8]) -> Cow<[u8]> {
30    let mut out: Vec<u8> = Vec::with_capacity(data.len());
31    loop {
32        let mut parts = data.splitn(2, |&c| c == b'%');
33        // first the decoded non-% part
34        out.extend_from_slice(parts.next().unwrap());
35        // then decode one %xx
36        match parts.next() {
37            None => {
38                if out.is_empty() {
39                    // avoids utf-8 check
40                    return data.into();
41                }
42                break;
43            },
44            Some(rest) => match rest.get(0..2) {
45                Some(&[first, second]) => match from_hex_digit(first) {
46                    Some(first_val) => match from_hex_digit(second) {
47                        Some(second_val) => {
48                            out.push((first_val << 4) | second_val);
49                            data = &rest[2..];
50                        },
51                        None => {
52                            out.extend_from_slice(&[b'%', first]);
53                            data = &rest[1..];
54                        },
55                    },
56                    None => {
57                        out.push(b'%');
58                        data = rest;
59                    },
60                },
61                _ => {
62                    // too short
63                    out.push(b'%');
64                    out.extend_from_slice(rest);
65                    break;
66                },
67            },
68        };
69    }
70    Cow::Owned(out)
71}