1use 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 percent_encode(out, input, is_unreserved)
46}
47
48pub fn for_uri_path(input: &str) -> String {
76 let mut out = String::with_capacity(input.len());
77 write_uri_path(&mut out, input).expect("writing to string cannot fail");
78 out
79}
80
81pub fn write_uri_path<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
85 percent_encode(out, input, |b| is_unreserved(b) || b == b'/')
86}
87
88pub fn for_form_urlencoded(input: &str) -> String {
121 let mut out = String::with_capacity(input.len());
122 write_form_urlencoded(&mut out, input).expect("writing to string cannot fail");
123 out
124}
125
126pub fn write_form_urlencoded<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
131 let mut segments = input.split(' ');
133 percent_encode(out, segments.next().unwrap_or(""), is_form_safe)?;
134 for segment in segments {
135 out.write_char('+')?;
136 percent_encode(out, segment, is_form_safe)?;
137 }
138 Ok(())
139}
140
141fn percent_encode<W: fmt::Write>(
148 out: &mut W,
149 input: &str,
150 keep: impl Fn(u8) -> bool,
151) -> fmt::Result {
152 let bytes = input.as_bytes();
153 let mut last_written = 0;
154
155 for (i, &byte) in bytes.iter().enumerate() {
156 if !keep(byte) {
157 if last_written < i {
158 out.write_str(&input[last_written..i])?;
159 }
160 write!(out, "%{:02X}", byte)?;
161 last_written = i + 1;
162 }
163 }
164
165 if last_written < bytes.len() {
166 out.write_str(&input[last_written..])?;
167 }
168 Ok(())
169}
170
171fn is_unreserved(b: u8) -> bool {
173 matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~')
174}
175
176fn is_form_safe(b: u8) -> bool {
180 matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'*' | b'-' | b'.' | b'_')
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn uri_component_no_encoding_needed() {
189 assert_eq!(for_uri_component("hello"), "hello");
190 assert_eq!(for_uri_component(""), "");
191 assert_eq!(for_uri_component("ABCxyz019"), "ABCxyz019");
192 assert_eq!(for_uri_component("-._~"), "-._~");
193 }
194
195 #[test]
196 fn uri_component_encodes_space() {
197 assert_eq!(for_uri_component("a b"), "a%20b");
198 }
199
200 #[test]
201 fn uri_component_encodes_reserved_chars() {
202 assert_eq!(for_uri_component("a=b"), "a%3Db");
203 assert_eq!(for_uri_component("a&b"), "a%26b");
204 assert_eq!(for_uri_component("a+b"), "a%2Bb");
205 assert_eq!(for_uri_component("a?b"), "a%3Fb");
206 assert_eq!(for_uri_component("a#b"), "a%23b");
207 assert_eq!(for_uri_component("a/b"), "a%2Fb");
208 }
209
210 #[test]
211 fn uri_component_encodes_html_significant() {
212 assert_eq!(for_uri_component("<script>"), "%3Cscript%3E");
213 assert_eq!(for_uri_component(r#""quoted""#), "%22quoted%22");
214 }
215
216 #[test]
217 fn uri_component_encodes_two_byte_utf8() {
218 assert_eq!(for_uri_component("\u{00A0}"), "%C2%A0");
220 assert_eq!(for_uri_component("é"), "%C3%A9");
222 }
223
224 #[test]
225 fn uri_component_encodes_three_byte_utf8() {
226 assert_eq!(for_uri_component("\u{0800}"), "%E0%A0%80");
228 assert_eq!(for_uri_component("世"), "%E4%B8%96");
230 }
231
232 #[test]
233 fn uri_component_encodes_four_byte_utf8() {
234 assert_eq!(for_uri_component("\u{10000}"), "%F0%90%80%80");
236 assert_eq!(for_uri_component("😀"), "%F0%9F%98%80");
238 }
239
240 #[test]
241 fn uri_component_encodes_control_chars() {
242 assert_eq!(for_uri_component("\x00"), "%00");
243 assert_eq!(for_uri_component("\x1F"), "%1F");
244 assert_eq!(for_uri_component("\x7F"), "%7F");
245 }
246
247 #[test]
248 fn uri_component_mixed() {
249 assert_eq!(
250 for_uri_component("key=hello world&foo=bar"),
251 "key%3Dhello%20world%26foo%3Dbar"
252 );
253 }
254
255 #[test]
256 fn uri_component_writer_variant() {
257 let mut out = String::new();
258 write_uri_component(&mut out, "a b").unwrap();
259 assert_eq!(out, "a%20b");
260 }
261
262 #[test]
265 fn uri_path_no_encoding_needed() {
266 assert_eq!(for_uri_path("hello"), "hello");
267 assert_eq!(for_uri_path(""), "");
268 assert_eq!(for_uri_path("-._~"), "-._~");
269 }
270
271 #[test]
272 fn uri_path_preserves_slashes() {
273 assert_eq!(for_uri_path("/a/b/c"), "/a/b/c");
274 assert_eq!(for_uri_path("/"), "/");
275 assert_eq!(for_uri_path("//"), "//");
276 assert_eq!(for_uri_path("a/b"), "a/b");
277 }
278
279 #[test]
280 fn uri_path_encodes_reserved_except_slash() {
281 assert_eq!(for_uri_path("a=b"), "a%3Db");
282 assert_eq!(for_uri_path("a&b"), "a%26b");
283 assert_eq!(for_uri_path("a?b"), "a%3Fb");
284 assert_eq!(for_uri_path("a#b"), "a%23b");
285 }
286
287 #[test]
288 fn uri_path_encodes_space() {
289 assert_eq!(for_uri_path("/a b/c d"), "/a%20b/c%20d");
290 }
291
292 #[test]
293 fn uri_path_encodes_multibyte() {
294 assert_eq!(for_uri_path("/café"), "/caf%C3%A9");
295 assert_eq!(for_uri_path("/世界"), "/%E4%B8%96%E7%95%8C");
296 assert_eq!(for_uri_path("/😀"), "/%F0%9F%98%80");
297 }
298
299 #[test]
300 fn uri_path_writer_variant() {
301 let mut out = String::new();
302 write_uri_path(&mut out, "/a b/c").unwrap();
303 assert_eq!(out, "/a%20b/c");
304 }
305
306 #[test]
309 fn form_no_encoding_needed() {
310 assert_eq!(for_form_urlencoded("hello"), "hello");
311 assert_eq!(for_form_urlencoded(""), "");
312 assert_eq!(for_form_urlencoded("ABCxyz019"), "ABCxyz019");
313 assert_eq!(for_form_urlencoded("-._*"), "-._*");
314 }
315
316 #[test]
317 fn form_space_becomes_plus() {
318 assert_eq!(for_form_urlencoded("a b"), "a+b");
319 assert_eq!(for_form_urlencoded(" "), "+++");
320 }
321
322 #[test]
323 fn form_tilde_encoded() {
324 assert_eq!(for_form_urlencoded("a~b"), "a%7Eb");
325 }
326
327 #[test]
328 fn form_asterisk_safe() {
329 assert_eq!(for_form_urlencoded("a*b"), "a*b");
330 }
331
332 #[test]
333 fn form_encodes_reserved_chars() {
334 assert_eq!(for_form_urlencoded("a=b"), "a%3Db");
335 assert_eq!(for_form_urlencoded("a&b"), "a%26b");
336 assert_eq!(for_form_urlencoded("a+b"), "a%2Bb");
337 assert_eq!(for_form_urlencoded("a?b"), "a%3Fb");
338 assert_eq!(for_form_urlencoded("a#b"), "a%23b");
339 assert_eq!(for_form_urlencoded("a/b"), "a%2Fb");
340 }
341
342 #[test]
343 fn form_encodes_multibyte() {
344 assert_eq!(for_form_urlencoded("é"), "%C3%A9");
345 assert_eq!(for_form_urlencoded("世"), "%E4%B8%96");
346 assert_eq!(for_form_urlencoded("😀"), "%F0%9F%98%80");
347 assert_eq!(for_form_urlencoded("café"), "caf%C3%A9");
348 }
349
350 #[test]
351 fn form_encodes_control_chars() {
352 assert_eq!(for_form_urlencoded("\x00"), "%00");
353 assert_eq!(for_form_urlencoded("\x1F"), "%1F");
354 assert_eq!(for_form_urlencoded("\x7F"), "%7F");
355 }
356
357 #[test]
358 fn form_mixed() {
359 assert_eq!(
360 for_form_urlencoded("key=hello world&foo=bar"),
361 "key%3Dhello+world%26foo%3Dbar"
362 );
363 }
364
365 #[test]
366 fn form_writer_variant() {
367 let mut out = String::new();
368 write_form_urlencoded(&mut out, "a b").unwrap();
369 assert_eq!(out, "a+b");
370 }
371}