1const ALWAYS_SAFE: &[u8] =
22 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-~";
23
24const HEX_UPPER: &[u8; 16] = b"0123456789ABCDEF";
25
26fn safe_table(safe: &str) -> [bool; 128] {
29 let mut table = [false; 128];
30 for &b in ALWAYS_SAFE {
31 table[b as usize] = true;
32 }
33 for b in safe.bytes() {
34 if b < 128 {
35 table[b as usize] = true;
36 }
37 }
38 table
39}
40
41pub fn quote_from_bytes(bytes: &[u8], safe: &str) -> String {
49 let table = safe_table(safe);
50 let mut out = String::with_capacity(bytes.len());
51 for &b in bytes {
52 if b < 128 && table[b as usize] {
53 out.push(b as char);
54 } else {
55 out.push('%');
56 out.push(HEX_UPPER[(b >> 4) as usize] as char);
57 out.push(HEX_UPPER[(b & 0x0f) as usize] as char);
58 }
59 }
60 out
61}
62
63pub fn quote(string: &str, safe: &str) -> String {
73 quote_from_bytes(string.as_bytes(), safe)
74}
75
76pub fn quote_plus(string: &str, safe: &str) -> String {
84 if !string.contains(' ') {
85 return quote(string, safe);
86 }
87 let mut safe_with_space = String::with_capacity(safe.len() + 1);
89 safe_with_space.push_str(safe);
90 safe_with_space.push(' ');
91 quote(string, &safe_with_space).replace(' ', "+")
92}
93
94fn hex_val(b: u8) -> Option<u8> {
95 match b {
96 b'0'..=b'9' => Some(b - b'0'),
97 b'a'..=b'f' => Some(b - b'a' + 10),
98 b'A'..=b'F' => Some(b - b'A' + 10),
99 _ => None,
100 }
101}
102
103fn unquote_impl(bytes: &[u8]) -> Vec<u8> {
106 let mut parts = bytes.split(|&b| b == b'%');
107 let first = parts.next().unwrap_or(&[]);
108 let mut res = first.to_vec();
109 for item in parts {
110 if item.len() >= 2
111 && let (Some(h), Some(l)) = (hex_val(item[0]), hex_val(item[1]))
112 {
113 res.push((h << 4) | l);
114 res.extend_from_slice(&item[2..]);
115 continue;
116 }
117 res.push(b'%');
118 res.extend_from_slice(item);
119 }
120 res
121}
122
123pub fn unquote_to_bytes(string: &str) -> Vec<u8> {
131 unquote_impl(string.as_bytes())
132}
133
134pub fn unquote(string: &str) -> String {
145 if !string.contains('%') {
146 return string.to_string();
147 }
148 let mut out = String::with_capacity(string.len());
149 let mut ascii_run = String::new();
150 let flush = |run: &mut String, out: &mut String| {
151 if !run.is_empty() {
152 out.push_str(&String::from_utf8_lossy(&unquote_impl(run.as_bytes())));
153 run.clear();
154 }
155 };
156 for ch in string.chars() {
157 if (ch as u32) <= 0x7f {
158 ascii_run.push(ch);
159 } else {
160 flush(&mut ascii_run, &mut out);
161 out.push(ch);
162 }
163 }
164 flush(&mut ascii_run, &mut out);
165 out
166}
167
168pub fn unquote_plus(string: &str) -> String {
176 unquote(&string.replace('+', " "))
177}
178
179const USES_NETLOC: &[&str] = &[
185 "", "ftp", "http", "gopher", "nntp", "telnet", "imap", "wais", "file", "mms", "https",
186 "shttp", "snews", "prospero", "rtsp", "rtsps", "rtspu", "rsync", "svn", "svn+ssh", "sftp",
187 "nfs", "git", "git+ssh", "ws", "wss", "itms-services",
188];
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct UrlError(pub String);
193
194impl std::fmt::Display for UrlError {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.write_str(&self.0)
197 }
198}
199
200impl std::error::Error for UrlError {}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct SplitResult {
205 pub scheme: String,
206 pub netloc: String,
207 pub path: String,
208 pub query: String,
209 pub fragment: String,
210}
211
212fn is_scheme_char(c: char) -> bool {
213 c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.'
214}
215
216fn is_c0_or_space(c: char) -> bool {
218 (c as u32) <= 0x20
219}
220
221fn split_netloc(url: &str, start: usize) -> (&str, &str) {
224 let mut delim = url.len();
225 for c in ['/', '?', '#'] {
226 if let Some(pos) = url[start..].find(c) {
227 delim = delim.min(start + pos);
228 }
229 }
230 (&url[start..delim], &url[delim..])
231}
232
233fn check_bracketed_netloc(netloc: &str) -> Result<(), UrlError> {
236 let hostname_and_port = match netloc.rfind('@') {
237 Some(at) => &netloc[at + 1..],
238 None => netloc,
239 };
240 let hostname = match hostname_and_port.find('[') {
241 Some(ob) => {
242 if ob != 0 {
244 return Err(UrlError("Invalid IPv6 URL".into()));
245 }
246 let bracketed = &hostname_and_port[ob + 1..];
247 let (hostname, port) = match bracketed.find(']') {
248 Some(cb) => (&bracketed[..cb], &bracketed[cb + 1..]),
249 None => (bracketed, ""),
250 };
251 if !port.is_empty() && !port.starts_with(':') {
253 return Err(UrlError("Invalid IPv6 URL".into()));
254 }
255 hostname
256 }
257 None => match hostname_and_port.find(':') {
258 Some(c) => &hostname_and_port[..c],
259 None => hostname_and_port,
260 },
261 };
262 check_bracketed_host(hostname)
263}
264
265fn check_bracketed_host(hostname: &str) -> Result<(), UrlError> {
268 if let Some(rest) = hostname.strip_prefix('v') {
269 let valid = match rest.find('.') {
271 Some(dot) => {
272 dot > 0
273 && rest[..dot].bytes().all(|b| b.is_ascii_hexdigit())
274 && rest.len() > dot + 1
275 }
276 None => false,
277 };
278 if !valid {
279 return Err(UrlError("IPvFuture address is invalid".into()));
280 }
281 Ok(())
282 } else {
283 match classify_bracketed_ip(hostname) {
284 Some(true) => Ok(()), Some(false) => Err(UrlError("An IPv4 address cannot be in brackets".into())),
286 None => Err(UrlError(format!(
287 "{hostname:?} does not appear to be an IPv4 or IPv6 address"
288 ))),
289 }
290 }
291}
292
293fn classify_bracketed_ip(host: &str) -> Option<bool> {
297 use std::net::{Ipv4Addr, Ipv6Addr};
298 if let Some((addr, zone)) = host.split_once('%') {
299 if zone.is_empty() || zone.contains('%') {
301 return None;
302 }
303 return addr.parse::<Ipv6Addr>().ok().map(|_| true);
304 }
305 if host.parse::<Ipv6Addr>().is_ok() {
306 Some(true)
307 } else if host.parse::<Ipv4Addr>().is_ok() {
308 Some(false)
309 } else {
310 None
311 }
312}
313
314fn checknetloc(netloc: &str) -> Result<(), UrlError> {
318 if netloc.is_empty() || netloc.is_ascii() {
319 return Ok(());
320 }
321 use unicode_normalization::UnicodeNormalization;
322 let n: String = netloc.chars().filter(|c| !matches!(c, '@' | ':' | '#' | '?')).collect();
325 let netloc2: String = n.nfkc().collect();
326 if n == netloc2 {
327 return Ok(());
328 }
329 if netloc2.contains(['/', '?', '#', '@', ':']) {
330 return Err(UrlError(format!(
331 "netloc '{netloc}' contains invalid characters under NFKC normalization"
332 )));
333 }
334 Ok(())
335}
336
337pub fn urlsplit(url: &str, scheme: &str, allow_fragments: bool) -> Result<SplitResult, UrlError> {
347 let mut url = url.trim_start_matches(is_c0_or_space).replace(['\t', '\r', '\n'], "");
349 let mut scheme = scheme.trim_matches(is_c0_or_space).replace(['\t', '\r', '\n'], "");
350
351 let mut netloc = String::new();
352 let mut query = String::new();
353 let mut fragment = String::new();
354
355 if let Some(i) = url.find(':')
356 && i > 0
357 && url.as_bytes()[0].is_ascii_alphabetic()
358 && url[..i].chars().all(is_scheme_char)
359 {
360 scheme = url[..i].to_ascii_lowercase();
361 url = url[i + 1..].to_string();
362 }
363
364 if url.starts_with("//") {
365 let (nl, rest) = split_netloc(&url, 2);
366 netloc = nl.to_string();
367 let has_open = netloc.contains('[');
368 let has_close = netloc.contains(']');
369 if has_open != has_close {
370 return Err(UrlError("Invalid IPv6 URL".into()));
371 }
372 if has_open && has_close {
373 check_bracketed_netloc(&netloc)?;
374 }
375 url = rest.to_string();
376 }
377
378 if allow_fragments && let Some(pos) = url.find('#') {
379 fragment = url[pos + 1..].to_string();
380 url.truncate(pos);
381 }
382
383 if let Some(pos) = url.find('?') {
384 query = url[pos + 1..].to_string();
385 url.truncate(pos);
386 }
387
388 checknetloc(&netloc)?;
389 Ok(SplitResult { scheme, netloc, path: url, query, fragment })
390}
391
392pub fn urlunsplit(
394 scheme: &str,
395 netloc: &str,
396 path: &str,
397 query: &str,
398 fragment: &str,
399) -> String {
400 let mut url = path.to_string();
401 if !netloc.is_empty() {
402 if !url.is_empty() && !url.starts_with('/') {
403 url = format!("/{url}");
404 }
405 url = format!("//{netloc}{url}");
406 } else if url.starts_with("//")
407 || (!scheme.is_empty()
410 && USES_NETLOC.contains(&scheme)
411 && (url.is_empty() || url.starts_with('/')))
412 {
413 url = format!("//{url}");
414 }
415 if !scheme.is_empty() {
416 url = format!("{scheme}:{url}");
417 }
418 if !query.is_empty() {
419 url = format!("{url}?{query}");
420 }
421 if !fragment.is_empty() {
422 url = format!("{url}#{fragment}");
423 }
424 url
425}
426
427fn netloc_userinfo(netloc: &str) -> (Option<&str>, Option<&str>) {
433 match netloc.rfind('@') {
434 Some(at) => {
435 let userinfo = &netloc[..at];
436 match userinfo.find(':') {
437 Some(c) => (Some(&userinfo[..c]), Some(&userinfo[c + 1..])),
438 None => (Some(userinfo), None),
439 }
440 }
441 None => (None, None),
442 }
443}
444
445fn netloc_hostinfo(netloc: &str) -> (&str, Option<&str>) {
447 let hostinfo = match netloc.rfind('@') {
448 Some(at) => &netloc[at + 1..],
449 None => netloc,
450 };
451 let (hostname, port) = if let Some(ob) = hostinfo.find('[') {
452 let bracketed = &hostinfo[ob + 1..];
453 let (hostname, after) = match bracketed.find(']') {
454 Some(cb) => (&bracketed[..cb], &bracketed[cb + 1..]),
455 None => (bracketed, ""),
456 };
457 let port = match after.find(':') {
458 Some(c) => &after[c + 1..],
459 None => "",
460 };
461 (hostname, port)
462 } else {
463 match hostinfo.find(':') {
464 Some(c) => (&hostinfo[..c], &hostinfo[c + 1..]),
465 None => (hostinfo, ""),
466 }
467 };
468 (hostname, if port.is_empty() { None } else { Some(port) })
469}
470
471fn netloc_hostname(netloc: &str) -> Option<String> {
474 let (host, _) = netloc_hostinfo(netloc);
475 if host.is_empty() {
476 return None;
477 }
478 Some(match host.find('%') {
479 Some(p) => format!("{}{}", host[..p].to_lowercase(), &host[p..]),
480 None => host.to_lowercase(),
481 })
482}
483
484fn netloc_port(netloc: &str) -> Result<Option<u16>, UrlError> {
487 let (_, port) = netloc_hostinfo(netloc);
488 let Some(port) = port else { return Ok(None) };
489 if port.bytes().all(|b| b.is_ascii_digit()) {
490 match port.parse::<u64>() {
492 Ok(n) if n <= 65535 => Ok(Some(n as u16)),
493 _ => Err(UrlError("Port out of range 0-65535".into())),
494 }
495 } else {
496 Err(UrlError(format!(
497 "Port could not be cast to integer value as '{port}'"
498 )))
499 }
500}
501
502impl SplitResult {
503 pub fn geturl(&self) -> String {
505 urlunsplit(&self.scheme, &self.netloc, &self.path, &self.query, &self.fragment)
506 }
507
508 pub fn username(&self) -> Option<&str> {
510 netloc_userinfo(&self.netloc).0
511 }
512
513 pub fn password(&self) -> Option<&str> {
515 netloc_userinfo(&self.netloc).1
516 }
517
518 pub fn hostname(&self) -> Option<String> {
520 netloc_hostname(&self.netloc)
521 }
522
523 pub fn port(&self) -> Result<Option<u16>, UrlError> {
525 netloc_port(&self.netloc)
526 }
527}
528
529const USES_PARAMS: &[&str] = &[
531 "", "ftp", "hdl", "prospero", "http", "imap", "https", "shttp", "rtsp", "rtsps", "rtspu",
532 "sip", "sips", "mms", "sftp", "tel",
533];
534
535#[derive(Debug, Clone, PartialEq, Eq)]
538pub struct ParseResult {
539 pub scheme: String,
540 pub netloc: String,
541 pub path: String,
542 pub params: String,
543 pub query: String,
544 pub fragment: String,
545}
546
547impl ParseResult {
548 pub fn geturl(&self) -> String {
550 urlunparse(
551 &self.scheme, &self.netloc, &self.path, &self.params, &self.query, &self.fragment,
552 )
553 }
554
555 pub fn username(&self) -> Option<&str> {
557 netloc_userinfo(&self.netloc).0
558 }
559
560 pub fn password(&self) -> Option<&str> {
562 netloc_userinfo(&self.netloc).1
563 }
564
565 pub fn hostname(&self) -> Option<String> {
567 netloc_hostname(&self.netloc)
568 }
569
570 pub fn port(&self) -> Result<Option<u16>, UrlError> {
572 netloc_port(&self.netloc)
573 }
574}
575
576fn split_params(url: &str) -> (&str, &str) {
578 let i = if let Some(slash) = url.rfind('/') {
579 match url[slash..].find(';') {
580 Some(rel) => slash + rel,
581 None => return (url, ""),
582 }
583 } else {
584 match url.find(';') {
585 Some(pos) => pos,
586 None => return (url, ""),
587 }
588 };
589 (&url[..i], &url[i + 1..])
590}
591
592pub fn urlparse(url: &str, scheme: &str, allow_fragments: bool) -> Result<ParseResult, UrlError> {
596 let s = urlsplit(url, scheme, allow_fragments)?;
597 let (path, params) = if USES_PARAMS.contains(&s.scheme.as_str()) && s.path.contains(';') {
598 let (p, pr) = split_params(&s.path);
599 (p.to_string(), pr.to_string())
600 } else {
601 (s.path, String::new())
602 };
603 Ok(ParseResult {
604 scheme: s.scheme,
605 netloc: s.netloc,
606 path,
607 params,
608 query: s.query,
609 fragment: s.fragment,
610 })
611}
612
613pub fn urlunparse(
615 scheme: &str,
616 netloc: &str,
617 path: &str,
618 params: &str,
619 query: &str,
620 fragment: &str,
621) -> String {
622 let path = if params.is_empty() {
623 path.to_string()
624 } else {
625 format!("{path};{params}")
626 };
627 urlunsplit(scheme, netloc, &path, query, fragment)
628}
629
630const USES_RELATIVE: &[&str] = &[
632 "", "ftp", "http", "gopher", "nntp", "imap", "wais", "file", "https", "shttp", "mms",
633 "prospero", "rtsp", "rtsps", "rtspu", "sftp", "svn", "svn+ssh", "ws", "wss",
634];
635
636pub fn urljoin(base: &str, url: &str, allow_fragments: bool) -> Result<String, UrlError> {
641 if base.is_empty() {
642 return Ok(url.to_string());
643 }
644 if url.is_empty() {
645 return Ok(base.to_string());
646 }
647
648 let b = urlparse(base, "", allow_fragments)?;
649 let u = urlparse(url, &b.scheme, allow_fragments)?;
650
651 let scheme = u.scheme;
652 let mut netloc = u.netloc;
653 let mut path = u.path;
654 let mut params = u.params;
655 let mut query = u.query;
656 let fragment = u.fragment;
657
658 if scheme != b.scheme || !USES_RELATIVE.contains(&scheme.as_str()) {
659 return Ok(url.to_string());
660 }
661 if USES_NETLOC.contains(&scheme.as_str()) {
662 if !netloc.is_empty() {
663 return Ok(urlunparse(&scheme, &netloc, &path, ¶ms, &query, &fragment));
664 }
665 netloc = b.netloc.clone();
666 }
667 if path.is_empty() && params.is_empty() {
668 path = b.path.clone();
669 params = b.params.clone();
670 if query.is_empty() {
671 query = b.query.clone();
672 }
673 return Ok(urlunparse(&scheme, &netloc, &path, ¶ms, &query, &fragment));
674 }
675
676 let mut base_parts: Vec<&str> = b.path.split('/').collect();
678 if base_parts.last() != Some(&"") {
679 base_parts.pop();
680 }
681
682 let segments: Vec<&str> = if path.starts_with('/') {
683 path.split('/').collect()
684 } else {
685 let mut segs = base_parts;
686 segs.extend(path.split('/'));
687 if segs.len() > 2 {
690 let mut rebuilt = Vec::with_capacity(segs.len());
691 rebuilt.push(segs[0]);
692 rebuilt.extend(segs[1..segs.len() - 1].iter().copied().filter(|s| !s.is_empty()));
693 rebuilt.push(segs[segs.len() - 1]);
694 segs = rebuilt;
695 }
696 segs
697 };
698
699 let mut resolved: Vec<&str> = Vec::with_capacity(segments.len());
700 for &seg in &segments {
701 match seg {
702 ".." => {
703 resolved.pop(); }
705 "." => continue,
706 _ => resolved.push(seg),
707 }
708 }
709 if matches!(segments.last(), Some(&"." | &"..")) {
710 resolved.push("");
712 }
713
714 let joined = resolved.join("/");
715 let resolved_path = if joined.is_empty() { "/".to_string() } else { joined };
716 Ok(urlunparse(&scheme, &netloc, &resolved_path, ¶ms, &query, &fragment))
717}
718
719#[derive(Debug, Clone, PartialEq, Eq)]
722pub struct DefragResult {
723 pub url: String,
724 pub fragment: String,
725}
726
727impl DefragResult {
728 pub fn geturl(&self) -> String {
730 if self.fragment.is_empty() {
731 self.url.clone()
732 } else {
733 format!("{}#{}", self.url, self.fragment)
734 }
735 }
736}
737
738pub fn urldefrag(url: &str) -> Result<DefragResult, UrlError> {
741 if url.contains('#') {
742 let p = urlparse(url, "", true)?;
743 let defrag = urlunparse(&p.scheme, &p.netloc, &p.path, &p.params, &p.query, "");
744 Ok(DefragResult { url: defrag, fragment: p.fragment })
745 } else {
746 Ok(DefragResult { url: url.to_string(), fragment: String::new() })
747 }
748}
749
750pub fn parse_qsl(
760 qs: &str,
761 keep_blank_values: bool,
762 strict_parsing: bool,
763 max_num_fields: Option<usize>,
764 separator: &str,
765) -> Result<Vec<(String, String)>, UrlError> {
766 if separator.is_empty() {
767 return Err(UrlError("Separator must be of type string or bytes.".into()));
768 }
769 if qs.is_empty() {
770 return Ok(Vec::new());
771 }
772 if let Some(max) = max_num_fields {
773 let num_fields = 1 + qs.matches(separator).count();
774 if max < num_fields {
775 return Err(UrlError("Max number of fields exceeded".into()));
776 }
777 }
778
779 let mut r = Vec::new();
780 for name_value in qs.split(separator) {
781 if name_value.is_empty() && !strict_parsing {
782 continue;
783 }
784 let (name, has_eq, value) = match name_value.find('=') {
785 Some(i) => (&name_value[..i], true, &name_value[i + 1..]),
786 None => (name_value, false, ""),
787 };
788 if !has_eq && strict_parsing {
789 return Err(UrlError(format!("bad query field: '{name_value}'")));
790 }
791 if !value.is_empty() || keep_blank_values {
792 r.push((unquote_plus(name), unquote_plus(value)));
793 }
794 }
795 Ok(r)
796}
797
798pub fn parse_qs(
801 qs: &str,
802 keep_blank_values: bool,
803 strict_parsing: bool,
804 max_num_fields: Option<usize>,
805 separator: &str,
806) -> Result<Vec<(String, Vec<String>)>, UrlError> {
807 let pairs = parse_qsl(qs, keep_blank_values, strict_parsing, max_num_fields, separator)?;
808 let mut result: Vec<(String, Vec<String>)> = Vec::new();
809 for (name, value) in pairs {
810 match result.iter_mut().find(|(k, _)| *k == name) {
811 Some(entry) => entry.1.push(value),
812 None => result.push((name, vec![value])),
813 }
814 }
815 Ok(result)
816}
817
818pub fn urlencode(query: &[(&str, &str)], safe: &str) -> String {
821 query
822 .iter()
823 .map(|(k, v)| format!("{}={}", quote_plus(k, safe), quote_plus(v, safe)))
824 .collect::<Vec<_>>()
825 .join("&")
826}
827
828pub fn urlencode_doseq(query: &[(&str, &[&str])], safe: &str) -> String {
831 let mut parts = Vec::new();
832 for (k, seq) in query {
833 let key = quote_plus(k, safe);
834 for elt in *seq {
835 parts.push(format!("{key}={}", quote_plus(elt, safe)));
836 }
837 }
838 parts.join("&")
839}
840
841#[cfg(test)]
842mod tests {
843 use super::*;
844
845 #[test]
846 fn quote_basics() {
847 assert_eq!(quote("abc def", "/"), "abc%20def");
848 assert_eq!(quote("a/b/c", "/"), "a/b/c");
849 assert_eq!(quote("a/b/c", ""), "a%2Fb%2Fc");
850 assert_eq!(quote("~_.-", "/"), "~_.-");
851 assert_eq!(quote("/El Niño/", "/"), "/El%20Ni%C3%B1o/");
852 }
853
854 #[test]
855 fn quote_plus_basics() {
856 assert_eq!(quote_plus("a b c", ""), "a+b+c");
857 assert_eq!(quote_plus("a+b", ""), "a%2Bb");
858 assert_eq!(quote_plus("/El Niño/", ""), "%2FEl+Ni%C3%B1o%2F");
859 }
860
861 #[test]
862 fn unquote_basics() {
863 assert_eq!(unquote("abc%20def"), "abc def");
864 assert_eq!(unquote("no-escapes"), "no-escapes");
865 assert_eq!(unquote("/El%20Ni%C3%B1o/"), "/El Niño/");
866 assert_eq!(unquote("%zz%"), "%zz%");
868 assert_eq!(unquote("100%"), "100%");
869 }
870
871 #[test]
872 fn unquote_plus_and_bytes() {
873 assert_eq!(unquote_plus("%7e/abc+def"), "~/abc def");
874 assert_eq!(unquote_to_bytes("a%26%EF"), b"a&\xef");
875 }
876
877 #[test]
878 fn quote_roundtrip_bytes() {
879 assert_eq!(quote_from_bytes(b"abc def\x3f", "/"), "abc%20def%3F");
880 assert_eq!(quote_from_bytes(&[], "/"), "");
881 }
882
883 fn split(url: &str) -> SplitResult {
884 urlsplit(url, "", true).unwrap()
885 }
886
887 #[test]
888 fn urlsplit_basics() {
889 let r = split("http://docs.python.org:80/3/library/urllib.parse.html?h=1#frag");
890 assert_eq!(r.scheme, "http");
891 assert_eq!(r.netloc, "docs.python.org:80");
892 assert_eq!(r.path, "/3/library/urllib.parse.html");
893 assert_eq!(r.query, "h=1");
894 assert_eq!(r.fragment, "frag");
895 }
896
897 #[test]
898 fn urlsplit_edge_cases() {
899 assert_eq!(split("//netloc/path").netloc, "netloc");
900 assert_eq!(split("path/only").scheme, "");
901 assert_eq!(split("path/only").path, "path/only");
902 assert_eq!(split("1http://x").scheme, "");
904 assert_eq!(split("HTTP://x").scheme, "http");
905 assert!(urlsplit("//[::1/path", "", true).is_err());
907 }
908
909 #[test]
910 fn urlsplit_bracketed_hosts() {
911 for url in [
913 "http://[::1]:8080/p",
914 "http://[fe80::1%eth0]:443/",
915 "http://[::ffff:1.2.3.4]/",
916 "http://[v1.fe80::a+b-c]/",
917 ] {
918 assert!(urlsplit(url, "", true).is_ok(), "should accept {url:?}");
919 }
920 for url in [
922 "http://[1.2.3.4]/", "http://[notanip]/", "http://[::1]x/", "http://x[::1]/", "http://[v.x]/", "http://[v1]/", "http://[fe80::1%]/", "http://[fe80::1%a%b]/", ] {
931 assert!(urlsplit(url, "", true).is_err(), "should reject {url:?}");
932 }
933 }
934
935 #[test]
936 fn urlsplit_nfkc_netloc() {
937 for url in ["http://naïve.com/p", "http://ABC/x", "http://üser@höst:80/"] {
940 assert!(urlsplit(url, "", true).is_ok(), "should accept {url:?}");
941 }
942 for url in [
945 "http://exa℀mple/",
946 "http://h℅o/",
947 "http://exa/mple/",
948 "http://user@ho@st/",
949 "http://h:p/",
950 ] {
951 assert!(urlsplit(url, "", true).is_err(), "should reject {url:?}");
952 }
953 }
954
955 #[test]
956 fn urlsplit_roundtrip() {
957 for url in [
958 "http://a.com/b?c#d",
959 "https://user:pass@host:8080/p",
960 "mailto:a@b.com",
961 "//host/path",
962 "path",
963 "?query",
964 "#frag",
965 ] {
966 let r = split(url);
967 assert_eq!(r.geturl(), url, "roundtrip failed for {url:?}");
968 }
969 }
970
971 #[test]
972 fn netloc_accessors() {
973 let r = split("http://user:pw@Example.COM:8080/p");
974 assert_eq!(r.username(), Some("user"));
975 assert_eq!(r.password(), Some("pw"));
976 assert_eq!(r.hostname().as_deref(), Some("example.com"));
977 assert_eq!(r.port(), Ok(Some(8080)));
978
979 let r = split("http://host/p");
980 assert_eq!(r.username(), None);
981 assert_eq!(r.password(), None);
982 assert_eq!(r.port(), Ok(None));
983
984 let r = split("http://[FE80::1%tESt]:1234/");
986 assert_eq!(r.hostname().as_deref(), Some("fe80::1%tESt"));
987 assert_eq!(r.port(), Ok(Some(1234)));
988
989 assert!(split("http://host:notaport/").port().is_err());
990 assert!(split("http://host:99999/").port().is_err());
991 }
992
993 #[test]
994 fn urlparse_params() {
995 let r = urlparse("http://h/a/b;type=d?q=1#f", "", true).unwrap();
997 assert_eq!(r.path, "/a/b");
998 assert_eq!(r.params, "type=d");
999 assert_eq!(r.query, "q=1");
1000 assert_eq!(r.fragment, "f");
1001
1002 let r = urlparse("http://h/a;x/b", "", true).unwrap();
1004 assert_eq!(r.path, "/a;x/b");
1005 assert_eq!(r.params, "");
1006
1007 let r = urlparse("git://h/a;b", "", true).unwrap();
1009 assert_eq!(r.path, "/a;b");
1010 assert_eq!(r.params, "");
1011 }
1012
1013 #[test]
1014 fn urlparse_roundtrip() {
1015 for url in [
1016 "http://h/a/b;type=d?q=1#f",
1017 "https://user:pass@host:8080/p;k=v?x#y",
1018 "mailto:a@b.com",
1019 "scheme:opaque",
1020 "//host/path",
1021 ] {
1022 let r = urlparse(url, "", true).unwrap();
1023 assert_eq!(r.geturl(), url, "roundtrip failed for {url:?}");
1024 }
1025 }
1026
1027 fn join(base: &str, rel: &str) -> String {
1028 urljoin(base, rel, true).unwrap()
1029 }
1030
1031 #[test]
1032 fn urljoin_rfc3986() {
1033 let base = "http://a/b/c/d;p?q";
1034 assert_eq!(join(base, "g"), "http://a/b/c/g");
1035 assert_eq!(join(base, "./g"), "http://a/b/c/g");
1036 assert_eq!(join(base, "/g"), "http://a/g");
1037 assert_eq!(join(base, "//g"), "http://g");
1038 assert_eq!(join(base, "?y"), "http://a/b/c/d;p?y");
1039 assert_eq!(join(base, "#s"), "http://a/b/c/d;p?q#s");
1040 assert_eq!(join(base, ".."), "http://a/b/");
1041 assert_eq!(join(base, "../g"), "http://a/b/g");
1042 assert_eq!(join(base, "../../g"), "http://a/g");
1043 assert_eq!(join(base, "http:g"), "http://a/b/c/g");
1045 assert_eq!(join(base, ""), base);
1047 assert_eq!(join("", "g"), "g");
1048 }
1049
1050 #[test]
1051 fn urldefrag_basics() {
1052 let r = urldefrag("http://h/p?q#frag").unwrap();
1053 assert_eq!(r.url, "http://h/p?q");
1054 assert_eq!(r.fragment, "frag");
1055 assert_eq!(r.geturl(), "http://h/p?q#frag");
1056
1057 let r = urldefrag("http://h/p?q").unwrap();
1058 assert_eq!(r.url, "http://h/p?q");
1059 assert_eq!(r.fragment, "");
1060 }
1061
1062 #[test]
1063 fn parse_qsl_basics() {
1064 assert_eq!(
1065 parse_qsl("a=1&b=2&a=3", false, false, None, "&").unwrap(),
1066 vec![
1067 ("a".to_string(), "1".to_string()),
1068 ("b".to_string(), "2".to_string()),
1069 ("a".to_string(), "3".to_string()),
1070 ]
1071 );
1072 assert_eq!(parse_qsl("a=&b=2", false, false, None, "&").unwrap().len(), 1);
1074 assert_eq!(parse_qsl("a=&b=2", true, false, None, "&").unwrap().len(), 2);
1075 assert_eq!(
1077 parse_qsl("k=%41+B", false, false, None, "&").unwrap(),
1078 vec![("k".to_string(), "A B".to_string())]
1079 );
1080 assert!(parse_qsl("a=1&b", false, true, None, "&").is_err());
1082 assert!(parse_qsl("a=1&b=2&c=3", false, false, Some(2), "&").is_err());
1084 }
1085
1086 #[test]
1087 fn parse_qs_groups() {
1088 assert_eq!(
1089 parse_qs("a=1&b=2&a=3", false, false, None, "&").unwrap(),
1090 vec![
1091 ("a".to_string(), vec!["1".to_string(), "3".to_string()]),
1092 ("b".to_string(), vec!["2".to_string()]),
1093 ]
1094 );
1095 }
1096
1097 #[test]
1098 fn urlencode_basics() {
1099 assert_eq!(urlencode(&[("a", "1"), ("b", "2")], ""), "a=1&b=2");
1100 assert_eq!(urlencode(&[("name", "El Niño")], ""), "name=El+Ni%C3%B1o");
1101 assert_eq!(urlencode(&[], ""), "");
1102 assert_eq!(
1103 urlencode_doseq(&[("a", &["1", "2", "3"][..])], ""),
1104 "a=1&a=2&a=3"
1105 );
1106 }
1107}