// Copyright 2013-2016 The rust-url developers. // // 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.
use alloc::string::String; use alloc::string::ToString; use core::fmt::{self, Formatter, Write}; use core::str;
usecrate::host::{Host, HostInternal}; usecrate::Url; use form_urlencoded::EncodingOverride; use percent_encoding::{percent_encode, utf8_percent_encode, AsciiSet, CONTROLS};
// The backslash (\) character is treated as a path separator in special URLs // so it needs to be additionally escaped in that case. pub(crate) const SPECIAL_PATH_SEGMENT: &AsciiSet = &PATH_SEGMENT.add(b'\\');
macro_rules! simple_enum_error {
($($name: ident => $description: expr,)+) => { /// Errors that can occur during parsing. /// /// This may be extended in the future so exhaustive matching is /// discouraged with an unused variant. #[derive(PartialEq, Eq, Clone, Copy, Debug)] #[non_exhaustive] pubenum ParseError {
$(
$name,
)+
}
#[cfg(feature = "std")] impl std::error::Error for ParseError {}
#[cfg(not(feature = "std"))] impl core::error::Error for ParseError {}
simple_enum_error! {
EmptyHost => "empty host",
IdnaError => "invalid international domain name",
InvalidPort => "invalid port number",
InvalidIpv4Address => "invalid IPv4 address",
InvalidIpv6Address => "invalid IPv6 address",
InvalidDomainCharacter => "invalid domain character",
RelativeUrlWithoutBase => "relative URL without a base",
RelativeUrlWithCannotBeABaseBase => "relative URL with a cannot-be-a-base base",
SetHostOnCannotBeABaseUrl => "a cannot-be-a-base URL doesn’t have a host to set",
Overflow => "URLs more than 4 GB are not supported",
}
syntax_violation_enum! {
Backslash => "backslash",
C0SpaceIgnored => "leading or trailing control or space character are ignored in URLs",
EmbeddedCredentials => "embedding authentication information (username or password) \ in an URL is not recommended",
ExpectedDoubleSlash => "expected //",
ExpectedFileDoubleSlash => "expected // after file:",
FileWithHostAndWindowsDrive => "file: with host and Windows drive letter",
NonUrlCodePoint => "non-URL code point",
NullInFragment => "NULL characters are ignored in URL fragment identifiers",
PercentDecode => "expected 2 hex digits after %",
TabOrNewlineIgnored => "tabs or newlines are ignored in URLs",
UnencodedAtSign => "unencoded @ sign in username or password",
}
fn parse_with_scheme(mutself, input: Input<'_>) -> ParseResult<Url> { usecrate::SyntaxViolation::{ExpectedDoubleSlash, ExpectedFileDoubleSlash}; let scheme_end = to_u32(self.serialization.len())?; let scheme_type = SchemeType::from(&self.serialization); self.serialization.push(':'); match scheme_type {
SchemeType::File => { self.log_violation_if(ExpectedFileDoubleSlash, || !input.starts_with("//")); let base_file_url = self.base_url.and_then(|base| { if base.scheme() == "file" {
Some(base)
} else {
None
}
}); self.serialization.clear(); self.parse_file(input, scheme_type, base_file_url)
}
SchemeType::SpecialNotFile => { // special relative or authority state let (slashes_count, remaining) = input.count_matching(|c| matches!(c, '/' | '\\')); iflet Some(base_url) = self.base_url { if slashes_count < 2
&& base_url.scheme() == &self.serialization[..scheme_end as usize]
{ // "Cannot-be-a-base" URLs only happen with "not special" schemes.
debug_assert!(!base_url.cannot_be_a_base()); self.serialization.clear(); returnself.parse_relative(input, scheme_type, base_url);
}
} // special authority slashes state self.log_violation_if(ExpectedDoubleSlash, || {
input
.clone()
.take_while(|&c| matches!(c, '/' | '\\'))
.collect::<String>()
!= "//"
}); self.after_double_slash(remaining, scheme_type, scheme_end)
}
SchemeType::NotSpecial => self.parse_non_special(input, scheme_type, scheme_end),
}
}
/// Scheme other than file, http, https, ws, ws, ftp. fn parse_non_special( mutself,
input: Input<'_>,
scheme_type: SchemeType,
scheme_end: u32,
) -> ParseResult<Url> { // path or authority state ( iflet Some(input) = input.split_prefix("//") { returnself.after_double_slash(input, scheme_type, scheme_end);
} // Anarchist URL (no authority) let path_start = to_u32(self.serialization.len())?; let username_end = path_start; let host_start = path_start; let host_end = path_start; let host = HostInternal::None; let port = None; let remaining = iflet Some(input) = input.split_prefix('/') { self.serialization.push('/'); self.parse_path(scheme_type, &mutfalse, path_start as usize, input)
} else { self.parse_cannot_be_a_base_path(input)
}; self.with_query_and_fragment(
scheme_type,
scheme_end,
username_end,
host_start,
host_end,
host,
port,
path_start,
remaining,
)
}
fn parse_file( mutself,
input: Input<'_>,
scheme_type: SchemeType,
base_file_url: Option<&Url>,
) -> ParseResult<Url> { usecrate::SyntaxViolation::Backslash; // file state
debug_assert!(self.serialization.is_empty()); let (first_char, input_after_first_char) = input.split_first(); if matches!(first_char, Some('/') | Some('\\')) { self.log_violation_if(SyntaxViolation::Backslash, || first_char == Some('\\')); // file slash state let (next_char, input_after_next_char) = input_after_first_char.split_first(); if matches!(next_char, Some('/') | Some('\\')) { self.log_violation_if(Backslash, || next_char == Some('\\')); // file host state self.serialization.push_str("file://"); let scheme_end = "file".len() as u32; let host_start = "file://".len() as u32; let (path_start, mut host, remaining) = self.parse_file_host(input_after_next_char)?; letmut host_end = to_u32(self.serialization.len())?; letmut has_host = !matches!(host, HostInternal::None); let remaining = if path_start { self.parse_path_start(SchemeType::File, &mut has_host, remaining)
} else { let path_start = self.serialization.len(); self.serialization.push('/'); self.parse_path(SchemeType::File, &mut has_host, path_start, remaining)
};
// For file URLs that have a host and whose path starts // with the windows drive letter we just remove the host. if !has_host { self.serialization
.drain(host_start as usize..host_end as usize);
host_end = host_start;
host = HostInternal::None;
} let (query_start, fragment_start) = self.parse_query_and_fragment(scheme_type, scheme_end, remaining)?; return Ok(Url {
serialization: self.serialization,
scheme_end,
username_end: host_start,
host_start,
host_end,
host,
port: None,
path_start: host_end,
query_start,
fragment_start,
});
} else { self.serialization.push_str("file://"); let scheme_end = "file".len() as u32; let host_start = "file://".len(); letmut host_end = host_start; letmut host = HostInternal::None; if !starts_with_windows_drive_letter_segment(&input_after_first_char) { iflet Some(base_url) = base_file_url { let first_segment = base_url.path_segments().unwrap().next().unwrap(); if is_normalized_windows_drive_letter(first_segment) { self.serialization.push('/'); self.serialization.push_str(first_segment);
} elseiflet Some(host_str) = base_url.host_str() { self.serialization.push_str(host_str);
host_end = self.serialization.len();
host = base_url.host;
}
}
} // 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 = iflet 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, &mutfalse, host_end, parse_path_input);
let host_start = host_start as u32;
let (query_start, fragment_start) = self.parse_query_and_fragment(scheme_type, scheme_end, remaining)?;
let host_end = host_end as u32; return Ok(Url {
serialization: self.serialization,
scheme_end,
username_end: host_start,
host_start,
host_end,
host,
port: None,
path_start: host_end,
query_start,
fragment_start,
});
}
} iflet Some(base_url) = base_file_url { match first_char {
None => { // Copy everything except the fragment let before_fragment = match base_url.fragment_start {
Some(i) => &base_url.serialization[..i as usize],
None => &*base_url.serialization,
}; self.serialization.push_str(before_fragment);
Ok(Url {
serialization: self.serialization,
fragment_start: None,
..*base_url
})
}
Some('?') => { // Copy everything up to the query string let before_query = match (base_url.query_start, base_url.fragment_start) {
(None, None) => &*base_url.serialization,
(Some(i), _) | (None, Some(i)) => base_url.slice(..i),
}; self.serialization.push_str(before_query); let (query_start, fragment_start) = self.parse_query_and_fragment(scheme_type, base_url.scheme_end, input)?;
Ok(Url {
serialization: self.serialization,
query_start,
fragment_start,
..*base_url
})
}
Some('#') => self.fragment_only(base_url, input),
_ => { if !starts_with_windows_drive_letter_segment(&input) { let before_query = match (base_url.query_start, base_url.fragment_start) {
(None, None) => &*base_url.serialization,
(Some(i), _) | (None, Some(i)) => base_url.slice(..i),
}; self.serialization.push_str(before_query); self.shorten_path(SchemeType::File, base_url.path_start as usize); let remaining = self.parse_path(
SchemeType::File,
&muttrue,
base_url.path_start as usize,
input,
); self.with_query_and_fragment(
SchemeType::File,
base_url.scheme_end,
base_url.username_end,
base_url.host_start,
base_url.host_end,
base_url.host,
base_url.port,
base_url.path_start,
remaining,
)
} else { self.serialization.push_str("file:///"); let scheme_end = "file".len() as u32; let path_start = "file://".len(); let remaining = self.parse_path(SchemeType::File, &mutfalse, path_start, input); let (query_start, fragment_start) = self.parse_query_and_fragment(SchemeType::File, scheme_end, remaining)?; let path_start = path_start as u32;
Ok(Url {
serialization: self.serialization,
scheme_end,
username_end: path_start,
host_start: path_start,
host_end: path_start,
host: HostInternal::None,
port: None,
path_start,
query_start,
fragment_start,
})
}
}
}
} else { self.serialization.push_str("file:///"); let scheme_end = "file".len() as u32; let path_start = "file://".len(); let remaining = self.parse_path(SchemeType::File, &mutfalse, path_start, input); let (query_start, fragment_start) = self.parse_query_and_fragment(SchemeType::File, scheme_end, remaining)?; let path_start = path_start as u32;
Ok(Url {
serialization: self.serialization,
scheme_end,
username_end: path_start,
host_start: path_start,
host_end: path_start,
host: HostInternal::None,
port: None,
path_start,
query_start,
fragment_start,
})
}
}
fn parse_relative( mutself,
input: Input<'_>,
scheme_type: SchemeType,
base_url: &Url,
) -> ParseResult<Url> { // relative state
debug_assert!(self.serialization.is_empty()); let (first_char, input_after_first_char) = input.split_first(); match first_char {
None => { // Copy everything except the fragment let before_fragment = match base_url.fragment_start {
Some(i) => &base_url.serialization[..i as usize],
None => &*base_url.serialization,
}; self.serialization.push_str(before_fragment);
Ok(Url {
serialization: self.serialization,
fragment_start: None,
..*base_url
})
}
Some('?') => { // Copy everything up to the query string let before_query = match (base_url.query_start, base_url.fragment_start) {
(None, None) => &*base_url.serialization,
(Some(i), _) | (None, Some(i)) => base_url.slice(..i),
}; self.serialization.push_str(before_query); let (query_start, fragment_start) = self.parse_query_and_fragment(scheme_type, base_url.scheme_end, input)?;
Ok(Url {
serialization: self.serialization,
query_start,
fragment_start,
..*base_url
})
}
Some('#') => self.fragment_only(base_url, input),
Some('/') | Some('\\') => { let (slashes_count, remaining) = input.count_matching(|c| matches!(c, '/' | '\\')); if slashes_count >= 2 { self.log_violation_if(SyntaxViolation::ExpectedDoubleSlash, || {
input
.clone()
.take_while(|&c| matches!(c, '/' | '\\'))
.collect::<String>()
!= "//"
}); let scheme_end = base_url.scheme_end;
debug_assert!(base_url.byte_at(scheme_end) == b':'); self.serialization
.push_str(base_url.slice(..scheme_end + 1)); iflet Some(after_prefix) = input.split_prefix("//") { returnself.after_double_slash(after_prefix, scheme_type, scheme_end);
} returnself.after_double_slash(remaining, scheme_type, scheme_end);
} let path_start = base_url.path_start; self.serialization.push_str(base_url.slice(..path_start)); self.serialization.push('/'); let remaining = self.parse_path(
scheme_type,
&muttrue,
path_start as usize,
input_after_first_char,
); self.with_query_and_fragment(
scheme_type,
base_url.scheme_end,
base_url.username_end,
base_url.host_start,
base_url.host_end,
base_url.host,
base_url.port,
base_url.path_start,
remaining,
)
}
_ => { let before_query = match (base_url.query_start, base_url.fragment_start) {
(None, None) => &*base_url.serialization,
(Some(i), _) | (None, Some(i)) => base_url.slice(..i),
}; self.serialization.push_str(before_query); // FIXME spec says just "remove last entry", not the "pop" algorithm self.pop_path(scheme_type, base_url.path_start as usize); // A special url always has a path. // A path always starts with '/' ifself.serialization.len() == base_url.path_start as usize
&& (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,
&muttrue,
base_url.path_start as usize,
remaining,
),
_ => { self.parse_path(scheme_type, &muttrue, base_url.path_start asusize, input)
}
}; self.with_query_and_fragment(
scheme_type,
base_url.scheme_end,
base_url.username_end,
base_url.host_start,
base_url.host_end,
base_url.host,
base_url.port,
base_url.path_start,
remaining,
)
}
}
}
fn after_double_slash( mutself,
input: Input<'_>,
scheme_type: SchemeType,
scheme_end: u32,
) -> ParseResult<Url> { self.serialization.push('/'); self.serialization.push('/'); // authority state let before_authority = self.serialization.len(); let (username_end, remaining) = self.parse_userinfo(input, scheme_type)?; let has_authority = before_authority != self.serialization.len(); // host state let host_start = to_u32(self.serialization.len())?; let (host_end, host, port, remaining) = self.parse_host_and_port(remaining, scheme_end, scheme_type)?; if host == HostInternal::None && has_authority { return Err(ParseError::EmptyHost);
} // path state let path_start = to_u32(self.serialization.len())?; let remaining = self.parse_path_start(scheme_type, &muttrue, remaining); self.with_query_and_fragment(
scheme_type,
scheme_end,
username_end,
host_start,
host_end,
host,
port,
path_start,
remaining,
)
}
/// Return (username_end, remaining) fn parse_userinfo<'i>(
&mutself, mut input: Input<'i>,
scheme_type: SchemeType,
) -> ParseResult<(u32, Input<'i>)> { letmut last_at = None; letmut remaining = input.clone(); letmut char_count = 0; whilelet Some(c) = remaining.next() { match c { '@' => { if last_at.is_some() { self.log_violation(SyntaxViolation::UnencodedAtSign)
} else { self.log_violation(SyntaxViolation::EmbeddedCredentials)
}
last_at = Some((char_count, remaining.clone()))
} '/' | '?' | '#' => break, '\\'if scheme_type.is_special() => break,
_ => (),
}
char_count += 1;
} let (mut userinfo_char_count, remaining) = match last_at {
None => return Ok((to_u32(self.serialization.len())?, input)),
Some((0, remaining)) => { // Otherwise, if one of the following is true // c is the EOF code point, U+002F (/), U+003F (?), or U+0023 (#) // url is special and c is U+005C (\) // If @ flag is set and buffer is the empty string, validation error, return failure. iflet (Some(c), _) = remaining.split_first() { if c == '/' || c == '?' || c == '#' || (scheme_type.is_special() && c == '\\') { return Err(ParseError::EmptyHost);
}
} return Ok((to_u32(self.serialization.len())?, remaining));
}
Some(x) => x,
};
letmut username_end = None; letmut has_password = false; letmut has_username = false; while userinfo_char_count > 0 { let (c, utf8_c) = input.next_utf8().unwrap();
userinfo_char_count -= 1; if c == ':' && username_end.is_none() { // Start parsing password
username_end = Some(to_u32(self.serialization.len())?); // We don't add a colon if the password is empty if userinfo_char_count > 0 { self.serialization.push(':');
has_password = true;
}
} else { if !has_password {
has_username = true;
} self.check_url_code_point(c, &input); self.serialization
.extend(utf8_percent_encode(utf8_c, USERINFO));
}
} let username_end = match username_end {
Some(i) => i,
None => to_u32(self.serialization.len())?,
}; if has_username || has_password { self.serialization.push('@');
}
Ok((username_end, remaining))
}
fn parse_host_and_port<'i>(
&mutself,
input: Input<'i>,
scheme_end: u32,
scheme_type: SchemeType,
) -> ParseResult<(u32, HostInternal, Option<u16>, Input<'i>)> { let (host, remaining) = Parser::parse_host(input, scheme_type)?;
write!(&mutself.serialization, "{}", host).unwrap(); let host_end = to_u32(self.serialization.len())?; iflet Host::Domain(h) = &host { if h.is_empty() { // Port with an empty host if remaining.starts_with(":") { return Err(ParseError::EmptyHost);
} if scheme_type.is_special() { return Err(ParseError::EmptyHost);
}
}
};
let (port, remaining) = iflet Some(remaining) = remaining.split_prefix(':') { let scheme = || default_port(&self.serialization[..scheme_end as usize]);
Parser::parse_port(remaining, scheme, self.context)?
} else {
(None, remaining)
}; iflet Some(port) = port {
write!(&mutself.serialization, ":{}", port).unwrap()
}
Ok((host_end, host.into(), port, remaining))
}
pubfn parse_host( mut input: Input<'_>,
scheme_type: SchemeType,
) -> ParseResult<(Host<String>, Input<'_>)> { if scheme_type.is_file() { return Parser::get_file_host(input);
} // Undo the Input abstraction here to avoid allocating in the common case // where the host part of the input does not contain any tab or newline let input_str = input.chars.as_str(); letmut inside_square_brackets = false; letmut has_ignored_chars = false; letmut non_ignored_chars = 0; letmut bytes = 0; for c in input_str.chars() { match c { ':'if !inside_square_brackets => break, '\\'if scheme_type.is_special() => break, '/' | '?' | '#' => break, '\t' | '\n' | '\r' => {
has_ignored_chars = true;
} '[' => {
inside_square_brackets = true;
non_ignored_chars += 1
} ']' => {
inside_square_brackets = false;
non_ignored_chars += 1
}
_ => non_ignored_chars += 1,
}
bytes += c.len_utf8();
} let replaced: String; let host_str;
{ let host_input = input.by_ref().take(non_ignored_chars); if has_ignored_chars {
replaced = host_input.collect();
host_str = &*replaced
} else { for _ in host_input {}
host_str = &input_str[..bytes]
}
} if scheme_type == SchemeType::SpecialNotFile && host_str.is_empty() { return Err(ParseError::EmptyHost);
} if !scheme_type.is_special() { let host = Host::parse_opaque(host_str)?; return Ok((host, input));
} let host = Host::parse(host_str)?;
Ok((host, input))
}
fn get_file_host(input: Input<'_>) -> ParseResult<(Host<String>, Input<'_>)> { let (_, host_str, remaining) = Parser::file_host(input)?; let host = match Host::parse(&host_str)? {
Host::Domain(ref d) if d == "localhost" => Host::Domain("".to_string()),
host => host,
};
Ok((host, remaining))
}
fn parse_file_host<'i>(
&mutself,
input: Input<'i>,
) -> ParseResult<(bool, HostInternal, Input<'i>)> { let has_host; let (_, host_str, remaining) = Parser::file_host(input)?; let host = if host_str.is_empty() {
has_host = false;
HostInternal::None
} else { match Host::parse(&host_str)? {
Host::Domain(ref d) if d == "localhost" => {
has_host = false;
HostInternal::None
}
host => {
write!(&mutself.serialization, "{}", host).unwrap();
has_host = true;
host.into()
}
}
};
Ok((has_host, host, remaining))
}
pubfn file_host(input: Input) -> ParseResult<(bool, String, Input)> { // Undo the Input abstraction here to avoid allocating in the common case // where the host part of the input does not contain any tab or newline let input_str = input.chars.as_str(); letmut has_ignored_chars = false; letmut non_ignored_chars = 0; letmut bytes = 0; for c in input_str.chars() { match c { '/' | '\\' | '?' | '#' => break, '\t' | '\n' | '\r' => has_ignored_chars = true,
_ => non_ignored_chars += 1,
}
bytes += c.len_utf8();
} let replaced: String; let host_str; letmut remaining = input.clone();
{ let host_input = remaining.by_ref().take(non_ignored_chars); if has_ignored_chars {
replaced = host_input.collect();
host_str = &*replaced
} else { for _ in host_input {}
host_str = &input_str[..bytes]
}
} if is_windows_drive_letter(host_str) { return Ok((false, "".to_string(), input));
}
Ok((true, host_str.to_string(), remaining))
}
letmut opt_port = Some(port as u16); if !has_any_digit || opt_port == default_port() {
opt_port = None;
}
Ok((opt_port, input))
}
pubfn parse_path_start<'i>(
&mutself,
scheme_type: SchemeType,
has_host: &mut bool,
input: Input<'i>,
) -> Input<'i> { 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('\\') { returnself.parse_path(scheme_type, has_host, path_start, remaining);
}
} returnself.parse_path(scheme_type, has_host, path_start, input);
} elseif 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;
}
if maybe_c.is_some() && maybe_c != Some('/') { self.serialization.push('/');
} // Otherwise, if c is not the EOF code point: self.parse_path(scheme_type, has_host, path_start, input)
}
// 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" => { self.serialization.truncate(segment_start); if !self.serialization.ends_with('/') { self.serialization.push('/');
}
}
_ => { // If url’s scheme is "file", url’s path is empty, and buffer is a Windows drive letter, then if scheme_type.is_file()
&& segment_start == path_start + 1
&& is_windows_drive_letter(segment_before_slash)
{ // Replace the second code point in buffer with U+003A (:). iflet Some(c) = segment_before_slash.chars().next() { self.serialization.truncate(segment_start); self.serialization.push(c); 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 { self.log_violation(SyntaxViolation::FileWithHostAndWindowsDrive);
*has_host = false; // FIXME account for this in callers
}
}
}
} if !ends_with_slash { break;
}
} if scheme_type.is_file() { // while url’s path’s size is greater than 1 // and url’s path[0] is the empty string, // validation error, remove the first item from url’s path. //FIXME: log violation let path = self.serialization.split_off(path_start); self.serialization.push('/'); self.serialization.push_str(path.trim_start_matches('/'));
}
input
}
fn last_slash_can_be_removed(serialization: &str, path_start: usize) -> bool { let url_before_segment = &serialization[..serialization.len() - 1]; iflet 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(&mutself, scheme_type: SchemeType, path_start: usize) { // If path is empty, then return. ifself.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. if scheme_type.is_file()
&& is_normalized_windows_drive_letter(&self.serialization[path_start..])
{ return;
} // Remove path’s last item. self.pop_path(scheme_type, path_start);
}
/// https://url.spec.whatwg.org/#pop-a-urls-path fn pop_path(&mutself, scheme_type: SchemeType, path_start: usize) { ifself.serialization.len() > path_start { let slash_position = self.serialization[path_start..].rfind('/').unwrap(); // + 1 since rfind returns the position before the slash. let segment_start = path_start + slash_position + 1; // Don’t pop a Windows drive letter if !(scheme_type.is_file()
&& is_normalized_windows_drive_letter(&self.serialization[segment_start..]))
{ self.serialization.truncate(segment_start);
}
}
}
#[allow(clippy::too_many_arguments)] fn with_query_and_fragment( mutself,
scheme_type: SchemeType,
scheme_end: u32,
username_end: u32,
host_start: u32,
host_end: u32,
host: HostInternal,
port: Option<u16>, mut path_start: u32,
remaining: Input<'_>,
) -> ParseResult<Url> { // Special case for anarchist URL's with a leading empty path segment // This prevents web+demo:/.//not-a-host/ or web+demo:/path/..//not-a-host/, // when parsed and then serialized, from ending up as web+demo://not-a-host/ // (they end up as web+demo:/.//not-a-host/). // // If url’s host is null, url does not have an opaque path, // url’s path’s size is greater than 1, and url’s path[0] is the empty string, // then append U+002F (/) followed by U+002E (.) to output. let scheme_end_as_usize = scheme_end as usize; let path_start_as_usize = path_start as usize; if path_start_as_usize == scheme_end_as_usize + 1 { // Anarchist URL ifself.serialization[path_start_as_usize..].starts_with("//") { // Case 1: The base URL did not have an empty path segment, but the resulting one does // Insert the "/." prefix self.serialization.insert_str(path_start_as_usize, "/.");
path_start += 2;
}
assert!(!self.serialization[scheme_end_as_usize..].starts_with("://"));
} elseif path_start_as_usize == scheme_end_as_usize + 3
&& &self.serialization[scheme_end_as_usize..path_start_as_usize] == ":/."
{ // Anarchist URL with leading empty path segment // The base URL has a "/." between the host and the path
assert_eq!(self.serialization.as_bytes()[path_start_as_usize], b'/'); ifself
.serialization
.as_bytes()
.get(path_start_as_usize + 1)
.copied()
!= Some(b'/')
{ // Case 2: The base URL had an empty path segment, but the resulting one does not // Remove the "/." prefix self.serialization
.replace_range(scheme_end_as_usize..path_start_as_usize, ":");
path_start -= 2;
}
assert!(!self.serialization[scheme_end_as_usize..].starts_with("://"));
}
/// Whether the scheme is file:, the path has a single segment, and that segment /// is a Windows drive letter #[inline] pubfn is_windows_drive_letter(segment: &str) -> bool {
segment.len() == 2 && starts_with_windows_drive_letter(segment)
}
/// Whether 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 { iflet Some(c) = s.as_bytes().first() {
matches!(c, b'/' | b'\\' | b'?' | b'#') && starts_with_windows_drive_letter(&s[1..])
} else { false
}
}
/// https://url.spec.whatwg.org/#start-with-a-windows-drive-letter fn starts_with_windows_drive_letter_segment(input: &Input<'_>) -> bool { letmut input = input.clone(); 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,
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.25 Sekunden
(vorverarbeitet am 2026-06-17)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.