Quellcode durchsuchen

Rewrite ALL THE THINGS!

This changes the data structure for `Url`:

Rather than having multiple `String` (or `Vec<String>`) components,
this uses a single `String` that contains the serialization of an URL
and some indices into it to access components in O(1) time.

This saves on memory allocations and makes serialization and some other
methods very cheap, as they return `&str` rather than building a new `String`.

As a consequence, most of `src/lib.rs` and `src/parser.rs` had to be rewritten.

Fixes #142.
Simon Sapin vor 10 Jahren
Ursprung
Commit
9e759f1872
12 geänderte Dateien mit 1325 neuen und 1602 gelöschten Zeilen
  1. 0 2
      Cargo.toml
  2. 2 0
      src/encoding.rs
  3. 0 81
      src/format.rs
  4. 62 40
      src/host.rs
  5. 293 588
      src/lib.rs
  6. 825 494
      src/parser.rs
  7. 6 9
      src/percent_encoding.rs
  8. 0 169
      src/urlutils.rs
  9. 0 67
      tests/format.rs
  10. 79 60
      tests/tests.rs
  11. 13 13
      tests/urltestdata.txt
  12. 45 79
      tests/wpt.rs

+ 0 - 2
Cargo.toml

@@ -11,8 +11,6 @@ readme = "README.md"
 keywords = ["url", "parser"]
 keywords = ["url", "parser"]
 license = "MIT/Apache-2.0"
 license = "MIT/Apache-2.0"
 
 
-[[test]]
-name = "format"
 [[test]]
 [[test]]
 name = "form_urlencoded"
 name = "form_urlencoded"
 [[test]]
 [[test]]

+ 2 - 0
src/encoding.rs

@@ -37,6 +37,7 @@ impl EncodingOverride {
         }
         }
     }
     }
 
 
+    #[inline]
     pub fn utf8() -> EncodingOverride {
     pub fn utf8() -> EncodingOverride {
         EncodingOverride { encoding: None }
         EncodingOverride { encoding: None }
     }
     }
@@ -75,6 +76,7 @@ pub struct EncodingOverride;
 
 
 #[cfg(not(feature = "query_encoding"))]
 #[cfg(not(feature = "query_encoding"))]
 impl EncodingOverride {
 impl EncodingOverride {
+    #[inline]
     pub fn utf8() -> EncodingOverride {
     pub fn utf8() -> EncodingOverride {
         EncodingOverride
         EncodingOverride
     }
     }

+ 0 - 81
src/format.rs

@@ -1,81 +0,0 @@
-// Copyright 2013-2015 Simon Sapin.
-//
-// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
-// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
-// option. This file may not be copied, modified, or distributed
-// except according to those terms.
-
-//! Formatting utilities for URLs.
-//!
-//! These formatters can be used to coerce various URL parts into strings.
-//!
-//! You can use `<formatter>.to_string()`, as the formatters implement `fmt::Display`.
-
-use std::fmt::{self, Formatter};
-use super::Url;
-
-/// Formatter and serializer for URL path data.
-pub struct PathFormatter<'a, T:'a> {
-    /// The path as a slice of string-like objects (String or &str).
-    pub path: &'a [T]
-}
-
-impl<'a, T: fmt::Display> fmt::Display for PathFormatter<'a, T> {
-    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
-        if self.path.is_empty() {
-            formatter.write_str("/")
-        } else {
-            for path_part in self.path {
-                try!("/".fmt(formatter));
-                try!(path_part.fmt(formatter));
-            }
-            Ok(())
-        }
-    }
-}
-
-
-/// Formatter and serializer for URL username and password data.
-pub struct UserInfoFormatter<'a> {
-    /// URL username as a string slice.
-    pub username: &'a str,
-
-    /// URL password as an optional string slice.
-    ///
-    /// You can convert an `Option<String>` with `.as_ref().map(|s| s)`.
-    pub password: Option<&'a str>
-}
-
-impl<'a> fmt::Display for UserInfoFormatter<'a> {
-    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
-        if !self.username.is_empty() || self.password.is_some() {
-            try!(formatter.write_str(self.username));
-            if let Some(password) = self.password {
-                try!(formatter.write_str(":"));
-                try!(formatter.write_str(password));
-            }
-            try!(formatter.write_str("@"));
-        }
-        Ok(())
-    }
-}
-
-
-/// Formatter for URLs which ignores the fragment field.
-pub struct UrlNoFragmentFormatter<'a> {
-    pub url: &'a Url
-}
-
-impl<'a> fmt::Display for UrlNoFragmentFormatter<'a> {
-    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
-        try!(formatter.write_str(&self.url.scheme));
-        try!(formatter.write_str(":"));
-        try!(self.url.scheme_data.fmt(formatter));
-        if let Some(ref query) = self.url.query {
-            try!(formatter.write_str("?"));
-            try!(formatter.write_str(query));
-        }
-        Ok(())
-    }
-}

+ 62 - 40
src/host.rs

@@ -6,39 +6,58 @@
 // option. This file may not be copied, modified, or distributed
 // option. This file may not be copied, modified, or distributed
 // except according to those terms.
 // except according to those terms.
 
 
-use std::ascii::AsciiExt;
 use std::cmp;
 use std::cmp;
-use std::fmt::{self, Formatter};
+use std::fmt::{self, Formatter, Write};
 use std::net::{Ipv4Addr, Ipv6Addr};
 use std::net::{Ipv4Addr, Ipv6Addr};
 use parser::{ParseResult, ParseError};
 use parser::{ParseResult, ParseError};
 use percent_encoding::{from_hex, percent_decode};
 use percent_encoding::{from_hex, percent_decode};
 use idna;
 use idna;
 
 
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature="heap_size", derive(HeapSizeOf))]
+pub enum HostInternal {
+    None,
+    Domain,
+    Ipv4(Ipv4Addr),
+    Ipv6(Ipv6Addr),
+}
 
 
 /// The host name of an URL.
 /// The host name of an URL.
-#[derive(PartialEq, Eq, Clone, Debug, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
 #[cfg_attr(feature="heap_size", derive(HeapSizeOf))]
 #[cfg_attr(feature="heap_size", derive(HeapSizeOf))]
-pub enum Host {
-    /// A (DNS) domain name.
-    Domain(String),
-    /// A IPv4 address, represented by four sequences of up to three ASCII digits.
+pub enum Host<S=String> {
+    /// A DNS domain name, as '.' dot-separated labels.
+    /// Non-ASCII labels are encoded in punycode per IDNA.
+    Domain(S),
+
+    /// An IPv4 address.
+    /// `Url::host_str` returns the serialization of this address,
+    /// as four decimal integers separated by `.` dots.
     Ipv4(Ipv4Addr),
     Ipv4(Ipv4Addr),
-    /// An IPv6 address, represented inside `[...]` square brackets
-    /// so that `:` colon characters in the address are not ambiguous
-    /// with the port number delimiter.
+
+    /// An IPv6 address.
+    /// `Url::host_str` returns the serialization of that address between `[` and `]` brackets,
+    /// in the format per [RFC 5952 *A Recommendation
+    /// for IPv6 Address Text Representation*](https://tools.ietf.org/html/rfc5952):
+    /// lowercase hexadecimal with maximal `::` compression.
     Ipv6(Ipv6Addr),
     Ipv6(Ipv6Addr),
 }
 }
 
 
+impl<'a> Host<&'a str> {
+    pub fn to_owned(&self) -> Host<String> {
+        match *self {
+            Host::Domain(domain) => Host::Domain(domain.to_owned()),
+            Host::Ipv4(address) => Host::Ipv4(address),
+            Host::Ipv6(address) => Host::Ipv6(address),
+        }
+    }
+}
 
 
-impl Host {
+impl Host<String> {
     /// Parse a host: either an IPv6 address in [] square brackets, or a domain.
     /// Parse a host: either an IPv6 address in [] square brackets, or a domain.
     ///
     ///
-    /// Returns `Err` for an empty host, an invalid IPv6 address,
-    /// or a or invalid non-ASCII domain.
-    pub fn parse(input: &str) -> ParseResult<Host> {
-        if input.len() == 0 {
-            return Err(ParseError::EmptyHost)
-        }
+    /// https://url.spec.whatwg.org/#host-parsing
+    pub fn parse(input: &str) -> Result<Self, ParseError> {
         if input.starts_with("[") {
         if input.starts_with("[") {
             if !input.ends_with("]") {
             if !input.ends_with("]") {
                 return Err(ParseError::InvalidIpv6Address)
                 return Err(ParseError::InvalidIpv6Address)
@@ -47,37 +66,24 @@ impl Host {
         }
         }
         let decoded = percent_decode(input.as_bytes());
         let decoded = percent_decode(input.as_bytes());
         let domain = String::from_utf8_lossy(&decoded);
         let domain = String::from_utf8_lossy(&decoded);
-
-        let domain = match idna::domain_to_ascii(&domain) {
-            Ok(s) => s,
-            Err(_) => return Err(ParseError::InvalidDomainCharacter)
-        };
-
-        if domain.find(&[
-            '\0', '\t', '\n', '\r', ' ', '#', '%', '/', ':', '?', '@', '[', '\\', ']'
-        ][..]).is_some() {
+        let domain = try!(idna::domain_to_ascii(&domain));
+        if domain.find(|c| matches!(c,
+            '\0' | '\t' | '\n' | '\r' | ' ' | '#' | '%' | '/' | ':' | '?' | '@' | '[' | '\\' | ']'
+        )).is_some() {
             return Err(ParseError::InvalidDomainCharacter)
             return Err(ParseError::InvalidDomainCharacter)
         }
         }
-        match parse_ipv4addr(&domain[..]) {
-            Ok(Some(ipv4addr)) => Ok(Host::Ipv4(ipv4addr)),
-            Ok(None) => Ok(Host::Domain(domain.to_ascii_lowercase())),
-            Err(e) => Err(e),
+        if let Some(address) = try!(parse_ipv4addr(&domain)) {
+            Ok(Host::Ipv4(address))
+        } else {
+            Ok(Host::Domain(domain.into()))
         }
         }
     }
     }
-
-    /// Serialize the host as a string.
-    ///
-    /// A domain a returned as-is, an IPv6 address between [] square brackets.
-    pub fn serialize(&self) -> String {
-        self.to_string()
-    }
 }
 }
 
 
-
-impl fmt::Display for Host {
+impl<S: AsRef<str>> fmt::Display for Host<S> {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
         match *self {
         match *self {
-            Host::Domain(ref domain) => domain.fmt(f),
+            Host::Domain(ref domain) => domain.as_ref().fmt(f),
             Host::Ipv4(ref addr) => addr.fmt(f),
             Host::Ipv4(ref addr) => addr.fmt(f),
             Host::Ipv6(ref addr) => {
             Host::Ipv6(ref addr) => {
                 try!(f.write_str("["));
                 try!(f.write_str("["));
@@ -88,6 +94,19 @@ impl fmt::Display for Host {
     }
     }
 }
 }
 
 
+/// Parse `input` as a host.
+/// If successful, write its serialization to `serialization`
+/// and return the internal representation for `Url`.
+pub fn parse(input: &str, serialization: &mut String) -> ParseResult<HostInternal> {
+    let host = try!(Host::parse(input));
+    write!(serialization, "{}", host).unwrap();
+    match host {
+        Host::Domain(_) => Ok(HostInternal::Domain),
+        Host::Ipv4(address) => Ok(HostInternal::Ipv4(address)),
+        Host::Ipv6(address) => Ok(HostInternal::Ipv6(address)),
+    }
+}
+
 fn write_ipv6(addr: &Ipv6Addr, f: &mut Formatter) -> fmt::Result {
 fn write_ipv6(addr: &Ipv6Addr, f: &mut Formatter) -> fmt::Result {
     let segments = addr.segments();
     let segments = addr.segments();
     let (compress_start, compress_end) = longest_zero_sequence(&segments);
     let (compress_start, compress_end) = longest_zero_sequence(&segments);
@@ -165,6 +184,9 @@ fn parse_ipv4number(mut input: &str) -> ParseResult<u32> {
 }
 }
 
 
 fn parse_ipv4addr(input: &str) -> ParseResult<Option<Ipv4Addr>> {
 fn parse_ipv4addr(input: &str) -> ParseResult<Option<Ipv4Addr>> {
+    if input.is_empty() {
+        return Ok(None)
+    }
     let mut parts: Vec<&str> = input.split('.').collect();
     let mut parts: Vec<&str> = input.split('.').collect();
     if parts.last() == Some(&"") {
     if parts.last() == Some(&"") {
         parts.pop();
         parts.pop();

Datei-Diff unterdrückt, da er zu groß ist
+ 293 - 588
src/lib.rs


Datei-Diff unterdrückt, da er zu groß ist
+ 825 - 494
src/parser.rs


+ 6 - 9
src/percent_encoding.rs

@@ -95,18 +95,15 @@ define_encode_set! {
 }
 }
 
 
 define_encode_set! {
 define_encode_set! {
-    /// This encode set is used in the URL parser for usernames and passwords.
-    pub USERINFO_ENCODE_SET = [DEFAULT_ENCODE_SET] | {'@'}
+    /// This encode set is used for username and password.
+    pub PATH_SEGMENT_ENCODE_SET = [DEFAULT_ENCODE_SET] | {'%'}
 }
 }
 
 
 define_encode_set! {
 define_encode_set! {
-    /// This encode set should be used when setting the password field of a parsed URL.
-    pub PASSWORD_ENCODE_SET = [USERINFO_ENCODE_SET] | {'\\', '/'}
-}
-
-define_encode_set! {
-    /// This encode set should be used when setting the username field of a parsed URL.
-    pub USERNAME_ENCODE_SET = [PASSWORD_ENCODE_SET] | {':'}
+    /// This encode set is used for username and password.
+    pub USERINFO_ENCODE_SET = [DEFAULT_ENCODE_SET] | {
+        '/', ':', ';', '=', '@', '[', '\\', ']', '^', '|'
+    }
 }
 }
 
 
 define_encode_set! {
 define_encode_set! {

+ 0 - 169
src/urlutils.rs

@@ -1,169 +0,0 @@
-// Copyright 2013-2014 Simon Sapin.
-//
-// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
-// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
-// option. This file may not be copied, modified, or distributed
-// except according to those terms.
-
-
-//! These methods are not meant for use in Rust code,
-//! only to help implement the JavaScript URLUtils API: http://url.spec.whatwg.org/#urlutils
-
-use super::{Url, UrlParser, SchemeType, SchemeData, RelativeSchemeData};
-use parser::{ParseError, ParseResult, Context};
-use percent_encoding::{utf8_percent_encode_to, USERNAME_ENCODE_SET, PASSWORD_ENCODE_SET};
-
-
-#[allow(dead_code)]
-pub struct UrlUtilsWrapper<'a> {
-    pub url: &'a mut Url,
-    pub parser: &'a UrlParser<'a>,
-}
-
-#[doc(hidden)]
-pub trait UrlUtils {
-    fn set_scheme(&mut self, input: &str) -> ParseResult<()>;
-    fn set_username(&mut self, input: &str) -> ParseResult<()>;
-    fn set_password(&mut self, input: &str) -> ParseResult<()>;
-    fn set_host_and_port(&mut self, input: &str) -> ParseResult<()>;
-    fn set_host(&mut self, input: &str) -> ParseResult<()>;
-    fn set_port(&mut self, input: &str) -> ParseResult<()>;
-    fn set_path(&mut self, input: &str) -> ParseResult<()>;
-    fn set_query(&mut self, input: &str) -> ParseResult<()>;
-    fn set_fragment(&mut self, input: &str) -> ParseResult<()>;
-}
-
-impl<'a> UrlUtils for UrlUtilsWrapper<'a> {
-    /// `URLUtils.protocol` setter
-    fn set_scheme(&mut self, input: &str) -> ParseResult<()> {
-        match ::parser::parse_scheme(input, Context::Setter) {
-            Some((scheme, _)) => {
-                if self.parser.get_scheme_type(&self.url.scheme).same_as(self.parser.get_scheme_type(&scheme)) {
-                    return Err(ParseError::InvalidScheme);
-                }
-                self.url.scheme = scheme;
-                Ok(())
-            },
-            None => Err(ParseError::InvalidScheme),
-        }
-    }
-
-    /// `URLUtils.username` setter
-    fn set_username(&mut self, input: &str) -> ParseResult<()> {
-        match self.url.scheme_data {
-            SchemeData::Relative(RelativeSchemeData { ref mut username, .. }) => {
-                username.truncate(0);
-                utf8_percent_encode_to(input, USERNAME_ENCODE_SET, username);
-                Ok(())
-            },
-            SchemeData::NonRelative(_) => Err(ParseError::CannotSetUsernameWithNonRelativeScheme)
-        }
-    }
-
-    /// `URLUtils.password` setter
-    fn set_password(&mut self, input: &str) -> ParseResult<()> {
-        match self.url.scheme_data {
-            SchemeData::Relative(RelativeSchemeData { ref mut password, .. }) => {
-                if input.len() == 0 {
-                    *password = None;
-                    return Ok(());
-                }
-                let mut new_password = String::new();
-                utf8_percent_encode_to(input, PASSWORD_ENCODE_SET, &mut new_password);
-                *password = Some(new_password);
-                Ok(())
-            },
-            SchemeData::NonRelative(_) => Err(ParseError::CannotSetPasswordWithNonRelativeScheme)
-        }
-    }
-
-    /// `URLUtils.host` setter
-    fn set_host_and_port(&mut self, input: &str) -> ParseResult<()> {
-        match self.url.scheme_data {
-            SchemeData::Relative(RelativeSchemeData {
-                ref mut host, ref mut port, ref mut default_port, ..
-            }) => {
-                let scheme_type = self.parser.get_scheme_type(&self.url.scheme);
-                let (new_host, new_port, new_default_port, _) = try!(::parser::parse_host(
-                    input, scheme_type, self.parser));
-                *host = new_host;
-                *port = new_port;
-                *default_port = new_default_port;
-                Ok(())
-            },
-            SchemeData::NonRelative(_) => Err(ParseError::CannotSetHostPortWithNonRelativeScheme)
-        }
-    }
-
-    /// `URLUtils.hostname` setter
-    fn set_host(&mut self, input: &str) -> ParseResult<()> {
-        match self.url.scheme_data {
-            SchemeData::Relative(RelativeSchemeData { ref mut host, .. }) => {
-                let (new_host, _) = try!(::parser::parse_hostname(input, self.parser));
-                *host = new_host;
-                Ok(())
-            },
-            SchemeData::NonRelative(_) => Err(ParseError::CannotSetHostWithNonRelativeScheme)
-        }
-    }
-
-    /// `URLUtils.port` setter
-    fn set_port(&mut self, input: &str) -> ParseResult<()> {
-        match self.url.scheme_data {
-            SchemeData::Relative(RelativeSchemeData { ref mut port, ref mut default_port, .. }) => {
-                let scheme_type = self.parser.get_scheme_type(&self.url.scheme);
-                if scheme_type == SchemeType::FileLike {
-                    return Err(ParseError::CannotSetPortWithFileLikeScheme);
-                }
-                let (new_port, new_default_port, _) = try!(::parser::parse_port(
-                    input, scheme_type, self.parser));
-                *port = new_port;
-                *default_port = new_default_port;
-                Ok(())
-            },
-            SchemeData::NonRelative(_) => Err(ParseError::CannotSetPortWithNonRelativeScheme)
-        }
-    }
-
-    /// `URLUtils.pathname` setter
-    fn set_path(&mut self, input: &str) -> ParseResult<()> {
-        match self.url.scheme_data {
-            SchemeData::Relative(RelativeSchemeData { ref mut path, .. }) => {
-                let scheme_type = self.parser.get_scheme_type(&self.url.scheme);
-                let (new_path, _) = try!(::parser::parse_path_start(
-                    input, Context::Setter, scheme_type, self.parser));
-                *path = new_path;
-                Ok(())
-            },
-            SchemeData::NonRelative(_) => Err(ParseError::CannotSetPathWithNonRelativeScheme)
-        }
-    }
-
-    /// `URLUtils.search` setter
-    fn set_query(&mut self, input: &str) -> ParseResult<()> {
-        self.url.query = if input.is_empty() {
-            None
-        } else {
-            let input = if input.starts_with("?") { &input[1..] } else { input };
-            let (new_query, _) = try!(::parser::parse_query(
-                input, Context::Setter, self.parser));
-            Some(new_query)
-        };
-        Ok(())
-    }
-
-    /// `URLUtils.hash` setter
-    fn set_fragment(&mut self, input: &str) -> ParseResult<()> {
-        if self.url.scheme == "javascript" {
-            return Err(ParseError::CannotSetJavascriptFragment)
-        }
-        self.url.fragment = if input.is_empty() {
-            None
-        } else {
-            let input = if input.starts_with("#") { &input[1..] } else { input };
-            Some(try!(::parser::parse_fragment(input, self.parser)))
-        };
-        Ok(())
-    }
-}

+ 0 - 67
tests/format.rs

@@ -1,67 +0,0 @@
-extern crate url;
-
-use url::{Url, Host};
-use url::format::{PathFormatter, UserInfoFormatter};
-
-#[test]
-fn path_formatting() {
-    let data = [
-        (vec![], "/"),
-        (vec![""], "/"),
-        (vec!["test", "path"], "/test/path"),
-        (vec!["test", "path", ""], "/test/path/")
-    ];
-    for &(ref path, result) in &data {
-        assert_eq!(PathFormatter {
-            path: path
-        }.to_string(), result.to_string());
-    }
-}
-
-#[test]
-fn host() {
-    // libstd’s `Display for Ipv6Addr` serializes 0:0:0:0:0:0:_:_ and 0:0:0:0:0:ffff:_:_
-    // using IPv4-like syntax, as suggested in https://tools.ietf.org/html/rfc5952#section-4
-    // but https://url.spec.whatwg.org/#concept-ipv6-serializer specifies not to.
-
-    // Not [::0.0.0.2] / [::ffff:0.0.0.2]
-    assert_eq!(Host::parse("[0::2]").unwrap().to_string(), "[::2]");
-    assert_eq!(Host::parse("[0::ffff:0:2]").unwrap().to_string(), "[::ffff:0:2]");
-}
-
-#[test]
-fn userinfo_formatting() {
-    // Test data as (username, password, result) tuples.
-    let data = [
-        ("", None, ""),
-        ("", Some(""), ":@"),
-        ("", Some("password"), ":password@"),
-        ("username", None, "username@"),
-        ("username", Some(""), "username:@"),
-        ("username", Some("password"), "username:password@")
-    ];
-    for &(username, password, result) in &data {
-        assert_eq!(UserInfoFormatter {
-            username: username,
-            password: password
-        }.to_string(), result.to_string());
-    }
-}
-
-#[test]
-fn relative_scheme_url_formatting() {
-    let data = [
-        ("http://example.com/", "http://example.com/"),
-        ("http://addslash.com", "http://addslash.com/"),
-        ("http://@emptyuser.com/", "http://emptyuser.com/"),
-        ("http://:@emptypass.com/", "http://:@emptypass.com/"),
-        ("http://user@user.com/", "http://user@user.com/"),
-        ("http://user:pass@userpass.com/", "http://user:pass@userpass.com/"),
-        ("http://slashquery.com/path/?q=something", "http://slashquery.com/path/?q=something"),
-        ("http://noslashquery.com/path?q=something", "http://noslashquery.com/path?q=something")
-    ];
-    for &(input, result) in &data {
-        let url = Url::parse(input).unwrap();
-        assert_eq!(url.to_string(), result.to_string());
-    }
-}

+ 79 - 60
tests/tests.rs

@@ -9,15 +9,28 @@
 extern crate url;
 extern crate url;
 
 
 use std::net::{Ipv4Addr, Ipv6Addr};
 use std::net::{Ipv4Addr, Ipv6Addr};
+use std::path::{Path, PathBuf};
 use url::{Host, Url};
 use url::{Host, Url};
 
 
+macro_rules! assert_from_file_path {
+    ($path: expr) => { assert_from_file_path!($path, $path) };
+    ($path: expr, $url_path: expr) => {{
+        let url = Url::from_file_path(Path::new($path)).unwrap();
+        assert_eq!(url.host(), None);
+        assert_eq!(url.path(), $url_path);
+        assert_eq!(url.to_file_path(), Ok(PathBuf::from($path)));
+    }};
+}
+
+
+
 #[test]
 #[test]
 fn new_file_paths() {
 fn new_file_paths() {
-    use std::path::{Path, PathBuf};
     if cfg!(unix) {
     if cfg!(unix) {
         assert_eq!(Url::from_file_path(Path::new("relative")), Err(()));
         assert_eq!(Url::from_file_path(Path::new("relative")), Err(()));
         assert_eq!(Url::from_file_path(Path::new("../relative")), Err(()));
         assert_eq!(Url::from_file_path(Path::new("../relative")), Err(()));
-    } else {
+    }
+    if cfg!(windows) {
         assert_eq!(Url::from_file_path(Path::new("relative")), Err(()));
         assert_eq!(Url::from_file_path(Path::new("relative")), Err(()));
         assert_eq!(Url::from_file_path(Path::new(r"..\relative")), Err(()));
         assert_eq!(Url::from_file_path(Path::new(r"..\relative")), Err(()));
         assert_eq!(Url::from_file_path(Path::new(r"\drive-relative")), Err(()));
         assert_eq!(Url::from_file_path(Path::new(r"\drive-relative")), Err(()));
@@ -25,16 +38,9 @@ fn new_file_paths() {
     }
     }
 
 
     if cfg!(unix) {
     if cfg!(unix) {
-        let mut url = Url::from_file_path(Path::new("/foo/bar")).unwrap();
-        assert_eq!(url.host(), Some(&Host::Domain("".to_string())));
-        assert_eq!(url.path(), Some(&["foo".to_string(), "bar".to_string()][..]));
-        assert!(url.to_file_path() == Ok(PathBuf::from("/foo/bar")));
-
-        url.path_mut().unwrap()[1] = "ba\0r".to_string();
-        url.to_file_path().is_ok();
-
-        url.path_mut().unwrap()[1] = "ba%00r".to_string();
-        url.to_file_path().is_ok();
+        assert_from_file_path!("/foo/bar");
+        assert_from_file_path!("/foo/ba\0r", "/foo/ba%00r");
+        assert_from_file_path!("/foo/ba%00r", "/foo/ba%2500r");
     }
     }
 }
 }
 
 
@@ -43,9 +49,8 @@ fn new_file_paths() {
 fn new_path_bad_utf8() {
 fn new_path_bad_utf8() {
     use std::ffi::OsStr;
     use std::ffi::OsStr;
     use std::os::unix::prelude::*;
     use std::os::unix::prelude::*;
-    use std::path::{Path, PathBuf};
 
 
-    let url = Url::from_file_path(Path::new("/foo/ba%80r")).unwrap();
+    let url = Url::from_file_path(Path::new(OsStr::from_bytes(b"/foo/ba\x80r"))).unwrap();
     let os_str = OsStr::from_bytes(b"/foo/ba\x80r");
     let os_str = OsStr::from_bytes(b"/foo/ba\x80r");
     assert_eq!(url.to_file_path(), Ok(PathBuf::from(os_str)));
     assert_eq!(url.to_file_path(), Ok(PathBuf::from(os_str)));
 }
 }
@@ -53,22 +58,11 @@ fn new_path_bad_utf8() {
 #[test]
 #[test]
 fn new_path_windows_fun() {
 fn new_path_windows_fun() {
     if cfg!(windows) {
     if cfg!(windows) {
-        use std::path::{Path, PathBuf};
-        let mut url = Url::from_file_path(Path::new(r"C:\foo\bar")).unwrap();
-        assert_eq!(url.host(), Some(&Host::Domain("".to_string())));
-        assert_eq!(url.path(), Some(&["C:".to_string(), "foo".to_string(), "bar".to_string()][..]));
-        assert_eq!(url.to_file_path(),
-                   Ok(PathBuf::from(r"C:\foo\bar")));
-
-        url.path_mut().unwrap()[2] = "ba\0r".to_string();
-        assert!(url.to_file_path().is_ok());
-
-        url.path_mut().unwrap()[2] = "ba%00r".to_string();
-        assert!(url.to_file_path().is_ok());
+        assert_from_file_path!(r"C:\foo\bar", "/C:/foo/bar");
+        assert_from_file_path!("C:\\foo\\ba\0r", "/C:/foo/ba%00r");
 
 
         // Invalid UTF-8
         // Invalid UTF-8
-        url.path_mut().unwrap()[2] = "ba%80r".to_string();
-        assert!(url.to_file_path().is_err());
+        assert!(Url::parse("file:///C:/foo/ba%80r").unwrap().to_file_path().is_err());
         
         
         // test windows canonicalized path        
         // test windows canonicalized path        
         let path = PathBuf::from(r"\\?\C:\foo\bar");
         let path = PathBuf::from(r"\\?\C:\foo\bar");
@@ -79,26 +73,23 @@ fn new_path_windows_fun() {
 
 
 #[test]
 #[test]
 fn new_directory_paths() {
 fn new_directory_paths() {
-    use std::path::Path;
-
     if cfg!(unix) {
     if cfg!(unix) {
         assert_eq!(Url::from_directory_path(Path::new("relative")), Err(()));
         assert_eq!(Url::from_directory_path(Path::new("relative")), Err(()));
         assert_eq!(Url::from_directory_path(Path::new("../relative")), Err(()));
         assert_eq!(Url::from_directory_path(Path::new("../relative")), Err(()));
 
 
         let url = Url::from_directory_path(Path::new("/foo/bar")).unwrap();
         let url = Url::from_directory_path(Path::new("/foo/bar")).unwrap();
-        assert_eq!(url.host(), Some(&Host::Domain("".to_string())));
-        assert_eq!(url.path(), Some(&["foo".to_string(), "bar".to_string(),
-                                      "".to_string()][..]));
-    } else {
+        assert_eq!(url.host(), None);
+        assert_eq!(url.path(), "/foo/bar/");
+    }
+    if cfg!(windows) {
         assert_eq!(Url::from_directory_path(Path::new("relative")), Err(()));
         assert_eq!(Url::from_directory_path(Path::new("relative")), Err(()));
         assert_eq!(Url::from_directory_path(Path::new(r"..\relative")), Err(()));
         assert_eq!(Url::from_directory_path(Path::new(r"..\relative")), Err(()));
         assert_eq!(Url::from_directory_path(Path::new(r"\drive-relative")), Err(()));
         assert_eq!(Url::from_directory_path(Path::new(r"\drive-relative")), Err(()));
         assert_eq!(Url::from_directory_path(Path::new(r"\\ucn\")), Err(()));
         assert_eq!(Url::from_directory_path(Path::new(r"\\ucn\")), Err(()));
 
 
         let url = Url::from_directory_path(Path::new(r"C:\foo\bar")).unwrap();
         let url = Url::from_directory_path(Path::new(r"C:\foo\bar")).unwrap();
-        assert_eq!(url.host(), Some(&Host::Domain("".to_string())));
-        assert_eq!(url.path(), Some(&["C:".to_string(), "foo".to_string(),
-                                      "bar".to_string(), "".to_string()][..]));
+        assert_eq!(url.host(), None);
+        assert_eq!(url.path(), "/C:/foo/bar/");
     }
     }
 }
 }
 
 
@@ -110,15 +101,15 @@ fn from_str() {
 #[test]
 #[test]
 fn issue_124() {
 fn issue_124() {
     let url: Url = "file:a".parse().unwrap();
     let url: Url = "file:a".parse().unwrap();
-    assert_eq!(url.path().unwrap(), ["a"]);
+    assert_eq!(url.path(), "/a");
     let url: Url = "file:...".parse().unwrap();
     let url: Url = "file:...".parse().unwrap();
-    assert_eq!(url.path().unwrap(), ["..."]);
+    assert_eq!(url.path(), "/...");
     let url: Url = "file:..".parse().unwrap();
     let url: Url = "file:..".parse().unwrap();
-    assert_eq!(url.path().unwrap(), [""]);
+    assert_eq!(url.path(), "/");
 }
 }
 
 
 #[test]
 #[test]
-fn relative_scheme_data_equality() {
+fn test_equality() {
     use std::hash::{Hash, Hasher, SipHasher};
     use std::hash::{Hash, Hasher, SipHasher};
 
 
     fn check_eq(a: &Url, b: &Url) {
     fn check_eq(a: &Url, b: &Url) {
@@ -145,7 +136,7 @@ fn relative_scheme_data_equality() {
     // Different ports
     // Different ports
     let a: Url = url("http://example.com/");
     let a: Url = url("http://example.com/");
     let b: Url = url("http://example.com:8080/");
     let b: Url = url("http://example.com:8080/");
-    assert!(a != b);
+    assert!(a != b, "{:?} != {:?}", a, b);
 
 
     // Different scheme
     // Different scheme
     let a: Url = url("http://example.com/");
     let a: Url = url("http://example.com/");
@@ -165,27 +156,55 @@ fn relative_scheme_data_equality() {
 
 
 #[test]
 #[test]
 fn host() {
 fn host() {
-    let a = Host::parse("www.mozilla.org").unwrap();
-    let b = Host::parse("1.35.33.49").unwrap();
-    let c = Host::parse("[2001:0db8:85a3:08d3:1319:8a2e:0370:7344]").unwrap();
-    let d = Host::parse("1.35.+33.49").unwrap();
-    assert_eq!(a, Host::Domain("www.mozilla.org".to_owned()));
-    assert_eq!(b, Host::Ipv4(Ipv4Addr::new(1, 35, 33, 49)));
-    assert_eq!(c, Host::Ipv6(Ipv6Addr::new(0x2001, 0x0db8, 0x85a3, 0x08d3,
-        0x1319, 0x8a2e, 0x0370, 0x7344)));
-    assert_eq!(d, Host::Domain("1.35.+33.49".to_owned()));
-    assert_eq!(Host::parse("[::]").unwrap(), Host::Ipv6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)));
-    assert_eq!(Host::parse("[::1]").unwrap(), Host::Ipv6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)));
-    assert_eq!(Host::parse("0x1.0X23.0x21.061").unwrap(), Host::Ipv4(Ipv4Addr::new(1, 35, 33, 49)));
-    assert_eq!(Host::parse("0x1232131").unwrap(), Host::Ipv4(Ipv4Addr::new(1, 35, 33, 49)));
-    assert!(Host::parse("42.0x1232131").is_err());
-    assert_eq!(Host::parse("111").unwrap(), Host::Ipv4(Ipv4Addr::new(0, 0, 0, 111)));
-    assert_eq!(Host::parse("2..2.3").unwrap(), Host::Domain("2..2.3".to_owned()));
-    assert!(Host::parse("192.168.0.257").is_err());
+    fn assert_host(input: &str, host: Host<&str>) {
+        assert_eq!(Url::parse(input).unwrap().host(), Some(host));
+    }
+    assert_host("http://www.mozilla.org", Host::Domain("www.mozilla.org"));
+    assert_host("http://1.35.33.49", Host::Ipv4(Ipv4Addr::new(1, 35, 33, 49)));
+    assert_host("http://[2001:0db8:85a3:08d3:1319:8a2e:0370:7344]", Host::Ipv6(Ipv6Addr::new(
+        0x2001, 0x0db8, 0x85a3, 0x08d3, 0x1319, 0x8a2e, 0x0370, 0x7344)));
+    assert_host("http://1.35.+33.49", Host::Domain("1.35.+33.49"));
+    assert_host("http://[::]", Host::Ipv6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)));
+    assert_host("http://[::1]", Host::Ipv6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)));
+    assert_host("http://0x1.0X23.0x21.061", Host::Ipv4(Ipv4Addr::new(1, 35, 33, 49)));
+    assert_host("http://0x1232131", Host::Ipv4(Ipv4Addr::new(1, 35, 33, 49)));
+    assert_host("http://111", Host::Ipv4(Ipv4Addr::new(0, 0, 0, 111)));
+    assert_host("http://2..2.3", Host::Domain("2..2.3"));
+    assert!(Url::parse("http://42.0x1232131").is_err());
+    assert!(Url::parse("http://192.168.0.257").is_err());
+}
+
+#[test]
+fn host_serialization() {
+    // libstd’s `Display for Ipv6Addr` serializes 0:0:0:0:0:0:_:_ and 0:0:0:0:0:ffff:_:_
+    // using IPv4-like syntax, as suggested in https://tools.ietf.org/html/rfc5952#section-4
+    // but https://url.spec.whatwg.org/#concept-ipv6-serializer specifies not to.
+
+    // Not [::0.0.0.2] / [::ffff:0.0.0.2]
+    assert_eq!(Url::parse("http://[0::2]").unwrap().host_str(), Some("[::2]"));
+    assert_eq!(Url::parse("http://[0::ffff:0:2]").unwrap().host_str(), Some("[::ffff:0:2]"));
 }
 }
 
 
 #[test]
 #[test]
 fn test_idna() {
 fn test_idna() {
     assert!("http://goșu.ro".parse::<Url>().is_ok());
     assert!("http://goșu.ro".parse::<Url>().is_ok());
-    assert_eq!(Url::parse("http://☃.net/").unwrap().domain(), Some("xn--n3h.net"));
+    assert_eq!(Url::parse("http://☃.net/").unwrap().host(), Some(Host::Domain("xn--n3h.net")));
+}
+
+#[test]
+fn test_serialization() {
+    let data = [
+        ("http://example.com/", "http://example.com/"),
+        ("http://addslash.com", "http://addslash.com/"),
+        ("http://@emptyuser.com/", "http://emptyuser.com/"),
+        ("http://:@emptypass.com/", "http://:@emptypass.com/"),
+        ("http://user@user.com/", "http://user@user.com/"),
+        ("http://user:pass@userpass.com/", "http://user:pass@userpass.com/"),
+        ("http://slashquery.com/path/?q=something", "http://slashquery.com/path/?q=something"),
+        ("http://noslashquery.com/path?q=something", "http://noslashquery.com/path?q=something")
+    ];
+    for &(input, result) in &data {
+        let url = Url::parse(input).unwrap();
+        assert_eq!(url.as_str(), result);
+    }
 }
 }

+ 13 - 13
tests/urltestdata.txt

@@ -41,20 +41,20 @@ http://f:\s21\s/\sb\s?\sd\s#\se\s
 /:23  s:http h:example.org p:/:23
 /:23  s:http h:example.org p:/:23
 ::  s:http h:example.org p:/foo/::
 ::  s:http h:example.org p:/foo/::
 ::23  s:http h:example.org p:/foo/::23
 ::23  s:http h:example.org p:/foo/::23
-foo://  s:foo p://
+foo://  s:foo p:/
 http://a:b@c:29/d  s:http u:a pass:b h:c port:29 p:/d
 http://a:b@c:29/d  s:http u:a pass:b h:c port:29 p:/d
 http::@c:29  s:http h:example.org p:/foo/:@c:29
 http::@c:29  s:http h:example.org p:/foo/:@c:29
-http://&a:foo(b]c@d:2/  s:http u:&a pass:foo(b]c h:d port:2 p:/
-http://::@c@d:2  s:http pass::%40c h:d port:2 p:/
+http://&a:foo(b]c@d:2/  s:http u:&a pass:foo(b%5Dc h:d port:2 p:/
+http://::@c@d:2  s:http pass:%3A%40c h:d port:2 p:/
 http://foo.com:b@d/  s:http u:foo.com pass:b h:d p:/
 http://foo.com:b@d/  s:http u:foo.com pass:b h:d p:/
 http://foo.com/\\@  s:http h:foo.com p://@
 http://foo.com/\\@  s:http h:foo.com p://@
 http:\\\\foo.com\\  s:http h:foo.com p:/
 http:\\\\foo.com\\  s:http h:foo.com p:/
 http:\\\\a\\b:c\\d@foo.com\\  s:http h:a p:/b:c/d@foo.com/
 http:\\\\a\\b:c\\d@foo.com\\  s:http h:a p:/b:c/d@foo.com/
 foo:/  s:foo p:/
 foo:/  s:foo p:/
 foo:/bar.com/  s:foo p:/bar.com/
 foo:/bar.com/  s:foo p:/bar.com/
-foo://///////  s:foo p://///////
-foo://///////bar.com/  s:foo p://///////bar.com/
-foo:////://///  s:foo p:////://///
+foo://///////  s:foo p:///////
+foo://///////bar.com/  s:foo p:///////bar.com/
+foo:////://///  s:foo p://://///
 c:/foo  s:c p:/foo
 c:/foo  s:c p:/foo
 //foo/bar  s:http h:foo p:/bar
 //foo/bar  s:http h:foo p:/bar
 http://foo/path;a??e#f#g  s:http h:foo p:/path;a q:??e f:#f#g
 http://foo/path;a??e#f#g  s:http h:foo p:/path;a q:??e f:#f#g
@@ -113,9 +113,9 @@ file:///home/me  s:file p:/home/me
 ///  s:file p:/
 ///  s:file p:/
 ///test  s:file p:/test
 ///test  s:file p:/test
 file://test  s:file h:test p:/
 file://test  s:file h:test p:/
-file://localhost  s:file h:localhost p:/
-file://localhost/  s:file h:localhost p:/
-file://localhost/test  s:file h:localhost p:/test
+file://localhost  s:file p:/
+file://localhost/  s:file p:/
+file://localhost/test  s:file p:/test
 test  s:file p:/tmp/mock/test
 test  s:file p:/tmp/mock/test
 file:test  s:file p:/tmp/mock/test
 file:test  s:file p:/tmp/mock/test
 
 
@@ -170,7 +170,7 @@ http://%25DOMAIN:foobar@foodomain.com/  s:http u:%25DOMAIN pass:foobar h:foodoma
 http:\\\\www.google.com\\foo  s:http h:www.google.com p:/foo
 http:\\\\www.google.com\\foo  s:http h:www.google.com p:/foo
 http://foo:80/  s:http h:foo p:/
 http://foo:80/  s:http h:foo p:/
 http://foo:81/  s:http h:foo port:81 p:/
 http://foo:81/  s:http h:foo port:81 p:/
-httpa://foo:80/  s:httpa p://foo:80/
+httpa://foo:80/  s:httpa h:foo port:80 p:/
 http://foo:-80/
 http://foo:-80/
 https://foo:443/  s:https h:foo p:/
 https://foo:443/  s:https h:foo p:/
 https://foo:80/  s:https h:foo port:80 p:/
 https://foo:80/  s:https h:foo port:80 p:/
@@ -310,8 +310,8 @@ http://%25
 http://hello%00
 http://hello%00
 
 
 # Escaped numbers should be treated like IP addresses if they are.
 # Escaped numbers should be treated like IP addresses if they are.
-XFAIL http://%30%78%63%30%2e%30%32%35%30.01  s:http p:/ h:127.0.0.1
-XFAIL http://%30%78%63%30%2e%30%32%35%30.01%2e
+http://%30%78%63%30%2e%30%32%35%30.01  s:http p:/ h:192.168.0.1
+http://%30%78%63%30%2e%30%32%35%30.01%2e  s:http p:/ h:192.168.0.1
 
 
 # Invalid escaping should trigger the regular host error handling.
 # Invalid escaping should trigger the regular host error handling.
 http://%3g%78%63%30%2e%30%32%35%30%2E.01
 http://%3g%78%63%30%2e%30%32%35%30%2E.01
@@ -325,5 +325,5 @@ http://192.168.0.1\shello
 http://\uff10\uff38\uff43\uff10\uff0e\uff10\uff12\uff15\uff10\uff0e\uff10\uff11  s:http p:/ h:192.168.0.1
 http://\uff10\uff38\uff43\uff10\uff0e\uff10\uff12\uff15\uff10\uff0e\uff10\uff11  s:http p:/ h:192.168.0.1
 
 
 # Broken IP addresses.
 # Broken IP addresses.
-XFAIL http://192.168.0.257
+http://192.168.0.257
 http://[google.com]
 http://[google.com]

+ 45 - 79
tests/wpt.rs

@@ -12,97 +12,63 @@ extern crate test;
 extern crate url;
 extern crate url;
 
 
 use std::char;
 use std::char;
-use url::{RelativeSchemeData, SchemeData, Url};
+use url::Url;
 
 
 
 
 fn run_one(entry: Entry) {
 fn run_one(entry: Entry) {
-    // FIXME: Don’t re-indent to make merging the 1.0 branch easier.
-    {
-        let Entry {
-            input,
-            base,
-            scheme: expected_scheme,
-            username: expected_username,
-            password: expected_password,
-            host: expected_host,
-            port: expected_port,
-            path: expected_path,
-            query: expected_query,
-            fragment: expected_fragment,
-            expected_failure,
-        } = entry;
-        let base = match Url::parse(&base) {
-            Ok(base) => base,
-            Err(message) => panic!("Error parsing base {}: {}", base, message)
-        };
-        let url = base.join(&input);
-        if expected_scheme.is_none() {
-            if url.is_ok() && !expected_failure {
-                panic!("Expected a parse error for URL {}", input);
-            }
+    let Entry {
+        input,
+        base,
+        scheme: expected_scheme,
+        username: expected_username,
+        password: expected_password,
+        host: expected_host,
+        port: expected_port,
+        path: expected_path,
+        query: expected_query,
+        fragment: expected_fragment,
+        expected_failure,
+    } = entry;
+    let base = match Url::parse(&base) {
+        Ok(base) => base,
+        Err(message) => panic!("Error parsing base {}: {}", base, message)
+    };
+    let expecting_err = expected_scheme.is_none() ^ expected_failure;
+    let url = match base.join(&input) {
+        Ok(url) => url,
+        Err(reason) => {
+            assert!(expecting_err, "Error parsing URL {}: {}", input, reason);
             return
             return
         }
         }
-        let Url { scheme, scheme_data, query, fragment, .. } = match url {
-            Ok(url) => url,
-            Err(message) => {
-                if expected_failure {
-                    return
-                } else {
-                    panic!("Error parsing URL {}: {}", input, message)
-                }
-            }
-        };
+    };
+    assert!(!expecting_err, "Expected a parse error for URL {}", input);
 
 
-        macro_rules! assert_eq {
-            ($a: expr, $b: expr) => {
-                {
-                    let a = $a;
-                    let b = $b;
-                    if a != b {
-                        if expected_failure {
-                            return
-                        } else {
-                            panic!("{:?} != {:?}", a, b)
-                        }
+    macro_rules! assert_eq {
+        ($a: expr, $b: expr) => {
+            {
+                let a = $a;
+                let b = $b;
+                if a != b {
+                    if expected_failure {
+                        return
+                    } else {
+                        panic!("{:?} != {:?} for {:?}", a, b, url)
                     }
                     }
                 }
                 }
             }
             }
         }
         }
-
-        assert_eq!(Some(scheme), expected_scheme);
-        match scheme_data {
-            SchemeData::Relative(RelativeSchemeData {
-                username, password, host, port, default_port: _, path,
-            }) => {
-                assert_eq!(username, expected_username);
-                assert_eq!(password, expected_password);
-                let host = host.serialize();
-                assert_eq!(host, expected_host);
-                assert_eq!(port, expected_port);
-                assert_eq!(Some(format!("/{}", str_join(&path, "/"))), expected_path);
-            },
-            SchemeData::NonRelative(scheme_data) => {
-                assert_eq!(Some(scheme_data), expected_path);
-                assert_eq!(String::new(), expected_username);
-                assert_eq!(None, expected_password);
-                assert_eq!(String::new(), expected_host);
-                assert_eq!(None, expected_port);
-            },
-        }
-        fn opt_prepend(prefix: &str, opt_s: Option<String>) -> Option<String> {
-            opt_s.map(|s| format!("{}{}", prefix, s))
-        }
-        assert_eq!(opt_prepend("?", query), expected_query);
-        assert_eq!(opt_prepend("#", fragment), expected_fragment);
-
-        assert!(!expected_failure, "Unexpected success for {}", input);
     }
     }
-}
 
 
-// FIMXE: Remove this when &[&str]::join (the new name) lands in the stable channel.
-#[allow(deprecated)]
-fn str_join<T: ::std::borrow::Borrow<str>>(pieces: &[T], separator: &str) -> String {
-    pieces.connect(separator)
+    assert_eq!(Some(url.scheme().to_owned()), expected_scheme);
+    assert_eq!(url.username(), expected_username);
+    assert_eq!(url.password().map(|s| s.to_owned()), expected_password);
+    assert_eq!(url.host_str().unwrap_or("").to_owned(), expected_host);
+    assert_eq!(url.port(), expected_port);
+    assert_eq!(Some(url.path().to_owned()), expected_path);
+    assert_eq!(url.query().map(|s| format!("?{}", s)), expected_query);
+    assert_eq!(url.fragment().map(|s| format!("#{}", s)), expected_fragment);
+
+    assert!(!expected_failure, "Unexpected success for {}", input);
 }
 }
 
 
 struct Entry {
 struct Entry {

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.