Compare commits

...

2 commits

Author SHA1 Message Date
soup 879003024c
Major refactoring 2024-11-03 21:51:29 -05:00
soup 21e8b9c8bb
Work work 2024-10-24 15:27:11 -04:00
15 changed files with 650 additions and 603 deletions

51
Cargo.lock generated
View file

@ -1,56 +1,7 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]]
name = "bytes"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
[[package]]
name = "fetchplz"
version = "0.0.0"
dependencies = [
"http",
"httplz",
"httplzx",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]] [[package]]
name = "httplz" name = "httplz"
version = "0.0.0" version = "0.0.0"
dependencies = [
"httplzx",
]
[[package]]
name = "httplzx"
version = "0.0.0"
dependencies = [
"http",
"httplz",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"

View file

@ -7,17 +7,17 @@ description = "A sans-io, no-std HTTP implementation"
[workspace] [workspace]
members = [ members = [
"crates/fetchplz", # "crates/fetchplz",
"crates/httplzx" # "crates/httplzx"
] ]
[workspace.dependencies] [workspace.dependencies]
httplz = { path = ".", features = ["std"] } httplz = { path = ".", features = ["std"] }
httplzx = { path = "./crates/httplzx" } # httplzx = { path = "./crates/httplzx" }
fetchplz = { path = "./crates/fetchplz" } # fetchplz = { path = "./crates/fetchplz" }
[features] [features]
std = [] std = []
[dev-dependencies] [dev-dependencies]
httplzx = { path = "./crates/httplzx" } # httplzx = { path = "./crates/httplzx" }

View file

@ -6,4 +6,8 @@ edition = "2021"
[dependencies] [dependencies]
httplz = { workspace = true } httplz = { workspace = true }
httplzx = { workspace = true } httplzx = { workspace = true }
http = { version = "1.1.0" } http = { version = "1.1.0" }
futures-lite = { version = "2.3.0" }
wove = { path = "../../../wove" }

View file

@ -1,181 +1,119 @@
use std::io::{Error as IOError, Read, Write}; use std::future::Future;
use std::net::TcpStream; use std::io::Error as IoError;
use std::str::FromStr; use std::pin::Pin;
use httplz::NeedsMoreData; use futures_lite::{Stream, StreamExt};
use httplzx::ToEvents; use http::uri::Scheme;
pub struct Body { use http::{HeaderValue, Response};
buf: httplz::Buf<Box<[u8]>, u8>, use wove::io::{AsyncReadLoan, AsyncWriteLoan, Transpose};
chunk_end: usize, use wove::io_impl::IoImpl;
read_amt: usize, use wove::net::TcpStream;
stream: TcpStream, use wove::streams::StreamExt as _;
conn: httplz::Connection, use wove::util::Ignore;
}
impl Body {
fn find_next_chunk_sz(&mut self) -> Result<(), IOError> {
self.buf.pop_front(self.chunk_end);
self.chunk_end = 0;
self.read_amt = 0;
while self.conn.is_recving() {
dbg!(self.conn, String::from_utf8_lossy(self.buf.filled()));
let res = self.conn.poll_recv(self.buf.filled());
if res.needs_more_data() {
self.buf.read_from(&mut self.stream)?;
continue;
}
let (amt, ev) = res.map_err(|e| IOError::other(format!("{e:?}")))?;
match ev {
httplz::Event::BodyChunk(b) => {
self.chunk_end = b.len();
return Ok(());
},
_ => self.buf.pop_front(amt),
};
}
Ok(())
}
}
impl Read for Body {
fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result<usize> {
if self.chunk_end == self.read_amt {
self.find_next_chunk_sz()?;
}
let chunk = &self.buf.filled()[self.read_amt..self.chunk_end];
let amt = buf.write(chunk)?;
self.read_amt += amt;
Ok(amt)
}
}
impl Body {
pub fn collect_bytes(self) -> Result<Vec<u8>, IOError> {
self.bytes().collect()
}
}
pub type Response = http::Response<Body>;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
IOError(IOError), InvalidUriScheme,
HttplzError(httplz::Error),
InvalidUri,
InvalidStatusCode, InvalidStatusCode,
InvalidHeaderValue, InvalidHeaderValue,
InvalidHeaderName,
IoError(IoError),
Httplz(httplz::Error),
} }
impl From<IOError> for Error { impl From<IoError> for Error {
fn from(value: IOError) -> Self { fn from(value: IoError) -> Self {
Self::IOError(value) Self::IoError(value)
} }
} }
impl From<httplz::Error> for Error { impl From<httplz::Error> for Error {
fn from(value: httplz::Error) -> Self { fn from(value: httplz::Error) -> Self {
Self::HttplzError(value) Self::Httplz(value)
} }
} }
pub fn fetch(path: &str) -> Result<Response, Error> { impl From<http::status::InvalidStatusCode> for Error {
let uri = http::Uri::from_str(path).map_err(|_| Error::InvalidUri)?; fn from(_: http::status::InvalidStatusCode) -> Self {
let scheme = uri.scheme().cloned().unwrap_or(http::uri::Scheme::HTTPS); Self::InvalidStatusCode
let host = uri.host().ok_or(Error::InvalidUri)?;
let mut stream = TcpStream::connect(format!(
"{}:{}",
host,
uri.port_u16().unwrap_or_else(|| match scheme.as_str() {
"http" => 80,
_ => 443,
})
))?;
let mut conn = httplz::Connection::new(httplz::Role::Client);
let (mut parts, _) = http::Request::new(()).into_parts();
parts
.headers
.insert("Host", http::HeaderValue::from_str(host).unwrap());
parts.uri = uri;
let mut events = parts.to_events();
events.extend_from_slice(&[httplz::Event::HeadersDone, httplz::Event::SendDone]);
dbg!("a");
let mut buf = httplz::Buf::new(vec![0; 4096].into_boxed_slice());
for event in events {
conn.handle_send(&event, &mut buf)?;
} }
}
stream.write_all(buf.filled())?; impl From<http::header::InvalidHeaderValue> for Error {
fn from(_: http::header::InvalidHeaderValue) -> Self {
Self::InvalidHeaderValue
}
}
assert!(conn.is_recving()); impl From<http::header::InvalidHeaderName> for Error {
fn from(_: http::header::InvalidHeaderName) -> Self {
Self::InvalidHeaderName
}
}
pub type FetchResult<T> = Result<T, Error>;
pub struct Body<S> {
pub size_hint: Option<usize>,
pub stream: S,
}
pub type FetchResponse<S> = http::Response<Body<S>>;
pub type FetchRequest<S> = http::Request<Body<S>>;
pub trait RequestExt<S, I> {
fn send(
self,
io: &I,
) -> impl Future<
Output = FetchResult<FetchResponse<impl Stream<Item = Result<Box<[u8]>, IoError>>>>,
>;
}
impl<S, I: IoImpl> RequestExt<S, I> for http::Request<Body<S>>
where
S: Stream<Item = Box<[u8]>>,
I: IoImpl,
I::TcpStream: AsyncReadLoan + AsyncWriteLoan,
{
async fn send(
self,
io: &I,
) -> FetchResult<FetchResponse<impl Stream<Item = Result<Box<[u8]>, IoError>>>> {
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();
buf.clear();
let (mut parts, _) = http::Response::new(()).into_parts(); let (mut parts, _) = http::Response::new(()).into_parts();
while conn.is_recving() { let mut leftovers = Vec::new();
let data = buf.filled(); while let Some(data) = stream.read_stream().next2().await {
let res = conn.poll_recv(data); let data = data?;
if res.needs_more_data() { leftovers.extend_from_slice(data.as_slice());
buf.read_from(&mut stream)?;
continue;
} }
let (amt, event) = res?; todo!()
let mut brk = false; as FetchResult<
match event { FetchResponse<Pin<Box<dyn Stream<Item = Result<Box<[u8]>, IoError>>>>>,
httplz::Event::StatusLine(sl) => { >
parts.status = http::StatusCode::from_u16(sl.status_code)
.map_err(|_| Error::InvalidStatusCode)?;
parts.version = http::Version::HTTP_11;
},
httplz::Event::Header(h) => {
parts.headers.insert(
http::HeaderName::from_str(h.name).unwrap(),
http::HeaderValue::from_bytes(h.value)
.map_err(|_| Error::InvalidHeaderValue)?,
);
},
httplz::Event::HeadersDone => brk = true,
_ => (),
};
dbg!(amt, brk);
buf.pop_front(amt);
if brk {
break;
} }
}
Ok(http::Response::from_parts(
parts,
Body {
buf,
stream,
conn,
chunk_end: 0,
read_amt: 0,
},
))
} }
#[cfg(test)] #[cfg(test)]
mod test_fetch { mod test {
use crate::fetch; use futures_lite::StreamExt;
use http::Request;
use wove::{
io::{AsyncReadLoan, AsyncWriteLoan, Transpose},
io_impl::io_uring::IoUring,
streams::StreamExt as _,
};
use crate::{Body, RequestExt};
#[test] #[test]
fn test_fetch_1() { fn test() {
let resp = fetch("http://httpbin.org/get")
.unwrap()
.map(|b| b.collect_bytes().unwrap());
let body = resp.into_body();
let body = String::from_utf8_lossy(&body);
assert_eq!(
body,
include_str!("../testing/snapshots/httpbin_get_empty.txt")
)
} }
} }

