buup/transformers/
url_decode.rs

1use crate::{Transform, TransformError, TransformerCategory};
2
3/// URL decode transformer
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct UrlDecode;
6
7impl Transform for UrlDecode {
8    fn name(&self) -> &'static str {
9        "URL Decode"
10    }
11
12    fn id(&self) -> &'static str {
13        "urldecode"
14    }
15
16    fn description(&self) -> &'static str {
17        "Decode URL-encoded text"
18    }
19
20    fn category(&self) -> TransformerCategory {
21        TransformerCategory::Decoder
22    }
23
24    fn transform(&self, input: &str) -> Result<String, TransformError> {
25        url_decode(input).map_err(|_e| TransformError::UrlDecodeError)
26    }
27
28    fn default_test_input(&self) -> &'static str {
29        "Hello%2C+World%21"
30    }
31}
32
33/// URL decodes a string without external dependencies
34fn url_decode(input: &str) -> Result<String, &'static str> {
35    // We'll collect decoded bytes and convert to UTF-8 at the end
36    let mut decoded_bytes = Vec::with_capacity(input.len());
37    let mut bytes = input.bytes();
38
39    while let Some(byte) = bytes.next() {
40        match byte {
41            // '+' is decoded as space
42            b'+' => decoded_bytes.push(b' '),
43            // '%' begins a percent-encoded sequence
44            b'%' => {
45                // Get the two hex digits
46                let hi = bytes
47                    .next()
48                    .ok_or("Invalid URL encoding: unexpected end of input")?;
49                let lo = bytes
50                    .next()
51                    .ok_or("Invalid URL encoding: unexpected end of input")?;
52
53                // Parse the hex digits to get the byte value
54                let hex_to_digit = |b| match b {
55                    b'0'..=b'9' => Ok(b - b'0'),
56                    b'A'..=b'F' => Ok(b - b'A' + 10),
57                    b'a'..=b'f' => Ok(b - b'a' + 10),
58                    _ => Err("Invalid URL encoding: invalid hex digit"),
59                };
60
61                let high_nibble = hex_to_digit(hi)?;
62                let low_nibble = hex_to_digit(lo)?;
63
64                let decoded_byte = (high_nibble << 4) | low_nibble;
65                decoded_bytes.push(decoded_byte);
66            }
67            // Regular characters
68            _ => decoded_bytes.push(byte),
69        }
70    }
71
72    // Convert the collected bytes to a UTF-8 string
73    String::from_utf8(decoded_bytes).map_err(|_| "Invalid UTF-8 sequence in decoded URL")
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_url_decode() {
82        let transformer = UrlDecode;
83        assert_eq!(
84            transformer
85                .transform(transformer.default_test_input())
86                .unwrap(),
87            "Hello, World!"
88        );
89        assert_eq!(transformer.transform("a+b").unwrap(), "a b");
90        assert_eq!(transformer.transform("100%25").unwrap(), "100%");
91    }
92}