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.
# It is not intended for manual editing.
version = 3
[[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",
]
version = 4
[[package]]
name = "httplz"
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]
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" }

View file

@ -6,4 +6,8 @@ edition = "2021"
[dependencies]
httplz = { workspace = true }
httplzx = { workspace = true }
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::net::TcpStream;
use std::str::FromStr;
use std::future::Future;
use std::io::Error as IoError;
use std::pin::Pin;
use httplz::NeedsMoreData;
use httplzx::ToEvents;
use futures_lite::{Stream, StreamExt};
use http::uri::Scheme;
pub struct Body {
buf: httplz::Buf<Box<[u8]>, u8>,
chunk_end: usize,
read_amt: usize,
stream: TcpStream,
conn: httplz::Connection,
}
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;
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;
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)]
pub enum Error {
IOError(IOError),
HttplzError(httplz::Error),
InvalidUri,
InvalidUriScheme,
InvalidStatusCode,
InvalidHeaderValue,
InvalidHeaderName,
IoError(IoError),
Httplz(httplz::Error),
}
impl From<IOError> for Error {
fn from(value: IOError) -> Self {
Self::IOError(value)
impl From<IoError> for Error {
fn from(value: IoError) -> Self {
Self::IoError(value)
}
}
impl From<httplz::Error> for Error {
fn from(value: httplz::Error) -> Self {
Self::HttplzError(value)
Self::Httplz(value)
}
}
pub fn fetch(path: &str) -> Result<Response, Error> {
let uri = http::Uri::from_str(path).map_err(|_| Error::InvalidUri)?;
let scheme = uri.scheme().cloned().unwrap_or(http::uri::Scheme::HTTPS);
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,
})
))?;
impl From<http::status::InvalidStatusCode> for Error {
fn from(_: http::status::InvalidStatusCode) -> Self {
Self::InvalidStatusCode
}
}
impl From<http::header::InvalidHeaderValue> for Error {
fn from(_: http::header::InvalidHeaderValue) -> Self {
Self::InvalidHeaderValue
}
}
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();
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())?;
assert!(conn.is_recving());
buf.clear();
let (mut parts, _) = http::Response::new(()).into_parts();
while conn.is_recving() {
let data = buf.filled();
let res = conn.poll_recv(data);
if res.needs_more_data() {
buf.read_from(&mut stream)?;
continue;
let mut leftovers = Vec::new();
while let Some(data) = stream.read_stream().next2().await {
let data = data?;
leftovers.extend_from_slice(data.as_slice());
}
let (amt, event) = res?;
let mut brk = false;
match event {
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;
todo!()
as FetchResult<
FetchResponse<Pin<Box<dyn Stream<Item = Result<Box<[u8]>, IoError>>>>>,
>
}
}
Ok(http::Response::from_parts(
parts,
Body {
buf,
stream,
conn,
chunk_end: 0,
read_amt: 0,
},
))
}
#[cfg(test)]
mod test_fetch {
use crate::fetch;
mod test {
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]
fn test_fetch_1() {
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")
)
fn test() {
}
}

View file

@ -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(())
}
*/

View file

@ -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>> {
}
}
}
*/

View file

