summaryrefslogtreecommitdiff
path: root/src/uri.rs
diff options
context:
space:
mode:
authorTyler Murphy <tylerm@tylerm.dev>2023-07-05 21:34:20 -0400
committerTyler Murphy <tylerm@tylerm.dev>2023-07-05 21:34:20 -0400
commit168b8937eb0fe88311fe474ab9569691a19d087f (patch)
tree3e70d7de89adc9e62987cea9341ba2cc10d6cf25 /src/uri.rs
downloadhttp-168b8937eb0fe88311fe474ab9569691a19d087f.tar.gz
http-168b8937eb0fe88311fe474ab9569691a19d087f.tar.bz2
http-168b8937eb0fe88311fe474ab9569691a19d087f.zip
changes
Diffstat (limited to 'src/uri.rs')
-rw-r--r--src/uri.rs345
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)
+}