summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/error.rs17
-rw-r--r--src/header.rs174
-rw-r--r--src/lib.rs9
-rw-r--r--src/method.rs65
-rw-r--r--src/parse.rs110
-rw-r--r--src/request.rs85
-rw-r--r--src/response.rs111
-rw-r--r--src/status.rs83
-rw-r--r--src/uri.rs345
-rw-r--r--src/version.rs34
10 files changed, 1033 insertions, 0 deletions
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<String>) -> Result<Self, HTTPError> {
+ 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<H: Hasher>(&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<String>) -> 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<HeaderName>, value: impl Into<HeaderValue>) -> Self {
+ Self { name: name.into(), value: value.into() }
+ }
+}
+
+impl TryParse for Header {
+ fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
+ 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<H,V> From<(H, V)> for Header
+where
+ H: Into<HeaderName>,
+ V: Into<HeaderValue>
+{
+ fn from(value: (H, V)) -> Self {
+ Self { name: value.0.into(), value: value.1.into() }
+ }
+}
+
+pub struct HeaderMap {
+ inner: MultiMap<HeaderName, Header>
+}
+
+impl HeaderMap {
+ pub fn new() -> Self {
+ Self::with_headers(Vec::new())
+ }
+
+ pub fn with_headers(headers: Vec<Header>) -> 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<Header>) {
+ let header = header.into();
+ self.inner.insert(header.name.clone(), Header::new(header.name, header.value))
+ }
+
+ pub fn remove(&mut self, name: &HeaderName) -> Option<Vec<Header>> {
+ self.inner.remove(name)
+ }
+
+ pub fn get(&mut self, name: &HeaderName) -> Option<&Vec<Header>> {
+ 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<Header>>
+}
+
+impl<'i> Iterator for HeaderMapIter<'i> {
+ type Item = &'i Vec<Header>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ 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<String>) -> 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<String>) -> Self;
+}
+
+pub trait TryParse
+where
+ Self: Sized
+{
+ fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError>;
+}
+
+macro_rules! try_into {
+ ($type:ty, $struct:ty) => {
+ impl TryFrom<$type> for $struct {
+ type Error = HTTPError;
+
+ fn try_from(value: $type) -> Result<Self, Self::Error> {
+ 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<T: FromStr + ToString> TryFrom<$type> for $struct {
+ type Error = HTTPError;
+
+ fn try_from(value: $type) -> Result<Self, Self::Error> {
+ Self::try_parse(value)
+ }
+ }
+ };
+}
+
+macro_rules! try_into_struct_res {
+ ($struct:ty) => {
+ impl<T: FromStr + ToString> 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<T>);
+try_into_struct_res!(Response<T>);
+
+macro_rules! into_struct {
+ ($struct:ty) => {
+ impl<T> From<T> for $struct
+ where
+ T: Into<String>
+ {
+ 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<T: ToString + FromStr> {
+ pub method: Method,
+ pub uri: URI,
+ pub version: Version,
+ pub headers: HeaderMap,
+ pub body: Option<T>
+}
+
+impl<T: ToString + FromStr> TryParse for Request<T> {
+ fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
+ 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<T: ToString + FromStr> ToString for Request<T> {
+ 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<T: ToString + FromStr> {
+ version: Version,
+ status: Status,
+ headers: HeaderMap,
+ body: Option<T>
+}
+
+impl<T: ToString + FromStr> Response<T> {
+ 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<Version>) -> Self {
+ self.version = version.into();
+ self
+ }
+
+ pub fn set_status(mut self, status: impl Into<Status>) -> Self {
+ self.status = status.into();
+ self
+ }
+
+ pub fn add_header(mut self, header: impl Into<Header>) -> Self {
+ self.headers.insert(header.into());
+ self
+ }
+
+ pub fn set_body(mut self, body: T) -> Self {
+ self.body = Some(body);
+ self
+ }
+}
+
+impl<T: ToString + FromStr> TryParse for Response<T> {
+ fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
+ 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<T: ToString + FromStr> ToString for Response<T> {
+ 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<String>) -> 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<String>) -> Result<Self, HTTPError> {
+ 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::<u16>() 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<T> From<($type, T)> for Status
+ where
+ T: Into<String>
+ {
+ 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<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)
+}
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<String>) -> 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()
+ }
+}