contextual_encoder/
uri.rs1use std::fmt;
15
16pub fn for_uri_component(input: &str) -> String {
37 let bytes = input.as_bytes();
38 let unreserved = bytes.iter().filter(|b| is_unreserved(**b)).count();
39 let capacity = unreserved + 3 * (bytes.len() - unreserved);
40 let mut out = String::with_capacity(capacity);
41 write_uri_component(&mut out, input).expect("writing to string cannot fail");
42 out
43}
44
45pub fn write_uri_component<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
49 let bytes = input.as_bytes();
50 let mut last_written = 0;
51
52 for (i, &byte) in bytes.iter().enumerate() {
53 if !is_unreserved(byte) {
54 if last_written < i {
56 out.write_str(&input[last_written..i])?;
58 }
59 write!(out, "%{:02X}", byte)?;
60 last_written = i + 1;
61 }
62 }
63
64 if last_written < bytes.len() {
66 out.write_str(&input[last_written..])?;
67 }
68 Ok(())
69}
70
71pub fn for_uri_path(input: &str) -> String {
103 let bytes = input.as_bytes();
104 let safe = bytes
105 .iter()
106 .filter(|b| is_unreserved(**b) || **b == b'/')
107 .count();
108 let capacity = safe + 3 * (bytes.len() - safe);
109 let mut out = String::with_capacity(capacity);
110 write_uri_path(&mut out, input).expect("writing to string cannot fail");
111 out
112}
113
114pub fn write_uri_path<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
118 let bytes = input.as_bytes();
119 let mut last_written = 0;
120
121 for (i, &byte) in bytes.iter().enumerate() {
122 if !is_unreserved(byte) && byte != b'/' {
123 if last_written < i {
125 out.write_str(&input[last_written..i])?;
126 }
127 write!(out, "%{:02X}", byte)?;
128 last_written = i + 1;
129 }
130 }
131
132 if last_written < bytes.len() {
134 out.write_str(&input[last_written..])?;
135 }
136 Ok(())
137}
138
139fn is_unreserved(b: u8) -> bool {
141 matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~')
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn uri_component_no_encoding_needed() {
150 assert_eq!(for_uri_component("hello"), "hello");
151 assert_eq!(for_uri_component(""), "");
152 assert_eq!(for_uri_component("ABCxyz019"), "ABCxyz019");
153 assert_eq!(for_uri_component("-._~"), "-._~");
154 }
155
156 #[test]
157 fn uri_component_encodes_space() {
158 assert_eq!(for_uri_component("a b"), "a%20b");
159 }
160
161 #[test]
162 fn uri_component_encodes_reserved_chars() {
163 assert_eq!(for_uri_component("a=b"), "a%3Db");
164 assert_eq!(for_uri_component("a&b"), "a%26b");
165 assert_eq!(for_uri_component("a+b"), "a%2Bb");
166 assert_eq!(for_uri_component("a?b"), "a%3Fb");
167 assert_eq!(for_uri_component("a#b"), "a%23b");
168 assert_eq!(for_uri_component("a/b"), "a%2Fb");
169 }
170
171 #[test]
172 fn uri_component_encodes_html_significant() {
173 assert_eq!(for_uri_component("<script>"), "%3Cscript%3E");
174 assert_eq!(for_uri_component(r#""quoted""#), "%22quoted%22");
175 }
176
177 #[test]
178 fn uri_component_encodes_two_byte_utf8() {
179 assert_eq!(for_uri_component("\u{00A0}"), "%C2%A0");
181 assert_eq!(for_uri_component("é"), "%C3%A9");
183 }
184
185 #[test]
186 fn uri_component_encodes_three_byte_utf8() {
187 assert_eq!(for_uri_component("\u{0800}"), "%E0%A0%80");
189 assert_eq!(for_uri_component("世"), "%E4%B8%96");
191 }
192
193 #[test]
194 fn uri_component_encodes_four_byte_utf8() {
195 assert_eq!(for_uri_component("\u{10000}"), "%F0%90%80%80");
197 assert_eq!(for_uri_component("😀"), "%F0%9F%98%80");
199 }
200
201 #[test]
202 fn uri_component_encodes_control_chars() {
203 assert_eq!(for_uri_component("\x00"), "%00");
204 assert_eq!(for_uri_component("\x1F"), "%1F");
205 assert_eq!(for_uri_component("\x7F"), "%7F");
206 }
207
208 #[test]
209 fn uri_component_mixed() {
210 assert_eq!(
211 for_uri_component("key=hello world&foo=bar"),
212 "key%3Dhello%20world%26foo%3Dbar"
213 );
214 }
215
216 #[test]
217 fn uri_component_writer_variant() {
218 let mut out = String::new();
219 write_uri_component(&mut out, "a b").unwrap();
220 assert_eq!(out, "a%20b");
221 }
222
223 #[test]
226 fn uri_path_no_encoding_needed() {
227 assert_eq!(for_uri_path("hello"), "hello");
228 assert_eq!(for_uri_path(""), "");
229 assert_eq!(for_uri_path("-._~"), "-._~");
230 }
231
232 #[test]
233 fn uri_path_preserves_slashes() {
234 assert_eq!(for_uri_path("/a/b/c"), "/a/b/c");
235 assert_eq!(for_uri_path("/"), "/");
236 assert_eq!(for_uri_path("//"), "//");
237 assert_eq!(for_uri_path("a/b"), "a/b");
238 }
239
240 #[test]
241 fn uri_path_encodes_reserved_except_slash() {
242 assert_eq!(for_uri_path("a=b"), "a%3Db");
243 assert_eq!(for_uri_path("a&b"), "a%26b");
244 assert_eq!(for_uri_path("a?b"), "a%3Fb");
245 assert_eq!(for_uri_path("a#b"), "a%23b");
246 }
247
248 #[test]
249 fn uri_path_encodes_space() {
250 assert_eq!(for_uri_path("/a b/c d"), "/a%20b/c%20d");
251 }
252
253 #[test]
254 fn uri_path_encodes_multibyte() {
255 assert_eq!(for_uri_path("/café"), "/caf%C3%A9");
256 assert_eq!(for_uri_path("/世界"), "/%E4%B8%96%E7%95%8C");
257 assert_eq!(for_uri_path("/😀"), "/%F0%9F%98%80");
258 }
259
260 #[test]
261 fn uri_path_writer_variant() {
262 let mut out = String::new();
263 write_uri_path(&mut out, "/a b/c").unwrap();
264 assert_eq!(out, "/a%20b/c");
265 }
266}