View file

@ -1,3 +1,8 @@
pub fn main() {
}
/*
//! A simple client that POSTs some data to a path //! A simple client that POSTs some data to a path
//! //!
//! Usage: //! Usage:
@ -27,7 +32,7 @@ pub fn main() -> Result<(), Box<dyn Error>> {
} }
.into(), .into(),
httplz::HeaderOther::from(("Host", host.as_bytes())).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::HeaderSpecial::ContentLength(data.len()).into(),
httplz::Event::HeadersDone, httplz::Event::HeadersDone,
httplz::Event::BodyChunk(data.as_bytes()), httplz::Event::BodyChunk(data.as_bytes()),
@ -70,3 +75,4 @@ pub fn main() -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
*/

View file

@ -1,3 +1,8 @@
pub fn main() {
}
/*
//! A simple echo server that echoes the body of a POST request, and returns a //! A simple echo server that echoes the body of a POST request, and returns a
//! 405 for any other method //! 405 for any other method
//! //!
@ -88,3 +93,4 @@ fn main() -> Result<(), Box<dyn Error>> {
} }
} }
} }
*/

View file

@ -21,7 +21,8 @@
pkgs = i.nixpkgs.legacyPackages.${system}; pkgs = i.nixpkgs.legacyPackages.${system};
pkgsDeno = i.deno-flake.packages.${system}; pkgsDeno = i.deno-flake.packages.${system};
pkgsFenix = i.fenix.packages.${system}; pkgsFenix = i.fenix.packages.${system};
nightly = pkgsFenix.default; minimal = pkgsFenix.minimal;
complete = pkgsFenix.complete;
stable = pkgsFenix.stable; stable = pkgsFenix.stable;
in { in {
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
@ -34,11 +35,10 @@
pkgs.fd pkgs.fd
(pkgsFenix.combine [ (pkgsFenix.combine [
(stable.withComponents [ (complete.withComponents [ "rust-src" "rust-analyzer" "rustfmt" "clippy" ])
"cargo" "rustc" "rust-src" "rust-analyzer" "clippy" (minimal.withComponents [ "cargo" "rustc" "rust-std" ])
])
nightly.rustfmt
]) ])
pkgs.cargo-watch pkgs.cargo-watch
]; ];
}; };

