Skip to main content

otp/encoding/
url.rs

1//! URL encoding/decoding, using [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1) (Percent-encoding).
2
3const SAFE_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~";
4
5/// # Example:
6/// ```rust
7/// use otp::encoding::url;
8///
9/// let s = b"hello@example.com";
10/// let encoded = url::encode(s);
11/// assert_eq!("hello%40example.com", encoded.as_str());
12/// ```
13pub 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
33/// Percent-decodes a URL-encoded string.
34pub 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 {}