diff --git a/url/src/lib.rs b/url/src/lib.rs index fa2803681..215944eeb 100644 --- a/url/src/lib.rs +++ b/url/src/lib.rs @@ -2025,7 +2025,17 @@ impl Url { let scheme_type = SchemeType::from(self.scheme()); if let Some(host) = host { - if host.is_empty() && scheme_type.is_special() && !scheme_type.is_file() { + // An empty host is only valid for a non-special scheme that has no + // credentials and no port. Setting it otherwise would produce a + // serialization (e.g. `foo://user@/` or `foo://:1/`) that the parser + // itself rejects with `EmptyHost`, breaking the parse/serialize + // roundtrip. Mirror the parser here. + if host.is_empty() + && ((scheme_type.is_special() && !scheme_type.is_file()) + || !self.username().is_empty() + || self.password().is_some() + || self.port().is_some()) + { return Err(ParseError::EmptyHost); } let mut host_substr = host; diff --git a/url/tests/unit.rs b/url/tests/unit.rs index faeb7c5f4..073c86046 100644 --- a/url/tests/unit.rs +++ b/url/tests/unit.rs @@ -97,6 +97,34 @@ fn test_set_empty_host() { assert_eq!(base.as_str(), "file://foo/share/foo/bar"); } +#[test] +/// https://github.com/servo/rust-url/issues/1144 +fn test_set_empty_host_non_special() { + // A non-special URL that has credentials or a port must reject an empty + // host: the resulting serialization (`foo://user@/`, `foo://:1/`) is one + // that `Url::parse` rejects with `EmptyHost`, so allowing it here would + // break the parse/serialize roundtrip. The URL must be left unchanged. + let cases = ["foo://user@host/", "foo://host:1/", "foo://user:pass@host/"]; + for input in cases.iter() { + let mut url = Url::parse(input).unwrap(); + let before = url.as_str().to_owned(); + assert_eq!( + url.set_host(Some("")), + Err(url::ParseError::EmptyHost), + "{}", + input + ); + assert_eq!(url.as_str(), before, "{}", input); + } + + // Without credentials or a port, an empty host stays legal for a + // non-special scheme, and the result must roundtrip through the parser. + let mut url = Url::parse("foo://host/path").unwrap(); + url.set_host(Some("")).unwrap(); + assert_eq!(url.as_str(), "foo:///path"); + assert_eq!(Url::parse(url.as_str()).unwrap(), url); +} + #[test] fn test_set_empty_username_and_password() { let mut base: Url = "moz://foo:bar@servo/baz".parse().unwrap();