use std::borrow::Cow;
pub(crate) enum Decoded<'a> {
Text(Cow<'a, str>),
Bytes(Vec<u8>),
}
pub(crate) fn decode(input: &str) -> Decoded<'_> {
let bytes = input.as_bytes();
let first = match bytes.iter().position(|&b| b == b'%' || b == b'+') {
Some(first) => first,
None => return Decoded::Text(Cow::Borrowed(input)),
};
let mut out = Vec::with_capacity(bytes.len());
out.extend_from_slice(&bytes[..first]);
let mut pos = first;
while pos < bytes.len() {
match bytes[pos] {
b'+' => {
out.push(b' ');
pos += 1;
}
b'%' => match (hex(bytes.get(pos + 1)), hex(bytes.get(pos + 2))) {
(Some(hi), Some(lo)) => {
out.push(hi << 4 | lo);
pos += 3;
}
_ => {
out.push(b'%');
pos += 1;
}
},
byte => {
out.push(byte);
pos += 1;
}
}
}
match String::from_utf8(out) {
Ok(text) => Decoded::Text(Cow::Owned(text)),
Err(err) => Decoded::Bytes(err.into_bytes()),
}
}
#[inline]
fn hex(byte: Option<&u8>) -> Option<u8> {
match *byte? {
b @ b'0'..=b'9' => Some(b - b'0'),
b @ b'a'..=b'f' => Some(b - b'a' + 10),
b @ b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
pub(crate) fn encode(input: &[u8], space_as_plus: bool, out: &mut String) {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
for &byte in input {
match byte {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'*' | b'-' | b'.' | b'_' => {
out.push(byte as char)
}
b' ' if space_as_plus => out.push('+'),
_ => {
out.push('%');
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0xf) as usize] as char);
}
}
}
}
#[cfg(test)]
fn decoded(input: &str) -> Result<String, Vec<u8>> {
match decode(input) {
Decoded::Text(text) => Ok(text.into_owned()),
Decoded::Bytes(bytes) => Err(bytes),
}
}
#[test]
fn test_decode() {
assert!(matches!(decode("abc"), Decoded::Text(Cow::Borrowed("abc"))));
assert_eq!(decoded("a+b%20c").unwrap(), "a b c");
assert_eq!(decoded("%5Ba%5d").unwrap(), "[a]");
assert_eq!(decoded("%C3%A4").unwrap(), "\u{e4}");
assert_eq!(decoded("100%").unwrap(), "100%");
assert_eq!(decoded("%2").unwrap(), "%2");
assert_eq!(decoded("%zz%41").unwrap(), "%zzA");
assert_eq!(decoded("%%41").unwrap(), "%A");
assert_eq!(decoded("%FF").unwrap_err(), [255]);
}
#[test]
fn test_encode() {
let mut out = String::new();
encode("a b&c=d/รค[]*-._~".as_bytes(), true, &mut out);
assert_eq!(out, "a+b%26c%3Dd%2F%C3%A4%5B%5D*-._%7E");
out.clear();
encode(b"a b", false, &mut out);
assert_eq!(out, "a%20b");
}