1const SAFE_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~";
4
5pub fn encode(data: &[u8]) -> String {
14 data.as_ref()
15 .iter()
16 .flat_map(|&b| {
17 if SAFE_CHARS.contains(&b) {
18 vec![b as char].into_iter()
19 } else {
20 let hex = format!("%{b:02X}");
21 hex.chars().collect::<Vec<_>>().into_iter()
22 }
23 })
24 .collect()
25}
26
27#[derive(Debug)]
28pub enum DecodeUrlError {
29 InvalidHex(String),
30 UnexpectedEnd,
31}
32
33pub fn decode(data: &str) -> Result<String, DecodeUrlError> {
35 let input = data.as_bytes();
36 let mut output = String::with_capacity(input.len());
37 let mut i = 0;
38
39 while i < input.len() {
40 match input[i] {
41 b'%' => {
42 if i + 2 >= input.len() {
43 return Err(DecodeUrlError::UnexpectedEnd);
44 }
45 let hex = &input[i + 1..=i + 2];
46 let hex_str = std::str::from_utf8(hex).unwrap_or("");
47 let byte = u8::from_str_radix(hex_str, 16)
48 .map_err(|_| DecodeUrlError::InvalidHex(hex_str.to_string()))?;
49 output.push(byte as char);
50 i += 3;
51 }
52 b => {
53 output.push(b as char);
54 i += 1;
55 }
56 }
57 }
58
59 Ok(output)
60}
61
62impl std::fmt::Display for DecodeUrlError {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 DecodeUrlError::InvalidHex(s) => write!(f, "invalid hex sequence '%{s}'"),
66 DecodeUrlError::UnexpectedEnd => write!(f, "unexpected end of percent-encoding"),
67 }
68 }
69}
70
71impl std::error::Error for DecodeUrlError {}