102
src/http.rs Normal file
View 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],
}

View file

@ -1,297 +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 parse;
pub mod parts;
pub mod util; pub mod util;
pub mod write;
pub use common::*; #[cfg(test)]
pub use parse::{NeedsMoreData, Parse}; mod test;
pub use parts::*;
pub use util::*;
pub use write::{Write, WriteCursor, Written};
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub enum Event<'a> { pub enum Error {
#[default] ParseError(ParseError),
Empty,
RequestLine(RequestLine<'a>),
Header(Header<'a>),
HeadersDone,
BodyChunk(&'a [u8]),
RecvDone,
StatusLine(StatusLine<'a>),
SendDone,
} }
impl<'a> From<Header<'a>> for Event<'a> { impl From<ParseError> for Error {
fn from(value: Header<'a>) -> Self { fn from(v: ParseError) -> Self {
Self::Header(value) Self::ParseError(v)
} }
} }
impl<'a> From<RequestLine<'a>> for Event<'a> { #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
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<'a> core::fmt::Display for Event<'a> {
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)]
pub enum Role { pub enum Role {
Client, Client,
Server, Server,
} }
#[derive(Debug, Copy, Clone)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
enum BodyState { pub enum BodyState {
ContentLength(usize), ContentLength(usize),
Chunked, Chunked,
} }
#[derive(Debug, Copy, Clone)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
enum StateRecv { pub enum State {
StartLine, StartLine,
Headers(Option<BodyState>), Headers(Option<BodyState>),
Body(BodyState), Body(Option<BodyState>),
ValidateDone,
} }
#[derive(Debug, Copy, Clone)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
enum StateSend {
StartLine,
Headers(Option<BodyState>),
Body(BodyState),
ValidateDone,
}
#[derive(Debug, Copy, Clone)]
enum StateConnection {
Recv(StateRecv),
Send(StateSend),
}
#[derive(Debug, Copy, Clone)]
pub struct Connection { pub struct Connection {
keep_alive: bool,
role: Role, role: Role,
state: StateConnection, send: State,
recv: State,
} }
impl Connection { impl Connection {
pub fn new(role: Role) -> Self { pub fn role(&self) -> Role {
let state = match role { self.role
Role::Server => StateConnection::Recv(StateRecv::StartLine),
Role::Client => StateConnection::Send(StateSend::StartLine),
};
Self { state, role }
} }
pub fn is_sending(&self) -> bool { pub fn send(&self) -> State {
matches!(self.state, StateConnection::Send(_)) self.send
} }
pub fn is_recving(&self) -> bool { pub fn recv(&self) -> State {
matches!(self.state, StateConnection::Recv(_)) self.recv
} }
pub fn poll_recv<'a>(&mut self, bytes: &'a [u8]) -> Parse<'a, Event<'a>> { pub fn keep_alive(&self) -> bool {
let recv = match &self.state { self.keep_alive
StateConnection::Recv(r) => r, }
_ => { }
return Err(ErrorKind::InvalidConnectionState.into());
}, impl Connection {
}; pub fn new(role: Role) -> Self {
Self {
let n = match (self.role, recv) { keep_alive: true,
(Role::Server, StateRecv::StartLine) => parse::request_line(bytes).map2(|rl| { role,
( send: State::StartLine,
Event::RequestLine(rl), recv: State::StartLine,
StateConnection::Recv(StateRecv::Headers(None)), }
) }
}),
pub fn recv_iter<'a>(&'a mut self, data: &'a [u8]) -> RecvIter<'a> {
(Role::Client, StateRecv::StartLine) => parse::status_line(bytes).map2(|rl| { RecvIter {
( conn: self,
Event::StatusLine(rl), data,
StateConnection::Recv(StateRecv::Headers(None)), read_amt: 0,
) err: None,
}), }
}
(_, StateRecv::Headers(body_state)) => { }
if bytes.starts_with(b"\r\n") {
Ok(( pub struct RecvIter<'a> {
2, conn: &'a mut Connection,
( data: &'a [u8],
Event::HeadersDone, read_amt: usize,
match body_state { err: Option<Error>,
None => StateConnection::Recv(StateRecv::ValidateDone), }
Some(b) => StateConnection::Recv(StateRecv::Body(*b)),
}, impl<'a> Iterator for RecvIter<'a> {
), type Item = Event<'a>;
))
} else { fn next(&mut self) -> Option<Self::Item> {
parse::header(bytes).map2(|h| { if self.err.is_some() {
let b = h return None;
.special() }
.map(|h| match h {
HeaderSpecial::TransferEncodingChunked => BodyState::Chunked, let data = &self.data[self.read_amt..];
HeaderSpecial::ContentLength(c) => BodyState::ContentLength(c),
}) let (res, next) = match (self.conn.role, self.conn.recv) {
.or(*body_state); (Role::Client, State::StartLine) => (
StatusLine::parse(data).map2(Event::from),
( State::Headers(None),
Event::Header(h), ),
StateConnection::Recv(StateRecv::Headers(b)),
) (Role::Server, State::StartLine) => (
}) RequestLine::parse(data).map2(Event::from),
} State::Headers(None),
}, ),
(_, StateRecv::Body(body_state)) => match body_state { (_, State::Headers(body_state)) => {
BodyState::ContentLength(remaining) => { if data.starts_with(LINE_ENDING) {
if bytes.is_empty() && *remaining != 0 { (
return fail(ErrorKind::NeedMoreData); Ok((LINE_ENDING.len(), Event::HeadersEnd)),
} State::Body(body_state),
if bytes.len() < *remaining { )
Ok(( } else {
bytes.len(), (
( Header::parse(data).map2(Event::from),
Event::BodyChunk(bytes), State::Headers(body_state),
StateConnection::Recv(StateRecv::Body( )
BodyState::ContentLength(remaining - bytes.len()), }
)), },
),
)) (_, State::Body(None)) => (Ok((0, Event::MessageEnd)), State::StartLine),
} else { (_, State::Body(Some(body_state))) => todo!(),
Ok(( };
*remaining,
( let (amt, event) = match res {
Event::BodyChunk(&bytes[..*remaining]), Ok(v) => v,
StateConnection::Recv(StateRecv::ValidateDone), Err(Some(e)) => {
), self.err = Some(e.into());
)) return None;
} },
}, Err(None) => {
_ => todo!(), return None;
}, },
};
(_, StateRecv::ValidateDone) => {
if bytes.is_empty() { self.read_amt += amt;
Ok(( self.conn.recv = next;
0,
(Event::RecvDone, StateConnection::Send(StateSend::StartLine)), Some(event)
)) }
} else { }
fail(ErrorKind::TrailingBytes)
} #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
}, pub enum Event<'a> {
}; RequestLine(RequestLine<'a>),
StatusLine(StatusLine<'a>),
n.map2(move |(ev, next_state)| { Header(Header<'a>),
self.state = next_state; HeadersEnd,
MessageEnd,
ev }
})
} impl<'a> From<Header<'a>> for Event<'a> {
fn from(v: Header<'a>) -> Self {
pub fn handle_send(&mut self, event: &Event, mut w: impl Write) -> Written { Self::Header(v)
let state = match self.state { }
StateConnection::Send(s) => s, }
_ => return fail(ErrorKind::InvalidConnectionState),
}; impl<'a> From<StatusLine<'a>> for Event<'a> {
fn from(v: StatusLine<'a>) -> Self {
let next = match (self.role, state, event) { Self::StatusLine(v)
(Role::Server, StateSend::StartLine, Event::StatusLine(sl)) => { }
write::status_line(sl, w) }
.map(|_| StateConnection::Send(StateSend::Headers(None)))
}, impl<'a> From<RequestLine<'a>> for Event<'a> {
fn from(v: RequestLine<'a>) -> Self {
(Role::Client, StateSend::StartLine, Event::RequestLine(rl)) => { Self::RequestLine(v)
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(())
} }
} }

