1pub fn decode(input: &str) -> String {
5 if !input.contains('%') && !input.contains('+') {
6 return input.to_string();
7 }
8
9 let bytes = input.as_bytes();
10 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
11 let mut i = 0;
12 while i < bytes.len() {
13 match bytes[i] {
14 b'%' if i + 2 < bytes.len() => match hex_pair(bytes[i + 1], bytes[i + 2]) {
15 Some(byte) => {
16 out.push(byte);
17 i += 3;
18 }
19 None => {
21 out.push(b'%');
22 i += 1;
23 }
24 },
25 b'+' => {
26 out.push(b' ');
27 i += 1;
28 }
29 byte => {
30 out.push(byte);
31 i += 1;
32 }
33 }
34 }
35
36 String::from_utf8_lossy(&out).into_owned()
37}
38
39fn hex_pair(high: u8, low: u8) -> Option<u8> {
40 Some(hex_digit(high)? << 4 | hex_digit(low)?)
41}
42
43fn hex_digit(byte: u8) -> Option<u8> {
44 match byte {
45 b'0'..=b'9' => Some(byte - b'0'),
46 b'a'..=b'f' => Some(byte - b'a' + 10),
47 b'A'..=b'F' => Some(byte - b'A' + 10),
48 _ => None,
49 }
50}
51
52pub fn encode(input: &str) -> String {
54 let mut out = String::with_capacity(input.len());
55 for byte in input.bytes() {
56 match byte {
57 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
58 out.push(byte as char)
59 }
60 _ => out.push_str(&format!("%{byte:02X}")),
61 }
62 }
63 out
64}
65
66pub fn parse_query(query: &str) -> Vec<(String, String)> {
71 query
72 .split('&')
73 .filter(|part| !part.is_empty())
74 .map(|part| match part.split_once('=') {
75 Some((key, value)) => (decode(key), decode(value)),
76 None => (decode(part), String::new()),
77 })
78 .collect()
79}
80
81pub fn split_target(target: &str) -> (&str, &str) {
83 match target.split_once('?') {
84 Some((path, query)) => (path, query),
85 None => (target, ""),
86 }
87}
88
89pub fn normalize_path(path: &str) -> Option<String> {
94 let mut segments: Vec<&str> = Vec::new();
95 for segment in path.split('/') {
96 match segment {
97 "" | "." => continue,
98 ".." => {
99 segments.pop()?;
100 }
101 s if s.contains('\\') || s.contains('\0') => return None,
103 s => segments.push(s),
104 }
105 }
106 Some(format!("/{}", segments.join("/")))
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn decodes_escapes_and_plus() {
115 assert_eq!(decode("hello+world"), "hello world");
116 assert_eq!(decode("caf%C3%A9"), "café");
117 assert_eq!(decode("100%"), "100%");
118 assert_eq!(decode("plain"), "plain");
119 }
120
121 #[test]
122 fn encoding_round_trips() {
123 let original = "a b/c?d=é";
124 assert_eq!(decode(&encode(original)), original);
125 }
126
127 #[test]
128 fn parses_query_pairs_in_order() {
129 let pairs = parse_query("name=Rust+lavel&tags=a&tags=b&empty");
130
131 assert_eq!(pairs[0], ("name".to_string(), "Rust lavel".to_string()));
132 assert_eq!(pairs[2], ("tags".to_string(), "b".to_string()));
133 assert_eq!(pairs[3], ("empty".to_string(), String::new()));
134 }
135
136 #[test]
137 fn splits_a_request_target() {
138 assert_eq!(split_target("/users?page=2"), ("/users", "page=2"));
139 assert_eq!(split_target("/users"), ("/users", ""));
140 }
141
142 #[test]
143 fn normalization_blocks_directory_traversal() {
144 assert_eq!(normalize_path("/css//app.css").as_deref(), Some("/css/app.css"));
145 assert_eq!(normalize_path("/a/./b").as_deref(), Some("/a/b"));
146 assert_eq!(normalize_path("/a/../b").as_deref(), Some("/b"));
147 assert_eq!(normalize_path("/../etc/passwd"), None);
148 }
149}