use super::UrlError;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Url {
scheme: String,
userinfo: Option<String>,
host: Option<String>,
port: Option<u16>,
path: String,
query: Option<String>,
fragment: Option<String>,
}
#[derive(Clone, Copy)]
struct Pieces<'a> {
authority: Option<&'a str>,
path: &'a str,
query: Option<&'a str>,
fragment: Option<&'a str>,
}
impl Url {
pub fn parse(input: &str) -> Result<Self, UrlError> {
check_characters(input)?;
let (scheme, rest) = split_scheme(input)?;
Self::from_pieces(scheme, split_pieces(rest))
}
pub fn join(&self, reference: &str) -> Result<Self, UrlError> {
check_characters(reference)?;
if has_scheme(reference) {
return Self::parse(reference);
}
let target = split_pieces(reference);
if target.authority.is_some() {
return Self::from_pieces(self.scheme.clone(), target);
}
let (path, query) = if target.path.is_empty() {
(
self.path.clone(),
target
.query
.map(str::to_string)
.or_else(|| self.query.clone()),
)
} else if target.path.starts_with('/') {
(
remove_dot_segments(target.path),
target.query.map(str::to_string),
)
} else {
(
remove_dot_segments(&self.merge(target.path)),
target.query.map(str::to_string),
)
};
Ok(Self {
scheme: self.scheme.clone(),
userinfo: self.userinfo.clone(),
host: self.host.clone(),
port: self.port,
path,
query,
fragment: target.fragment.map(str::to_string),
})
}
#[must_use]
pub fn scheme(&self) -> &str {
&self.scheme
}
#[must_use]
pub fn userinfo(&self) -> Option<&str> {
self.userinfo.as_deref()
}
#[must_use]
pub fn host(&self) -> Option<&str> {
self.host.as_deref()
}
#[must_use]
pub fn port(&self) -> Option<u16> {
self.port
}
#[must_use]
pub fn port_or_default(&self) -> Option<u16> {
self.port.or(match self.scheme.as_str() {
"http" | "ws" => Some(80),
"https" | "wss" => Some(443),
"ftp" => Some(21),
_ => None,
})
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub fn query(&self) -> Option<&str> {
self.query.as_deref()
}
#[must_use]
pub fn fragment(&self) -> Option<&str> {
self.fragment.as_deref()
}
fn from_pieces(scheme: String, pieces: Pieces<'_>) -> Result<Self, UrlError> {
let (userinfo, host, port) = match pieces.authority {
Some(authority) => {
let (userinfo, host, port) = split_authority(authority)?;
(userinfo, Some(host), port)
}
None => (None, None, None),
};
Ok(Self {
scheme,
userinfo,
host,
port,
path: pieces.path.to_string(),
query: pieces.query.map(str::to_string),
fragment: pieces.fragment.map(str::to_string),
})
}
fn merge(&self, relative: &str) -> String {
if self.host.is_some() && self.path.is_empty() {
return format!("/{relative}");
}
match self.path.rfind('/') {
Some(slash) => format!("{}{relative}", &self.path[..=slash]),
None => relative.to_string(),
}
}
}
impl fmt::Display for Url {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut out = format!("{}:", self.scheme);
if let Some(host) = &self.host {
out.push_str("//");
if let Some(userinfo) = &self.userinfo {
out.push_str(userinfo);
out.push('@');
}
out.push_str(host);
if let Some(port) = self.port {
out.push(':');
out.push_str(&port.to_string());
}
}
out.push_str(&self.path);
if let Some(query) = &self.query {
out.push('?');
out.push_str(query);
}
if let Some(fragment) = &self.fragment {
out.push('#');
out.push_str(fragment);
}
f.write_str(&out)
}
}
impl FromStr for Url {
type Err = UrlError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::parse(input)
}
}
fn check_characters(input: &str) -> Result<(), UrlError> {
match input.bytes().position(|b| b <= 0x20 || b == 0x7f) {
Some(index) => Err(UrlError::InvalidCharacter { index }),
None => Ok(()),
}
}
fn has_scheme(reference: &str) -> bool {
reference
.find([':', '/', '?', '#'])
.is_some_and(|i| reference.as_bytes()[i] == b':')
}
fn split_scheme(input: &str) -> Result<(String, &str), UrlError> {
if !has_scheme(input) {
return Err(UrlError::MissingScheme);
}
let (scheme, rest) = input.split_once(':').unwrap_or((input, ""));
let bad = scheme.bytes().enumerate().position(|(i, b)| {
if i == 0 {
!b.is_ascii_alphabetic()
} else {
!(b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b'.'))
}
});
match bad {
Some(index) => Err(UrlError::InvalidScheme { index }),
None if scheme.is_empty() => Err(UrlError::InvalidScheme { index: 0 }),
None => Ok((scheme.to_ascii_lowercase(), rest)),
}
}
fn split_pieces(rest: &str) -> Pieces<'_> {
let (rest, fragment) = match rest.split_once('#') {
Some((before, fragment)) => (before, Some(fragment)),
None => (rest, None),
};
let (rest, query) = match rest.split_once('?') {
Some((before, query)) => (before, Some(query)),
None => (rest, None),
};
let (authority, path) = match rest.strip_prefix("//") {
Some(after) => match after.find('/') {
Some(slash) => (Some(&after[..slash]), &after[slash..]),
None => (Some(after), ""),
},
None => (None, rest),
};
Pieces {
authority,
path,
query,
fragment,
}
}
fn split_authority(authority: &str) -> Result<(Option<String>, String, Option<u16>), UrlError> {
let (userinfo, host_port) = match authority.rsplit_once('@') {
Some((userinfo, host_port)) => (Some(userinfo.to_string()), host_port),
None => (None, authority),
};
let (host, port) = if host_port.starts_with('[') {
let close = host_port.find(']').ok_or(UrlError::InvalidHost)?;
let (host, after) = host_port.split_at(close + 1);
match after {
"" => (host, ""),
_ => (host, after.strip_prefix(':').ok_or(UrlError::InvalidHost)?),
}
} else {
host_port.split_once(':').unwrap_or((host_port, ""))
};
let port = if port.is_empty() {
None
} else {
Some(port.parse::<u16>().map_err(|_| UrlError::InvalidPort)?)
};
Ok((userinfo, host.to_ascii_lowercase(), port))
}
fn remove_dot_segments(path: &str) -> String {
let absolute = path.starts_with('/');
let segments: Vec<&str> = path.split('/').collect();
let last = segments.len() - 1;
let mut out: Vec<&str> = Vec::new();
for (i, segment) in segments.iter().enumerate() {
match *segment {
"." => {
if i == last {
out.push("");
}
}
".." => {
if out.len() > usize::from(absolute) {
out.pop();
}
if i == last {
out.push("");
}
}
other => out.push(other),
}
}
out.join("/")
}
#[cfg(test)]
#[path = "parsed_url.test.rs"]
mod tests;
#[cfg(test)]
#[path = "parsed_url.spec.rs"]
mod spec;