Skip to main content

secure_output/
url.rs

1//! URL context encoder.
2
3use std::borrow::Cow;
4
5use crate::encode::OutputEncoder;
6
7/// Encodes strings for safe inclusion in URLs (percent-encoding per RFC 3986).
8///
9/// Only unreserved characters (`A-Z`, `a-z`, `0-9`, `-`, `_`, `.`, `~`) are
10/// passed through unchanged. All other bytes are percent-encoded with uppercase
11/// hex digits. Null bytes are stripped (not percent-encoded as `%00`).
12///
13/// Returns [`Cow::Borrowed`] when the input contains only unreserved characters.
14///
15/// # Examples
16///
17/// ```
18/// use secure_output::url;
19///
20/// let safe = url::encode("hello world");
21/// assert_eq!(safe, "hello%20world");
22///
23/// let safe = url::encode("safe-value_123");
24/// assert_eq!(safe, "safe-value_123");
25/// ```
26#[derive(Clone, Copy, Debug, Default)]
27pub struct UrlEncoder;
28
29fn is_unreserved(b: u8) -> bool {
30    b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~')
31}
32
33impl OutputEncoder for UrlEncoder {
34    fn encode<'a>(&self, input: &'a str) -> Cow<'a, str> {
35        let needs = input.bytes().any(|b| !is_unreserved(b));
36        if !needs {
37            return Cow::Borrowed(input);
38        }
39        let mut out = String::with_capacity(input.len() * 2);
40        for b in input.bytes() {
41            if b == 0 {
42                // strip null bytes
43                continue;
44            }
45            if is_unreserved(b) {
46                out.push(b as char);
47            } else {
48                let hi = char::from_digit(u32::from(b >> 4), 16)
49                    .unwrap_or('0')
50                    .to_ascii_uppercase();
51                let lo = char::from_digit(u32::from(b & 0xF), 16)
52                    .unwrap_or('0')
53                    .to_ascii_uppercase();
54                out.push('%');
55                out.push(hi);
56                out.push(lo);
57            }
58        }
59        Cow::Owned(out)
60    }
61}
62
63/// Convenience free function for URL percent-encoding.
64///
65/// Equivalent to `UrlEncoder.encode(input)`.
66///
67/// # Examples
68///
69/// ```
70/// use secure_output::url;
71///
72/// assert_eq!(url::encode("a b"), "a%20b");
73/// ```
74#[must_use]
75pub fn encode(input: &str) -> Cow<'_, str> {
76    UrlEncoder.encode(input)
77}