@ -21,7 +21,8 @@
pkgs = i.nixpkgs.legacyPackages.${system};
pkgsDeno = i.deno-flake.packages.${system};
pkgsFenix = i.fenix.packages.${system};
nightly = pkgsFenix.default;
minimal = pkgsFenix.minimal;
complete = pkgsFenix.complete;
stable = pkgsFenix.stable;
in {
devShells.default = pkgs.mkShell {
@ -34,11 +35,10 @@
pkgs.fd
(pkgsFenix.combine [
(stable.withComponents [
"cargo" "rustc" "rust-src" "rust-analyzer" "clippy"
])
nightly.rustfmt
(complete.withComponents [ "rust-src" "rust-analyzer" "rustfmt" "clippy" ])
(minimal.withComponents [ "cargo" "rustc" "rust-std" ])
])
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 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<'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)]
#[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 role(&self) -> Role {
self.role
}
pub fn send(&self) -> State {
self.send
}
pub fn recv(&self) -> State {
self.recv
}
pub fn keep_alive(&self) -> bool {
self.keep_alive
}
}
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 }
Self {
keep_alive: true,
role,
send: State::StartLine,
recv: State::StartLine,
}
}
pub fn is_sending(&self) -> bool {
matches!(self.state, StateConnection::Send(_))
pub fn recv_iter<'a>(&'a mut self, data: &'a [u8]) -> RecvIter<'a> {
RecvIter {
conn: self,
data,
read_amt: 0,
err: None,
}
}
}
pub fn is_recving(&self) -> bool {
matches!(self.state, StateConnection::Recv(_))
pub struct RecvIter<'a> {
conn: &'a mut Connection,
data: &'a [u8],
read_amt: usize,
err: Option<Error>,
}
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());
},
};
impl<'a> Iterator for RecvIter<'a> {
type Item = Event<'a>;
let n = match (self.role, recv) {
(Role::Server, StateRecv::StartLine) => parse::request_line(bytes).map2(|rl| {
(
Event::RequestLine(rl),
StateConnection::Recv(StateRecv::Headers(None)),
)
}),
fn next(&mut self) -> Option<Self::Item> {
if self.err.is_some() {
return None;
}
(Role::Client, StateRecv::StartLine) => parse::status_line(bytes).map2(|rl| {
(
Event::StatusLine(rl),
StateConnection::Recv(StateRecv::Headers(None)),
)
}),
let data = &self.data[self.read_amt..];
(_, 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)),
},
let (res, next) = match (self.conn.role, self.conn.recv) {
(Role::Client, State::StartLine) => (
StatusLine::parse(data).map2(Event::from),
State::Headers(None),
),
))
} 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);
(Role::Server, State::StartLine) => (
RequestLine::parse(data).map2(Event::from),
State::Headers(None),
),
(_, State::Headers(body_state)) => {
if data.starts_with(LINE_ENDING) {
(
Event::Header(h),
StateConnection::Recv(StateRecv::Headers(b)),
Ok((LINE_ENDING.len(), Event::HeadersEnd)),
State::Body(body_state),
)
} else {
(
Header::parse(data).map2(Event::from),
State::Headers(body_state),
)
})
}
},
(_, 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!(),
},
(_, State::Body(None)) => (Ok((0, Event::MessageEnd)), State::StartLine),
(_, State::Body(Some(body_state))) => todo!(),
};
(_, StateRecv::ValidateDone) => {
if bytes.is_empty() {
Ok((
0,
(Event::RecvDone, StateConnection::Send(StateSend::StartLine)),
))
} else {
fail(ErrorKind::TrailingBytes)
}
let (amt, event) = match res {
Ok(v) => v,
Err(Some(e)) => {
self.err = Some(e.into());
return None;
},
Err(None) => {
return None;
},
};
n.map2(move |(ev, next_state)| {
self.state = next_state;
self.read_amt += amt;
self.conn.recv = next;
ev
})
}
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(())
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)
}
}

View file

@ -1,69 +1,99 @@
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))?;
let mut it = line
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 (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 method = tokens.next().ok_or(ParseError {
ctx: "request line",
details: "missing method",
})?;
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 target = tokens.next().ok_or(ParseError {
ctx: "request line",
details: "missing target",
})?;
let target = match core::str::from_utf8(target) {
Ok(m) => m,
_ => {
return fail_details(ErrorKind::Parse, "expected target to be ascii");
},
};
let version = tokens.next().ok_or(ParseError {
ctx: "request line",
details: "missing 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");
},
};
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)?;
Ok((
amt,
line_amt,
RequestLine {
method,
target,
@ -71,72 +101,44 @@ pub fn request_line(d: &[u8]) -> Parse<RequestLine> {
},
))
}
pub fn status_line(d: &[u8]) -> Parse<StatusLine> {
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?;
let mut it = line
.split(|b| b.is_ascii_whitespace())
.filter(|bs| !bs.is_empty());
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 = 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,
},
))
}
pub fn header(d: &[u8]) -> Parse<Header> {
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 == b':').filter(|bs| !bs.is_empty());
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 (version, status) =
line.split_at(line.iter().position(|b| b.is_ascii_whitespace()).ok_or(
ParseError {
ctx: "status line",
details: "missing status",
},
};
)?);
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 (_, version) = Version::parse(version)?;
let (_, status) = Status::parse(status.trim_ascii())?;
let value = value.trim_ascii();
Ok((amt, Header::from((name, value))))
Ok((line_amt, Self { version, status }))
}
}
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 (name, value) =
line.split_at(line.iter().position(|b| *b == b':').ok_or(ParseError {
ctx: "header",
details: "missing :",
})?);
let name = from_utf8(name).map_err(|_| ParseError {
ctx: "header",
details: "name is not valid utf8",
})?;
let value = &value[1..].trim_ascii();
Ok((line_amt, Self { 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 {
use Method::*;
@ -80,7 +80,7 @@ pub struct Header<'a> {
pub name: &'a str,
pub value: &'a [u8],
}
impl<'a> Header<'a> {
impl Header<'_> {
pub fn special(&self) -> Option<HeaderSpecial> {
let Self { name, value } = self;
@ -90,15 +90,10 @@ impl<'a> Header<'a> {
{
Some(HeaderSpecial::TransferEncodingChunked)
},
_ if name.eq_ignore_ascii_case("content-length") => {
match core::str::from_utf8(value)
_ if name.eq_ignore_ascii_case("content-length") => core::str::from_utf8(value)
.ok()
.and_then(|s| s.parse().ok())
{
Some(v) => Some(HeaderSpecial::ContentLength(v)),
_ => None,
}
},
.map(HeaderSpecial::ContentLength),
_ => 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> {
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())
}
}
*/