contextual_encoder/
uri.rs1use std::fmt;
18
19pub fn for_uri_component(input: &str) -> String {
36 let mut out = String::with_capacity(input.len());
37 write_uri_component(&mut out, input).expect("writing to string cannot fail");
38 out
39}
40
41pub fn write_uri_component<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
45 let bytes = input.as_bytes();
46 let mut last_written = 0;
47
48 for (i, &byte) in bytes.iter().enumerate() {
49 if !is_unreserved(byte) {
50 if last_written < i {
52 out.write_str(&input[last_written..i])?;
54 }
55 write!(out, "%{:02X}", byte)?;
56 last_written = i + 1;
57 }
58 }
59
60 if last_written < bytes.len() {
62 out.write_str(&input[last_written..])?;
63 }
64 Ok(())
65}
66
67pub fn for_uri_path(input: &str) -> String {
95 let mut out = String::with_capacity(input.len());
96 write_uri_path(&mut out, input).expect("writing to string cannot fail");
97 out
98}
99
100pub fn write_uri_path<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
104 let bytes = input.as_bytes();
105 let mut last_written = 0;
106
107 for (i, &byte) in bytes.iter().enumerate() {
108 if !is_unreserved(byte) && byte != b'/' {
109 if last_written < i {
111 out.write_str(&input[last_written..i])?;
112 }
113 write!(out, "%{:02X}", byte)?;
114 last_written = i + 1;
115 }
116 }
117
118 if last_written < bytes.len() {
120 out.write_str(&input[last_written..])?;
121 }
122 Ok(())
123}
124
125pub fn for_form_urlencoded(input: &str) -> String {
158 let mut out = String::with_capacity(input.len());
159 write_form_urlencoded(&mut out, input).expect("writing to string cannot fail");
160 out
161}
162
163pub fn write_form_urlencoded<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
168 let bytes = input.as_bytes();
169 let mut last_written = 0;
170
171 for (i, &byte) in bytes.iter().enumerate() {
172 if byte == b' ' {
173 if last_written < i {
174 out.write_str(&input[last_written..i])?;
175 }
176 out.write_char('+')?;
177 last_written = i + 1;
178 } else if !is_form_safe(byte) {
179 if last_written < i {
180 out.write_str(&input[last_written..i])?;
181 }
182 write!(out, "%{:02X}", byte)?;
183 last_written = i + 1;
184 }
185 }
186
187 if last_written < bytes.len() {
188 out.write_str(&input[last_written..])?;
189 }
190 Ok(())
191}
192
193fn is_unreserved(b: u8) -> bool {
195 matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~')
196}
197
198fn is_form_safe(b: u8) -> bool {
202 matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'*' | b'-' | b'.' | b'_')
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn uri_component_no_encoding_needed() {
211 assert_eq!(for_uri_component("hello"), "hello");
212 assert_eq!(for_uri_component(""), "");
213 assert_eq!(for_uri_component("ABCxyz019"), "ABCxyz019");
214 assert_eq!(for_uri_component("-._~"), "-._~");
215 }
216
217 #[test]
218 fn uri_component_encodes_space() {
219 assert_eq!(for_uri_component("a b"), "a%20b");
220 }
221
222 #[test]
223 fn uri_component_encodes_reserved_chars() {
224 assert_eq!(for_uri_component("a=b"), "a%3Db");
225 assert_eq!(for_uri_component("a&b"), "a%26b");
226 assert_eq!(for_uri_component("a+b"), "a%2Bb");
227 assert_eq!(for_uri_component("a?b"), "a%3Fb");
228 assert_eq!(for_uri_component("a#b"), "a%23b");
229 assert_eq!(for_uri_component("a/b"), "a%2Fb");
230 }
231
232 #[test]
233 fn uri_component_encodes_html_significant() {
234 assert_eq!(for_uri_component("<script>"), "%3Cscript%3E");
235 assert_eq!(for_uri_component(r#""quoted""#), "%22quoted%22");
236 }
237
238 #[test]
239 fn uri_component_encodes_two_byte_utf8() {
240 assert_eq!(for_uri_component("\u{00A0}"), "%C2%A0");
242 assert_eq!(for_uri_component("é"), "%C3%A9");
244 }
245
246 #[test]
247 fn uri_component_encodes_three_byte_utf8() {
248 assert_eq!(for_uri_component("\u{0800}"), "%E0%A0%80");
250 assert_eq!(for_uri_component("世"), "%E4%B8%96");
252 }
253
254 #[test]
255 fn uri_component_encodes_four_byte_utf8() {
256 assert_eq!(for_uri_component("\u{10000}"), "%F0%90%80%80");
258 assert_eq!(for_uri_component("😀"), "%F0%9F%98%80");
260 }
261
262 #[test]
263 fn uri_component_encodes_control_chars() {
264 assert_eq!(for_uri_component("\x00"), "%00");
265 assert_eq!(for_uri_component("\x1F"), "%1F");
266 assert_eq!(for_uri_component("\x7F"), "%7F");
267 }
268
269 #[test]
270 fn uri_component_mixed() {
271 assert_eq!(
272 for_uri_component("key=hello world&foo=bar"),
273 "key%3Dhello%20world%26foo%3Dbar"
274 );
275 }
276
277 #[test]
278 fn uri_component_writer_variant() {
279 let mut out = String::new();
280 write_uri_component(&mut out, "a b").unwrap();
281 assert_eq!(out, "a%20b");
282 }
283
284 #[test]
287 fn uri_path_no_encoding_needed() {
288 assert_eq!(for_uri_path("hello"), "hello");
289 assert_eq!(for_uri_path(""), "");
290 assert_eq!(for_uri_path("-._~"), "-._~");
291 }
292
293 #[test]
294 fn uri_path_preserves_slashes() {
295 assert_eq!(for_uri_path("/a/b/c"), "/a/b/c");
296 assert_eq!(for_uri_path("/"), "/");
297 assert_eq!(for_uri_path("//"), "//");
298 assert_eq!(for_uri_path("a/b"), "a/b");
299 }
300
301 #[test]
302 fn uri_path_encodes_reserved_except_slash() {
303 assert_eq!(for_uri_path("a=b"), "a%3Db");
304 assert_eq!(for_uri_path("a&b"), "a%26b");
305 assert_eq!(for_uri_path("a?b"), "a%3Fb");
306 assert_eq!(for_uri_path("a#b"), "a%23b");
307 }
308
309 #[test]
310 fn uri_path_encodes_space() {
311 assert_eq!(for_uri_path("/a b/c d"), "/a%20b/c%20d");
312 }
313
314 #[test]
315 fn uri_path_encodes_multibyte() {
316 assert_eq!(for_uri_path("/café"), "/caf%C3%A9");
317 assert_eq!(for_uri_path("/世界"), "/%E4%B8%96%E7%95%8C");
318 assert_eq!(for_uri_path("/😀"), "/%F0%9F%98%80");
319 }
320
321 #[test]
322 fn uri_path_writer_variant() {
323 let mut out = String::new();
324 write_uri_path(&mut out, "/a b/c").unwrap();
325 assert_eq!(out, "/a%20b/c");
326 }
327
328 #[test]
331 fn form_no_encoding_needed() {
332 assert_eq!(for_form_urlencoded("hello"), "hello");
333 assert_eq!(for_form_urlencoded(""), "");
334 assert_eq!(for_form_urlencoded("ABCxyz019"), "ABCxyz019");
335 assert_eq!(for_form_urlencoded("-._*"), "-._*");
336 }
337
338 #[test]
339 fn form_space_becomes_plus() {
340 assert_eq!(for_form_urlencoded("a b"), "a+b");
341 assert_eq!(for_form_urlencoded(" "), "+++");
342 }
343
344 #[test]
345 fn form_tilde_encoded() {
346 assert_eq!(for_form_urlencoded("a~b"), "a%7Eb");
347 }
348
349 #[test]
350 fn form_asterisk_safe() {
351 assert_eq!(for_form_urlencoded("a*b"), "a*b");
352 }
353
354 #[test]
355 fn form_encodes_reserved_chars() {
356 assert_eq!(for_form_urlencoded("a=b"), "a%3Db");
357 assert_eq!(for_form_urlencoded("a&b"), "a%26b");
358 assert_eq!(for_form_urlencoded("a+b"), "a%2Bb");
359 assert_eq!(for_form_urlencoded("a?b"), "a%3Fb");
360 assert_eq!(for_form_urlencoded("a#b"), "a%23b");
361 assert_eq!(for_form_urlencoded("a/b"), "a%2Fb");
362 }
363
364 #[test]
365 fn form_encodes_multibyte() {
366 assert_eq!(for_form_urlencoded("é"), "%C3%A9");
367 assert_eq!(for_form_urlencoded("世"), "%E4%B8%96");
368 assert_eq!(for_form_urlencoded("😀"), "%F0%9F%98%80");
369 assert_eq!(for_form_urlencoded("café"), "caf%C3%A9");
370 }
371
372 #[test]
373 fn form_encodes_control_chars() {
374 assert_eq!(for_form_urlencoded("\x00"), "%00");
375 assert_eq!(for_form_urlencoded("\x1F"), "%1F");
376 assert_eq!(for_form_urlencoded("\x7F"), "%7F");
377 }
378
379 #[test]
380 fn form_mixed() {
381 assert_eq!(
382 for_form_urlencoded("key=hello world&foo=bar"),
383 "key%3Dhello+world%26foo%3Dbar"
384 );
385 }
386
387 #[test]
388 fn form_writer_variant() {
389 let mut out = String::new();
390 write_form_urlencoded(&mut out, "a b").unwrap();
391 assert_eq!(out, "a+b");
392 }
393}