changes
This commit is contained in:
commit
168b8937eb
12 changed files with 1044 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/Cargo.lock
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
@ -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"
|
17
src/error.rs
Normal file
17
src/error.rs
Normal file
|
@ -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
|
||||
}
|
174
src/header.rs
Normal file
174
src/header.rs
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
9
src/lib.rs
Normal file
9
src/lib.rs
Normal file
|
@ -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;
|
65
src/method.rs
Normal file
65
src/method.rs
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
110
src/parse.rs
Normal file
110
src/parse.rs
Normal file
|
@ -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);
|
85
src/request.rs
Normal file
85
src/request.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
111
src/response.rs
Normal file
111
src/response.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
|
83
src/status.rs
Normal file
83
src/status.rs
Normal file
|
@ -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");
|
345
src/uri.rs
Normal file
345
src/uri.rs
Normal file
|
@ -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)
|
||||
}
|
34
src/version.rs
Normal file
34
src/version.rs
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue