Major refactoring
This commit is contained in:
parent
21e8b9c8bb
commit
879003024c
133
Cargo.lock
generated
133
Cargo.lock
generated
|
|
@ -2,139 +2,6 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||
|
||||
[[package]]
|
||||
name = "fetchplz"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"futures-lite",
|
||||
"http",
|
||||
"httplz",
|
||||
"httplzx",
|
||||
"wove",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httplz"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"httplzx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httplzx"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"http",
|
||||
"httplz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c9c844e08c94e8558389fb9b8944cb99fc697e231c975e4274b42bc99e0625b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.161"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
|
||||
|
||||
[[package]]
|
||||
name = "wove"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"futures-lite",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"parking",
|
||||
]
|
||||
|
|
|
|||
10
Cargo.toml
10
Cargo.toml
|
|
@ -7,17 +7,17 @@ description = "A sans-io, no-std HTTP implementation"
|
|||
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/fetchplz",
|
||||
"crates/httplzx"
|
||||
# "crates/fetchplz",
|
||||
# "crates/httplzx"
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
httplz = { path = ".", features = ["std"] }
|
||||
httplzx = { path = "./crates/httplzx" }
|
||||
fetchplz = { path = "./crates/fetchplz" }
|
||||
# httplzx = { path = "./crates/httplzx" }
|
||||
# fetchplz = { path = "./crates/fetchplz" }
|
||||
|
||||
[features]
|
||||
std = []
|
||||
|
||||
[dev-dependencies]
|
||||
httplzx = { path = "./crates/httplzx" }
|
||||
# httplzx = { path = "./crates/httplzx" }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::future::Future;
|
||||
use std::io::Error as IoError;
|
||||
use std::pin::Pin;
|
||||
|
||||
use futures_lite::{Stream, StreamExt};
|
||||
use http::uri::Scheme;
|
||||
|
|
@ -7,6 +8,8 @@ use http::uri::Scheme;
|
|||
use http::{HeaderValue, Response};
|
||||
use wove::io::{AsyncReadLoan, AsyncWriteLoan, Transpose};
|
||||
use wove::io_impl::IoImpl;
|
||||
use wove::net::TcpStream;
|
||||
use wove::streams::StreamExt as _;
|
||||
use wove::util::Ignore;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -77,146 +80,24 @@ where
|
|||
self,
|
||||
io: &I,
|
||||
) -> FetchResult<FetchResponse<impl Stream<Item = Result<Box<[u8]>, IoError>>>> {
|
||||
let uri = self.uri();
|
||||
let host = uri.host().unwrap_or("localhost").to_string();
|
||||
let port = self
|
||||
.uri()
|
||||
.port()
|
||||
.map(|p| Ok(p.as_u16()))
|
||||
.or_else(|| match uri.scheme() {
|
||||
Some(s) if *s == Scheme::HTTP => Some(Ok(80)),
|
||||
Some(s) if *s == Scheme::HTTPS => Some(Ok(443)),
|
||||
_ => Some(Err(Error::InvalidUriScheme)),
|
||||
})
|
||||
.unwrap_or(Err(Error::InvalidUriScheme))?;
|
||||
|
||||
let mut stream = wove::net::TcpStream::connect(io, (host.as_str(), port)).await?;
|
||||
|
||||
let uri = uri.to_string();
|
||||
let (parts, body) = self.into_parts();
|
||||
let mut events: Vec<httplz::Event> = vec![
|
||||
httplz::RequestLine {
|
||||
method: httplz::Method::new_from_str(parts.method.as_str()),
|
||||
target: parts
|
||||
.uri
|
||||
.path_and_query()
|
||||
.map(|p| p.as_str())
|
||||
.unwrap_or("/"),
|
||||
version: httplz::Version::HTTP1_1,
|
||||
}
|
||||
.into(),
|
||||
httplz::Header {
|
||||
name: "Location",
|
||||
value: uri.as_bytes(),
|
||||
}
|
||||
.into(),
|
||||
httplz::Header {
|
||||
name: "Host",
|
||||
value: host.as_bytes(),
|
||||
}
|
||||
.into(),
|
||||
];
|
||||
|
||||
for header in parts.headers.iter() {
|
||||
events.push(
|
||||
httplz::Header {
|
||||
name: header.0.as_str(),
|
||||
value: header.1.as_bytes(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
let mut stream = TcpStream::connect(io, "google.com:80").await?;
|
||||
stream.write("GET / HTTP/1.1\r\n\r\n").await.transpose()?;
|
||||
|
||||
let mut conn = httplz::Connection::new(httplz::Role::Client);
|
||||
conn.start_recving();
|
||||
|
||||
let buf = vec![0; 4096];
|
||||
let mut buf = httplz::Buf::new(buf);
|
||||
for event in events.iter() {
|
||||
loop {
|
||||
match conn.handle_send(event, &mut buf) {
|
||||
Ok(_) => break,
|
||||
Err(httplz::Error {
|
||||
kind: httplz::ErrorKind::BufNotBigEnough,
|
||||
..
|
||||
}) => {
|
||||
stream.write(Vec::from(buf.filled())).await.transpose()?;
|
||||
buf.clear();
|
||||
},
|
||||
Err(e) => {
|
||||
return Err(Error::Httplz(e));
|
||||
},
|
||||
}
|
||||
}
|
||||
let (mut parts, _) = http::Response::new(()).into_parts();
|
||||
|
||||
let mut leftovers = Vec::new();
|
||||
while let Some(data) = stream.read_stream().next2().await {
|
||||
let data = data?;
|
||||
leftovers.extend_from_slice(data.as_slice());
|
||||
}
|
||||
eprintln!("{}", String::from_utf8_lossy(buf.filled()));
|
||||
stream.write(Vec::from(buf.filled())).await.transpose()?;
|
||||
buf.clear();
|
||||
|
||||
match body.size_hint {
|
||||
Some(len) => conn.handle_send(
|
||||
&httplz::Header {
|
||||
name: "Content-Length",
|
||||
value: format!("{len}").as_bytes(),
|
||||
}
|
||||
.into(),
|
||||
&mut buf,
|
||||
)?,
|
||||
None => todo!(),
|
||||
}
|
||||
conn.handle_send(&httplz::Event::HeadersDone, &mut buf)?;
|
||||
stream.write(Vec::from(buf.filled())).await.transpose()?;
|
||||
|
||||
let (mut parts, _) = Response::new(()).into_parts();
|
||||
|
||||
stream
|
||||
.read_stream()
|
||||
.map(|r| r.map_err(Error::IoError))
|
||||
.scan(Vec::new(), |leftovers, r| {
|
||||
r.and_then(|data| {
|
||||
leftovers.extend_from_slice(&data);
|
||||
let mut read_total = 0;
|
||||
for res in conn.poll_recv_iter(leftovers) {
|
||||
let (read, event) = res?;
|
||||
read_total += read;
|
||||
dbg!(event);
|
||||
|
||||
match event {
|
||||
httplz::Event::StatusLine(sl) => {
|
||||
parts.status = http::StatusCode::from_u16(sl.status_code)?;
|
||||
parts.version = match sl.version {
|
||||
httplz::Version::HTTP1_1 => http::Version::HTTP_11,
|
||||
}
|
||||
},
|
||||
httplz::Event::Header(httplz::Header { name, value }) => {
|
||||
parts.headers.insert(
|
||||
name.parse::<http::HeaderName>()?,
|
||||
HeaderValue::from_bytes(value)?,
|
||||
);
|
||||
},
|
||||
httplz::Event::HeadersDone => return Ok(None),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
leftovers.drain(0..read_total);
|
||||
|
||||
FetchResult::Ok(Some(()))
|
||||
})
|
||||
.transpose()
|
||||
})
|
||||
.fuse()
|
||||
.collect::<Ignore>()
|
||||
.await;
|
||||
|
||||
Ok(Response::from_parts(
|
||||
parts,
|
||||
Body {
|
||||
size_hint: None,
|
||||
stream: stream
|
||||
.into_read_stream()
|
||||
.map(|r| r.map(|v| v.into_boxed_slice())),
|
||||
},
|
||||
))
|
||||
todo!()
|
||||
as FetchResult<
|
||||
FetchResponse<Pin<Box<dyn Stream<Item = Result<Box<[u8]>, IoError>>>>>,
|
||||
>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -234,46 +115,5 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test() {
|
||||
let data = "Hello, world";
|
||||
let request = Request::builder()
|
||||
.method(http::method::Method::POST)
|
||||
.uri("http://google.com")
|
||||
.body(Body {
|
||||
size_hint: Some(data.len()),
|
||||
stream: futures_lite::stream::once(
|
||||
data.to_string().into_bytes().into_boxed_slice(),
|
||||
),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let uring = &IoUring::new().unwrap();
|
||||
uring.block_on(async {
|
||||
let response = request.send(uring).await.unwrap();
|
||||
let (parts, body) = response.into_parts();
|
||||
dbg!(parts);
|
||||
|
||||
let body: Vec<u8> = body.stream.try_collect_chunks().await.unwrap();
|
||||
dbg!(String::from_utf8_lossy(&body));
|
||||
let mut test = wove::net::TcpStream::connect(uring, "google.com:80")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
test.write("GET / HTTP/1.1\r\n\r\n".to_string())
|
||||
.await
|
||||
.transpose()
|
||||
.unwrap();
|
||||
|
||||
let body: Vec<u8> = test
|
||||
.read_stream()
|
||||
.inspect(|v| {
|
||||
dbg!(v.as_ref().map(|v| String::from_utf8_lossy(v.as_ref())));
|
||||
})
|
||||
.fuse()
|
||||
.try_collect_chunks()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
dbg!(String::from_utf8_lossy(&body));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
pub fn main() {
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
//! A simple client that POSTs some data to a path
|
||||
//!
|
||||
//! Usage:
|
||||
|
|
@ -27,7 +32,7 @@ pub fn main() -> Result<(), Box<dyn Error>> {
|
|||
}
|
||||
.into(),
|
||||
httplz::HeaderOther::from(("Host", host.as_bytes())).into(),
|
||||
httplz::HeaderOther::from(("Accept", "*/*")).into(),
|
||||
httplz::HeaderOther::from(("Accept", "**")).into(),
|
||||
httplz::HeaderSpecial::ContentLength(data.len()).into(),
|
||||
httplz::Event::HeadersDone,
|
||||
httplz::Event::BodyChunk(data.as_bytes()),
|
||||
|
|
@ -70,3 +75,4 @@ pub fn main() -> Result<(), Box<dyn Error>> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
pub fn main() {
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
//! A simple echo server that echoes the body of a POST request, and returns a
|
||||
//! 405 for any other method
|
||||
//!
|
||||
|
|
@ -88,3 +93,4 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
102
src/http.rs
Normal file
102
src/http.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
use crate::parse::{Line, Parse, ParseError};
|
||||
use core::str::from_utf8;
|
||||
|
||||
pub const LINE_ENDING: &[u8] = b"\r\n";
|
||||
|
||||
macro_rules! versions {
|
||||
($(($name:ident, $str:expr)),* $(,)?) => {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub enum Version<'a> {
|
||||
$($name,)*
|
||||
Unknown(&'a [u8]),
|
||||
}
|
||||
|
||||
impl<'a> Parse<'a> for Version<'a> {
|
||||
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||
$({
|
||||
const STR: &[u8] = $str.as_bytes();
|
||||
let len = ((STR.len() <= data.len()) as usize) * STR.len();
|
||||
|
||||
let data = &data[..len];
|
||||
if data.eq_ignore_ascii_case(STR) {
|
||||
return Ok((STR.len(), Self::$name));
|
||||
}
|
||||
};)*
|
||||
|
||||
Ok((0, Self::Unknown(data)))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
versions! {
|
||||
(V1_0, "http/1.0"),
|
||||
(V1_1, "http/1.1"),
|
||||
}
|
||||
|
||||
macro_rules! methods {
|
||||
($(($name:ident, $str:expr)),* $(,)?) => {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub enum Method<'a> {
|
||||
$($name,)*
|
||||
Other(&'a str),
|
||||
}
|
||||
|
||||
|
||||
impl<'a> Parse<'a> for Method<'a> {
|
||||
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||
$({
|
||||
const STR: &[u8] = $str.as_bytes();
|
||||
let len = ((STR.len() <= data.len()) as usize) * STR.len();
|
||||
|
||||
let data = &data[..len];
|
||||
if data.eq_ignore_ascii_case(STR) {
|
||||
return Ok((STR.len(), Self::$name));
|
||||
}
|
||||
};)*
|
||||
|
||||
from_utf8(data).map(|s| (s.len(), Self::Other(s))).map_err(|_| Some(ParseError {
|
||||
ctx: "method",
|
||||
details: "unknown method was not a valid str",
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
methods! {
|
||||
(Get, "GET"),
|
||||
(Head, "HEAD"),
|
||||
(Options, "OPTIONS"),
|
||||
(Trace, "TRACE"),
|
||||
(Put, "PUT"),
|
||||
(Delete, "DELETE"),
|
||||
(Post, "POST"),
|
||||
(Patch, "PATCH"),
|
||||
(Connect, "CONNECT"),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub struct Status<'a> {
|
||||
pub code: u16,
|
||||
pub text: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub struct RequestLine<'a> {
|
||||
pub method: Method<'a>,
|
||||
pub target: &'a str,
|
||||
pub version: Version<'a>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub struct StatusLine<'a> {
|
||||
pub version: Version<'a>,
|
||||
pub status: Status<'a>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub struct Header<'a> {
|
||||
pub name: &'a str,
|
||||
pub value: &'a [u8],
|
||||
}
|
||||
409
src/lib.rs
409
src/lib.rs
|
|
@ -1,319 +1,176 @@
|
|||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
pub use http::*;
|
||||
use parse::{Parse, ParseError};
|
||||
use util::ResultTupExt;
|
||||
|
||||
pub mod common;
|
||||
pub mod http;
|
||||
pub mod parse;
|
||||
pub mod parts;
|
||||
pub mod util;
|
||||
pub mod write;
|
||||
|
||||
pub use common::*;
|
||||
pub use parse::{NeedsMoreData, Parse};
|
||||
pub use parts::*;
|
||||
pub use util::*;
|
||||
pub use write::{Write, WriteCursor, Written};
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub enum Event<'a> {
|
||||
#[default]
|
||||
Empty,
|
||||
RequestLine(RequestLine<'a>),
|
||||
Header(Header<'a>),
|
||||
HeadersDone,
|
||||
BodyChunk(&'a [u8]),
|
||||
RecvDone,
|
||||
StatusLine(StatusLine<'a>),
|
||||
SendDone,
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub enum Error {
|
||||
ParseError(ParseError),
|
||||
}
|
||||
|
||||
impl<'a> From<Header<'a>> for Event<'a> {
|
||||
fn from(value: Header<'a>) -> Self {
|
||||
Self::Header(value)
|
||||
impl From<ParseError> for Error {
|
||||
fn from(v: ParseError) -> Self {
|
||||
Self::ParseError(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<RequestLine<'a>> for Event<'a> {
|
||||
fn from(value: RequestLine<'a>) -> Self {
|
||||
Self::RequestLine(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<StatusLine<'a>> for Event<'a> {
|
||||
fn from(value: StatusLine<'a>) -> Self {
|
||||
Self::StatusLine(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Event<'_> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Event::Empty | Event::HeadersDone | Event::RecvDone | Event::SendDone => Ok(()),
|
||||
Event::RequestLine(r) => write!(f, "{} {} {}", r.method, r.target, r.version),
|
||||
Event::StatusLine(s) => {
|
||||
write!(f, "{} {} {}", s.version, s.status_code, s.status_text)
|
||||
},
|
||||
Event::Header(Header { name, value }) => write!(
|
||||
f,
|
||||
"{}: {}",
|
||||
name,
|
||||
core::str::from_utf8(value).unwrap_or("<binary>")
|
||||
),
|
||||
Event::BodyChunk(b) => {
|
||||
write!(f, "{}", core::str::from_utf8(b).unwrap_or("<binary>"))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub enum Role {
|
||||
Client,
|
||||
Server,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum BodyState {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub enum BodyState {
|
||||
ContentLength(usize),
|
||||
Chunked,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum StateRecv {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub enum State {
|
||||
StartLine,
|
||||
Headers(Option<BodyState>),
|
||||
Body(BodyState),
|
||||
ValidateDone,
|
||||
Body(Option<BodyState>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum StateSend {
|
||||
StartLine,
|
||||
Headers(Option<BodyState>),
|
||||
Body(BodyState),
|
||||
ValidateDone,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum StateConnection {
|
||||
Recv(StateRecv),
|
||||
Send(StateSend),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub struct Connection {
|
||||
keep_alive: bool,
|
||||
role: Role,
|
||||
state: StateConnection,
|
||||
send: State,
|
||||
recv: State,
|
||||
}
|
||||
impl Connection {
|
||||
pub fn new(role: Role) -> Self {
|
||||
let state = match role {
|
||||
Role::Server => StateConnection::Recv(StateRecv::StartLine),
|
||||
Role::Client => StateConnection::Send(StateSend::StartLine),
|
||||
};
|
||||
|
||||
Self { state, role }
|
||||
pub fn role(&self) -> Role {
|
||||
self.role
|
||||
}
|
||||
|
||||
pub fn is_sending(&self) -> bool {
|
||||
matches!(self.state, StateConnection::Send(_))
|
||||
pub fn send(&self) -> State {
|
||||
self.send
|
||||
}
|
||||
|
||||
pub fn is_recving(&self) -> bool {
|
||||
matches!(self.state, StateConnection::Recv(_))
|
||||
pub fn recv(&self) -> State {
|
||||
self.recv
|
||||
}
|
||||
|
||||
pub fn poll_recv<'a>(&mut self, bytes: &'a [u8]) -> Parse<'a, Event<'a>> {
|
||||
let recv = match &self.state {
|
||||
StateConnection::Recv(r) => r,
|
||||
_ => {
|
||||
return Err(ErrorKind::InvalidConnectionState.into());
|
||||
},
|
||||
};
|
||||
|
||||
let n = match (self.role, recv) {
|
||||
(Role::Server, StateRecv::StartLine) => parse::request_line(bytes).map2(|rl| {
|
||||
(
|
||||
Event::RequestLine(rl),
|
||||
StateConnection::Recv(StateRecv::Headers(None)),
|
||||
)
|
||||
}),
|
||||
|
||||
(Role::Client, StateRecv::StartLine) => parse::status_line(bytes).map2(|rl| {
|
||||
(
|
||||
Event::StatusLine(rl),
|
||||
StateConnection::Recv(StateRecv::Headers(None)),
|
||||
)
|
||||
}),
|
||||
|
||||
(_, StateRecv::Headers(body_state)) => {
|
||||
if bytes.starts_with(b"\r\n") {
|
||||
Ok((
|
||||
2,
|
||||
(
|
||||
Event::HeadersDone,
|
||||
match body_state {
|
||||
None => StateConnection::Recv(StateRecv::ValidateDone),
|
||||
Some(b) => StateConnection::Recv(StateRecv::Body(*b)),
|
||||
},
|
||||
),
|
||||
))
|
||||
} else {
|
||||
parse::header(bytes).map2(|h| {
|
||||
let b = h
|
||||
.special()
|
||||
.map(|h| match h {
|
||||
HeaderSpecial::TransferEncodingChunked => BodyState::Chunked,
|
||||
HeaderSpecial::ContentLength(c) => BodyState::ContentLength(c),
|
||||
})
|
||||
.or(*body_state);
|
||||
|
||||
(
|
||||
Event::Header(h),
|
||||
StateConnection::Recv(StateRecv::Headers(b)),
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
(_, StateRecv::Body(body_state)) => match body_state {
|
||||
BodyState::ContentLength(remaining) => {
|
||||
if bytes.is_empty() && *remaining != 0 {
|
||||
return fail(ErrorKind::NeedMoreData);
|
||||
}
|
||||
if bytes.len() < *remaining {
|
||||
Ok((
|
||||
bytes.len(),
|
||||
(
|
||||
Event::BodyChunk(bytes),
|
||||
StateConnection::Recv(StateRecv::Body(
|
||||
BodyState::ContentLength(remaining - bytes.len()),
|
||||
)),
|
||||
),
|
||||
))
|
||||
} else {
|
||||
Ok((
|
||||
*remaining,
|
||||
(
|
||||
Event::BodyChunk(&bytes[..*remaining]),
|
||||
StateConnection::Recv(StateRecv::ValidateDone),
|
||||
),
|
||||
))
|
||||
}
|
||||
},
|
||||
_ => todo!(),
|
||||
},
|
||||
|
||||
(_, StateRecv::ValidateDone) => {
|
||||
if bytes.is_empty() {
|
||||
Ok((
|
||||
0,
|
||||
(Event::RecvDone, StateConnection::Send(StateSend::StartLine)),
|
||||
))
|
||||
} else {
|
||||
fail(ErrorKind::TrailingBytes)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
n.map2(move |(ev, next_state)| {
|
||||
self.state = next_state;
|
||||
|
||||
ev
|
||||
})
|
||||
}
|
||||
|
||||
pub fn poll_recv_iter<'a>(&'a mut self, bytes: &'a [u8]) -> PollRecvIter<'a> {
|
||||
PollRecvIter(self, bytes)
|
||||
}
|
||||
|
||||
pub fn handle_send(&mut self, event: &Event, mut w: impl Write) -> Written {
|
||||
let state = match self.state {
|
||||
StateConnection::Send(s) => s,
|
||||
_ => return fail(ErrorKind::InvalidConnectionState),
|
||||
};
|
||||
|
||||
let next = match (self.role, state, event) {
|
||||
(Role::Server, StateSend::StartLine, Event::StatusLine(sl)) => {
|
||||
write::status_line(sl, w)
|
||||
.map(|_| StateConnection::Send(StateSend::Headers(None)))
|
||||
},
|
||||
|
||||
(Role::Client, StateSend::StartLine, Event::RequestLine(rl)) => {
|
||||
write::request_line(rl, w)
|
||||
.map(|_| StateConnection::Send(StateSend::Headers(None)))
|
||||
},
|
||||
|
||||
(_, StateSend::Headers(body_state), Event::Header(h)) => {
|
||||
write::header(h, w)?;
|
||||
|
||||
let bs = h
|
||||
.special()
|
||||
.map(|h| match h {
|
||||
HeaderSpecial::TransferEncodingChunked => BodyState::Chunked,
|
||||
HeaderSpecial::ContentLength(cl) => BodyState::ContentLength(cl),
|
||||
})
|
||||
.or(body_state);
|
||||
|
||||
Ok(StateConnection::Send(StateSend::Headers(bs)))
|
||||
},
|
||||
|
||||
(_, StateSend::Headers(body_state), Event::HeadersDone) => {
|
||||
write!(w, "\r\n")?;
|
||||
match body_state {
|
||||
Some(bs) => Ok(StateConnection::Send(StateSend::Body(bs))),
|
||||
None => Ok(StateConnection::Send(StateSend::ValidateDone)),
|
||||
}
|
||||
},
|
||||
|
||||
(_, StateSend::Body(b), Event::BodyChunk(c)) => match b {
|
||||
BodyState::ContentLength(cl) => match () {
|
||||
_ if c.len() < cl => {
|
||||
w.write(c)?;
|
||||
|
||||
Ok(StateConnection::Send(StateSend::Body(
|
||||
BodyState::ContentLength(cl - c.len()),
|
||||
)))
|
||||
},
|
||||
|
||||
_ if c.len() == cl => {
|
||||
w.write(c)?;
|
||||
|
||||
Ok(StateConnection::Send(StateSend::ValidateDone))
|
||||
},
|
||||
|
||||
_ => {
|
||||
return fail(ErrorKind::BodySizeMismatch);
|
||||
},
|
||||
},
|
||||
BodyState::Chunked => todo!(),
|
||||
},
|
||||
|
||||
(_, StateSend::ValidateDone, Event::SendDone) => {
|
||||
Ok(StateConnection::Recv(StateRecv::StartLine))
|
||||
},
|
||||
|
||||
_ => return Err(Error::from(ErrorKind::InvalidEventForConnectionState)),
|
||||
}?;
|
||||
|
||||
self.state = next;
|
||||
|
||||
Ok(())
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
self.keep_alive
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PollRecvIter<'a>(&'a mut Connection, &'a [u8]);
|
||||
impl<'a> Iterator for PollRecvIter<'a> {
|
||||
type Item = Parse<'a, Event<'a>>;
|
||||
impl Connection {
|
||||
pub fn new(role: Role) -> Self {
|
||||
Self {
|
||||
keep_alive: true,
|
||||
role,
|
||||
send: State::StartLine,
|
||||
recv: State::StartLine,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_iter<'a>(&'a mut self, data: &'a [u8]) -> RecvIter<'a> {
|
||||
RecvIter {
|
||||
conn: self,
|
||||
data,
|
||||
read_amt: 0,
|
||||
err: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RecvIter<'a> {
|
||||
conn: &'a mut Connection,
|
||||
data: &'a [u8],
|
||||
read_amt: usize,
|
||||
err: Option<Error>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RecvIter<'a> {
|
||||
type Item = Event<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let res = self.0.poll_recv(self.1);
|
||||
if let Err(Error {
|
||||
kind: ErrorKind::NeedMoreData,
|
||||
..
|
||||
}) = res
|
||||
{
|
||||
if self.err.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(res)
|
||||
let data = &self.data[self.read_amt..];
|
||||
|
||||
let (res, next) = match (self.conn.role, self.conn.recv) {
|
||||
(Role::Client, State::StartLine) => (
|
||||
StatusLine::parse(data).map2(Event::from),
|
||||
State::Headers(None),
|
||||
),
|
||||
|
||||
(Role::Server, State::StartLine) => (
|
||||
RequestLine::parse(data).map2(Event::from),
|
||||
State::Headers(None),
|
||||
),
|
||||
|
||||
(_, State::Headers(body_state)) => {
|
||||
if data.starts_with(LINE_ENDING) {
|
||||
(
|
||||
Ok((LINE_ENDING.len(), Event::HeadersEnd)),
|
||||
State::Body(body_state),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Header::parse(data).map2(Event::from),
|
||||
State::Headers(body_state),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
(_, State::Body(None)) => (Ok((0, Event::MessageEnd)), State::StartLine),
|
||||
(_, State::Body(Some(body_state))) => todo!(),
|
||||
};
|
||||
|
||||
let (amt, event) = match res {
|
||||
Ok(v) => v,
|
||||
Err(Some(e)) => {
|
||||
self.err = Some(e.into());
|
||||
return None;
|
||||
},
|
||||
Err(None) => {
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
self.read_amt += amt;
|
||||
self.conn.recv = next;
|
||||
|
||||
Some(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub enum Event<'a> {
|
||||
RequestLine(RequestLine<'a>),
|
||||
StatusLine(StatusLine<'a>),
|
||||
Header(Header<'a>),
|
||||
HeadersEnd,
|
||||
MessageEnd,
|
||||
}
|
||||
|
||||
impl<'a> From<Header<'a>> for Event<'a> {
|
||||
fn from(v: Header<'a>) -> Self {
|
||||
Self::Header(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<StatusLine<'a>> for Event<'a> {
|
||||
fn from(v: StatusLine<'a>) -> Self {
|
||||
Self::StatusLine(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<RequestLine<'a>> for Event<'a> {
|
||||
fn from(v: RequestLine<'a>) -> Self {
|
||||
Self::RequestLine(v)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
232
src/parse.rs
232
src/parse.rs
|
|
@ -1,142 +1,144 @@
|
|||
use std::str::from_utf8;
|
||||
|
||||
use crate::{
|
||||
fail_details,
|
||||
parts::{RequestLine, Version},
|
||||
Error, ErrorKind, Header, Method, StatusLine,
|
||||
http::{Header, Method, RequestLine, Status, StatusLine, Version},
|
||||
LINE_ENDING,
|
||||
};
|
||||
|
||||
pub type Parse<'a, T> = Result<(usize, T), Error>;
|
||||
pub trait NeedsMoreData {
|
||||
fn needs_more_data(&self) -> bool;
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub struct ParseError {
|
||||
pub ctx: &'static str,
|
||||
pub details: &'static str,
|
||||
}
|
||||
impl<'a, T> NeedsMoreData for Parse<'a, T> {
|
||||
fn needs_more_data(&self) -> bool {
|
||||
self.as_ref()
|
||||
.err()
|
||||
.map(|e| e.kind)
|
||||
.is_some_and(|e| e == ErrorKind::NeedMoreData)
|
||||
|
||||
pub trait Parse<'a>: Sized {
|
||||
/// `None` in the return indicates that the parser is not applicable
|
||||
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>>;
|
||||
}
|
||||
|
||||
pub struct Line<'a>(pub &'a [u8]);
|
||||
impl<'a> Parse<'a> for Line<'a> {
|
||||
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||
let rn_idx = data.windows(2).position(|w| w == LINE_ENDING).ok_or(None)?;
|
||||
|
||||
Ok((rn_idx + 2, Line(&data[..rn_idx])))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split_crlf(d: &[u8]) -> Option<(&[u8], usize)> {
|
||||
let p = d.windows(2).position(|w| w == b"\r\n")?;
|
||||
impl<'a> Parse<'a> for Status<'a> {
|
||||
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||
let mut skipped_chars = 0;
|
||||
let mut parts = data
|
||||
.split(|c| {
|
||||
let b = c.is_ascii_whitespace();
|
||||
skipped_chars += b as usize;
|
||||
b
|
||||
})
|
||||
.filter(|b| !b.is_empty());
|
||||
|
||||
Some((&d[..p], p + 2))
|
||||
let code = parts.next().ok_or(ParseError {
|
||||
ctx: "status",
|
||||
details: "missing status code",
|
||||
})?;
|
||||
let code_len = code.len();
|
||||
let text = parts.next().unwrap_or(b"");
|
||||
let text_len = text.len();
|
||||
|
||||
let code = from_utf8(code).map_err(|_| ParseError {
|
||||
ctx: "status code",
|
||||
details: "invalid utf8",
|
||||
})?;
|
||||
|
||||
let code: u16 = code.parse().map_err(|_| ParseError {
|
||||
ctx: "status code",
|
||||
details: "not a number",
|
||||
})?;
|
||||
|
||||
let text = from_utf8(text).map_err(|_| ParseError {
|
||||
ctx: "status text",
|
||||
details: "invalid utf8",
|
||||
})?;
|
||||
|
||||
Ok((code_len + text_len + skipped_chars, Status { code, text }))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_line(d: &[u8]) -> Parse<RequestLine> {
|
||||
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?;
|
||||
impl<'a> Parse<'a> for RequestLine<'a> {
|
||||
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||
let (line_amt, Line(line)) = Line::parse(data)?;
|
||||
let mut tokens = line
|
||||
.split(|b| b.is_ascii_whitespace())
|
||||
.filter(|bs| !bs.is_empty());
|
||||
|
||||
let mut it = line
|
||||
.split(|b| b.is_ascii_whitespace())
|
||||
.filter(|bs| !bs.is_empty());
|
||||
let method = tokens.next().ok_or(ParseError {
|
||||
ctx: "request line",
|
||||
details: "missing method",
|
||||
})?;
|
||||
|
||||
let (method, target, version) = match (it.next(), it.next(), it.next()) {
|
||||
(Some(m), Some(t), Some(v)) => (m, t, v),
|
||||
_ => {
|
||||
return fail_details(
|
||||
ErrorKind::Parse,
|
||||
"request line doesn't have required number of elements",
|
||||
);
|
||||
},
|
||||
};
|
||||
let target = tokens.next().ok_or(ParseError {
|
||||
ctx: "request line",
|
||||
details: "missing target",
|
||||
})?;
|
||||
|
||||
let method = match core::str::from_utf8(method) {
|
||||
Ok(m) => m,
|
||||
_ => {
|
||||
return fail_details(ErrorKind::Parse, "expected method to be ascii");
|
||||
},
|
||||
};
|
||||
let method = Method::new_from_str(method);
|
||||
let version = tokens.next().ok_or(ParseError {
|
||||
ctx: "request line",
|
||||
details: "missing version",
|
||||
})?;
|
||||
|
||||
let target = match core::str::from_utf8(target) {
|
||||
Ok(m) => m,
|
||||
_ => {
|
||||
return fail_details(ErrorKind::Parse, "expected target to be ascii");
|
||||
},
|
||||
};
|
||||
let (_, method) = Method::parse(method)?;
|
||||
let target = from_utf8(target).map_err(|_| ParseError {
|
||||
ctx: "request line",
|
||||
details: "target is not valid utf8",
|
||||
})?;
|
||||
let (_, version) = Version::parse(version)?;
|
||||
|
||||
let version = match () {
|
||||
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
|
||||
_ => {
|
||||
return fail_details(ErrorKind::Parse, "unknown http version");
|
||||
},
|
||||
};
|
||||
|
||||
Ok((
|
||||
amt,
|
||||
RequestLine {
|
||||
method,
|
||||
target,
|
||||
version,
|
||||
},
|
||||
))
|
||||
Ok((
|
||||
line_amt,
|
||||
RequestLine {
|
||||
method,
|
||||
target,
|
||||
version,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status_line(d: &[u8]) -> Parse<StatusLine> {
|
||||
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?;
|
||||
impl<'a> Parse<'a> for StatusLine<'a> {
|
||||
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||
let (line_amt, Line(line)) = Line::parse(data)?;
|
||||
|
||||
let mut it = line
|
||||
.split(|b| b.is_ascii_whitespace())
|
||||
.filter(|bs| !bs.is_empty());
|
||||
let (version, status) =
|
||||
line.split_at(line.iter().position(|b| b.is_ascii_whitespace()).ok_or(
|
||||
ParseError {
|
||||
ctx: "status line",
|
||||
details: "missing status",
|
||||
},
|
||||
)?);
|
||||
|
||||
let (version, status_code, status_text) = match (it.next(), it.next(), it.next()) {
|
||||
(Some(m), Some(t), Some(v)) => (m, t, v),
|
||||
_ => {
|
||||
return fail_details(
|
||||
ErrorKind::Parse,
|
||||
"status line doesn't have required number of elements",
|
||||
);
|
||||
},
|
||||
};
|
||||
let (_, version) = Version::parse(version)?;
|
||||
let (_, status) = Status::parse(status.trim_ascii())?;
|
||||
|
||||
let version = match () {
|
||||
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
|
||||
_ => {
|
||||
return fail_details(ErrorKind::Parse, "unknown http version");
|
||||
},
|
||||
};
|
||||
|
||||
let status_code = core::str::from_utf8(status_code)
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.ok_or_else(|| Error::with_details(ErrorKind::Parse, "invalid status code"))?;
|
||||
|
||||
let status_text = core::str::from_utf8(status_text)
|
||||
.ok()
|
||||
.ok_or_else(|| Error::with_details(ErrorKind::Parse, "invalid status text"))?;
|
||||
|
||||
Ok((
|
||||
amt,
|
||||
StatusLine {
|
||||
version,
|
||||
status_code,
|
||||
status_text,
|
||||
},
|
||||
))
|
||||
Ok((line_amt, Self { version, status }))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(d: &[u8]) -> Parse<Header> {
|
||||
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?;
|
||||
impl<'a> Parse<'a> for Header<'a> {
|
||||
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||
let (line_amt, Line(line)) = Line::parse(data)?;
|
||||
|
||||
let mut it = line.split(|b| *b == b':').filter(|bs| !bs.is_empty());
|
||||
let (name, value) =
|
||||
line.split_at(line.iter().position(|b| *b == b':').ok_or(ParseError {
|
||||
ctx: "header",
|
||||
details: "missing :",
|
||||
})?);
|
||||
|
||||
let (name, value) = match (it.next(), it.next()) {
|
||||
(Some(n), Some(v)) => (n, v),
|
||||
_ => {
|
||||
return fail_details(
|
||||
ErrorKind::Parse,
|
||||
"header doesn't have required number of elements",
|
||||
);
|
||||
},
|
||||
};
|
||||
let name = from_utf8(name).map_err(|_| ParseError {
|
||||
ctx: "header",
|
||||
details: "name is not valid utf8",
|
||||
})?;
|
||||
|
||||
let name = match core::str::from_utf8(name) {
|
||||
Ok(m) => m,
|
||||
_ => return fail_details(ErrorKind::Parse, "expected target to be ascii"),
|
||||
};
|
||||
let name = name.trim();
|
||||
let value = &value[1..].trim_ascii();
|
||||
|
||||
let value = value.trim_ascii();
|
||||
|
||||
Ok((amt, Header::from((name, value))))
|
||||
Ok((line_amt, Self { name, value }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
src/state.rs
Normal file
1
src/state.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
1
src/test/mod.rs
Normal file
1
src/test/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
mod recv;
|
||||
162
src/test/recv.rs
Normal file
162
src/test/recv.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
macro_rules! go {
|
||||
($role:expr, $input:expr, $expected_events:expr) => {
|
||||
let mut conn = Connection::new($role);
|
||||
let data = $input;
|
||||
let iter = conn.recv_iter(data.as_bytes());
|
||||
let events: Vec<_> = iter.collect();
|
||||
|
||||
assert_eq!(events, $expected_events);
|
||||
};
|
||||
}
|
||||
|
||||
mod server {
|
||||
use crate::{Connection, Event, Header, Method, RequestLine, Role, Version};
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
go!(
|
||||
Role::Server,
|
||||
"GET / HTTP/1.1\r\n\r\n",
|
||||
[
|
||||
RequestLine {
|
||||
method: Method::Get,
|
||||
target: "/",
|
||||
version: Version::V1_1
|
||||
}
|
||||
.into(),
|
||||
Event::HeadersEnd,
|
||||
Event::MessageEnd
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_one_header() {
|
||||
go!(
|
||||
Role::Server,
|
||||
"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n",
|
||||
[
|
||||
RequestLine {
|
||||
method: Method::Get,
|
||||
target: "/",
|
||||
version: Version::V1_1
|
||||
}
|
||||
.into(),
|
||||
Header {
|
||||
name: "Host",
|
||||
value: b"localhost"
|
||||
}
|
||||
.into(),
|
||||
Event::HeadersEnd,
|
||||
Event::MessageEnd
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_two_headers() {
|
||||
go!(
|
||||
Role::Server,
|
||||
"GET / HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\n\r\n",
|
||||
[
|
||||
RequestLine {
|
||||
method: Method::Get,
|
||||
target: "/",
|
||||
version: Version::V1_1
|
||||
}
|
||||
.into(),
|
||||
Header {
|
||||
name: "Host",
|
||||
value: b"localhost"
|
||||
}
|
||||
.into(),
|
||||
Header {
|
||||
name: "Accept",
|
||||
value: b"*/*"
|
||||
}
|
||||
.into(),
|
||||
Event::HeadersEnd,
|
||||
Event::MessageEnd
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod client {
|
||||
use crate::{Connection, Event, Header, Method, Role, Status, StatusLine, Version};
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
go!(
|
||||
Role::Client,
|
||||
"HTTP/1.1 200 OK\r\n\r\n",
|
||||
[
|
||||
StatusLine {
|
||||
version: Version::V1_1,
|
||||
status: Status {
|
||||
code: 200,
|
||||
text: "OK"
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
Event::HeadersEnd,
|
||||
Event::MessageEnd
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_one_header() {
|
||||
go!(
|
||||
Role::Client,
|
||||
"HTTP/1.1 200 OK\r\nHost: localhost\r\n\r\n",
|
||||
[
|
||||
StatusLine {
|
||||
version: Version::V1_1,
|
||||
status: Status {
|
||||
code: 200,
|
||||
text: "OK"
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
Header {
|
||||
name: "Host",
|
||||
value: b"localhost"
|
||||
}
|
||||
.into(),
|
||||
Event::HeadersEnd,
|
||||
Event::MessageEnd
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_two_headers() {
|
||||
go!(
|
||||
Role::Client,
|
||||
"HTTP/1.1 200 OK\r\nHost: localhost\r\nAccept: */*\r\n\r\n",
|
||||
[
|
||||
StatusLine {
|
||||
version: Version::V1_1,
|
||||
status: Status {
|
||||
code: 200,
|
||||
text: "OK"
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
Header {
|
||||
name: "Host",
|
||||
value: b"localhost"
|
||||
}
|
||||
.into(),
|
||||
Header {
|
||||
name: "Accept",
|
||||
value: b"*/*"
|
||||
}
|
||||
.into(),
|
||||
Event::HeadersEnd,
|
||||
Event::MessageEnd
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
use crate::{ErrorKind, Write, Written};
|
||||
|
||||
pub trait ResultTupExt<A, T, E> {
|
||||
fn map2<U>(self, f: impl FnOnce(T) -> U) -> Result<(A, U), E>;
|
||||
}
|
||||
|
|
@ -12,6 +10,7 @@ impl<A, T, E> ResultTupExt<A, T, E> for Result<(A, T), E> {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
pub struct NotEnoughSpace;
|
||||
pub struct Buf<T, U> {
|
||||
written_len: usize,
|
||||
|
|
@ -141,3 +140,4 @@ where
|
|||
.map_err(|_| ErrorKind::BufNotBigEnough.into())
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue