diff options
author | Tyler Murphy <tylerm@tylerm.dev> | 2023-07-05 21:34:20 -0400 |
---|---|---|
committer | Tyler Murphy <tylerm@tylerm.dev> | 2023-07-05 21:34:20 -0400 |
commit | 168b8937eb0fe88311fe474ab9569691a19d087f (patch) | |
tree | 3e70d7de89adc9e62987cea9341ba2cc10d6cf25 /src/uri.rs | |
download | http-168b8937eb0fe88311fe474ab9569691a19d087f.tar.gz http-168b8937eb0fe88311fe474ab9569691a19d087f.tar.bz2 http-168b8937eb0fe88311fe474ab9569691a19d087f.zip |
changes
Diffstat (limited to 'src/uri.rs')
-rw-r--r-- | src/uri.rs | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/src/uri.rs b/src/uri.rs new file mode 100644 index 0000000..f2fa022 --- /dev/null +++ b/src/uri.rs @@ -0,0 +1,345 @@ +use std::collections::HashMap; + +use crate::{error::HTTPError, parse::{TryParse, Parse}}; + +#[derive(Debug, Clone)] +pub struct Path { + inner: String +} + +impl Path { + pub fn as_str(&self) -> &str { + self.inner.as_ref() + } +} + +impl TryParse for Path { + fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> { + let s: String = s.into(); + let Some(first) = s.chars().nth(0) else { + return Err(HTTPError::InvalidPath(s)) + }; + if first != '/' { + return Err(HTTPError::InvalidPath(s)) + } else { + return Ok(Self { inner: s }) + } + } +} + +impl ToString for Path { + fn to_string(&self) -> String { + self.as_str().to_string() + } +} + +#[derive(Debug, Clone)] +pub enum Scheme { + Http, + Https, + Unkown(String) +} + +impl Scheme { + pub fn as_str(&self) -> &str { + match self { + Scheme::Http => "http", + Scheme::Https => "https", + Scheme::Unkown(ref s) => s, + } + } +} + +impl Parse for Scheme { + fn parse(s: impl Into<String>) -> Self { + let s = s.into(); + match s.as_str() { + "http" => Self::Http, + "https" => Self::Https, + _ => Self::Unkown(s) + } + } +} + +impl ToString for Scheme { + fn to_string(&self) -> String { + self.as_str().to_string() + } +} + +#[derive(Debug, Clone)] +pub struct Authority { + pub sub_domain: Option<String>, + pub domain: String, + pub tld: Option<String>, + pub port: Option<u16> +} + +impl TryParse for Authority { + fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> { + let s = s.into(); + let domain_end = s.find(":").unwrap_or(s.len()); + let authority = &s[..domain_end]; + let tld_start = authority.rfind(".").unwrap_or(authority.len()); + let full_domain = &authority[..tld_start]; + let domain_start = match full_domain.find(".") { + Some(u) => u + 1, + None => 0 + }; + + let domain = &full_domain[domain_start..]; + + let sub_domain; + if domain_start > 0 { + sub_domain = Some(full_domain[..(domain_start-1)].to_string()); + } else { + sub_domain = None; + } + + let tld; + if tld_start < authority.len() { + tld = Some(authority[(tld_start+1)..domain_end].to_string()); + } else { + tld = None; + } + + let port; + if domain_end < s.len() { + let port_str = &s[(domain_end+1)..]; + let num = match port_str.parse::<u16>() { + Ok(n) => n, + Err(_) => return Err(HTTPError::InvalidPort(port_str.to_string())) + }; + port = Some(num); + } else { + port = None; + } + + Ok(Self { sub_domain, domain: domain.to_string(), tld, port } ) + } +} + +impl ToString for Authority { + fn to_string(&self) -> String { + let mut s = String::new(); + if let Some(sub_domain) = &self.sub_domain { + s.push_str(sub_domain); + s.push('.'); + } + s.push_str(&self.domain); + if let Some(tld) = &self.tld { + s.push('.'); + s.push_str(tld); + }; + if let Some(port) = self.port { + s.push(':'); + s.push_str(&format!("{port}")); + } + s + } +} + +#[derive(Debug, Clone)] +pub struct QueryFragment { + pub name: String, + pub value: String +} + +impl QueryFragment { + pub fn new(name: impl Into<String>, value: impl Into<String>) -> Result<Self, HTTPError> { + let name = name.into(); + if name.len() < 1 { + return Err(HTTPError::InvalidQueryFragmentName(name)) + } + Ok(Self { name, value: value.into() }) + } +} + +impl TryParse for QueryFragment { + fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> { + let s = s.into(); + let Some(index) = s.find('=') else { + return Err(HTTPError::InvalidQueryFragment(s.to_string())) + }; + if index == 0 { + return Err(HTTPError::InvalidQueryFragment(s.to_string())) + } + Ok(Self { + name: decode_str(&s[0..index])?, + value: decode_str(&s[(index+1)..])? + }) + } +} + +impl ToString for QueryFragment { + fn to_string(&self) -> String { + let mut s = String::new(); + s.push_str(&encode_str(&self.name)); + s.push('='); + s.push_str(&encode_str(&self.value)); + s + } +} + +#[derive(Debug, Clone)] +pub struct QueryMap { + inner: HashMap<String, QueryFragment> +} + +impl QueryMap { + fn new() -> Self { + Self::with_fragments(Vec::new()) + } + + pub fn with_fragments(fragments: Vec<QueryFragment>) -> Self { + let mut inner = HashMap::with_capacity(fragments.len()); + for fragment in fragments { + inner.insert(fragment.name.clone(), fragment); + } + + Self { inner } + } + + pub fn insert(&mut self, fragment: &QueryFragment) { + self.inner.insert(fragment.name.clone(), fragment.clone()); + } + + pub fn remove(&mut self, name: &str) -> Option<QueryFragment> { + self.inner.remove(name) + } + + pub fn get(&self, name: &str) -> Option<&QueryFragment> { + self.inner.get(name) + } +} + +impl TryParse for QueryMap { + fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> { + let s = s.into(); + let parts: Vec<QueryFragment> = s.split('&').map(|f| QueryFragment::try_parse(f)).flatten().collect(); + Ok(Self::with_fragments(parts)) + } +} + +impl ToString for QueryMap { + fn to_string(&self) -> String { + let mut s = String::new(); + for (i, fragment) in self.inner.values().enumerate() { + if i == 0 { + s.push('?'); + } else { + s.push('&'); + } + s.push_str(&fragment.to_string()); + } + s + } +} + +#[derive(Debug, Clone)] +pub struct URI { + pub scheme: Option<Scheme>, + pub authority: Option<Authority>, + pub path: Path, + pub query: QueryMap +} + +impl TryParse for URI { + fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> { + + let s = s.into(); + let mut n = s.as_str(); + + let scheme: Option<Scheme>; + if let Some(scheme_end) = n.find("://") { + scheme = Some(Scheme::parse(&n[..scheme_end])); + n = &n[(scheme_end + 3)..]; + } else { + scheme = None; + } + + let authority_end = match n.find("/") { + Some(i) => i, + None => return Err(HTTPError::InvalidURI(s.to_string())) + }; + + let authority: Option<Authority>; + if authority_end == 0 { + if scheme.is_some() { + return Err(HTTPError::InvalidURI(s.to_string())) + } + authority = None; + } else { + authority = Some(Authority::try_parse(&n[..authority_end])?); + n = &n[authority_end..]; + } + + let path_end = n.find("?").unwrap_or(n.len()); + let path = Path::try_parse(&n[..path_end])?; + + let query: QueryMap; + if path_end + 1 < n.len() { + query = QueryMap::try_parse(&n[(path_end+1)..])?; + } else { + query = QueryMap::new(); + } + + Ok(Self { scheme, authority, path, query }) + } + +} + +impl ToString for URI { + fn to_string(&self) -> String { + let mut s = String::new(); + if let Some(scheme) = &self.scheme { + s.push_str(scheme.as_str()); + } + if let Some(authority) = &self.authority { + s.push_str(&authority.to_string()); + } + s.push_str(self.path.as_str()); + s.push_str(&self.query.to_string()); + s + } +} + +fn encoded(c: char) -> bool { + match c { + '/' | '&' | '%' | '?' | '=' => true, + _ => false + } +} + +pub fn encode_str(s: &str) -> String { + let mut e = String::new(); + for c in s.chars() { + if encoded(c) { + e.push('%'); + e.push_str(&format!("{:02X}", c as u8)); + } else { + e.push(c); + } + } + e +} + +pub fn decode_str(s: &str) -> Result<String, HTTPError> { + let mut e = String::new(); + let mut iter = s.chars(); + loop { + let Some(char) = iter.next() else { break }; + if char == '%' { + let err = HTTPError::InvalidEncodedString(s.to_string()); + let f = iter.next().ok_or(err.clone())?; + let s = iter.next().ok_or(err.clone())?; + let Some(fd) = f.to_digit(16) else { return Err(err) }; + let Some(sd) = s.to_digit(16) else { return Err(err) }; + let b = (fd * 16 + sd) as u8; + e.push(b as char); + } else { + e.push(char); + } + } + Ok(e) +} |