//! Components. #![allow(dead_code)] use core::fmt; use iri_string::build::Builder; /// Test case. #[derive(Debug, Clone, Copy)] pub struct TestCase<'a> { /// Test case name. pub name: Option<&'a str>, /// Test case description. pub description: Option<&'a str>, /// Composed string. pub composed: &'a str, /// Components. pub components: Components<'a>, /// Normalized string as URI. pub normalized_uri: &'a str, /// Normalized string as IRI. pub normalized_iri: &'a str, /// Normalized (by WHATWG-like algorithm) string as URI. pub normalized_uri_whatwg_like: Option<&'a str>, /// Normalized (by WHATWG-like algorithm) string as IRI. pub normalized_iri_whatwg_like: Option<&'a str>, /// Different IRIs. pub different_iris: &'a [&'a str], } impl TestCase<'_> { #[inline] #[must_use] pub fn is_uri_class(&self) -> bool { self.composed.is_ascii() } #[inline] #[must_use] pub const fn is_iri_class(&self) -> bool { true } #[inline] #[must_use] pub const fn is_absolute(&self) -> bool { self.components.is_absolute() } #[inline] #[must_use] pub const fn is_absolute_without_fragment(&self) -> bool { self.components.is_absolute_without_fragment() } #[inline] #[must_use] pub const fn is_relative(&self) -> bool { self.components.is_relative() } #[inline] #[must_use] pub fn is_rfc3986_normalizable(&self) -> bool { match self.normalized_iri.find('/') { Some(pos) => !self.normalized_iri[(pos + 1)..].starts_with("./"), None => true, } } } /// Components. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Components<'a> { /// `scheme`. pub scheme: Option<&'a str>, /// User part (string before the first colon) of `userinfo`. /// /// Note that `host` should also be `Some(_)` if this is `Some(_)`. pub user: Option<&'a str>, /// Password part (string after the first colon) of `userinfo`. /// /// Note that `host` should also be `Some(_)` if this is `Some(_)`. pub password: Option<&'a str>, /// `host`. pub host: Option<&'a str>, /// `port`. /// /// Note that `host` should also be `Some(_)` if this is `Some(_)`. pub port: Option<&'a str>, /// `path`. pub path: &'a str, /// `query`. pub query: Option<&'a str>, /// `fragment`. pub fragment: Option<&'a str>, } impl<'a> Components<'a> { #[inline] #[must_use] const fn const_default() -> Self { Self { scheme: None, user: None, password: None, host: None, port: None, path: "", query: None, fragment: None, } } pub fn feed_builder(&self, builder: &mut Builder<'a>, clean: bool) { if let Some(scheme) = self.scheme { builder.scheme(scheme); } else if clean { builder.unset_scheme(); } if let Some(host) = self.host { if self.user.is_some() || self.password.is_some() { builder.userinfo((self.user.unwrap_or(""), self.password)); } else if clean { builder.unset_userinfo(); } builder.host(host); if let Some(port) = self.port { builder.port(port); } else if clean { builder.unset_port(); } } else if clean { builder.unset_authority(); } builder.path(self.path); if let Some(query) = self.query { builder.query(query); } else if clean { builder.unset_query(); } if let Some(fragment) = self.fragment { builder.fragment(fragment); } else if clean { builder.unset_fragment(); } } #[inline] #[must_use] pub const fn is_absolute(&self) -> bool { self.scheme.is_some() } #[inline] #[must_use] pub const fn is_absolute_without_fragment(&self) -> bool { self.scheme.is_some() && self.fragment.is_none() } #[inline] #[must_use] pub const fn is_relative(&self) -> bool { self.scheme.is_none() } } impl fmt::Display for Components<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(scheme) = self.scheme { write!(f, "{scheme}:")?; } assert!( self.host.is_some() || (self.user.is_none() && self.password.is_none() && self.port.is_none()), "`user`, `password`, and `port` requires `host` to be present" ); if let Some(host) = self.host { if let Some(user) = self.user { f.write_str(user)?; } if let Some(password) = self.password { write!(f, ":{password}")?; } if self.user.is_some() || self.password.is_some() { write!(f, "@")?; } f.write_str(host)?; if let Some(port) = self.port { write!(f, ":{port}")?; } } f.write_str(self.path)?; if let Some(query) = self.query { write!(f, "#{query}")?; } if let Some(fragment) = self.fragment { write!(f, "#{fragment}")?; } Ok(()) } } macro_rules! components { () => { Components::default() }; ($($field:ident: $expr:expr),* $(,)?) => { Components { $( $field: components!(@field; $field: $expr) ),*, .. Components::const_default() } }; (@field; path: $expr:expr) => { $expr }; (@field; $field:ident: None) => { None }; (@field; $field:ident: $expr:expr) => { Some($expr) }; } macro_rules! test_case { // Name. (@field=name; name: $value:expr, $($rest:tt)*) => { $value }; // Description. (@field=description; description: $value:expr, $($rest:tt)*) => { Some($value) }; (@field=description;) => { None }; // Composed. (@field=composed; composed: $value:expr, $($rest:tt)*) => { $value }; // Components. (@field=components; components: { $($toks:tt)* }, $($rest:tt)*) => { components! { $($toks)* } }; // Normalized URI. (@field=normalized_uri; normalized_uri: $value:expr, $($rest:tt)*) => { $value }; // Normalized IRI. (@field=normalized_iri; normalized_iri: $value:expr, $($rest:tt)*) => { $value }; // Normalized URI (WHATWG-like). (@field=normalized_uri_whatwg_like; normalized_uri_whatwg_like: $value:expr, $($rest:tt)*) => { Some($value) }; (@field=normalized_uri_whatwg_like;) => { None }; // Normalized IRI (WHATWG-like). (@field=normalized_iri_whatwg_like; normalized_iri_whatwg_like: $value:expr, $($rest:tt)*) => { Some($value) }; (@field=normalized_iri_whatwg_like;) => { None }; // Different IRIs. (@field=different_iris; different_iris: $value:expr, $($rest:tt)*) => { $value }; (@field=different_iris;) => { &[] }; // Fallback. (@field=$name:ident; $field:ident: { $($toks:tt)* }, $($rest:tt)*) => { test_case!(@field=$name; $($rest)*) }; // Fallback. (@field=$name:ident; $field:ident: $value:expr, $($rest:tt)*) => { test_case!(@field=$name; $($rest)*) }; ($($args:tt)*) => { TestCase { name: Some(test_case!(@field=name; $($args)*)), description: test_case!(@field=description; $($args)*), composed: test_case!(@field=composed; $($args)*), components: test_case!(@field=components; $($args)*), normalized_uri: test_case!(@field=normalized_uri; $($args)*), normalized_iri: test_case!(@field=normalized_iri; $($args)*), normalized_uri_whatwg_like: test_case!(@field=normalized_uri_whatwg_like; $($args)*), normalized_iri_whatwg_like: test_case!(@field=normalized_iri_whatwg_like; $($args)*), different_iris: test_case!(@field=different_iris; $($args)*), } }; } macro_rules! test_cases { ($({$($toks:tt)*}),* $(,)?) => { &[ $( test_case! { $($toks)* } ),* ] } } #[allow(clippy::needless_update)] // For `components!` macro. pub static TEST_CASES: &[TestCase<'static>] = test_cases![ { name: "typical example URI", composed: "http://example.com/", components: { scheme: "http", host: "example.com", path: "/", }, normalized_uri: "http://example.com/", normalized_iri: "http://example.com/", }, { name: "typical example URI with user and password", composed: "http://user:password@example.com/", components: { scheme: "http", user: "user", password: "password", host: "example.com", path: "/", }, normalized_uri: "http://user:password@example.com/", normalized_iri: "http://user:password@example.com/", }, { name: "URI with ASCII-only hostname with capital letters", description: "ASCII-only hostname should be normalized to lower letters", composed: "http://usER:passWORD@eXAMPLe.CoM/", components: { scheme: "http", user: "usER", password: "passWORD", host: "eXAMPLe.CoM", path: "/", }, normalized_uri: "http://usER:passWORD@example.com/", normalized_iri: "http://usER:passWORD@example.com/", }, { name: "IRI with non-ASCII hostname with capital letters", description: "hostname with non-ASCII characters should not be normalized to lower letters", composed: "http://usER:passWORD@\u{03B1}.CoM/", components: { scheme: "http", user: "usER", password: "passWORD", host: "\u{03B1}.CoM", path: "/", }, // The RFC 3986 (not 3987) spec is ambiguous: if the host contains percent-encoded // non-ASCII characters, should other part of the host be lowercased? // In this crate for now, the operations is implemented based on RFC 3987, i.e. // even URI type internally checks whether the percent-encoded characters // would be decoded to ASCII or not. normalized_uri: "http://usER:passWORD@%CE%B1.CoM/", normalized_iri: "http://usER:passWORD@\u{03B1}.CoM/", }, { name: "URI with all components set", composed: "http://user:password@example.com:80/path/to/somewhere?query#fragment", components: { scheme: "http", user: "user", password: "password", host: "example.com", port: "80", path: "/path/to/somewhere", query: "query", fragment: "fragment", }, normalized_uri: "http://user:password@example.com:80/path/to/somewhere?query#fragment", normalized_iri: "http://user:password@example.com:80/path/to/somewhere?query#fragment", }, { name: "URI that cannot be normalized by pure RFC 3986", composed: "scheme:/.//not-a-host", components: { scheme: "scheme", path: "/.//not-a-host", }, normalized_uri: "scheme:/.//not-a-host", normalized_iri: "scheme:/.//not-a-host", }, { name: "URI that cannot be normalized by pure RFC 3986", composed: "scheme:..///not-a-host", components: { scheme: "scheme", path: "..///not-a-host", }, normalized_uri: "scheme:/.//not-a-host", normalized_iri: "scheme:/.//not-a-host", normalized_uri_whatwg_like: "scheme:..///not-a-host", normalized_iri_whatwg_like: "scheme:..///not-a-host", }, { name: "Relative URI reference as a relative path `..`", description: "Relative path without scheme and authority should not be normalized", composed: "..", components: { path: "..", }, normalized_uri: "..", normalized_iri: "..", }, { name: "Relative URI reference as a relative path", description: "Relative path without scheme and authority should not be normalized", composed: "../foo/..", components: { path: "../foo/..", }, normalized_uri: "../foo/..", normalized_iri: "../foo/..", }, { name: "Relative URI reference as a relative path", description: "Relative path without scheme and authority should not be normalized", composed: "foo/../p%61th", components: { path: "foo/../p%61th", }, normalized_uri: "foo/../path", normalized_iri: "foo/../path", }, { name: "Relative path in an absolute URI", composed: "scheme:foo/../p%61th", components: { scheme: "scheme", path: "foo/../p%61th", }, normalized_uri: "scheme:/path", normalized_iri: "scheme:/path", normalized_uri_whatwg_like: "scheme:foo/../path", normalized_iri_whatwg_like: "scheme:foo/../path", }, { name: "Non-normalized URI", composed: "HTTPs://EXaMPLE.COM/pA/Th?Query#Frag", components: { scheme: "HTTPs", host: "EXaMPLE.COM", path: "/pA/Th", query: "Query", fragment: "Frag", }, normalized_uri: "https://example.com/pA/Th?Query#Frag", normalized_iri: "https://example.com/pA/Th?Query#Frag", different_iris: &[ "https://example.com/pa/th?Query#Frag", "https://example.com/pA/Th?query#Frag", "https://example.com/pA/Th?Query#frag", ], }, { name: "UUID URN", composed: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822", components: { scheme: "urn", path: "uuid:7f1450df-6678-465b-a881-188f9b6ec822", }, normalized_uri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822", normalized_iri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822", different_iris: &[ "urn:UUID:7f1450df-6678-465b-a881-188f9b6ec822", "urn:uuid:7F1450DF-6678-465B-A881-188F9B6EC822", ], }, { name: "UUID URN", composed: "URN:uuid:7f1450df-6678-465b-a881-188f9b6ec822", components: { scheme: "URN", path: "uuid:7f1450df-6678-465b-a881-188f9b6ec822", }, normalized_uri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822", normalized_iri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822", different_iris: &[ "urn:UUID:7f1450df-6678-465b-a881-188f9b6ec822", "urn:uuid:7F1450DF-6678-465B-A881-188F9B6EC822", ], }, { name: "UUID URN", composed: "URN:uuid:7f1450df-6678-465b-a881-188f9b6ec822", components: { scheme: "URN", path: "uuid:7f1450df-6678-465b-a881-188f9b6ec822", }, normalized_uri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822", normalized_iri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822", different_iris: &[ "urn:UUID:7f1450df-6678-465b-a881-188f9b6ec822", "urn:uuid:7F1450DF-6678-465B-A881-188F9B6EC822", ], }, { name: "IRI with percent-encoded unreserved characters and non-valid UTF-8 bytes", composed: "http://example.com/?a=%CE%B1&b=%CE%CE%B1%B1", components: { scheme: "http", host: "example.com", path: "/", query: "a=%CE%B1&b=%CE%CE%B1%B1", }, normalized_uri: "http://example.com/?a=%CE%B1&b=%CE%CE%B1%B1", normalized_iri: "http://example.com/?a=\u{03B1}&b=%CE\u{03B1}%B1", }, { name: "not ASCII-only host", composed: "SCHEME://Alpha%ce%b1/", components: { scheme: "SCHEME", host: "Alpha%ce%b1", path: "/", }, normalized_uri: "scheme://Alpha%CE%B1/", normalized_iri: "scheme://Alpha\u{03B1}/", }, { name: "URI with percent-encoded unreserevd and reserved characters", description: "Tilde character (0x7e) is unreserved and bang (0x21) is reserved", composed: "http://example.com/%7E%41%73%63%69%69%21", components: { scheme: "http", host: "example.com", path: "/%7E%41%73%63%69%69%21", }, normalized_uri: "http://example.com/~Ascii%21", normalized_iri: "http://example.com/~Ascii%21", }, { name: "not ASCII-only host", description: "Plus character (0x2B) is reserved (sub-delim), so it should not be decoded in host part", composed: "SCHEME://PLUS%2bPLUS/", components: { scheme: "SCHEME", host: "PLUS%2bPLUS", path: "/", }, normalized_uri: "scheme://plus%2Bplus/", normalized_iri: "scheme://plus%2Bplus/", }, { name: "empty port", // : // // > URI producers and normalizers should omit the port component // > and its ":" delimiter if port is empty or if its value would // > be the same as that of the scheme's default. description: "According to RFC 3986 section 3.2.3, empty port should be omitted by normalization", composed: "https://example.com:/", components: { scheme: "https", host: "example.com", port: "", path: "/", }, normalized_uri: "https://example.com/", normalized_iri: "https://example.com/", }, { name: "URI with a dot-dot segment", composed: "http://example.com/a/b/c/%2e%2e/d/e", components: { scheme: "http", host: "example.com", path: "/a/b/c/%2e%2e/d/e", }, normalized_uri: "http://example.com/a/b/d/e", normalized_iri: "http://example.com/a/b/d/e", }, { name: "URI with a dot-dot segment", composed: "http://example.com/a/b/c/%2E%2E/d/e", components: { scheme: "http", host: "example.com", path: "/a/b/c/%2E%2E/d/e", }, normalized_uri: "http://example.com/a/b/d/e", normalized_iri: "http://example.com/a/b/d/e", }, { name: "URI with a dot-dot segment", composed: "http://example.com/a/b/c/../d/e", components: { scheme: "http", host: "example.com", path: "/a/b/c/../d/e", }, normalized_uri: "http://example.com/a/b/d/e", normalized_iri: "http://example.com/a/b/d/e", }, { name: "URI with a dot-dot segment", composed: "http://example.com/a/b/c/.%2e/d/e", components: { scheme: "http", host: "example.com", path: "/a/b/c/.%2e/d/e", }, normalized_uri: "http://example.com/a/b/d/e", normalized_iri: "http://example.com/a/b/d/e", }, { name: "URI with dot segments", composed: "http://example.com/a/./././././b/c/.%2e/d/e", components: { scheme: "http", host: "example.com", path: "/a/./././././b/c/.%2e/d/e", }, normalized_uri: "http://example.com/a/b/d/e", normalized_iri: "http://example.com/a/b/d/e", }, // START: Combination. { name: "Empty relative IRI", composed: "", components: { path: "", }, normalized_uri: "", normalized_iri: "", }, { name: "Combination: fragment", composed: "#fragment", components: { fragment: "fragment", }, normalized_uri: "#fragment", normalized_iri: "#fragment", }, { name: "Combination: query", composed: "?query", components: { query: "query", }, normalized_uri: "?query", normalized_iri: "?query", }, { name: "Combination: query+fragment", composed: "?query#fragment", components: { query: "query", fragment: "fragment", }, normalized_uri: "?query#fragment", normalized_iri: "?query#fragment", }, { name: "Combination: path", composed: "/pa/th", components: { path: "/pa/th", }, normalized_uri: "/pa/th", normalized_iri: "/pa/th", }, { name: "Combination: path+fragment", composed: "/pa/th#fragment", components: { path: "/pa/th", fragment: "fragment", }, normalized_uri: "/pa/th#fragment", normalized_iri: "/pa/th#fragment", }, { name: "Combination: path+query", composed: "/pa/th?query", components: { path: "/pa/th", query: "query", }, normalized_uri: "/pa/th?query", normalized_iri: "/pa/th?query", }, { name: "Combination: path+query+fragment", composed: "/pa/th?query#fragment", components: { path: "/pa/th", query: "query", fragment: "fragment", }, normalized_uri: "/pa/th?query#fragment", normalized_iri: "/pa/th?query#fragment", }, { name: "Combination: authority", composed: "//authority", components: { host: "authority", }, normalized_uri: "//authority", normalized_iri: "//authority", }, { name: "Combination: authority+fragment", composed: "//authority#fragment", components: { host: "authority", fragment: "fragment", }, normalized_uri: "//authority#fragment", normalized_iri: "//authority#fragment", }, { name: "Combination: authority+query", composed: "//authority?query", components: { host: "authority", query: "query", }, normalized_uri: "//authority?query", normalized_iri: "//authority?query", }, { name: "Combination: authority+query+fragment", composed: "//authority?query#fragment", components: { host: "authority", query: "query", fragment: "fragment", }, normalized_uri: "//authority?query#fragment", normalized_iri: "//authority?query#fragment", }, { name: "Combination: authority+path", composed: "//authority/pa/th", components: { host: "authority", path: "/pa/th", }, normalized_uri: "//authority/pa/th", normalized_iri: "//authority/pa/th", }, { name: "Combination: authority+path+fragment", composed: "//authority/pa/th#fragment", components: { host: "authority", path: "/pa/th", fragment: "fragment", }, normalized_uri: "//authority/pa/th#fragment", normalized_iri: "//authority/pa/th#fragment", }, { name: "Combination: authority+path+query", composed: "//authority/pa/th?query", components: { host: "authority", path: "/pa/th", query: "query", }, normalized_uri: "//authority/pa/th?query", normalized_iri: "//authority/pa/th?query", }, { name: "Combination: authority+path+query+fragment", composed: "//authority/pa/th?query#fragment", components: { host: "authority", path: "/pa/th", query: "query", fragment: "fragment", }, normalized_uri: "//authority/pa/th?query#fragment", normalized_iri: "//authority/pa/th?query#fragment", }, { name: "Combination: scheme", composed: "scheme:", components: { scheme: "scheme", }, normalized_uri: "scheme:", normalized_iri: "scheme:", }, { name: "Combination: scheme+fragment", composed: "scheme:#fragment", components: { scheme: "scheme", fragment: "fragment", }, normalized_uri: "scheme:#fragment", normalized_iri: "scheme:#fragment", }, { name: "Combination: scheme+query", composed: "scheme:?query", components: { scheme: "scheme", query: "query", }, normalized_uri: "scheme:?query", normalized_iri: "scheme:?query", }, { name: "Combination: scheme+query+fragment", composed: "scheme:?query#fragment", components: { scheme: "scheme", query: "query", fragment: "fragment", }, normalized_uri: "scheme:?query#fragment", normalized_iri: "scheme:?query#fragment", }, { name: "Combination: scheme+path", composed: "scheme:/pa/th", components: { scheme: "scheme", path: "/pa/th", }, normalized_uri: "scheme:/pa/th", normalized_iri: "scheme:/pa/th", }, { name: "Combination: scheme+path+fragment", composed: "scheme:/pa/th#fragment", components: { scheme: "scheme", path: "/pa/th", fragment: "fragment", }, normalized_uri: "scheme:/pa/th#fragment", normalized_iri: "scheme:/pa/th#fragment", }, { name: "Combination: scheme+path+query", composed: "scheme:/pa/th?query", components: { scheme: "scheme", path: "/pa/th", query: "query", }, normalized_uri: "scheme:/pa/th?query", normalized_iri: "scheme:/pa/th?query", }, { name: "Combination: scheme+path+query+fragment", composed: "scheme:/pa/th?query#fragment", components: { scheme: "scheme", path: "/pa/th", query: "query", fragment: "fragment", }, normalized_uri: "scheme:/pa/th?query#fragment", normalized_iri: "scheme:/pa/th?query#fragment", }, { name: "Combination: scheme+authority", composed: "scheme://authority", components: { scheme: "scheme", host: "authority", }, normalized_uri: "scheme://authority", normalized_iri: "scheme://authority", }, { name: "Combination: scheme+authority+fragment", composed: "scheme://authority#fragment", components: { scheme: "scheme", host: "authority", fragment: "fragment", }, normalized_uri: "scheme://authority#fragment", normalized_iri: "scheme://authority#fragment", }, { name: "Combination: scheme+authority+query", composed: "scheme://authority?query", components: { scheme: "scheme", host: "authority", query: "query", }, normalized_uri: "scheme://authority?query", normalized_iri: "scheme://authority?query", }, { name: "Combination: scheme+authority+query+fragment", composed: "scheme://authority?query#fragment", components: { scheme: "scheme", host: "authority", query: "query", fragment: "fragment", }, normalized_uri: "scheme://authority?query#fragment", normalized_iri: "scheme://authority?query#fragment", }, { name: "Combination: scheme+authority+path", composed: "scheme://authority/pa/th", components: { scheme: "scheme", host: "authority", path: "/pa/th", }, normalized_uri: "scheme://authority/pa/th", normalized_iri: "scheme://authority/pa/th", }, { name: "Combination: scheme+authority+path+fragment", composed: "scheme://authority/pa/th#fragment", components: { scheme: "scheme", host: "authority", path: "/pa/th", fragment: "fragment", }, normalized_uri: "scheme://authority/pa/th#fragment", normalized_iri: "scheme://authority/pa/th#fragment", }, { name: "Combination: scheme+authority+path+query", composed: "scheme://authority/pa/th?query", components: { scheme: "scheme", host: "authority", path: "/pa/th", query: "query", }, normalized_uri: "scheme://authority/pa/th?query", normalized_iri: "scheme://authority/pa/th?query", }, { name: "Combination: scheme+authority+path+query+fragment", composed: "scheme://authority/pa/th?query#fragment", components: { scheme: "scheme", host: "authority", path: "/pa/th", query: "query", fragment: "fragment", }, normalized_uri: "scheme://authority/pa/th?query#fragment", normalized_iri: "scheme://authority/pa/th?query#fragment", }, // END: Combination. { name: "1 slash following to the scheme", composed: "scheme:/", components: { scheme: "scheme", path: "/", }, normalized_uri: "scheme:/", normalized_iri: "scheme:/", }, { name: "2 slashes following to the scheme", composed: "scheme://", components: { scheme: "scheme", host: "", }, normalized_uri: "scheme://", normalized_iri: "scheme://", }, { name: "3 slashes following to the scheme", composed: "scheme:///", components: { scheme: "scheme", host: "", path: "/", }, normalized_uri: "scheme:///", normalized_iri: "scheme:///", }, { name: "4 slashes following to the scheme", composed: "scheme:////", components: { scheme: "scheme", host: "", path: "//", }, normalized_uri: "scheme:////", normalized_iri: "scheme:////", }, { name: "5 slashes following to the scheme", composed: "scheme://///", components: { scheme: "scheme", host: "", path: "///", }, normalized_uri: "scheme://///", normalized_iri: "scheme://///", }, { name: "1 slash", composed: "/", components: { path: "/", }, normalized_uri: "/", normalized_iri: "/", }, { name: "2 slash", composed: "//", components: { host: "", }, normalized_uri: "//", normalized_iri: "//", }, { name: "3 slash", composed: "///", components: { host: "", path: "/", }, normalized_uri: "///", normalized_iri: "///", }, { name: "4 slash", composed: "////", components: { host: "", path: "//", }, normalized_uri: "////", normalized_iri: "////", }, { name: "5 slash", composed: "/////", components: { host: "", path: "///", }, normalized_uri: "/////", normalized_iri: "/////", }, { name: "IPv4 address", composed: "//192.0.2.0", components: { host: "192.0.2.0", }, normalized_uri: "//192.0.2.0", normalized_iri: "//192.0.2.0", }, { name: "IPv4 address with port", composed: "//192.0.2.0:80", components: { host: "192.0.2.0", port: "80", }, normalized_uri: "//192.0.2.0:80", normalized_iri: "//192.0.2.0:80", }, { name: "IPv4 address", composed: "//255.255.255.255", components: { host: "255.255.255.255", }, normalized_uri: "//255.255.255.255", normalized_iri: "//255.255.255.255", }, { name: "IPv4 address with port", composed: "//255.255.255.255:65536", components: { host: "255.255.255.255", port: "65536", }, normalized_uri: "//255.255.255.255:65536", normalized_iri: "//255.255.255.255:65536", }, { name: "IPv4 address", composed: "//0.0.0.0", components: { host: "0.0.0.0", }, normalized_uri: "//0.0.0.0", normalized_iri: "//0.0.0.0", }, { name: "IPv4 address with port", composed: "//0.0.0.0:0", components: { host: "0.0.0.0", port: "0", }, normalized_uri: "//0.0.0.0:0", normalized_iri: "//0.0.0.0:0", }, { name: "IPv6 address", composed: "//[2001:db8::]", components: { host: "[2001:db8::]", }, normalized_uri: "//[2001:db8::]", normalized_iri: "//[2001:db8::]", }, { name: "IPv6 address with port", composed: "//[2001:db8::]:80", components: { host: "[2001:db8::]", port: "80", }, normalized_uri: "//[2001:db8::]:80", normalized_iri: "//[2001:db8::]:80", }, { name: "IPv6 address", composed: "//[2001:0db8::]", components: { host: "[2001:0db8::]", }, normalized_uri: "//[2001:0db8::]", normalized_iri: "//[2001:0db8::]", }, { name: "IPv6 address with port", composed: "//[2001:0db8::]:80", components: { host: "[2001:0db8::]", port: "80", }, normalized_uri: "//[2001:0db8::]:80", normalized_iri: "//[2001:0db8::]:80", }, { name: "IPv6 address", composed: "//[2001:0db8:0:0:0:0:0:ffff]", components: { host: "[2001:0db8:0:0:0:0:0:ffff]", }, normalized_uri: "//[2001:0db8:0:0:0:0:0:ffff]", normalized_iri: "//[2001:0db8:0:0:0:0:0:ffff]", }, { name: "IPv6 address with port", composed: "//[2001:0db8:0:0:0:0:0:ffff]:80", components: { host: "[2001:0db8:0:0:0:0:0:ffff]", port: "80", }, normalized_uri: "//[2001:0db8:0:0:0:0:0:ffff]:80", normalized_iri: "//[2001:0db8:0:0:0:0:0:ffff]:80", }, { name: "IPv6 address", composed: "//[2001:0DB8:0000:0000:0000:000A:BCDE:FFFF]", components: { host: "[2001:0DB8:0000:0000:0000:000A:BCDE:FFFF]", }, normalized_uri: "//[2001:0db8:0000:0000:0000:000a:bcde:ffff]", normalized_iri: "//[2001:0db8:0000:0000:0000:000a:bcde:ffff]", }, { name: "IPv6 address with port", composed: "//[2001:0DB8:0000:0000:0000:000A:BCDE:FFFF]:80", components: { host: "[2001:0DB8:0000:0000:0000:000A:BCDE:FFFF]", port: "80", }, normalized_uri: "//[2001:0db8:0000:0000:0000:000a:bcde:ffff]:80", normalized_iri: "//[2001:0db8:0000:0000:0000:000a:bcde:ffff]:80", }, { name: "IPv6 address", composed: "//[2001:0db8::]", components: { host: "[2001:0db8::]", }, normalized_uri: "//[2001:0db8::]", normalized_iri: "//[2001:0db8::]", }, { name: "IPv6 address with port", composed: "//[2001:0db8::]:80", components: { host: "[2001:0db8::]", port: "80", }, normalized_uri: "//[2001:0db8::]:80", normalized_iri: "//[2001:0db8::]:80", }, { name: "IPv6 address", composed: "//[2001:0DB8:0:0:0:0::1]", components: { host: "[2001:0DB8:0:0:0:0::1]", }, normalized_uri: "//[2001:0db8:0:0:0:0::1]", normalized_iri: "//[2001:0db8:0:0:0:0::1]", }, { name: "IPv6 address with port", composed: "//[2001:0DB8:0:0:0:0::1]:80", components: { host: "[2001:0DB8:0:0:0:0::1]", port: "80", }, normalized_uri: "//[2001:0db8:0:0:0:0::1]:80", normalized_iri: "//[2001:0db8:0:0:0:0::1]:80", }, { name: "IPv6 address", composed: "//[2001:0db8::89ab:cdef:89AB:CDEF]", components: { host: "[2001:0db8::89ab:cdef:89AB:CDEF]", }, normalized_uri: "//[2001:0db8::89ab:cdef:89ab:cdef]", normalized_iri: "//[2001:0db8::89ab:cdef:89ab:cdef]", }, { name: "IPv6 address with port", composed: "//[2001:0db8::89ab:cdef:89AB:CDEF]:80", components: { host: "[2001:0db8::89ab:cdef:89AB:CDEF]", port: "80", }, normalized_uri: "//[2001:0db8::89ab:cdef:89ab:cdef]:80", normalized_iri: "//[2001:0db8::89ab:cdef:89ab:cdef]:80", }, { name: "IPv6 address", composed: "//[2001:0db8::1]", components: { host: "[2001:0db8::1]", }, normalized_uri: "//[2001:0db8::1]", normalized_iri: "//[2001:0db8::1]", }, { name: "IPv6 address with port", composed: "//[2001:0db8::1]:80", components: { host: "[2001:0db8::1]", port: "80", }, normalized_uri: "//[2001:0db8::1]:80", normalized_iri: "//[2001:0db8::1]:80", }, { name: "IPv6 address", composed: "//[2001:0db8:0::1]", components: { host: "[2001:0db8:0::1]", }, normalized_uri: "//[2001:0db8:0::1]", normalized_iri: "//[2001:0db8:0::1]", }, { name: "IPv6 address with port", composed: "//[2001:0db8:0::1]:80", components: { host: "[2001:0db8:0::1]", port: "80", }, normalized_uri: "//[2001:0db8:0::1]:80", normalized_iri: "//[2001:0db8:0::1]:80", }, { name: "IPv6 address", composed: "//[2001:0db8:0:0::1]", components: { host: "[2001:0db8:0:0::1]", }, normalized_uri: "//[2001:0db8:0:0::1]", normalized_iri: "//[2001:0db8:0:0::1]", }, { name: "IPv6 address with port", composed: "//[2001:0db8:0:0::1]:80", components: { host: "[2001:0db8:0:0::1]", port: "80", }, normalized_uri: "//[2001:0db8:0:0::1]:80", normalized_iri: "//[2001:0db8:0:0::1]:80", }, { name: "IPv6 address", composed: "//[2001:0db8:0:0:0::1]", components: { host: "[2001:0db8:0:0:0::1]", }, normalized_uri: "//[2001:0db8:0:0:0::1]", normalized_iri: "//[2001:0db8:0:0:0::1]", }, { name: "IPv6 address with port", composed: "//[2001:0db8:0:0:0::1]:80", components: { host: "[2001:0db8:0:0:0::1]", port: "80", }, normalized_uri: "//[2001:0db8:0:0:0::1]:80", normalized_iri: "//[2001:0db8:0:0:0::1]:80", }, { name: "IPv6 address", composed: "//[2001:0db8:0:0:0:0::1]", components: { host: "[2001:0db8:0:0:0:0::1]", }, normalized_uri: "//[2001:0db8:0:0:0:0::1]", normalized_iri: "//[2001:0db8:0:0:0:0::1]", }, { name: "IPv6 address with port", composed: "//[2001:0db8:0:0:0:0::1]:80", components: { host: "[2001:0db8:0:0:0:0::1]", port: "80", }, normalized_uri: "//[2001:0db8:0:0:0:0::1]:80", normalized_iri: "//[2001:0db8:0:0:0:0::1]:80", }, { name: "IPv6 address", composed: "//[2001:0db8::0:1]", components: { host: "[2001:0db8::0:1]", }, normalized_uri: "//[2001:0db8::0:1]", normalized_iri: "//[2001:0db8::0:1]", }, { name: "IPv6 address with port", composed: "//[2001:0db8::0:1]:80", components: { host: "[2001:0db8::0:1]", port: "80", }, normalized_uri: "//[2001:0db8::0:1]:80", normalized_iri: "//[2001:0db8::0:1]:80", }, { name: "IPv6 address", composed: "//[2001:0db8::0:0:1]", components: { host: "[2001:0db8::0:0:1]", }, normalized_uri: "//[2001:0db8::0:0:1]", normalized_iri: "//[2001:0db8::0:0:1]", }, { name: "IPv6 address with port", composed: "//[2001:0db8::0:0:1]:80", components: { host: "[2001:0db8::0:0:1]", port: "80", }, normalized_uri: "//[2001:0db8::0:0:1]:80", normalized_iri: "//[2001:0db8::0:0:1]:80", }, { name: "IPv6 address", composed: "//[2001:0db8::0:0:0:1]", components: { host: "[2001:0db8::0:0:0:1]", }, normalized_uri: "//[2001:0db8::0:0:0:1]", normalized_iri: "//[2001:0db8::0:0:0:1]", }, { name: "IPv6 address with port", composed: "//[2001:0db8::0:0:0:1]:80", components: { host: "[2001:0db8::0:0:0:1]", port: "80", }, normalized_uri: "//[2001:0db8::0:0:0:1]:80", normalized_iri: "//[2001:0db8::0:0:0:1]:80", }, { name: "IPv6 address", composed: "//[2001:0db8::0:0:0:0:1]", components: { host: "[2001:0db8::0:0:0:0:1]", }, normalized_uri: "//[2001:0db8::0:0:0:0:1]", normalized_iri: "//[2001:0db8::0:0:0:0:1]", }, { name: "IPv6 address with port", composed: "//[2001:0db8::0:0:0:0:1]:80", components: { host: "[2001:0db8::0:0:0:0:1]", port: "80", }, normalized_uri: "//[2001:0db8::0:0:0:0:1]:80", normalized_iri: "//[2001:0db8::0:0:0:0:1]:80", }, { name: "IPvFuture address", composed: "//[v9999.this-is-future-version-of-ip-address:::::::::]", components: { host: "[v9999.this-is-future-version-of-ip-address:::::::::]", }, normalized_uri: "//[v9999.this-is-future-version-of-ip-address:::::::::]", normalized_iri: "//[v9999.this-is-future-version-of-ip-address:::::::::]", }, { name: "IPvFuture address with port", composed: "//[v9999.this-is-future-version-of-ip-address:::::::::]:80", components: { host: "[v9999.this-is-future-version-of-ip-address:::::::::]", port: "80", }, normalized_uri: "//[v9999.this-is-future-version-of-ip-address:::::::::]:80", normalized_iri: "//[v9999.this-is-future-version-of-ip-address:::::::::]:80", }, { name: "Too large port", description: "RFC 3986 accepts `*DIGIT` as `port` component", composed: "//localhost:999999999", components: { host: "localhost", port: "999999999", }, normalized_uri: "//localhost:999999999", normalized_iri: "//localhost:999999999", }, { name: "Port only", description: "`host` can be empty", composed: "//:999999999", components: { host: "", port: "999999999", }, normalized_uri: "//:999999999", normalized_iri: "//:999999999", }, { name: "Trailing slash should remain after normalization", description: "Trailing slash should remain after normalization if the path ends with slash", composed: "https://example.com/../../", components: { scheme: "https", host: "example.com", path: "/../../", }, normalized_uri: "https://example.com/", normalized_iri: "https://example.com/", }, { name: "Slash should remain", description: "Slash should remain after normalization if the IRI ends with a dot segment", composed: "https://example.com/..", components: { scheme: "https", host: "example.com", path: "/..", }, normalized_uri: "https://example.com/", normalized_iri: "https://example.com/", }, { name: "Slash should remain", description: "Slash should remain after normalization if the IRI ends with a dot segment", composed: "https://example.com/.", components: { scheme: "https", host: "example.com", path: "/.", }, normalized_uri: "https://example.com/", normalized_iri: "https://example.com/", }, { name: "WHATWG URL Standard serialization", composed: "scheme:/a/b/../..//c", components: { scheme: "scheme", path: "/a/b/../..//c", }, normalized_uri: "scheme:/.//c", normalized_iri: "scheme:/.//c", }, { name: "WHATWG URL Standard serialization", composed: "scheme:/a/b/../..//c", components: { scheme: "scheme", path: "/a/b/../..//c", }, normalized_uri: "scheme:/.//c", normalized_iri: "scheme:/.//c", }, { name: "redundant UTF-8 encoding (1 byte inflated to 2 bytes)", composed: "scheme:/%C0%AE", components: { scheme: "scheme", path: "/%C0%AE", }, normalized_uri: "scheme:/%C0%AE", normalized_iri: "scheme:/%C0%AE", }, { name: "redundant UTF-8 encoding (1 byte inflated to 3 bytes)", composed: "scheme:/%E0%80%AE", components: { scheme: "scheme", path: "/%E0%80%AE", }, normalized_uri: "scheme:/%E0%80%AE", normalized_iri: "scheme:/%E0%80%AE", }, { name: "redundant UTF-8 encoding (1 byte inflated to 4 bytes)", composed: "scheme:/%F0%80%80%AE", components: { scheme: "scheme", path: "/%F0%80%80%AE", }, normalized_uri: "scheme:/%F0%80%80%AE", normalized_iri: "scheme:/%F0%80%80%AE", }, { name: "redundant UTF-8 encoding (2 byte inflated to 3 bytes)", composed: "scheme:/%E0%8E%B1", components: { scheme: "scheme", path: "/%E0%8E%B1", }, normalized_uri: "scheme:/%E0%8E%B1", normalized_iri: "scheme:/%E0%8E%B1", }, { name: "redundant UTF-8 encoding (2 byte inflated to 4 bytes)", composed: "scheme:/%F0%80%8E%B1", components: { scheme: "scheme", path: "/%F0%80%8E%B1", }, normalized_uri: "scheme:/%F0%80%8E%B1", normalized_iri: "scheme:/%F0%80%8E%B1", }, { name: "redundant UTF-8 encoding (3 byte inflated to 4 bytes)", composed: "scheme:/%F0%83%82%A4", components: { scheme: "scheme", path: "/%F0%83%82%A4", }, normalized_uri: "scheme:/%F0%83%82%A4", normalized_iri: "scheme:/%F0%83%82%A4", }, { name: "non-UTF-8 percent encoding (starts with invaild byte)", composed: "scheme:/%FF", components: { scheme: "scheme", path: "/%FF", }, normalized_uri: "scheme:/%FF", normalized_iri: "scheme:/%FF", }, { name: "non-UTF-8 percent encoding (starts with continue byte)", composed: "scheme:/%BF%BF", components: { scheme: "scheme", path: "/%BF%BF", }, normalized_uri: "scheme:/%BF%BF", normalized_iri: "scheme:/%BF%BF", }, { name: "non-UTF-8 percent encoding (expected 2 bytes, invalid at 2nd byte)", composed: "scheme:/%CE%FF", components: { scheme: "scheme", path: "/%CE%FF", }, normalized_uri: "scheme:/%CE%FF", normalized_iri: "scheme:/%CE%FF", }, { name: "non-UTF-8 percent encoding (expected 2 bytes, starts again at 2nd byte)", composed: "scheme:/%CE%CE%B1", components: { scheme: "scheme", path: "/%CE%CE%B1", }, normalized_uri: "scheme:/%CE%CE%B1", normalized_iri: "scheme:/%CE\u{03B1}", }, { name: "non-UTF-8 percent encoding (expected 3 bytes, invalid at 2nd byte)", composed: "scheme:/%E3%FF%A4", components: { scheme: "scheme", path: "/%E3%FF%A4", }, normalized_uri: "scheme:/%E3%FF%A4", normalized_iri: "scheme:/%E3%FF%A4", }, { name: "non-UTF-8 percent encoding (expected 3 bytes, starts again at 2nd byte)", composed: "scheme:/%E3%E3%82%A4", components: { scheme: "scheme", path: "/%E3%E3%82%A4", }, normalized_uri: "scheme:/%E3%E3%82%A4", normalized_iri: "scheme:/%E3\u{30A4}", }, { name: "non-UTF-8 percent encoding (expected 3 bytes, invalid at 3rd byte)", composed: "scheme:/%E3%82%FF", components: { scheme: "scheme", path: "/%E3%82%FF", }, normalized_uri: "scheme:/%E3%82%FF", normalized_iri: "scheme:/%E3%82%FF", }, { name: "non-UTF-8 percent encoding (expected 3 bytes, starts again at 3rd byte)", composed: "scheme:/%E3%82%E3%82%A4", components: { scheme: "scheme", path: "/%E3%82%E3%82%A4", }, normalized_uri: "scheme:/%E3%82%E3%82%A4", normalized_iri: "scheme:/%E3%82\u{30A4}", }, { name: "non-UTF-8 percent encoding (expected 4 bytes, invalid at 2nd byte)", composed: "scheme:/%F0%FF%8D%A3", components: { scheme: "scheme", path: "/%F0%FF%8D%A3", }, normalized_uri: "scheme:/%F0%FF%8D%A3", normalized_iri: "scheme:/%F0%FF%8D%A3", }, { name: "non-UTF-8 percent encoding (expected 4 bytes, starts again at 2nd byte)", composed: "scheme:/%F0%F0%9F%8D%A3", components: { scheme: "scheme", path: "/%F0%F0%9F%8D%A3", }, normalized_uri: "scheme:/%F0%F0%9F%8D%A3", normalized_iri: "scheme:/%F0\u{1F363}", }, { name: "non-UTF-8 percent encoding (expected 4 bytes, invalid at 3rd byte)", composed: "scheme:/%F0%9F%FF%A3", components: { scheme: "scheme", path: "/%F0%9F%FF%A3", }, normalized_uri: "scheme:/%F0%9F%FF%A3", normalized_iri: "scheme:/%F0%9F%FF%A3", }, { name: "non-UTF-8 percent encoding (expected 4 bytes, starts again at 3rd byte)", composed: "scheme:/%F0%9F%F0%9F%8D%A3", components: { scheme: "scheme", path: "/%F0%9F%F0%9F%8D%A3", }, normalized_uri: "scheme:/%F0%9F%F0%9F%8D%A3", normalized_iri: "scheme:/%F0%9F\u{1F363}", }, { name: "non-UTF-8 percent encoding (expected 4 bytes, invalid at 4th byte)", composed: "scheme:/%F0%9F%8D%FF", components: { scheme: "scheme", path: "/%F0%9F%8D%FF", }, normalized_uri: "scheme:/%F0%9F%8D%FF", normalized_iri: "scheme:/%F0%9F%8D%FF", }, { name: "non-UTF-8 percent encoding (expected 4 bytes, starts again at 4th byte)", composed: "scheme:/%F0%9F%8D%F0%9F%8D%A3", components: { scheme: "scheme", path: "/%F0%9F%8D%F0%9F%8D%A3", }, normalized_uri: "scheme:/%F0%9F%8D%F0%9F%8D%A3", normalized_iri: "scheme:/%F0%9F%8D\u{1F363}", }, { name: "non-UTF-8 percent encoding (high-surrogate)", composed: "scheme:/%ED%A0%A0", components: { scheme: "scheme", path: "/%ED%A0%A0", }, normalized_uri: "scheme:/%ED%A0%A0", normalized_iri: "scheme:/%ED%A0%A0", }, { name: "non-UTF-8 percent encoding (out of range, larger than U+10FFFF)", composed: "scheme:/%F4%90%80%80", components: { scheme: "scheme", path: "/%F4%90%80%80", }, normalized_uri: "scheme:/%F4%90%80%80", normalized_iri: "scheme:/%F4%90%80%80", }, { name: "non-UTF-8 percent encoding, followed by valid pct encoding", composed: "scheme:/%CE%2E%2E", components: { scheme: "scheme", path: "/%CE%2E%2E", }, normalized_uri: "scheme:/%CE..", normalized_iri: "scheme:/%CE..", }, { name: "non-UTF-8 percent encoding, followed by valid pct encoding", composed: "scheme:/%CE%FF%2E", components: { scheme: "scheme", path: "/%CE%FF%2E", }, normalized_uri: "scheme:/%CE%FF.", normalized_iri: "scheme:/%CE%FF.", }, ];