From ff88082eadf2a4658ac6e08b2388c4a88b6fbd86 Mon Sep 17 00:00:00 2001 From: Jeff Vander Stoep Date: Fri, 7 May 2021 16:28:07 +0200 Subject: Update to 2.2.2 Test: atest Change-Id: I45105d8fe4acfba4a507c3506c2e1f5583f7a442 --- .cargo_vcs_info.json | 2 +- Cargo.toml | 2 +- Cargo.toml.orig | 2 +- METADATA | 8 +- TEST_MAPPING | 11 +- src/lib.rs | 166 +++++++++++++++++-- src/quirks.rs | 12 +- tests/unit.rs | 439 ++++++++++++++++++++++++++++++++++++++++++++++++++- 8 files changed, 613 insertions(+), 29 deletions(-) diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json index da07783..fb5f61f 100644 --- a/.cargo_vcs_info.json +++ b/.cargo_vcs_info.json @@ -1,5 +1,5 @@ { "git": { - "sha1": "7bb95a183d0b236fa756dbd1e0f0ee210bbd2b35" + "sha1": "6c22912c313064a8c5a6fa043882c9ad55dba162" } } diff --git a/Cargo.toml b/Cargo.toml index 6e0b2ba..108b149 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ [package] edition = "2018" name = "url" -version = "2.2.1" +version = "2.2.2" authors = ["The rust-url developers"] include = ["src/**/*", "LICENSE-*", "README.md", "tests/**"] description = "URL library for Rust, based on the WHATWG URL Standard" diff --git a/Cargo.toml.orig b/Cargo.toml.orig index 5f7a0fe..e384659 100644 --- a/Cargo.toml.orig +++ b/Cargo.toml.orig @@ -2,7 +2,7 @@ name = "url" # When updating version, also modify html_root_url in the lib.rs -version = "2.2.1" +version = "2.2.2" authors = ["The rust-url developers"] description = "URL library for Rust, based on the WHATWG URL Standard" diff --git a/METADATA b/METADATA index 14ae2a5..742a22a 100644 --- a/METADATA +++ b/METADATA @@ -7,13 +7,13 @@ third_party { } url { type: ARCHIVE - value: "https://static.crates.io/crates/url/url-2.2.1.crate" + value: "https://static.crates.io/crates/url/url-2.2.2.crate" } - version: "2.2.1" + version: "2.2.2" license_type: NOTICE last_upgrade_date { year: 2021 - month: 2 - day: 18 + month: 5 + day: 7 } } diff --git a/TEST_MAPPING b/TEST_MAPPING index 39f2e10..be39865 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -2,16 +2,19 @@ { "presubmit": [ { - "name": "url_device_test_tests_unit" - }, - { - "name": "url_device_test_tests_data" + "name": "doh_unit_test" }, { "name": "quiche_device_test_src_lib" }, { "name": "url_device_test_src_lib" + }, + { + "name": "url_device_test_tests_data" + }, + { + "name": "url_device_test_tests_unit" } ] } diff --git a/src/lib.rs b/src/lib.rs index afe511e..42793cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,7 +120,7 @@ url = { version = "2", features = ["serde"] } ``` */ -#![doc(html_root_url = "https://docs.rs/url/2.2.1")] +#![doc(html_root_url = "https://docs.rs/url/2.2.2")] #[macro_use] extern crate matches; @@ -320,6 +320,8 @@ impl Url { /// Parse a string as an URL, with this URL as the base URL. /// + /// The inverse of this is [`make_relative`]. + /// /// Note: a trailing slash is significant. /// Without it, the last path component is considered to be a “file” name /// to be removed to get at the “directory” that is used as the base: @@ -349,11 +351,144 @@ impl Url { /// with this URL as the base URL, a [`ParseError`] variant will be returned. /// /// [`ParseError`]: enum.ParseError.html + /// [`make_relative`]: #method.make_relative #[inline] pub fn join(&self, input: &str) -> Result { Url::options().base_url(Some(self)).parse(input) } + /// Creates a relative URL if possible, with this URL as the base URL. + /// + /// This is the inverse of [`join`]. + /// + /// # Examples + /// + /// ```rust + /// use url::Url; + /// # use url::ParseError; + /// + /// # fn run() -> Result<(), ParseError> { + /// let base = Url::parse("https://example.net/a/b.html")?; + /// let url = Url::parse("https://example.net/a/c.png")?; + /// let relative = base.make_relative(&url); + /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("c.png")); + /// + /// let base = Url::parse("https://example.net/a/b/")?; + /// let url = Url::parse("https://example.net/a/b/c.png")?; + /// let relative = base.make_relative(&url); + /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("c.png")); + /// + /// let base = Url::parse("https://example.net/a/b/")?; + /// let url = Url::parse("https://example.net/a/d/c.png")?; + /// let relative = base.make_relative(&url); + /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("../d/c.png")); + /// + /// let base = Url::parse("https://example.net/a/b.html?c=d")?; + /// let url = Url::parse("https://example.net/a/b.html?e=f")?; + /// let relative = base.make_relative(&url); + /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("?e=f")); + /// # Ok(()) + /// # } + /// # run().unwrap(); + /// ``` + /// + /// # Errors + /// + /// If this URL can't be a base for the given URL, `None` is returned. + /// This is for example the case if the scheme, host or port are not the same. + /// + /// [`join`]: #method.join + pub fn make_relative(&self, url: &Url) -> Option { + if self.cannot_be_a_base() { + return None; + } + + // Scheme, host and port need to be the same + if self.scheme() != url.scheme() || self.host() != url.host() || self.port() != url.port() { + return None; + } + + // We ignore username/password at this point + + // The path has to be transformed + let mut relative = String::new(); + + // Extract the filename of both URIs, these need to be handled separately + fn extract_path_filename(s: &str) -> (&str, &str) { + let last_slash_idx = s.rfind('/').unwrap_or(0); + let (path, filename) = s.split_at(last_slash_idx); + if filename.is_empty() { + (path, "") + } else { + (path, &filename[1..]) + } + } + + let (base_path, base_filename) = extract_path_filename(self.path()); + let (url_path, url_filename) = extract_path_filename(url.path()); + + let mut base_path = base_path.split('/').peekable(); + let mut url_path = url_path.split('/').peekable(); + + // Skip over the common prefix + while base_path.peek().is_some() && base_path.peek() == url_path.peek() { + base_path.next(); + url_path.next(); + } + + // Add `..` segments for the remainder of the base path + for base_path_segment in base_path { + // Skip empty last segments + if base_path_segment.is_empty() { + break; + } + + if !relative.is_empty() { + relative.push('/'); + } + + relative.push_str(".."); + } + + // Append the remainder of the other URI + for url_path_segment in url_path { + if !relative.is_empty() { + relative.push('/'); + } + + relative.push_str(url_path_segment); + } + + // Add the filename if they are not the same + if base_filename != url_filename { + // If the URIs filename is empty this means that it was a directory + // so we'll have to append a '/'. + // + // Otherwise append it directly as the new filename. + if url_filename.is_empty() { + relative.push('/'); + } else { + if !relative.is_empty() { + relative.push('/'); + } + relative.push_str(url_filename); + } + } + + // Query and fragment are only taken from the other URI + if let Some(query) = url.query() { + relative.push('?'); + relative.push_str(query); + } + + if let Some(fragment) = url.fragment() { + relative.push('#'); + relative.push_str(fragment); + } + + Some(relative) + } + /// Return a default `ParseOptions` that can fully configure the URL parser. /// /// # Examples @@ -417,14 +552,15 @@ impl Url { /// # fn run() -> Result<(), ParseError> { /// let url_str = "https://example.net/"; /// let url = Url::parse(url_str)?; - /// assert_eq!(url.into_string(), url_str); + /// assert_eq!(String::from(url), url_str); /// # Ok(()) /// # } /// # run().unwrap(); /// ``` #[inline] + #[deprecated(since = "2.3.0", note = "use Into")] pub fn into_string(self) -> String { - self.serialization + self.into() } /// For internal testing, not part of the public API. @@ -1433,7 +1569,7 @@ impl Url { /// Return an object with methods to manipulate this URL’s path segments. /// /// Return `Err(())` if this URL is cannot-be-a-base. - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn path_segments_mut(&mut self) -> Result, ()> { if self.cannot_be_a_base() { Err(()) @@ -1517,7 +1653,7 @@ impl Url { /// # } /// # run().unwrap(); /// ``` - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn set_port(&mut self, mut port: Option) -> Result<(), ()> { // has_host implies !cannot_be_a_base if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" { @@ -1788,7 +1924,7 @@ impl Url { /// # run().unwrap(); /// ``` /// - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn set_ip_host(&mut self, address: IpAddr) -> Result<(), ()> { if self.cannot_be_a_base() { return Err(()); @@ -1828,7 +1964,7 @@ impl Url { /// # } /// # run().unwrap(); /// ``` - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn set_password(&mut self, password: Option<&str>) -> Result<(), ()> { // has_host implies !cannot_be_a_base if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" { @@ -1921,7 +2057,7 @@ impl Url { /// # } /// # run().unwrap(); /// ``` - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn set_username(&mut self, username: &str) -> Result<(), ()> { // has_host implies !cannot_be_a_base if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" { @@ -2163,7 +2299,7 @@ impl Url { /// # } /// ``` #[cfg(any(unix, windows, target_os = "redox"))] - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn from_file_path>(path: P) -> Result { let mut serialization = "file://".to_owned(); let host_start = serialization.len() as u32; @@ -2200,7 +2336,7 @@ impl Url { /// Note that `std::path` does not consider trailing slashes significant /// and usually does not include them (e.g. in `Path::parent()`). #[cfg(any(unix, windows, target_os = "redox"))] - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn from_directory_path>(path: P) -> Result { let mut url = Url::from_file_path(path)?; if !url.serialization.ends_with('/') { @@ -2317,7 +2453,7 @@ impl Url { /// for a Windows path, is not UTF-8.) #[inline] #[cfg(any(unix, windows, target_os = "redox"))] - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn to_file_path(&self) -> Result { if let Some(segments) = self.path_segments() { let host = match self.host() { @@ -2375,6 +2511,13 @@ impl fmt::Display for Url { } } +/// String converstion. +impl From for String { + fn from(value: Url) -> String { + value.serialization + } +} + /// Debug the serialization of this URL. impl fmt::Debug for Url { #[inline] @@ -2382,6 +2525,7 @@ impl fmt::Debug for Url { formatter .debug_struct("Url") .field("scheme", &self.scheme()) + .field("cannot_be_a_base", &self.cannot_be_a_base()) .field("username", &self.username()) .field("password", &self.password()) .field("host", &self.host()) diff --git a/src/quirks.rs b/src/quirks.rs index 72dadaa..0dbc6eb 100644 --- a/src/quirks.rs +++ b/src/quirks.rs @@ -56,7 +56,7 @@ pub fn protocol(url: &Url) -> &str { } /// Setter for https://url.spec.whatwg.org/#dom-url-protocol -#[allow(clippy::clippy::result_unit_err)] +#[allow(clippy::result_unit_err)] pub fn set_protocol(url: &mut Url, mut new_protocol: &str) -> Result<(), ()> { // The scheme state in the spec ignores everything after the first `:`, // but `set_scheme` errors if there is more. @@ -73,7 +73,7 @@ pub fn username(url: &Url) -> &str { } /// Setter for https://url.spec.whatwg.org/#dom-url-username -#[allow(clippy::clippy::result_unit_err)] +#[allow(clippy::result_unit_err)] pub fn set_username(url: &mut Url, new_username: &str) -> Result<(), ()> { url.set_username(new_username) } @@ -85,7 +85,7 @@ pub fn password(url: &Url) -> &str { } /// Setter for https://url.spec.whatwg.org/#dom-url-password -#[allow(clippy::clippy::result_unit_err)] +#[allow(clippy::result_unit_err)] pub fn set_password(url: &mut Url, new_password: &str) -> Result<(), ()> { url.set_password(if new_password.is_empty() { None @@ -101,7 +101,7 @@ pub fn host(url: &Url) -> &str { } /// Setter for https://url.spec.whatwg.org/#dom-url-host -#[allow(clippy::clippy::result_unit_err)] +#[allow(clippy::result_unit_err)] pub fn set_host(url: &mut Url, new_host: &str) -> Result<(), ()> { // If context object’s url’s cannot-be-a-base-URL flag is set, then return. if url.cannot_be_a_base() { @@ -158,7 +158,7 @@ pub fn hostname(url: &Url) -> &str { } /// Setter for https://url.spec.whatwg.org/#dom-url-hostname -#[allow(clippy::clippy::result_unit_err)] +#[allow(clippy::result_unit_err)] pub fn set_hostname(url: &mut Url, new_hostname: &str) -> Result<(), ()> { if url.cannot_be_a_base() { return Err(()); @@ -200,7 +200,7 @@ pub fn port(url: &Url) -> &str { } /// Setter for https://url.spec.whatwg.org/#dom-url-port -#[allow(clippy::clippy::result_unit_err)] +#[allow(clippy::result_unit_err)] pub fn set_port(url: &mut Url, new_port: &str) -> Result<(), ()> { let result; { diff --git a/tests/unit.rs b/tests/unit.rs index 4c25198..13055a4 100644 --- a/tests/unit.rs +++ b/tests/unit.rs @@ -12,7 +12,7 @@ use std::borrow::Cow; use std::cell::{Cell, RefCell}; use std::net::{Ipv4Addr, Ipv6Addr}; use std::path::{Path, PathBuf}; -use url::{form_urlencoded, Host, Url}; +use url::{form_urlencoded, Host, Origin, Url}; #[test] fn size() { @@ -517,6 +517,209 @@ fn test_origin_hash() { assert_ne!(hash(&opaque_origin), hash(&other_opaque_origin)); } +#[test] +fn test_origin_blob_equality() { + let origin = &Url::parse("http://example.net/").unwrap().origin(); + let blob_origin = &Url::parse("blob:http://example.net/").unwrap().origin(); + + assert_eq!(origin, blob_origin); +} + +#[test] +fn test_origin_opaque() { + assert!(!Origin::new_opaque().is_tuple()); + assert!(!&Url::parse("blob:malformed//").unwrap().origin().is_tuple()) +} + +#[test] +fn test_origin_unicode_serialization() { + let data = [ + ("http://😅.com", "http://😅.com"), + ("ftp://😅:🙂@🙂.com", "ftp://🙂.com"), + ("https://user@😅.com", "https://😅.com"), + ("http://😅.🙂:40", "http://😅.🙂:40"), + ]; + for &(unicode_url, expected_serialization) in &data { + let origin = Url::parse(unicode_url).unwrap().origin(); + assert_eq!(origin.unicode_serialization(), *expected_serialization); + } + + let ascii_origins = [ + Url::parse("http://example.net/").unwrap().origin(), + Url::parse("http://example.net:80/").unwrap().origin(), + Url::parse("http://example.net:81/").unwrap().origin(), + Url::parse("http://example.net").unwrap().origin(), + Url::parse("http://example.net/hello").unwrap().origin(), + Url::parse("https://example.net").unwrap().origin(), + Url::parse("ftp://example.net").unwrap().origin(), + Url::parse("file://example.net").unwrap().origin(), + Url::parse("http://user@example.net/").unwrap().origin(), + Url::parse("http://user:pass@example.net/") + .unwrap() + .origin(), + Url::parse("http://127.0.0.1").unwrap().origin(), + ]; + for ascii_origin in &ascii_origins { + assert_eq!( + ascii_origin.ascii_serialization(), + ascii_origin.unicode_serialization() + ); + } +} + +#[test] +fn test_socket_addrs() { + use std::net::ToSocketAddrs; + + let data = [ + ("https://127.0.0.1/", "127.0.0.1", 443), + ("https://127.0.0.1:9742/", "127.0.0.1", 9742), + ("custom-protocol://127.0.0.1:9742/", "127.0.0.1", 9742), + ("custom-protocol://127.0.0.1/", "127.0.0.1", 9743), + ("https://[::1]/", "::1", 443), + ("https://[::1]:9742/", "::1", 9742), + ("custom-protocol://[::1]:9742/", "::1", 9742), + ("custom-protocol://[::1]/", "::1", 9743), + ("https://localhost/", "localhost", 443), + ("https://localhost:9742/", "localhost", 9742), + ("custom-protocol://localhost:9742/", "localhost", 9742), + ("custom-protocol://localhost/", "localhost", 9743), + ]; + + for (url_string, host, port) in &data { + let url = url::Url::parse(url_string).unwrap(); + let addrs = url + .socket_addrs(|| match url.scheme() { + "custom-protocol" => Some(9743), + _ => None, + }) + .unwrap(); + assert_eq!( + Some(addrs[0]), + (*host, *port).to_socket_addrs().unwrap().next() + ); + } +} + +#[test] +fn test_no_base_url() { + let mut no_base_url = Url::parse("mailto:test@example.net").unwrap(); + + assert!(no_base_url.cannot_be_a_base()); + assert!(no_base_url.path_segments().is_none()); + assert!(no_base_url.path_segments_mut().is_err()); + assert!(no_base_url.set_host(Some("foo")).is_err()); + assert!(no_base_url + .set_ip_host("127.0.0.1".parse().unwrap()) + .is_err()); + + no_base_url.set_path("/foo"); + assert_eq!(no_base_url.path(), "%2Ffoo"); +} + +#[test] +fn test_domain() { + let url = Url::parse("https://127.0.0.1/").unwrap(); + assert_eq!(url.domain(), None); + + let url = Url::parse("mailto:test@example.net").unwrap(); + assert_eq!(url.domain(), None); + + let url = Url::parse("https://example.com/").unwrap(); + assert_eq!(url.domain(), Some("example.com")); +} + +#[test] +fn test_query() { + let url = Url::parse("https://example.com/products?page=2#fragment").unwrap(); + assert_eq!(url.query(), Some("page=2")); + assert_eq!( + url.query_pairs().next(), + Some((Cow::Borrowed("page"), Cow::Borrowed("2"))) + ); + + let url = Url::parse("https://example.com/products").unwrap(); + assert!(url.query().is_none()); + assert_eq!(url.query_pairs().count(), 0); + + let url = Url::parse("https://example.com/?country=español").unwrap(); + assert_eq!(url.query(), Some("country=espa%C3%B1ol")); + assert_eq!( + url.query_pairs().next(), + Some((Cow::Borrowed("country"), Cow::Borrowed("español"))) + ); + + let url = Url::parse("https://example.com/products?page=2&sort=desc").unwrap(); + assert_eq!(url.query(), Some("page=2&sort=desc")); + let mut pairs = url.query_pairs(); + assert_eq!(pairs.count(), 2); + assert_eq!( + pairs.next(), + Some((Cow::Borrowed("page"), Cow::Borrowed("2"))) + ); + assert_eq!( + pairs.next(), + Some((Cow::Borrowed("sort"), Cow::Borrowed("desc"))) + ); +} + +#[test] +fn test_fragment() { + let url = Url::parse("https://example.com/#fragment").unwrap(); + assert_eq!(url.fragment(), Some("fragment")); + + let url = Url::parse("https://example.com/").unwrap(); + assert_eq!(url.fragment(), None); +} + +#[test] +fn test_set_ip_host() { + let mut url = Url::parse("http://example.com").unwrap(); + + url.set_ip_host("127.0.0.1".parse().unwrap()).unwrap(); + assert_eq!(url.host_str(), Some("127.0.0.1")); + + url.set_ip_host("::1".parse().unwrap()).unwrap(); + assert_eq!(url.host_str(), Some("[::1]")); +} + +#[test] +fn test_set_href() { + use url::quirks::set_href; + + let mut url = Url::parse("https://existing.url").unwrap(); + + assert!(set_href(&mut url, "mal//formed").is_err()); + + assert!(set_href( + &mut url, + "https://user:pass@domain.com:9742/path/file.ext?key=val&key2=val2#fragment" + ) + .is_ok()); + assert_eq!( + url, + Url::parse("https://user:pass@domain.com:9742/path/file.ext?key=val&key2=val2#fragment") + .unwrap() + ); +} + +#[test] +fn test_domain_encoding_quirks() { + use url::quirks::{domain_to_ascii, domain_to_unicode}; + + let data = [ + ("http://example.com", "", ""), + ("😅.🙂", "xn--j28h.xn--938h", "😅.🙂"), + ("example.com", "example.com", "example.com"), + ("mailto:test@example.net", "", ""), + ]; + + for url in &data { + assert_eq!(domain_to_ascii(url.0), url.1); + assert_eq!(domain_to_unicode(url.0), url.2); + } +} + #[test] fn test_windows_unc_path() { if !cfg!(windows) { @@ -581,6 +784,38 @@ fn test_syntax_violation_callback_lifetimes() { assert_eq!(violation.take(), Some(Backslash)); } +#[test] +fn test_syntax_violation_callback_types() { + use url::SyntaxViolation::*; + + let data = [ + ("http://mozilla.org/\\foo", Backslash, "backslash"), + (" http://mozilla.org", C0SpaceIgnored, "leading or trailing control or space character are ignored in URLs"), + ("http://user:pass@mozilla.org", EmbeddedCredentials, "embedding authentication information (username or password) in an URL is not recommended"), + ("http:///mozilla.org", ExpectedDoubleSlash, "expected //"), + ("file:/foo.txt", ExpectedFileDoubleSlash, "expected // after file:"), + ("file://mozilla.org/c:/file.txt", FileWithHostAndWindowsDrive, "file: with host and Windows drive letter"), + ("http://mozilla.org/^", NonUrlCodePoint, "non-URL code point"), + ("http://mozilla.org/#\00", NullInFragment, "NULL characters are ignored in URL fragment identifiers"), + ("http://mozilla.org/%1", PercentDecode, "expected 2 hex digits after %"), + ("http://mozilla.org\t/foo", TabOrNewlineIgnored, "tabs or newlines are ignored in URLs"), + ("http://user@:pass@mozilla.org", UnencodedAtSign, "unencoded @ sign in username or password") + ]; + + for test_case in &data { + let violation = Cell::new(None); + Url::options() + .syntax_violation_callback(Some(&|v| violation.set(Some(v)))) + .parse(test_case.0) + .unwrap(); + + let v = violation.take(); + assert_eq!(v, Some(test_case.1)); + assert_eq!(v.unwrap().description(), test_case.2); + assert_eq!(v.unwrap().to_string(), test_case.2); + } +} + #[test] fn test_options_reuse() { use url::SyntaxViolation::*; @@ -679,3 +914,205 @@ fn pop_if_empty_in_bounds() { segments.pop_if_empty(); segments.pop(); } + +#[test] +fn test_slicing() { + use url::Position::*; + + #[derive(Default)] + struct ExpectedSlices<'a> { + full: &'a str, + scheme: &'a str, + username: &'a str, + password: &'a str, + host: &'a str, + port: &'a str, + path: &'a str, + query: &'a str, + fragment: &'a str, + } + + let data = [ + ExpectedSlices { + full: "https://user:pass@domain.com:9742/path/file.ext?key=val&key2=val2#fragment", + scheme: "https", + username: "user", + password: "pass", + host: "domain.com", + port: "9742", + path: "/path/file.ext", + query: "key=val&key2=val2", + fragment: "fragment", + }, + ExpectedSlices { + full: "https://domain.com:9742/path/file.ext#fragment", + scheme: "https", + host: "domain.com", + port: "9742", + path: "/path/file.ext", + fragment: "fragment", + ..Default::default() + }, + ExpectedSlices { + full: "https://domain.com:9742/path/file.ext", + scheme: "https", + host: "domain.com", + port: "9742", + path: "/path/file.ext", + ..Default::default() + }, + ExpectedSlices { + full: "blob:blob-info", + scheme: "blob", + path: "blob-info", + ..Default::default() + }, + ]; + + for expected_slices in &data { + let url = Url::parse(expected_slices.full).unwrap(); + assert_eq!(&url[..], expected_slices.full); + assert_eq!(&url[BeforeScheme..AfterScheme], expected_slices.scheme); + assert_eq!( + &url[BeforeUsername..AfterUsername], + expected_slices.username + ); + assert_eq!( + &url[BeforePassword..AfterPassword], + expected_slices.password + ); + assert_eq!(&url[BeforeHost..AfterHost], expected_slices.host); + assert_eq!(&url[BeforePort..AfterPort], expected_slices.port); + assert_eq!(&url[BeforePath..AfterPath], expected_slices.path); + assert_eq!(&url[BeforeQuery..AfterQuery], expected_slices.query); + assert_eq!( + &url[BeforeFragment..AfterFragment], + expected_slices.fragment + ); + assert_eq!(&url[..AfterFragment], expected_slices.full); + } +} + +#[test] +fn test_make_relative() { + let tests = [ + ( + "http://127.0.0.1:8080/test", + "http://127.0.0.1:8080/test", + "", + ), + ( + "http://127.0.0.1:8080/test", + "http://127.0.0.1:8080/test/", + "test/", + ), + ( + "http://127.0.0.1:8080/test/", + "http://127.0.0.1:8080/test", + "../test", + ), + ( + "http://127.0.0.1:8080/", + "http://127.0.0.1:8080/?foo=bar#123", + "?foo=bar#123", + ), + ( + "http://127.0.0.1:8080/", + "http://127.0.0.1:8080/test/video", + "test/video", + ), + ( + "http://127.0.0.1:8080/test", + "http://127.0.0.1:8080/test/video", + "test/video", + ), + ( + "http://127.0.0.1:8080/test/", + "http://127.0.0.1:8080/test/video", + "video", + ), + ( + "http://127.0.0.1:8080/test", + "http://127.0.0.1:8080/test2/video", + "test2/video", + ), + ( + "http://127.0.0.1:8080/test/", + "http://127.0.0.1:8080/test2/video", + "../test2/video", + ), + ( + "http://127.0.0.1:8080/test/bla", + "http://127.0.0.1:8080/test2/video", + "../test2/video", + ), + ( + "http://127.0.0.1:8080/test/bla/", + "http://127.0.0.1:8080/test2/video", + "../../test2/video", + ), + ( + "http://127.0.0.1:8080/test/?foo=bar#123", + "http://127.0.0.1:8080/test/video", + "video", + ), + ( + "http://127.0.0.1:8080/test/", + "http://127.0.0.1:8080/test/video?baz=meh#456", + "video?baz=meh#456", + ), + ( + "http://127.0.0.1:8080/test", + "http://127.0.0.1:8080/test?baz=meh#456", + "?baz=meh#456", + ), + ( + "http://127.0.0.1:8080/test/", + "http://127.0.0.1:8080/test?baz=meh#456", + "../test?baz=meh#456", + ), + ( + "http://127.0.0.1:8080/test/", + "http://127.0.0.1:8080/test/?baz=meh#456", + "?baz=meh#456", + ), + ( + "http://127.0.0.1:8080/test/?foo=bar#123", + "http://127.0.0.1:8080/test/video?baz=meh#456", + "video?baz=meh#456", + ), + ]; + + for (base, uri, relative) in &tests { + let base_uri = url::Url::parse(base).unwrap(); + let relative_uri = url::Url::parse(uri).unwrap(); + let make_relative = base_uri.make_relative(&relative_uri).unwrap(); + assert_eq!( + make_relative, *relative, + "base: {}, uri: {}, relative: {}", + base, uri, relative + ); + assert_eq!( + base_uri.join(&relative).unwrap().as_str(), + *uri, + "base: {}, uri: {}, relative: {}", + base, + uri, + relative + ); + } + + let error_tests = [ + ("http://127.0.0.1:8080/", "https://127.0.0.1:8080/test/"), + ("http://127.0.0.1:8080/", "http://127.0.0.1:8081/test/"), + ("http://127.0.0.1:8080/", "http://127.0.0.2:8080/test/"), + ("mailto:a@example.com", "mailto:b@example.com"), + ]; + + for (base, uri) in &error_tests { + let base_uri = url::Url::parse(base).unwrap(); + let relative_uri = url::Url::parse(uri).unwrap(); + let make_relative = base_uri.make_relative(&relative_uri); + assert_eq!(make_relative, None, "base: {}, uri: {}", base, uri); + } +} -- cgit v1.2.3