Jeremy Lempereur 7 лет назад
Родитель
Сommit
54a158b7a2
6 измененных файлов с 300 добавлено и 63 удалено
  1. 5 1
      src/host.rs
  2. 29 12
      src/lib.rs
  3. 211 45
      src/parser.rs
  4. 13 2
      src/path_segments.rs
  5. 26 1
      src/quirks.rs
  6. 16 2
      tests/unit.rs

+ 5 - 1
src/host.rs

@@ -24,9 +24,13 @@ pub(crate) enum HostInternal {
     Ipv6(Ipv6Addr),
     Ipv6(Ipv6Addr),
 }
 }
 
 
-impl<S> From<Host<S>> for HostInternal {
+impl<S> From<Host<S>> for HostInternal
+where
+    S: ToString,
+{
     fn from(host: Host<S>) -> HostInternal {
     fn from(host: Host<S>) -> HostInternal {
         match host {
         match host {
+            Host::Domain(ref s) if s.to_string().is_empty() => HostInternal::None,
             Host::Domain(_) => HostInternal::Domain,
             Host::Domain(_) => HostInternal::Domain,
             Host::Ipv4(address) => HostInternal::Ipv4(address),
             Host::Ipv4(address) => HostInternal::Ipv4(address),
             Host::Ipv6(address) => HostInternal::Ipv6(address),
             Host::Ipv6(address) => HostInternal::Ipv6(address),

+ 29 - 12
src/lib.rs

@@ -456,13 +456,15 @@ impl Url {
 
 
         if self.slice(self.scheme_end + 1..).starts_with("//") {
         if self.slice(self.scheme_end + 1..).starts_with("//") {
             // URL with authority
             // URL with authority
-            match self.byte_at(self.username_end) {
-                b':' => {
-                    assert!(self.host_start >= self.username_end + 2);
-                    assert_eq!(self.byte_at(self.host_start - 1), b'@');
+            if self.username_end != self.serialization.len() as u32 {
+                match self.byte_at(self.username_end) {
+                    b':' => {
+                        assert!(self.host_start >= self.username_end + 2);
+                        assert_eq!(self.byte_at(self.host_start - 1), b'@');
+                    }
+                    b'@' => assert!(self.host_start == self.username_end + 1),
+                    _ => assert_eq!(self.username_end, self.scheme_end + 3),
                 }
                 }
-                b'@' => assert!(self.host_start == self.username_end + 1),
-                _ => assert_eq!(self.username_end, self.scheme_end + 3),
             }
             }
             assert!(self.host_start >= self.username_end);
             assert!(self.host_start >= self.username_end);
             assert!(self.host_end >= self.host_start);
             assert!(self.host_end >= self.host_start);
@@ -490,7 +492,10 @@ impl Url {
                     Some(port_str.parse::<u16>().expect("Couldn't parse port?"))
                     Some(port_str.parse::<u16>().expect("Couldn't parse port?"))
                 );
                 );
             }
             }
-            assert_eq!(self.byte_at(self.path_start), b'/');
+            assert!(
+                self.path_start as usize == self.serialization.len()
+                    || matches!(self.byte_at(self.path_start), b'/' | b'#' | b'?')
+            );
         } else {
         } else {
             // Anarchist URL (no authority)
             // Anarchist URL (no authority)
             assert_eq!(self.username_end, self.scheme_end + 1);
             assert_eq!(self.username_end, self.scheme_end + 1);
@@ -501,11 +506,11 @@ impl Url {
             assert_eq!(self.path_start, self.scheme_end + 1);
             assert_eq!(self.path_start, self.scheme_end + 1);
         }
         }
         if let Some(start) = self.query_start {
         if let Some(start) = self.query_start {
-            assert!(start > self.path_start);
+            assert!(start >= self.path_start);
             assert_eq!(self.byte_at(start), b'?');
             assert_eq!(self.byte_at(start), b'?');
         }
         }
         if let Some(start) = self.fragment_start {
         if let Some(start) = self.fragment_start {
-            assert!(start > self.path_start);
+            assert!(start >= self.path_start);
             assert_eq!(self.byte_at(start), b'#');
             assert_eq!(self.byte_at(start), b'#');
         }
         }
         if let (Some(query_start), Some(fragment_start)) = (self.query_start, self.fragment_start) {
         if let (Some(query_start), Some(fragment_start)) = (self.query_start, self.fragment_start) {
@@ -745,7 +750,10 @@ impl Url {
     pub fn password(&self) -> Option<&str> {
     pub fn password(&self) -> Option<&str> {
         // This ':' is not the one marking a port number since a host can not be empty.
         // This ':' is not the one marking a port number since a host can not be empty.
         // (Except for file: URLs, which do not have port numbers.)
         // (Except for file: URLs, which do not have port numbers.)
-        if self.has_authority() && self.byte_at(self.username_end) == b':' {
+        if self.has_authority()
+            && self.username_end != self.serialization.len() as u32
+            && self.byte_at(self.username_end) == b':'
+        {
             debug_assert!(self.byte_at(self.host_start - 1) == b'@');
             debug_assert!(self.byte_at(self.host_start - 1) == b'@');
             Some(self.slice(self.username_end + 1..self.host_start - 1))
             Some(self.slice(self.username_end + 1..self.host_start - 1))
         } else {
         } else {
@@ -1226,7 +1234,7 @@ impl Url {
         if let Some(input) = fragment {
         if let Some(input) = fragment {
             self.fragment_start = Some(to_u32(self.serialization.len()).unwrap());
             self.fragment_start = Some(to_u32(self.serialization.len()).unwrap());
             self.serialization.push('#');
             self.serialization.push('#');
-            self.mutate(|parser| parser.parse_fragment(parser::Input::new(input)))
+            self.mutate(|parser| parser.parse_fragment(parser::Input::no_trim(input)))
         } else {
         } else {
             self.fragment_start = None
             self.fragment_start = None
         }
         }
@@ -1284,7 +1292,12 @@ impl Url {
             let scheme_type = SchemeType::from(self.scheme());
             let scheme_type = SchemeType::from(self.scheme());
             let scheme_end = self.scheme_end;
             let scheme_end = self.scheme_end;
             self.mutate(|parser| {
             self.mutate(|parser| {
-                parser.parse_query(scheme_type, scheme_end, parser::Input::new(input))
+                let vfn = parser.violation_fn;
+                parser.parse_query(
+                    scheme_type,
+                    scheme_end,
+                    parser::Input::trim_tab_and_newlines(input, vfn),
+                )
             });
             });
         }
         }
 
 
@@ -1390,8 +1403,12 @@ impl Url {
                 }
                 }
                 parser.parse_cannot_be_a_base_path(parser::Input::new(path));
                 parser.parse_cannot_be_a_base_path(parser::Input::new(path));
             } else {
             } else {
+                let path_start = parser.serialization.len();
                 let mut has_host = true; // FIXME
                 let mut has_host = true; // FIXME
                 parser.parse_path_start(scheme_type, &mut has_host, parser::Input::new(path));
                 parser.parse_path_start(scheme_type, &mut has_host, parser::Input::new(path));
+                if scheme_type.is_file() {
+                    parser::trim_path(&mut parser.serialization, path_start);
+                }
             }
             }
         });
         });
         self.restore_after_path(old_after_path_pos, &after_path);
         self.restore_after_path(old_after_path_pos, &after_path);

+ 211 - 45
src/parser.rs

@@ -201,6 +201,30 @@ impl<'i> Input<'i> {
         Input::with_log(input, None)
         Input::with_log(input, None)
     }
     }
 
 
+    pub fn no_trim(input: &'i str) -> Self {
+        Input {
+            chars: input.chars(),
+        }
+    }
+
+    pub fn trim_tab_and_newlines(
+        original_input: &'i str,
+        vfn: Option<&dyn Fn(SyntaxViolation)>,
+    ) -> Self {
+        let input = original_input.trim_matches(ascii_tab_or_new_line);
+        if let Some(vfn) = vfn {
+            if input.len() < original_input.len() {
+                vfn(SyntaxViolation::C0SpaceIgnored)
+            }
+            if input.chars().any(|c| matches!(c, '\t' | '\n' | '\r')) {
+                vfn(SyntaxViolation::TabOrNewlineIgnored)
+            }
+        }
+        Input {
+            chars: input.chars(),
+        }
+    }
+
     pub fn with_log(original_input: &'i str, vfn: Option<&dyn Fn(SyntaxViolation)>) -> Self {
     pub fn with_log(original_input: &'i str, vfn: Option<&dyn Fn(SyntaxViolation)>) -> Self {
         let input = original_input.trim_matches(c0_control_or_space);
         let input = original_input.trim_matches(c0_control_or_space);
         if let Some(vfn) = vfn {
         if let Some(vfn) = vfn {
@@ -515,6 +539,8 @@ impl<'a> Parser<'a> {
                     self.serialization.push('/');
                     self.serialization.push('/');
                     self.parse_path(SchemeType::File, &mut has_host, path_start, remaining)
                     self.parse_path(SchemeType::File, &mut has_host, path_start, remaining)
                 };
                 };
+
+                trim_path(&mut self.serialization, host_end as usize);
                 // For file URLs that have a host and whose path starts
                 // For file URLs that have a host and whose path starts
                 // with the windows drive letter we just remove the host.
                 // with the windows drive letter we just remove the host.
                 if !has_host {
                 if !has_host {
@@ -556,16 +582,27 @@ impl<'a> Parser<'a> {
                         }
                         }
                     }
                     }
                 }
                 }
-                self.serialization.push('/');
-                let remaining = self.parse_path(
-                    SchemeType::File,
-                    &mut false,
-                    host_end,
-                    input_after_first_char,
-                );
+                // If c is the EOF code point, U+002F (/), U+005C (\), U+003F (?), or U+0023 (#), then decrease pointer by one
+                let parse_path_input = if let Some(c) = first_char {
+                    if c == '/' || c == '\\' || c == '?' || c == '#' {
+                        input
+                    } else {
+                        input_after_first_char
+                    }
+                } else {
+                    input_after_first_char
+                };
+
+                let remaining =
+                    self.parse_path(SchemeType::File, &mut false, host_end, parse_path_input);
+
+                let host_start = host_start as u32;
+
+                trim_path(&mut self.serialization, host_end);
+
                 let (query_start, fragment_start) =
                 let (query_start, fragment_start) =
                     self.parse_query_and_fragment(scheme_type, scheme_end, remaining)?;
                     self.parse_query_and_fragment(scheme_type, scheme_end, remaining)?;
-                let host_start = host_start as u32;
+
                 let host_end = host_end as u32;
                 let host_end = host_end as u32;
                 return Ok(Url {
                 return Ok(Url {
                     serialization: self.serialization,
                     serialization: self.serialization,
@@ -620,7 +657,7 @@ impl<'a> Parser<'a> {
                             (Some(i), _) | (None, Some(i)) => base_url.slice(..i),
                             (Some(i), _) | (None, Some(i)) => base_url.slice(..i),
                         };
                         };
                         self.serialization.push_str(before_query);
                         self.serialization.push_str(before_query);
-                        self.pop_path(SchemeType::File, base_url.path_start as usize);
+                        self.shorten_path(SchemeType::File, base_url.path_start as usize);
                         let remaining = self.parse_path(
                         let remaining = self.parse_path(
                             SchemeType::File,
                             SchemeType::File,
                             &mut true,
                             &mut true,
@@ -739,12 +776,14 @@ impl<'a> Parser<'a> {
                     debug_assert!(base_url.byte_at(scheme_end) == b':');
                     debug_assert!(base_url.byte_at(scheme_end) == b':');
                     self.serialization
                     self.serialization
                         .push_str(base_url.slice(..scheme_end + 1));
                         .push_str(base_url.slice(..scheme_end + 1));
+                    if let Some(after_prefix) = input.split_prefix("//") {
+                        return self.after_double_slash(after_prefix, scheme_type, scheme_end);
+                    }
                     return self.after_double_slash(remaining, scheme_type, scheme_end);
                     return self.after_double_slash(remaining, scheme_type, scheme_end);
                 }
                 }
                 let path_start = base_url.path_start;
                 let path_start = base_url.path_start;
-                debug_assert!(base_url.byte_at(path_start) == b'/');
-                self.serialization
-                    .push_str(base_url.slice(..path_start + 1));
+                self.serialization.push_str(base_url.slice(..path_start));
+                self.serialization.push_str("/");
                 let remaining = self.parse_path(
                 let remaining = self.parse_path(
                     scheme_type,
                     scheme_type,
                     &mut true,
                     &mut true,
@@ -771,8 +810,24 @@ impl<'a> Parser<'a> {
                 self.serialization.push_str(before_query);
                 self.serialization.push_str(before_query);
                 // FIXME spec says just "remove last entry", not the "pop" algorithm
                 // FIXME spec says just "remove last entry", not the "pop" algorithm
                 self.pop_path(scheme_type, base_url.path_start as usize);
                 self.pop_path(scheme_type, base_url.path_start as usize);
-                let remaining =
-                    self.parse_path(scheme_type, &mut true, base_url.path_start as usize, input);
+                // A special url always has a path.
+                // A path always starts with '/'
+                if self.serialization.len() == base_url.path_start as usize {
+                    if SchemeType::from(base_url.scheme()).is_special() || !input.is_empty() {
+                        self.serialization.push('/');
+                    }
+                }
+                let remaining = match input.split_first() {
+                    (Some('/'), remaining) => self.parse_path(
+                        scheme_type,
+                        &mut true,
+                        base_url.path_start as usize,
+                        remaining,
+                    ),
+                    _ => {
+                        self.parse_path(scheme_type, &mut true, base_url.path_start as usize, input)
+                    }
+                };
                 self.with_query_and_fragment(
                 self.with_query_and_fragment(
                     scheme_type,
                     scheme_type,
                     base_url.scheme_end,
                     base_url.scheme_end,
@@ -946,7 +1001,7 @@ impl<'a> Parser<'a> {
                 host_str = &input_str[..bytes]
                 host_str = &input_str[..bytes]
             }
             }
         }
         }
-        if scheme_type.is_special() && host_str.is_empty() {
+        if scheme_type == SchemeType::SpecialNotFile && host_str.is_empty() {
             return Err(ParseError::EmptyHost);
             return Err(ParseError::EmptyHost);
         }
         }
         if !scheme_type.is_special() {
         if !scheme_type.is_special() {
@@ -1040,21 +1095,34 @@ impl<'a> Parser<'a> {
         &mut self,
         &mut self,
         scheme_type: SchemeType,
         scheme_type: SchemeType,
         has_host: &mut bool,
         has_host: &mut bool,
-        mut input: Input<'i>,
+        input: Input<'i>,
     ) -> Input<'i> {
     ) -> Input<'i> {
-        // Path start state
-        match input.split_first() {
-            (Some('/'), remaining) => input = remaining,
-            (Some('\\'), remaining) => {
-                if scheme_type.is_special() {
-                    self.log_violation(SyntaxViolation::Backslash);
-                    input = remaining
+        let path_start = self.serialization.len();
+        let (maybe_c, remaining) = input.split_first();
+        // If url is special, then:
+        if scheme_type.is_special() {
+            if maybe_c == Some('\\') {
+                // If c is U+005C (\), validation error.
+                self.log_violation(SyntaxViolation::Backslash);
+            }
+            // A special URL always has a non-empty path.
+            if !self.serialization.ends_with("/") {
+                self.serialization.push('/');
+                // We have already made sure the forward slash is present.
+                if maybe_c == Some('/') || maybe_c == Some('\\') {
+                    return self.parse_path(scheme_type, has_host, path_start, remaining);
                 }
                 }
             }
             }
-            _ => {}
+            return self.parse_path(scheme_type, has_host, path_start, input);
+        } else if maybe_c == Some('?') || maybe_c == Some('#') {
+            // Otherwise, if state override is not given and c is U+003F (?),
+            // set url’s query to the empty string and state to query state.
+            // Otherwise, if state override is not given and c is U+0023 (#),
+            // set url’s fragment to the empty string and state to fragment state.
+            // The query and path states will be handled by the caller.
+            return input;
         }
         }
-        let path_start = self.serialization.len();
-        self.serialization.push('/');
+        // Otherwise, if c is not the EOF code point:
         self.parse_path(scheme_type, has_host, path_start, input)
         self.parse_path(scheme_type, has_host, path_start, input)
     }
     }
 
 
@@ -1066,7 +1134,6 @@ impl<'a> Parser<'a> {
         mut input: Input<'i>,
         mut input: Input<'i>,
     ) -> Input<'i> {
     ) -> Input<'i> {
         // Relative path state
         // Relative path state
-        debug_assert!(self.serialization.ends_with('/'));
         loop {
         loop {
             let segment_start = self.serialization.len();
             let segment_start = self.serialization.len();
             let mut ends_with_slash = false;
             let mut ends_with_slash = false;
@@ -1079,6 +1146,7 @@ impl<'a> Parser<'a> {
                 };
                 };
                 match c {
                 match c {
                     '/' if self.context != Context::PathSegmentSetter => {
                     '/' if self.context != Context::PathSegmentSetter => {
+                        self.serialization.push(c);
                         ends_with_slash = true;
                         ends_with_slash = true;
                         break;
                         break;
                     }
                     }
@@ -1086,6 +1154,7 @@ impl<'a> Parser<'a> {
                         && scheme_type.is_special() =>
                         && scheme_type.is_special() =>
                     {
                     {
                         self.log_violation(SyntaxViolation::Backslash);
                         self.log_violation(SyntaxViolation::Backslash);
+                        self.serialization.push('/');
                         ends_with_slash = true;
                         ends_with_slash = true;
                         break;
                         break;
                     }
                     }
@@ -1109,35 +1178,57 @@ impl<'a> Parser<'a> {
                     }
                     }
                 }
                 }
             }
             }
-            match &self.serialization[segment_start..] {
+
+            let segment_before_slash = if ends_with_slash {
+                &self.serialization[segment_start..self.serialization.len() - 1]
+            } else {
+                &self.serialization[segment_start..self.serialization.len()]
+            };
+            match segment_before_slash {
+                // If buffer is a double-dot path segment, shorten url’s path,
                 ".." | "%2e%2e" | "%2e%2E" | "%2E%2e" | "%2E%2E" | "%2e." | "%2E." | ".%2e"
                 ".." | "%2e%2e" | "%2e%2E" | "%2E%2e" | "%2E%2E" | "%2e." | "%2E." | ".%2e"
                 | ".%2E" => {
                 | ".%2E" => {
                     debug_assert!(self.serialization.as_bytes()[segment_start - 1] == b'/');
                     debug_assert!(self.serialization.as_bytes()[segment_start - 1] == b'/');
-                    self.serialization.truncate(segment_start - 1); // Truncate "/.."
-                    self.pop_path(scheme_type, path_start);
-                    if !self.serialization[path_start..].ends_with('/') {
-                        self.serialization.push('/')
+                    self.serialization.truncate(segment_start);
+                    if self.serialization.ends_with("/")
+                        && Parser::last_slash_can_be_removed(&self.serialization, path_start)
+                    {
+                        self.serialization.pop();
+                    }
+                    self.shorten_path(scheme_type, path_start);
+
+                    // and then if neither c is U+002F (/), nor url is special and c is U+005C (\), append the empty string to url’s path.
+                    if ends_with_slash && !self.serialization.ends_with("/") {
+                        self.serialization.push('/');
                     }
                     }
                 }
                 }
+                // Otherwise, if buffer is a single-dot path segment and if neither c is U+002F (/),
+                // nor url is special and c is U+005C (\), append the empty string to url’s path.
                 "." | "%2e" | "%2E" => {
                 "." | "%2e" | "%2E" => {
                     self.serialization.truncate(segment_start);
                     self.serialization.truncate(segment_start);
+                    if !self.serialization.ends_with("/") {
+                        self.serialization.push('/');
+                    }
                 }
                 }
                 _ => {
                 _ => {
-                    if scheme_type.is_file()
-                        && is_windows_drive_letter(&self.serialization[path_start + 1..])
-                    {
-                        if self.serialization.ends_with('|') {
-                            self.serialization.pop();
+                    // If url’s scheme is "file", url’s path is empty, and buffer is a Windows drive letter, then
+                    if scheme_type.is_file() && is_windows_drive_letter(segment_before_slash) {
+                        // Replace the second code point in buffer with U+003A (:).
+                        if let Some(c) = segment_before_slash.chars().nth(0) {
+                            self.serialization.truncate(segment_start);
+                            self.serialization.push(c);
                             self.serialization.push(':');
                             self.serialization.push(':');
+                            if ends_with_slash {
+                                self.serialization.push('/');
+                            }
                         }
                         }
+                        // If url’s host is neither the empty string nor null,
+                        // validation error, set url’s host to the empty string.
                         if *has_host {
                         if *has_host {
                             self.log_violation(SyntaxViolation::FileWithHostAndWindowsDrive);
                             self.log_violation(SyntaxViolation::FileWithHostAndWindowsDrive);
                             *has_host = false; // FIXME account for this in callers
                             *has_host = false; // FIXME account for this in callers
                         }
                         }
                     }
                     }
-                    if ends_with_slash {
-                        self.serialization.push('/')
-                    }
                 }
                 }
             }
             }
             if !ends_with_slash {
             if !ends_with_slash {
@@ -1147,6 +1238,39 @@ impl<'a> Parser<'a> {
         input
         input
     }
     }
 
 
+    fn last_slash_can_be_removed(serialization: &String, path_start: usize) -> bool {
+        let url_before_segment = &serialization[..serialization.len() - 1];
+        if let Some(segment_before_start) = url_before_segment.rfind("/") {
+            // Do not remove the root slash
+            segment_before_start >= path_start
+                // Or a windows drive letter slash
+                && !path_starts_with_windows_drive_letter(&serialization[segment_before_start..])
+        } else {
+            false
+        }
+    }
+
+    /// https://url.spec.whatwg.org/#shorten-a-urls-path
+    fn shorten_path(&mut self, scheme_type: SchemeType, path_start: usize) {
+        // If path is empty, then return.
+        if self.serialization.len() == path_start {
+            return;
+        }
+        // If url’s scheme is "file", path’s size is 1, and path[0] is a normalized Windows drive letter, then return.
+        let segments: Vec<&str> = self.serialization[path_start..]
+            .split('/')
+            .filter(|s| !s.is_empty())
+            .collect();
+        if scheme_type.is_file()
+            && segments.len() == 1
+            && is_normalized_windows_drive_letter(segments[0])
+        {
+            return;
+        }
+        // Remove path’s last item.
+        self.pop_path(scheme_type, path_start);
+    }
+
     /// https://url.spec.whatwg.org/#pop-a-urls-path
     /// https://url.spec.whatwg.org/#pop-a-urls-path
     fn pop_path(&mut self, scheme_type: SchemeType, path_start: usize) {
     fn pop_path(&mut self, scheme_type: SchemeType, path_start: usize) {
         if self.serialization.len() > path_start {
         if self.serialization.len() > path_start {
@@ -1154,9 +1278,8 @@ impl<'a> Parser<'a> {
             // + 1 since rfind returns the position before the slash.
             // + 1 since rfind returns the position before the slash.
             let segment_start = path_start + slash_position + 1;
             let segment_start = path_start + slash_position + 1;
             // Don’t pop a Windows drive letter
             // Don’t pop a Windows drive letter
-            // FIXME: *normalized* Windows drive letter
             if !(scheme_type.is_file()
             if !(scheme_type.is_file()
-                && is_windows_drive_letter(&self.serialization[segment_start..]))
+                && is_normalized_windows_drive_letter(&self.serialization[segment_start..]))
             {
             {
                 self.serialization.truncate(segment_start);
                 self.serialization.truncate(segment_start);
             }
             }
@@ -1318,6 +1441,18 @@ impl<'a> Parser<'a> {
     }
     }
 }
 }
 
 
+// Trim path start forward slashes when no authority is present
+// https://github.com/whatwg/url/issues/232
+pub fn trim_path(serialization: &mut String, path_start: usize) {
+    let path = serialization.split_off(path_start);
+    if path.starts_with("/") {
+        serialization.push('/');
+        serialization.push_str(&path.trim_start_matches("/"));
+    } else {
+        serialization.push_str(&path);
+    }
+}
+
 #[inline]
 #[inline]
 fn is_ascii_hex_digit(c: char) -> bool {
 fn is_ascii_hex_digit(c: char) -> bool {
     matches!(c, 'a'..='f' | 'A'..='F' | '0'..='9')
     matches!(c, 'a'..='f' | 'A'..='F' | '0'..='9')
@@ -1355,6 +1490,12 @@ fn c0_control_or_space(ch: char) -> bool {
     ch <= ' ' // U+0000 to U+0020
     ch <= ' ' // U+0000 to U+0020
 }
 }
 
 
+/// https://infra.spec.whatwg.org/#ascii-tab-or-newline
+#[inline]
+pub fn ascii_tab_or_new_line(ch: char) -> bool {
+    matches!(ch, '\t' | '\r' | '\n')
+}
+
 /// https://url.spec.whatwg.org/#ascii-alpha
 /// https://url.spec.whatwg.org/#ascii-alpha
 #[inline]
 #[inline]
 pub fn ascii_alpha(ch: char) -> bool {
 pub fn ascii_alpha(ch: char) -> bool {
@@ -1380,12 +1521,37 @@ fn is_windows_drive_letter(segment: &str) -> bool {
     segment.len() == 2 && starts_with_windows_drive_letter(segment)
     segment.len() == 2 && starts_with_windows_drive_letter(segment)
 }
 }
 
 
+/// Wether path starts with a root slash
+/// and a windows drive letter eg: "/c:" or "/a:/"
+fn path_starts_with_windows_drive_letter(s: &str) -> bool {
+    if let Some(c) = s.as_bytes().get(0) {
+        matches!(c, b'/' | b'\\' | b'?' | b'#') && starts_with_windows_drive_letter(&s[1..])
+    } else {
+        false
+    }
+}
+
 fn starts_with_windows_drive_letter(s: &str) -> bool {
 fn starts_with_windows_drive_letter(s: &str) -> bool {
-    ascii_alpha(s.as_bytes()[0] as char) && matches!(s.as_bytes()[1], b':' | b'|')
+    s.len() >= 2
+        && ascii_alpha(s.as_bytes()[0] as char)
+        && matches!(s.as_bytes()[1], b':' | b'|')
+        && (s.len() == 2 || matches!(s.as_bytes()[2], b'/' | b'\\' | b'?' | b'#'))
 }
 }
 
 
+/// https://url.spec.whatwg.org/#start-with-a-windows-drive-letter
 fn starts_with_windows_drive_letter_segment(input: &Input) -> bool {
 fn starts_with_windows_drive_letter_segment(input: &Input) -> bool {
     let mut input = input.clone();
     let mut input = input.clone();
-    matches!((input.next(), input.next(), input.next()), (Some(a), Some(b), Some(c))
-             if ascii_alpha(a) && matches!(b, ':' | '|') && matches!(c, '/' | '\\' | '?' | '#'))
+    match (input.next(), input.next(), input.next()) {
+        // its first two code points are a Windows drive letter
+        // its third code point is U+002F (/), U+005C (\), U+003F (?), or U+0023 (#).
+        (Some(a), Some(b), Some(c))
+            if ascii_alpha(a) && matches!(b, ':' | '|') && matches!(c, '/' | '\\' | '?' | '#') =>
+        {
+            true
+        }
+        // its first two code points are a Windows drive letter
+        // its length is 2
+        (Some(a), Some(b), None) if ascii_alpha(a) && matches!(b, ':' | '|') => true,
+        _ => false,
+    }
 }
 }

+ 13 - 2
src/path_segments.rs

@@ -45,7 +45,15 @@ pub struct PathSegmentsMut<'a> {
 pub fn new(url: &mut Url) -> PathSegmentsMut {
 pub fn new(url: &mut Url) -> PathSegmentsMut {
     let after_path = url.take_after_path();
     let after_path = url.take_after_path();
     let old_after_path_position = to_u32(url.serialization.len()).unwrap();
     let old_after_path_position = to_u32(url.serialization.len()).unwrap();
-    debug_assert!(url.byte_at(url.path_start) == b'/');
+    // Special urls always have a non empty path
+    if SchemeType::from(url.scheme()).is_special() {
+        debug_assert!(url.byte_at(url.path_start) == b'/');
+    } else {
+        debug_assert!(
+            url.serialization.len() == url.path_start as usize
+                || url.byte_at(url.path_start) == b'/'
+        );
+    }
     PathSegmentsMut {
     PathSegmentsMut {
         after_first_slash: url.path_start as usize + "/".len(),
         after_first_slash: url.path_start as usize + "/".len(),
         url,
         url,
@@ -212,7 +220,10 @@ impl<'a> PathSegmentsMut<'a> {
                 if matches!(segment, "." | "..") {
                 if matches!(segment, "." | "..") {
                     continue;
                     continue;
                 }
                 }
-                if parser.serialization.len() > path_start + 1 {
+                if parser.serialization.len() > path_start + 1
+                    // Non special url's path might still be empty
+                    || parser.serialization.len() == path_start
+                {
                     parser.serialization.push('/');
                     parser.serialization.push('/');
                 }
                 }
                 let mut has_host = true; // FIXME account for this?
                 let mut has_host = true; // FIXME account for this?

+ 26 - 1
src/quirks.rs

@@ -99,9 +99,13 @@ pub fn host(url: &Url) -> &str {
 
 
 /// Setter for https://url.spec.whatwg.org/#dom-url-host
 /// Setter for https://url.spec.whatwg.org/#dom-url-host
 pub fn set_host(url: &mut Url, new_host: &str) -> Result<(), ()> {
 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() {
     if url.cannot_be_a_base() {
         return Err(());
         return Err(());
     }
     }
+    // Host parsing rules are strict,
+    // We don't want to trim the input
+    let input = Input::no_trim(new_host);
     let host;
     let host;
     let opt_port;
     let opt_port;
     {
     {
@@ -121,6 +125,20 @@ pub fn set_host(url: &mut Url, new_host: &str) -> Result<(), ()> {
             Err(_) => return Err(()),
             Err(_) => return Err(()),
         }
         }
     }
     }
+    // Make sure we won't set an empty host to a url with a username or a port
+    if host == Host::Domain("".to_string()) {
+        if !username(&url).is_empty() {
+            return Err(());
+        }
+        if let Some(p) = opt_port {
+            if let Some(_) = p {
+                return Err(());
+            }
+        }
+        if url.port().is_some() {
+            return Err(());
+        }
+    }
     url.set_host_internal(host, opt_port);
     url.set_host_internal(host, opt_port);
     Ok(())
     Ok(())
 }
 }
@@ -182,7 +200,14 @@ pub fn pathname(url: &Url) -> &str {
 
 
 /// Setter for https://url.spec.whatwg.org/#dom-url-pathname
 /// Setter for https://url.spec.whatwg.org/#dom-url-pathname
 pub fn set_pathname(url: &mut Url, new_pathname: &str) {
 pub fn set_pathname(url: &mut Url, new_pathname: &str) {
-    if !url.cannot_be_a_base() {
+    if url.cannot_be_a_base() {
+        return;
+    }
+    if Some('/') == new_pathname.chars().nth(0)
+        || SchemeType::from(url.scheme()).is_special()
+        // \ is a segment delimiter for 'special' URLs"
+        && Some('\\') == new_pathname.chars().nth(0)
+    {
         url.set_path(new_pathname)
         url.set_path(new_pathname)
     }
     }
 }
 }

+ 16 - 2
tests/unit.rs

@@ -23,6 +23,20 @@ fn size() {
     assert_eq!(size_of::<Url>(), size_of::<Option<Url>>());
     assert_eq!(size_of::<Url>(), size_of::<Option<Url>>());
 }
 }
 
 
+#[test]
+fn test_relative() {
+    let base: Url = "sc://%C3%B1".parse().unwrap();
+    let url = base.join("/resources/testharness.js").unwrap();
+    assert_eq!(url.as_str(), "sc://%C3%B1/resources/testharness.js");
+}
+
+#[test]
+fn test_relative_empty() {
+    let base: Url = "sc://%C3%B1".parse().unwrap();
+    let url = base.join("").unwrap();
+    assert_eq!(url.as_str(), "sc://%C3%B1");
+}
+
 macro_rules! assert_from_file_path {
 macro_rules! assert_from_file_path {
     ($path: expr) => {
     ($path: expr) => {
         assert_from_file_path!($path, $path)
         assert_from_file_path!($path, $path)
@@ -413,9 +427,9 @@ fn test_set_host() {
     assert_eq!(url.as_str(), "foobar:/hello");
     assert_eq!(url.as_str(), "foobar:/hello");
 
 
     let mut url = Url::parse("foo://ș").unwrap();
     let mut url = Url::parse("foo://ș").unwrap();
-    assert_eq!(url.as_str(), "foo://%C8%99/");
+    assert_eq!(url.as_str(), "foo://%C8%99");
     url.set_host(Some("goșu.ro")).unwrap();
     url.set_host(Some("goșu.ro")).unwrap();
-    assert_eq!(url.as_str(), "foo://go%C8%99u.ro/");
+    assert_eq!(url.as_str(), "foo://go%C8%99u.ro");
 }
 }
 
 
 #[test]
 #[test]