View file

@ -1,142 +1,144 @@
use std::str::from_utf8;
use crate::{ use crate::{
fail_details, http::{Header, Method, RequestLine, Status, StatusLine, Version},
parts::{RequestLine, Version}, LINE_ENDING,
Error, ErrorKind, Header, Method, StatusLine,
}; };
pub type Parse<'a, T> = Result<(usize, T), Error>; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub trait NeedsMoreData { pub struct ParseError {
fn needs_more_data(&self) -> bool; pub ctx: &'static str,
pub details: &'static str,
} }
impl<'a, T> NeedsMoreData for Parse<'a, T> {
fn needs_more_data(&self) -> bool { pub trait Parse<'a>: Sized {
self.as_ref() /// `None` in the return indicates that the parser is not applicable
.err() fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>>;
.map(|e| e.kind) }
.is_some_and(|e| e == ErrorKind::NeedMoreData)
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)> { impl<'a> Parse<'a> for Status<'a> {
let p = d.windows(2).position(|w| w == b"\r\n")?; 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> { impl<'a> Parse<'a> for RequestLine<'a> {
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?; fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
let (line_amt, Line(line)) = Line::parse(data)?;
let mut it = line let mut tokens = line
.split(|b| b.is_ascii_whitespace()) .split(|b| b.is_ascii_whitespace())
.filter(|bs| !bs.is_empty()); .filter(|bs| !bs.is_empty());
let (method, target, version) = match (it.next(), it.next(), it.next()) { let method = tokens.next().ok_or(ParseError {
(Some(m), Some(t), Some(v)) => (m, t, v), ctx: "request line",
_ => { details: "missing method",
return fail_details( })?;
ErrorKind::Parse,
"request line doesn't have required number of elements",
);
},
};
let method = match core::str::from_utf8(method) { let target = tokens.next().ok_or(ParseError {
Ok(m) => m, ctx: "request line",
_ => { details: "missing target",
return fail_details(ErrorKind::Parse, "expected method to be ascii"); })?;
},
};
let method = Method::new_from_str(method);
let target = match core::str::from_utf8(target) { let version = tokens.next().ok_or(ParseError {
Ok(m) => m, ctx: "request line",
_ => { details: "missing version",
return fail_details(ErrorKind::Parse, "expected target to be ascii"); })?;
},
};
let version = match () { let (_, method) = Method::parse(method)?;
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1, let target = from_utf8(target).map_err(|_| ParseError {
_ => { ctx: "request line",
return fail_details(ErrorKind::Parse, "unknown http version"); details: "target is not valid utf8",
}, })?;
}; let (_, version) = Version::parse(version)?;
Ok(( Ok((
amt, line_amt,
RequestLine { RequestLine {
method, method,
target, target,
version, version,
}, },
)) ))
}
} }
pub fn status_line(d: &[u8]) -> Parse<StatusLine> { impl<'a> Parse<'a> for StatusLine<'a> {
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?; fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
let (line_amt, Line(line)) = Line::parse(data)?;
let mut it = line let (version, status) =
.split(|b| b.is_ascii_whitespace()) line.split_at(line.iter().position(|b| b.is_ascii_whitespace()).ok_or(
.filter(|bs| !bs.is_empty()); ParseError {
ctx: "status line",
let (version, status_code, status_text) = match (it.next(), it.next(), it.next()) { details: "missing status",
(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 = match () { let (_, version) = Version::parse(version)?;
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1, let (_, status) = Status::parse(status.trim_ascii())?;
_ => {
return fail_details(ErrorKind::Parse, "unknown http version");
},
};
let status_code = core::str::from_utf8(status_code) Ok((line_amt, Self { version, status }))
.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,
},
))
} }
pub fn header(d: &[u8]) -> Parse<Header> { impl<'a> Parse<'a> for Header<'a> {
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?; 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()) { let name = from_utf8(name).map_err(|_| ParseError {
(Some(n), Some(v)) => (n, v), ctx: "header",
_ => { details: "name is not valid utf8",
return fail_details( })?;
ErrorKind::Parse,
"header doesn't have required number of elements",
);
},
};
let name = match core::str::from_utf8(name) { let value = &value[1..].trim_ascii();
Ok(m) => m,
_ => return fail_details(ErrorKind::Parse, "expected target to be ascii"),
};
let name = name.trim();
let value = value.trim_ascii(); Ok((line_amt, Self { name, value }))
}
Ok((amt, Header::from((name, value))))
} }

