From 879003024c9a85c79111ceaec8b48030509414bc Mon Sep 17 00:00:00 2001 From: soup Date: Sun, 3 Nov 2024 21:51:29 -0500 Subject: [PATCH] Major refactoring --- Cargo.lock | 133 ------------ Cargo.toml | 10 +- crates/fetchplz/src/lib.rs | 192 ++--------------- examples/client.rs | 8 +- examples/echo.rs | 6 + src/http.rs | 102 +++++++++ src/lib.rs | 409 ++++++++++++------------------------- src/parse.rs | 232 ++++++++++----------- src/state.rs | 1 + src/test/mod.rs | 1 + src/test/recv.rs | 162 +++++++++++++++ src/util.rs | 4 +- 12 files changed, 552 insertions(+), 708 deletions(-) create mode 100644 src/http.rs create mode 100644 src/state.rs create mode 100644 src/test/mod.rs create mode 100644 src/test/recv.rs diff --git a/Cargo.lock b/Cargo.lock index d2d725b..4d1a2b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", -] diff --git a/Cargo.toml b/Cargo.toml index 7d52f88..1415df9 100644 --- a/Cargo.toml +++ b/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" } diff --git a/crates/fetchplz/src/lib.rs b/crates/fetchplz/src/lib.rs index 6397975..ff091fd 100644 --- a/crates/fetchplz/src/lib.rs +++ b/crates/fetchplz/src/lib.rs @@ -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, 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 = 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::()?, - HeaderValue::from_bytes(value)?, - ); - }, - httplz::Event::HeadersDone => return Ok(None), - _ => (), - } - } - - leftovers.drain(0..read_total); - - FetchResult::Ok(Some(())) - }) - .transpose() - }) - .fuse() - .collect::() - .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, 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 = 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 = 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)); - }); } } diff --git a/examples/client.rs b/examples/client.rs index 2eadf2a..cefa58e 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -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> { } .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> { Ok(()) } +*/ diff --git a/examples/echo.rs b/examples/echo.rs index eac8016..c6058e7 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -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> { } } } +*/ diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..6a40ebc --- /dev/null +++ b/src/http.rs @@ -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> { + $({ + 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> { + $({ + 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], +} diff --git a/src/lib.rs b/src/lib.rs index f9d2ceb..41cefc6 100644 --- a/src/lib.rs +++ b/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> for Event<'a> { - fn from(value: Header<'a>) -> Self { - Self::Header(value) +impl From for Error { + fn from(v: ParseError) -> Self { + Self::ParseError(v) } } -impl<'a> From> for Event<'a> { - fn from(value: RequestLine<'a>) -> Self { - Self::RequestLine(value) - } -} - -impl<'a> From> 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("") - ), - Event::BodyChunk(b) => { - write!(f, "{}", core::str::from_utf8(b).unwrap_or("")) - }, - } - } -} - -#[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), - Body(BodyState), - ValidateDone, + Body(Option), } -#[derive(Debug, Copy, Clone)] -enum StateSend { - StartLine, - Headers(Option), - 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, +} + +impl<'a> Iterator for RecvIter<'a> { + type Item = Event<'a>; fn next(&mut self) -> Option { - 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> for Event<'a> { + fn from(v: Header<'a>) -> Self { + Self::Header(v) + } +} + +impl<'a> From> for Event<'a> { + fn from(v: StatusLine<'a>) -> Self { + Self::StatusLine(v) + } +} + +impl<'a> From> for Event<'a> { + fn from(v: RequestLine<'a>) -> Self { + Self::RequestLine(v) } } diff --git a/src/parse.rs b/src/parse.rs index 116ce4f..fa0e32a 100644 --- a/src/parse.rs +++ b/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>; +} + +pub struct Line<'a>(pub &'a [u8]); +impl<'a> Parse<'a> for Line<'a> { + fn parse(data: &'a [u8]) -> Result<(usize, Self), Option> { + 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> { + 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 { - 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> { + 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 { - 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> { + 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
{ - 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> { + 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 })) + } } diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/state.rs @@ -0,0 +1 @@ + diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 0000000..f1fc530 --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1 @@ +mod recv; diff --git a/src/test/recv.rs b/src/test/recv.rs new file mode 100644 index 0000000..009773c --- /dev/null +++ b/src/test/recv.rs @@ -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 + ] + ); + } +} diff --git a/src/util.rs b/src/util.rs index a5fad49..2a0734d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,3 @@ -use crate::{ErrorKind, Write, Written}; - pub trait ResultTupExt { fn map2(self, f: impl FnOnce(T) -> U) -> Result<(A, U), E>; } @@ -12,6 +10,7 @@ impl ResultTupExt for Result<(A, T), E> { } } +/* pub struct NotEnoughSpace; pub struct Buf { written_len: usize, @@ -141,3 +140,4 @@ where .map_err(|_| ErrorKind::BufNotBigEnough.into()) } } +*/