#[derive(Debug, Clone)]
pub struct Url {
pub scheme: Option<String>,
pub authority: Authority,
pub path: Option<Path>,
pub query: Option<Query>,
}
impl core::fmt::Display for Url {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match &self.scheme {
Some(s) => write!(f, "{}://", s)?,
None => (),
}
write!(f, "{}", self.authority)?;
match &self.path {
Some(p) => write!(f, "{}", p)?,
None => (),
}
match &self.query {
Some(q) => write!(f, "{}", q)?,
None => (),
}
Ok(())
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum UrlParseError {
AuthorityParseError(AuthorityParseError),
EmptyURL,
}
impl core::fmt::Display for UrlParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
UrlParseError::AuthorityParseError(e) => write!(f, "URL parse error: {}", e),
UrlParseError::EmptyURL => write!(f, "URL Parse error: Empty URL"),
}
}
}
impl std::error::Error for UrlParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
UrlParseError::AuthorityParseError(e) => Some(e),
UrlParseError::EmptyURL => None,
}
}
}
impl core::convert::TryFrom<&str> for Url {
type Error = UrlParseError;
fn try_from(raw_url: &str) -> Result<Self, UrlParseError> {
if raw_url.trim().is_empty() {
return Err(UrlParseError::EmptyURL);
}
let raw_url = raw_url.trim();
let mut scheme_ret: Option<String> = None;
let remainder = match raw_url.split_once("://") {
Some((s, u)) => {
scheme_ret = Some(s.to_string());
u
}
None => raw_url,
};
let (auth, path) = match remainder.split_once('/') {
Some((a, p)) => (a, Some(p)),
None => (remainder, None),
};
let authority = match Authority::try_from(auth) {
Ok(a) => a,
Err(e) => return Err(UrlParseError::AuthorityParseError(e)),
};
if path.is_none() {
return Ok(Self {
scheme: scheme_ret,
authority,
path: None,
query: None,
});
}
let path = path.unwrap();
let (parsed_path, query_str) = match path.split_once('?') {
Some((p, q)) => (Path::from(("/".to_string() + &p).as_ref()), Some(q)),
None => (Path::from(("/".to_string() + &path).as_ref()), None),
};
let query = match query_str {
None => None,
Some(q) => Some(Query::from(q)),
};
Ok(Self {
scheme: scheme_ret,
authority,
path: Some(parsed_path),
query,
})
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Authority {
pub host: String,
pub port: Option<u16>,
}
impl core::fmt::Display for Authority {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(
f,
"{}{}",
self.host,
match self.port {
Some(p) => String::from(":") + &p.to_string(),
None => String::from(""),
}
)
}
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum AuthorityParseError {
InvalidPort,
MissingHost,
}
impl core::fmt::Display for AuthorityParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
AuthorityParseError::InvalidPort => write!(f, "Error parsing authority: Invalid port"),
AuthorityParseError::MissingHost => write!(f, "Error parsing authority: Missing host"),
}
}
}
impl std::error::Error for AuthorityParseError {}
impl core::convert::TryFrom<&str> for Authority {
type Error = AuthorityParseError;
fn try_from(s: &str) -> Result<Self, AuthorityParseError> {
let host;
let mut port = None;
if let Some(new_host) = s.split_once(':') {
host = String::from(new_host.0);
if !new_host.1.is_empty() {
port = Some({
match new_host.1.parse::<u16>() {
Err(_) => return Err(AuthorityParseError::InvalidPort),
Ok(n) => n,
}
})
}
} else {
host = s.to_string();
}
if host.is_empty() {
return Err(AuthorityParseError::MissingHost);
}
Ok(Self { host, port })
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Path {
pub raw_path: String,
}
impl core::fmt::Display for Path {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.raw_path)
}
}
impl From<&str> for Path {
fn from(s: &str) -> Self {
Self {
raw_path: s.to_owned(),
}
}
}
impl Path {
pub fn parent(&self) -> Option<Self> {
if self.raw_path == "/" {
return None;
}
if self.raw_path == "" {
return None;
}
let raw_path = {
if self.raw_path.ends_with('/') {
self.raw_path[..self.raw_path.len() - 1].to_owned()
} else {
self.raw_path.clone()
}
};
match raw_path.rsplit_once('/') {
None => None,
Some((parent,_)) => Some({
let mut raw_path = parent.to_owned();
raw_path.push('/');
Self { raw_path }
}),
}
}
pub fn is_absolute(&self) -> bool {
self.raw_path.starts_with('/')
}
pub fn is_relative(&self) -> bool {
!self.is_absolute()
}
pub fn file_name(&self) -> Option<&str> {
if self.raw_path.trim().is_empty() {
return None;
}
if self.raw_path.trim().ends_with('/') {
return None;
}
return Some(self.raw_path.trim().rsplit_once('/').unwrap().1);
}
pub fn merge_path(&self, other_path: &Self) -> Self {
if other_path.raw_path.trim().is_empty() {
return self.clone();
}
if other_path.is_absolute() {
Self {
raw_path: other_path.to_string(),
}
} else {
if self.raw_path.trim().is_empty() {
let mut new_path = String::from("/");
new_path.push_str(&other_path.raw_path);
return Self { raw_path: new_path };
}
if self.raw_path.ends_with('/') {
let mut new_path = self.clone();
new_path.raw_path.push_str(&other_path.raw_path);
return new_path;
}
let path = self.raw_path.trim().rsplit_once('/').unwrap().0;
let mut new_raw_path = String::from(path);
new_raw_path.push('/');
new_raw_path.push_str(&other_path.raw_path);
Self {
raw_path: new_raw_path,
}
}
}
pub fn dedotify(&mut self) {
let mut input = self.raw_path.clone();
let input_ends_with_slash = input.ends_with('/');
let mut output = String::new();
while !input.is_empty() && input != "/" {
if input.starts_with("../") || input.starts_with("./") {
input = input
.trim_start_matches("../")
.trim_start_matches("./")
.to_string();
}
if input.starts_with("/./") {
input = input.replacen("/./", "/", 1);
}
if input.starts_with("/.") && !input.starts_with("/..") {
input = input.replacen("/.", "/", 1);
}
if input.starts_with("/../") {
input = input.replacen("/../", "/", 1);
let output_split = output.rsplit_once('/').unwrap_or(("", ""));
let output_split = if output_split.1 == "" {
output_split.0.rsplit_once('/').unwrap_or(("", ""))
} else {
output_split
};
output = String::from(output_split.0);
}
if input.starts_with("/..") {
input = input.replacen("/..", "/", 1);
let output_split = output.rsplit_once('/').unwrap_or(("", ""));
let output_split = if output_split.1 == "" {
output_split.0.rsplit_once('/').unwrap_or(("", ""))
} else {
output_split
};
output = String::from(output_split.0);
}
if input == "." || input == ".." {
input = String::new();
}
if input.starts_with('/') {
input = (&input[1..]).to_string();
output.push('/');
}
let input_clone = input.clone();
let (input_left, input_right) = input.split_once('/').unwrap_or((&input_clone, ""));
output.push_str(input_left);
let mut new_input = String::from('/');
new_input.push_str(input_right);
input = new_input;
println!("out: {}, in: {}", output, input);
}
if input_ends_with_slash && !output.ends_with('/') {
output.push('/');
}
self.raw_path = output;
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Query {
pub fragments: Vec<String>,
}
impl core::fmt::Display for Query {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "?{}", self.fragments[0])?;
for p in self.fragments[1..].iter() {
write!(f, "#{}", p)?;
}
Ok(())
}
}
impl From<&str> for Query {
fn from(s: &str) -> Self {
Self::parse_str(s)
}
}
impl Query {
pub fn parse_str(raw_query: &str) -> Self {
Self {
fragments: raw_query.split('#').map(|s| String::from(s)).collect(),
}
}
}
pub fn is_reserved_char(c: char) -> bool {
if c.is_alphanumeric() {
return false;
}
match c {
'-' | '.' | '_' | '~' => false,
_ => true,
}
}
pub fn percent_encode_reserved_characters(data: &str) -> String {
let mut ret = String::new();
for c in data.chars() {
if is_reserved_char(c) {
ret.push_str(&percent_encode(c));
} else {
ret.push(c);
}
}
ret
}
pub fn percent_encode(c: char) -> String {
let mut ret = String::new();
let mut buf = [0; 4];
c.encode_utf8(&mut buf);
for i in 0..c.len_utf8() {
ret.push_str(&format!("%{:02x}", buf[i]));
}
ret
}
#[cfg(test)]
mod test {
mod bare_fns {
use crate::url::*;
#[test]
fn test_percent_encode() {
assert_eq!(percent_encode(' '), "%20");
assert_eq!(percent_encode('か'), "%e3%81%8b");
}
#[test]
fn test_percent_encode_reserved_characters() {
assert_eq!(
percent_encode_reserved_characters("this is a test"),
"this%20is%20a%20test"
);
}
}
mod authority {
use std::convert::TryFrom;
use crate::url::*;
#[test]
fn authority_parse_str_simple() {
assert_eq!(
Authority::try_from("example.com"),
Ok(Authority {
host: "example.com".to_string(),
port: None,
})
);
}
#[test]
fn authority_parse_str_with_port() {
assert_eq!(
Authority::try_from("example.com:1234"),
Ok(Authority {
host: "example.com".to_string(),
port: Some(1234),
})
);
}
#[test]
fn authority_parse_str_invalid_port() {
assert_eq!(
Authority::try_from("example.com:fjdklg"),
Err(AuthorityParseError::InvalidPort)
);
}
#[test]
fn authority_parse_str_missing_host() {
assert_eq!(
Authority::try_from(""),
Err(AuthorityParseError::MissingHost)
);
assert_eq!(
Authority::try_from(":1323"),
Err(AuthorityParseError::MissingHost)
);
}
}
mod query {
use crate::url::*;
#[test]
fn query_test() {
let q = Query::from("this=test#is_this");
assert_eq!(q.fragments, vec!["this=test", "is_this"]);
}
}
mod path {
use crate::url::*;
#[test]
fn path_parent() {
let path = Path::from("/just/a/test/path.txt");
let parent = path.parent().unwrap();
assert_eq!(parent.raw_path, "/just/a/test/");
}
#[test]
fn path_ancestors() {
}
#[test]
fn path_merge() {
let path_ending_file = Path::from("/a/test/path");
let path_ending_dir = Path::from("/a/test/path/");
let empty_path = Path::from("");
let root_path = Path::from("/");
let new_relative_path = Path::from("with/the/new/part");
let new_absolute_path = Path::from("/this/is/the/new/part/");
assert_eq!(
path_ending_file.merge_path(&new_relative_path).raw_path,
"/a/test/with/the/new/part"
);
assert_eq!(
path_ending_dir.merge_path(&new_relative_path).raw_path,
"/a/test/path/with/the/new/part"
);
assert_eq!(
path_ending_file.merge_path(&new_absolute_path).raw_path,
"/this/is/the/new/part/"
);
assert_eq!(
path_ending_dir.merge_path(&new_absolute_path).raw_path,
"/this/is/the/new/part/"
);
assert_eq!(
path_ending_file.merge_path(&empty_path).raw_path,
"/a/test/path"
);
assert_eq!(
path_ending_dir.merge_path(&empty_path).raw_path,
"/a/test/path/"
);
assert_eq!(path_ending_file.merge_path(&root_path).raw_path, "/");
assert_eq!(path_ending_dir.merge_path(&root_path).raw_path, "/");
assert_eq!(
empty_path.merge_path(&new_relative_path).raw_path,
"/with/the/new/part"
);
assert_eq!(
empty_path.merge_path(&new_absolute_path).raw_path,
"/this/is/the/new/part/"
);
assert_eq!(root_path.merge_path(&empty_path).raw_path, "/");
assert_eq!(
root_path.merge_path(&new_relative_path).raw_path,
"/with/the/new/part"
);
assert_eq!(
root_path.merge_path(&new_absolute_path).raw_path,
"/this/is/the/new/part/"
);
}
#[test]
fn dedotify() {
std::thread::sleep(std::time::Duration::from_secs(1));
let mut p = Path::from("help/");
p.dedotify();
assert_eq!(p.raw_path, "help/");
}
}
}