From 168b8937eb0fe88311fe474ab9569691a19d087f Mon Sep 17 00:00:00 2001 From: Tyler Murphy Date: Wed, 5 Jul 2023 21:34:20 -0400 Subject: [PATCH] changes --- .gitignore | 2 + Cargo.toml | 9 ++ src/error.rs | 17 +++ src/header.rs | 174 ++++++++++++++++++++++++ src/lib.rs | 9 ++ src/method.rs | 65 +++++++++ src/parse.rs | 110 +++++++++++++++ src/request.rs | 85 ++++++++++++ src/response.rs | 111 ++++++++++++++++ src/status.rs | 83 ++++++++++++ src/uri.rs | 345 ++++++++++++++++++++++++++++++++++++++++++++++++ src/version.rs | 34 +++++ 12 files changed, 1044 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/error.rs create mode 100644 src/header.rs create mode 100644 src/lib.rs create mode 100644 src/method.rs create mode 100644 src/parse.rs create mode 100644 src/request.rs create mode 100644 src/response.rs create mode 100644 src/status.rs create mode 100644 src/uri.rs create mode 100644 src/version.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a4d26c2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "http" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +multimap = "0.9.0" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..73b173b --- /dev/null +++ b/src/error.rs @@ -0,0 +1,17 @@ +#[derive(Debug, Clone)] +pub enum HTTPError { + InvalidHeaderName(String), + InvalidHeader(String), + InvalidQueryFragment(String), + InvalidQueryFragmentName(String), + InvalidEncodedString(String), + InvalidPath(String), + InvalidScheme(String), + InvalidPort(String), + InvalidURI(String), + InvalidMethod(String), + InvalidStatus(String), + MissingHeader, + MissingHeaderBreak, + InvalidBody +} diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000..74786e6 --- /dev/null +++ b/src/header.rs @@ -0,0 +1,174 @@ +use std::{string::ToString, cmp::{PartialEq, Eq}, hash::{Hash, Hasher}}; +use multimap::{MultiMap, IterAll}; +use crate::{error::HTTPError, parse::{TryParse, Parse}}; + +#[derive(Debug, Clone, Eq)] +pub struct HeaderName { + inner: String +} + +impl HeaderName { + pub fn as_str(&self) -> &str { + self.inner.as_ref() + } +} + +impl TryParse for HeaderName { + fn try_parse(s: impl Into) -> Result { + let name = s.into(); + if name.len() < 1 { + return Err(HTTPError::InvalidHeaderName(name)) + } else { + Ok(Self { inner: name }) + } + } + +} + +impl ToString for HeaderName { + fn to_string(&self) -> String { + self.inner.clone() + } +} + +impl PartialEq for HeaderName { + fn eq(&self, other: &Self) -> bool { + self.inner.eq_ignore_ascii_case(&other.inner) + } +} + +impl Hash for HeaderName { + fn hash(&self, state: &mut H) { + let mut hash: i32 = 0; + for char in self.inner.chars() { + let byte = char.to_ascii_lowercase() as u8; + hash = hash ^ ((byte as i32) << 0); + hash = hash ^ ((byte as i32) << 8); + hash = hash ^ ((byte as i32) << 16); + hash = hash ^ ((byte as i32) << 24); + hash = hash % 16777213; + } + state.write_i32(hash); + } +} + +#[derive(Debug, Clone)] +pub struct HeaderValue { + inner: String +} + +impl HeaderValue { + pub fn as_str(&self) -> &str { + self.inner.as_ref() + } +} + +impl Parse for HeaderValue { + fn parse(value: impl Into) -> Self { + Self { inner: value.into() } + } + +} + +impl ToString for HeaderValue { + fn to_string(&self) -> String { + self.inner.clone() + } +} + +pub struct Header { + pub name: HeaderName, + pub value: HeaderValue +} + +impl Header { + pub fn new(name: impl Into, value: impl Into) -> Self { + Self { name: name.into(), value: value.into() } + } +} + +impl TryParse for Header { + fn try_parse(s: impl Into) -> Result { + let s = s.into(); + let Some(mid) = s.find(": ") else { + return Err(HTTPError::InvalidHeader(s.to_string())) + }; + if mid == 0 || mid >= s.len() - 2 { + return Err(HTTPError::InvalidHeader(s.to_string())) + } + let name = HeaderName::try_parse(&s[0..mid])?; + let value = HeaderValue::parse(&s[(mid+2)..]); + Ok(Header::new(name, value)) + } +} + +impl ToString for Header { + fn to_string(&self) -> String { + let mut s = String::new(); + s.push_str(self.name.as_str()); + s.push_str(": "); + s.push_str(self.value.as_str()); + s + } +} + +impl From<(H, V)> for Header +where + H: Into, + V: Into +{ + fn from(value: (H, V)) -> Self { + Self { name: value.0.into(), value: value.1.into() } + } +} + +pub struct HeaderMap { + inner: MultiMap +} + +impl HeaderMap { + pub fn new() -> Self { + Self::with_headers(Vec::new()) + } + + pub fn with_headers(headers: Vec
) -> Self { + let mut inner = MultiMap::with_capacity(headers.len()); + for header in headers { + inner.insert(header.name.clone(), Header::new(header.name, header.value)); + } + + Self { inner } + } + + pub fn insert(&mut self, header: impl Into
) { + let header = header.into(); + self.inner.insert(header.name.clone(), Header::new(header.name, header.value)) + } + + pub fn remove(&mut self, name: &HeaderName) -> Option> { + self.inner.remove(name) + } + + pub fn get(&mut self, name: &HeaderName) -> Option<&Vec
> { + self.inner.get_vec(name) + } + + pub fn iter(&self) -> HeaderMapIter { + HeaderMapIter { inner: self.inner.iter_all() } + } +} + +pub struct HeaderMapIter<'i> { + inner: IterAll<'i, HeaderName, Vec
> +} + +impl<'i> Iterator for HeaderMapIter<'i> { + type Item = &'i Vec
; + + fn next(&mut self) -> Option { + match self.inner.next() { + Some(h) => Some(h.1), + None => None, + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..12c0b50 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +pub mod error; +pub mod header; +pub mod uri; +pub mod method; +pub mod version; +pub mod status; +pub mod request; +pub mod response; +pub mod parse; diff --git a/src/method.rs b/src/method.rs new file mode 100644 index 0000000..004ce8d --- /dev/null +++ b/src/method.rs @@ -0,0 +1,65 @@ +use crate::parse::Parse; + +pub enum Method { + Get, + Head, + Post, + Put, + Delete, + Connect, + Options, + Trace, + Patch, + Brew, + PropFind, + When, + Unkown(String) +} + +impl Method { + pub fn as_str(&self) -> &str { + match self { + Self::Get => "GET", + Self::Head => "HEAD", + Self::Post => "POST", + Self::Put => "PUT", + Self::Delete => "DELETE", + Self::Connect => "CONNECT", + Self::Options => "OPTIONS", + Self::Trace => "TRACE", + Self::Patch => "PATCH", + Self::Brew => "BREW", + Self::PropFind => "PROPFIND", + Self::When => "WHEN", + Self::Unkown(ref s) => s + } + } +} + +impl Parse for Method { + fn parse(s: impl Into) -> Self { + let str: String = s.into(); + match str.as_str() { + "GET" => Self::Get, + "HEAD" => Self::Head, + "POST" => Self::Post, + "PUT" => Self::Put, + "DELETE" => Self::Delete, + "CONNECT" => Self::Connect, + "OPTIONS" => Self::Options, + "TRACE" => Self::Trace, + "PATCH" => Self::Patch, + "BREW" => Self::Brew, + "PROPFIND" => Self::PropFind, + "WHEN" => Self::When, + _ => Self::Unkown(str) + } + } +} + +impl ToString for Method { + fn to_string(&self) -> String { + self.as_str().to_string() + } +} + diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..60bfc67 --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,110 @@ +use crate::header::{Header, HeaderValue, HeaderName}; +use crate::error::HTTPError; +use crate::method::Method; +use crate::status::Status; +use crate::uri::{QueryMap, QueryFragment, Authority, Path, Scheme, URI}; +use crate::version::Version; +use crate::response::Response; +use crate::request::Request; +use std::convert::{From, TryFrom}; +use std::str::FromStr; +use std::borrow::Cow; + +pub trait Parse +where + Self: Sized +{ + fn parse(s: impl Into) -> Self; +} + +pub trait TryParse +where + Self: Sized +{ + fn try_parse(s: impl Into) -> Result; +} + +macro_rules! try_into { + ($type:ty, $struct:ty) => { + impl TryFrom<$type> for $struct { + type Error = HTTPError; + + fn try_from(value: $type) -> Result { + Self::try_parse(value) + } + } + }; +} + +macro_rules! try_into_struct { + ($struct:ty) => { + impl From<$struct> for String { + fn from(value: $struct) -> Self { + value.to_string() + } + } + + try_into!(String, $struct); + try_into!(&String, $struct); + try_into!(&str, $struct); + try_into!(Cow<'_, str>, $struct); + }; +} + +try_into_struct!(Header); +try_into_struct!(HeaderName); +try_into_struct!(QueryMap); +try_into_struct!(QueryFragment); +try_into_struct!(Authority); +try_into_struct!(Path); +try_into_struct!(URI); +try_into_struct!(Status); + +macro_rules! try_into_res { + ($type:ty, $struct:ty) => { + impl TryFrom<$type> for $struct { + type Error = HTTPError; + + fn try_from(value: $type) -> Result { + Self::try_parse(value) + } + } + }; +} + +macro_rules! try_into_struct_res { + ($struct:ty) => { + impl From<$struct> for String { + fn from(value: $struct) -> Self { + value.to_string() + } + } + + try_into_res!(String, $struct); + try_into_res!(&String, $struct); + try_into_res!(&str, $struct); + try_into_res!(Cow<'_, str>, $struct); + }; +} + +try_into_struct_res!(Request); +try_into_struct_res!(Response); + +macro_rules! into_struct { + ($struct:ty) => { + impl From for $struct + where + T: Into + { + fn from(value: T) -> Self { + Self::parse(value) + } + } + + }; +} + +into_struct!(Scheme); +into_struct!(HeaderValue); +into_struct!(Method); +into_struct!(Version); diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..f337556 --- /dev/null +++ b/src/request.rs @@ -0,0 +1,85 @@ +use std::str::FromStr; +use crate::{version::Version, method::Method, uri::URI, header::{HeaderMap, Header}, error::HTTPError, parse::TryParse}; + +pub struct Request { + pub method: Method, + pub uri: URI, + pub version: Version, + pub headers: HeaderMap, + pub body: Option +} + +impl TryParse for Request { + fn try_parse(s: impl Into) -> Result { + let s = s.into(); + let mut lines = s.lines(); + let Some(head) = lines.next() else { + return Err(HTTPError::MissingHeader) + }; + + let mut head_parts = head.split_whitespace(); + let err = HTTPError::InvalidHeader(s.clone()); + + let Some(method_str) = head_parts.next() else { return Err(err) }; + let method = method_str.into(); + + let Some(uri_str) = head_parts.next() else { return Err(err) }; + let uri = uri_str.try_into()?; + + let Some(version_str) = head_parts.next() else { return Err(err) }; + let version = version_str.into(); + + let mut headers = HeaderMap::new(); + + loop { + let Some(next) = lines.next() else { + return Err(HTTPError::MissingHeaderBreak) + }; + + if next.len() < 1 { break } + + let header = Header::try_parse(next)?; + headers.insert(header); + } + + let rest: String = lines.collect(); + let body; + if rest.len() < 1 { + body = None + } else { + body = match T::from_str(&rest) { + Ok(body) => Some(body), + Err(_) => return Err(HTTPError::InvalidBody) + } + } + + Ok(Self { method, uri, version, headers, body }) + } +} + +impl ToString for Request { + fn to_string(&self) -> String { + let mut s = String::new(); + + s.push_str(&self.method.as_str()); + s.push(' '); + s.push_str(&self.uri.to_string()); + s.push(' '); + s.push_str(self.version.as_str()); + s.push_str("\r\n"); + let iter = self.headers.iter(); + for headers in iter { + for header in headers { + s.push_str(header.name.as_str()); + s.push_str(": "); + s.push_str(header.value.as_str()); + s.push_str("\r\n"); + } + } + s.push_str("\r\n"); + if let Some(body) = &self.body { + s.push_str(&body.to_string()); + } + s + } +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..7f6f326 --- /dev/null +++ b/src/response.rs @@ -0,0 +1,111 @@ +use std::str::FromStr; +use crate::{status::{Status, self}, version::Version, header::{HeaderMap, Header}, parse::TryParse, error::HTTPError}; + +pub struct Response { + version: Version, + status: Status, + headers: HeaderMap, + body: Option +} + +impl Response { + pub fn new() -> Self { + Self { + version: Version::HTTP11, + status: status::SUCCESS, + headers: HeaderMap::new(), + body: None + } + } + + pub fn set_version(mut self, version: impl Into) -> Self { + self.version = version.into(); + self + } + + pub fn set_status(mut self, status: impl Into) -> Self { + self.status = status.into(); + self + } + + pub fn add_header(mut self, header: impl Into
) -> Self { + self.headers.insert(header.into()); + self + } + + pub fn set_body(mut self, body: T) -> Self { + self.body = Some(body); + self + } +} + +impl TryParse for Response { + fn try_parse(s: impl Into) -> Result { + let s = s.into(); + let mut lines = s.lines(); + + let Some(header_str) = lines.next() else { + return Err(HTTPError::MissingHeader) + }; + + let mut header_parts = header_str.split(" "); + let Some(version_str) = header_parts.next() else { + return Err(HTTPError::InvalidHeader(header_str.into())) + }; + let version = version_str.into(); + + let status_str: String = header_parts.collect(); + let status = status_str.try_into()?; + + let mut headers = HeaderMap::new(); + + loop { + let Some(next) = lines.next() else { + return Err(HTTPError::MissingHeaderBreak) + }; + + if next.len() < 1 { break } + + let header = Header::try_parse(next)?; + headers.insert(header); + } + + let rest: String = lines.collect(); + let body; + if rest.len() < 1 { + body = None + } else { + body = match T::from_str(&rest) { + Ok(body) => Some(body), + Err(_) => return Err(HTTPError::InvalidBody) + } + } + + Ok(Self { version, status, headers, body }) + } +} + +impl ToString for Response { + fn to_string(&self) -> String { + let mut s = String::new(); + s.push_str(self.version.as_str()); + s.push(' '); + s.push_str(&self.status.to_string()); + s.push_str("\r\n"); + let iter = self.headers.iter(); + for headers in iter { + for header in headers { + s.push_str(header.name.as_str()); + s.push_str(": "); + s.push_str(header.value.as_str()); + s.push_str("\r\n"); + } + } + s.push_str("\r\n"); + if let Some(body) = &self.body { + s.push_str(&body.to_string()); + } + s + } +} + diff --git a/src/status.rs b/src/status.rs new file mode 100644 index 0000000..14f5ca3 --- /dev/null +++ b/src/status.rs @@ -0,0 +1,83 @@ +use std::borrow::Cow; +use crate::{parse::TryParse, error::HTTPError}; + +pub struct Status(u16, Cow<'static, str>); + +impl Status { + pub fn new(code: u16, msg: impl Into) -> Self { + Self(code, Cow::Owned(msg.into())) + } + + pub fn code(&self) -> u16 { + self.0 + } + + pub fn msg(&self) -> &str { + self.1.as_ref() + } +} + +impl ToString for Status { + fn to_string(&self) -> String { + if self.1.len() < 1 { + format!("{}", self.0) + } else { + format!("{} {}", self.0, self.1) + } + } +} + +impl TryParse for Status { + fn try_parse(s: impl Into) -> Result { + let s = s.into(); + let mut parts = s.split(" "); + let Some(code_str) = parts.next() else { + return Err(HTTPError::InvalidStatus(s)) + }; + let Ok(code) = code_str.parse::() else { + return Err(HTTPError::InvalidStatus(s)) + }; + + let msg: String = parts.collect(); + Ok(Self::new(code, msg)) + } +} + +impl PartialEq for Status { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +macro_rules! status_from { + ($type:ty) => { + impl From<$type> for Status { + fn from(value: $type) -> Self { + Self(value as u16, Cow::Owned(String::new())) + } + } + + impl From<($type, T)> for Status + where + T: Into + { + fn from(value: ($type, T)) -> Self { + Self(value.0 as u16, Cow::Owned(value.1.into())) + } + } + }; +} + +status_from!(u8); +status_from!(u16); +status_from!(u32); +status_from!(u64); +status_from!(u128); + +macro_rules! status { + ($name:ident, $code:literal, $msg:literal) => { + pub const $name: Status = Status($code, Cow::Borrowed($msg)); + }; +} + +status!(SUCCESS, 200, "Ok"); 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) -> Result { + 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) -> 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, + pub domain: String, + pub tld: Option, + pub port: Option +} + +impl TryParse for Authority { + fn try_parse(s: impl Into) -> Result { + 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::() { + 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, value: impl Into) -> Result { + 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) -> Result { + 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 +} + +impl QueryMap { + fn new() -> Self { + Self::with_fragments(Vec::new()) + } + + pub fn with_fragments(fragments: Vec) -> 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 { + 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) -> Result { + let s = s.into(); + let parts: Vec = 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, + pub authority: Option, + pub path: Path, + pub query: QueryMap +} + +impl TryParse for URI { + fn try_parse(s: impl Into) -> Result { + + let s = s.into(); + let mut n = s.as_str(); + + let scheme: Option; + 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; + 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 { + 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) +} diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..6b6d5fb --- /dev/null +++ b/src/version.rs @@ -0,0 +1,34 @@ +use crate::parse::Parse; + +pub enum Version { + HTTP11, + HTTP2, + Unknown(String) +} + +impl Version { + pub fn as_str(&self) -> &str { + match self { + Self::HTTP11 => "HTTP/1.1", + Self::HTTP2 => "HTTP/2", + Self::Unknown(ref s) => s, + } + } +} + +impl Parse for Version { + fn parse(s: impl Into) -> Self { + let s = s.into(); + match s.as_str() { + "HTTP/1.1" => Self::HTTP11, + "HTTP/2" => Self::HTTP2, + _ => Self::Unknown(s) + } + } +} + +impl ToString for Version { + fn to_string(&self) -> String { + self.as_str().to_string() + } +}