View file

@ -40,7 +40,7 @@ impl<'a> Method<'a> {
} }
} }
impl<'a> core::fmt::Display for Method<'a> { impl core::fmt::Display for Method<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
use Method::*; use Method::*;
@ -80,7 +80,7 @@ pub struct Header<'a> {
pub name: &'a str, pub name: &'a str,
pub value: &'a [u8], pub value: &'a [u8],
} }
impl<'a> Header<'a> { impl Header<'_> {
pub fn special(&self) -> Option<HeaderSpecial> { pub fn special(&self) -> Option<HeaderSpecial> {
let Self { name, value } = self; let Self { name, value } = self;
@ -90,15 +90,10 @@ impl<'a> Header<'a> {
{ {
Some(HeaderSpecial::TransferEncodingChunked) Some(HeaderSpecial::TransferEncodingChunked)
}, },
_ if name.eq_ignore_ascii_case("content-length") => { _ if name.eq_ignore_ascii_case("content-length") => core::str::from_utf8(value)
match core::str::from_utf8(value)
.ok() .ok()
.and_then(|s| s.parse().ok()) .and_then(|s| s.parse().ok())
{ .map(HeaderSpecial::ContentLength),
Some(v) => Some(HeaderSpecial::ContentLength(v)),
_ => None,
}
},
_ => None, _ => None,
} }
} }

1
src/state.rs Normal file
View file

@ -0,0 +1 @@

1
src/test/mod.rs Normal file
View file

@ -0,0 +1 @@
mod recv;

162
src/test/recv.rs Normal file
View 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
]
);
}
}

View file

@ -1,5 +1,3 @@
use crate::{ErrorKind, Write, Written};
pub trait ResultTupExt<A, T, E> { pub trait ResultTupExt<A, T, E> {
fn map2<U>(self, f: impl FnOnce(T) -> U) -> Result<(A, U), 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 NotEnoughSpace;
pub struct Buf<T, U> { pub struct Buf<T, U> {
written_len: usize, written_len: usize,
@ -141,3 +140,4 @@ where
.map_err(|_| ErrorKind::BufNotBigEnough.into()) .map_err(|_| ErrorKind::BufNotBigEnough.into())
} }
} }
*/