use std::fmt::{self, Write};
use crate::Error;
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum Port {
ImplicitHttp,
ImplicitHttps,
Explicit(u32),
}
impl Port {
pub(crate) fn port(self) -> u32 {
match self {
Port::ImplicitHttp => 80,
Port::ImplicitHttps => 443,
Port::Explicit(port) => port,
}
}
}
#[derive(Clone, PartialEq)]
pub(crate) struct HttpUrl {
pub(crate) https: bool,
pub(crate) host: String,
pub(crate) port: Port,
pub(crate) path_and_query: String,
pub(crate) fragment: Option<String>,
}
impl HttpUrl {
pub(crate) fn parse(url: &str, redirected_from: Option<&HttpUrl>) -> Result<HttpUrl, Error> {
enum UrlParseStatus {
Host,
Port,
PathAndQuery,
Fragment,
}
let (url, https) = if let Some(after_protocol) = url.strip_prefix("http://") {
(after_protocol, false)
} else if let Some(after_protocol) = url.strip_prefix("https://") {
(after_protocol, true)
} else {
return Err(Error::InvalidProtocol);
};
let mut host = String::new();
let mut port = String::new();
let mut resource = String::new(); let mut path_and_query = None;
let mut status = UrlParseStatus::Host;
for c in url.chars() {
match status {
UrlParseStatus::Host => {
match c {
'/' | '?' => {
status = UrlParseStatus::PathAndQuery;
resource.push(c);
}
':' => status = UrlParseStatus::Port,
_ => host.push(c),
}
}
UrlParseStatus::Port => match c {
'/' | '?' => {
status = UrlParseStatus::PathAndQuery;
resource.push(c);
}
_ => port.push(c),
},
UrlParseStatus::PathAndQuery if c == '#' => {
status = UrlParseStatus::Fragment;
path_and_query = Some(resource);
resource = String::new();
}
#[cfg(not(feature = "urlencoding"))]
UrlParseStatus::PathAndQuery | UrlParseStatus::Fragment => resource.push(c),
#[cfg(feature = "urlencoding")]
UrlParseStatus::PathAndQuery | UrlParseStatus::Fragment => match c {
'0'..='9'
| 'A'..='Z'
| 'a'..='z'
| '-'
| '.'
| '_'
| '~'
| '&'
| '#'
| '='
| '/'
| '?' => {
resource.push(c);
}
_ => {
let mut utf8_buf = [0u8; 4];
c.encode_utf8(&mut utf8_buf);
utf8_buf[..c.len_utf8()].iter().for_each(|byte| {
let rem = *byte % 16;
let right_char = to_hex_digit(rem);
let left_char = to_hex_digit((*byte - rem) >> 4);
resource.push('%');
resource.push(left_char);
resource.push(right_char);
});
}
},
}
}
let (mut path_and_query, mut fragment) = if let Some(path_and_query) = path_and_query {
(path_and_query, Some(resource))
} else {
(resource, None)
};
if fragment.is_none() {
if let Some(old_fragment) = redirected_from.and_then(|url| url.fragment.clone()) {
fragment = Some(old_fragment);
}
}
if path_and_query.is_empty() {
path_and_query.push('/');
}
let port = port.parse::<u32>().map(Port::Explicit).unwrap_or_else(|_| {
if https {
Port::ImplicitHttps
} else {
Port::ImplicitHttp
}
});
Ok(HttpUrl {
https,
host,
port,
path_and_query,
fragment,
})
}
pub(crate) fn write_base_url_to<W: Write>(&self, dst: &mut W) -> fmt::Result {
write!(
dst,
"http{s}://{host}",
s = if self.https { "s" } else { "" },
host = &self.host,
)?;
if let Port::Explicit(port) = self.port {
write!(dst, ":{}", port)?;
}
Ok(())
}
pub(crate) fn write_resource_to<W: Write>(&self, dst: &mut W) -> fmt::Result {
write!(
dst,
"{path_and_query}{maybe_hash}{maybe_fragment}",
path_and_query = &self.path_and_query,
maybe_hash = if self.fragment.is_some() { "#" } else { "" },
maybe_fragment = self.fragment.as_deref().unwrap_or(""),
)
}
}
#[cfg(feature = "urlencoding")]
fn to_hex_digit(digit: u8) -> char {
match digit {
0..=9 => (b'0' + digit) as char,
10..=255 => (b'A' - 10 + digit) as char,
}
}