This commit is contained in:
soup 2024-10-16 23:40:53 -04:00
parent 40177f03e5
commit 523bbd2e4c
No known key found for this signature in database
17 changed files with 333 additions and 222 deletions

40
Cargo.lock generated
View file

@ -3,15 +3,45 @@
version = 3 version = 3
[[package]] [[package]]
name = "httplz" name = "bytes"
version = "0.1.0" version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
[[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 = [ dependencies = [
"httplz-ext", "bytes",
"fnv",
"itoa",
] ]
[[package]] [[package]]
name = "httplz-ext" name = "httplz"
version = "0.1.0" version = "0.0.0"
dependencies = [ dependencies = [
"httplzx",
]
[[package]]
name = "httplzx"
version = "0.0.0"
dependencies = [
"http",
"httplz", "httplz",
] ]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"

View file

@ -1,9 +1,17 @@
[package] [package]
name = "httplz" name = "httplz"
version = "0.1.0" version = "0.0.0"
edition = "2021" edition = "2021"
license-file = ".gitignore"
description = "A sans-io, no-std HTTP implementation"
[dependencies] [workspace]
members = [
"crates/httplzx"
]
[features]
std = []
[dev-dependencies] [dev-dependencies]
httplz-ext = { path = "./crates/httplz-ext" } httplzx = { path = "./crates/httplzx" }

View file

@ -1,7 +0,0 @@
[package]
name = "httplz-ext"
version = "0.1.0"
edition = "2021"
[dependencies]
httplz = { path = "../.." }

View file

@ -1,116 +0,0 @@
use std::io::{Read, Result as IOResult};
pub struct NotEnoughSpace;
pub struct Buf<T, U> {
written_len: usize,
data: T,
_dt: core::marker::PhantomData<U>,
}
impl<T, U> Buf<T, U> {
pub fn new(data: T) -> Self {
Self {
data,
written_len: 0,
_dt: Default::default(),
}
}
}
impl<T, U> Buf<T, U>
where
T: AsRef<[U]>,
{
pub fn remaining(&self) -> &[U] {
&self.data.as_ref()[self.written_len..]
}
pub fn filled(&self) -> &[U] {
&self.data.as_ref()[..self.written_len]
}
}
impl<T, U> Buf<T, U>
where
T: AsRef<[U]>,
T: AsMut<[U]>,
{
pub fn remaining_mut(&mut self) -> &mut [U] {
&mut self.data.as_mut()[self.written_len..]
}
pub fn extend_from_slice(&mut self, s: &[U]) -> Result<(), NotEnoughSpace>
where
U: Copy,
{
if self.remaining().len() < s.len() {
return Err(NotEnoughSpace);
}
self.remaining_mut()[..s.len()].copy_from_slice(s);
self.written_len += s.len();
Ok(())
}
pub fn clear(&mut self) {
self.written_len = 0;
}
pub fn pop_front_alloc(&mut self, amt: usize)
where
U: Copy + Default,
{
let to_copy = &self.filled()[amt..];
let buf_sz = to_copy.len();
let mut buf = vec![Default::default(); buf_sz].into_boxed_slice();
buf.copy_from_slice(to_copy);
self.clear();
let _ = self.extend_from_slice(&buf);
}
pub fn pop_front(&mut self, amt: usize)
where
U: Copy,
{
if amt > self.filled().len() {
panic!("Filled not big enough");
}
let data = self.data.as_mut();
let src = &data[amt..];
let count = data.len() - amt;
// SAFETY:
// - src comes from data
// - U is copy
// - count is within bounds of data
unsafe { core::ptr::copy(src.as_ptr(), data.as_mut_ptr(), count) }
self.written_len -= amt;
}
}
impl<T> Buf<T, u8>
where
T: AsRef<[u8]> + AsMut<[u8]>,
{
pub fn read_from(&mut self, mut r: impl Read) -> IOResult<usize> {
let remaining = self.remaining_mut();
let amt = r.read(remaining)?;
self.written_len += amt;
Ok(amt)
}
}
impl<T> httplz::Write for Buf<T, u8>
where
T: AsRef<[u8]> + AsMut<[u8]>,
{
fn write(&mut self, buf: &[u8]) -> httplz::Written {
self.extend_from_slice(buf)
.map_err(|_| httplz::ErrorKind::BufNotBigEnough.into())
}
}

17
crates/httplzx/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "httplzx"
version = "0.0.0"
edition = "2021"
license-file = "Cargo.toml"
description = "Extensions for httplz"
[dependencies]
httplz = { path = "../..", version = "0.0.0" }
http = { version = "*", optional = true }
[features]
default = ["utils", "compat-http"]
utils = ["utils-buf"]
utils-buf = []
compat-http = ["dep:http"]

View file

@ -0,0 +1,22 @@
use httplz::{Event, Header, StatusLine};
use crate::ToEvents;
impl ToEvents for http::response::Parts {
fn to_events(&self) -> Vec<Event> {
let mut out = vec![StatusLine {
version: httplz::Version::HTTP1_1,
status_code: self.status.as_u16(),
status_text: self.status.canonical_reason().unwrap_or(""),
}
.into()];
out.extend(
self.headers
.iter()
.map(|(n, v)| Event::from(Header::from((n, v)))),
);
out
}
}

11
crates/httplzx/src/lib.rs Normal file
View file

@ -0,0 +1,11 @@
use httplz::Event;
#[cfg(feature = "utils")]
pub mod utils;
#[cfg(feature = "compat-http")]
pub mod compat_http;
pub trait ToEvents {
fn to_events(&self) -> Vec<Event>;
}

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,2 @@
#[cfg(feature = "utils-buf")]
pub mod buf;

View file

@ -44,7 +44,7 @@ pub fn main() -> Result<(), Box<dyn Error>> {
buf.clear(); buf.clear();
loop { loop {
let data = buf.filled(); let data = buf.filled();
let (d, r) = conn.handle_recv(data).lift(); let (d, r) = conn.poll_recv(data).lift();
match r { match r {
Err(httplz::Error { Err(httplz::Error {

View file

@ -28,7 +28,7 @@ fn main() -> Result<(), Box<dyn Error>> {
loop { loop {
let data = buf.filled(); let data = buf.filled();
let (remaining, r) = conn.handle_recv(data).lift(); let (remaining, r) = conn.poll_recv(data).lift();
match r.map_err(|e| e.kind) { match r.map_err(|e| e.kind) {
Err(httplz::ErrorKind::NeedMoreData) => { Err(httplz::ErrorKind::NeedMoreData) => {
buf.read_from(&mut stream)?; buf.read_from(&mut stream)?;

View file

@ -1,4 +1,4 @@
#[derive(Debug)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub enum ErrorKind { pub enum ErrorKind {
NeedMoreData, NeedMoreData,
InvalidConnectionState, InvalidConnectionState,
@ -9,7 +9,7 @@ pub enum ErrorKind {
BodySizeMismatch, BodySizeMismatch,
} }
#[derive(Debug)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct Error { pub struct Error {
pub kind: ErrorKind, pub kind: ErrorKind,
pub details: &'static str, pub details: &'static str,

View file

@ -1,4 +1,5 @@
#![no_std] #![cfg_attr(not(feature = "std"), no_std)]
pub mod common; pub mod common;
pub mod parse; pub mod parse;
pub mod parts; pub mod parts;
@ -6,7 +7,7 @@ pub mod util;
pub mod write; pub mod write;
pub use common::*; pub use common::*;
pub use parse::Parse; pub use parse::{NeedsMoreData, Parse};
pub use parts::*; pub use parts::*;
pub use util::*; pub use util::*;
pub use write::{Write, WriteCursor, Written}; pub use write::{Write, WriteCursor, Written};
@ -24,18 +25,6 @@ pub enum Event<'a> {
SendDone, SendDone,
} }
impl<'a> From<HeaderSpecial> for Event<'a> {
fn from(value: HeaderSpecial) -> Self {
Self::Header(value.into())
}
}
impl<'a> From<HeaderOther<'a>> for Event<'a> {
fn from(value: HeaderOther<'a>) -> Self {
Self::Header(value.into())
}
}
impl<'a> From<Header<'a>> for Event<'a> { impl<'a> From<Header<'a>> for Event<'a> {
fn from(value: Header<'a>) -> Self { fn from(value: Header<'a>) -> Self {
Self::Header(value) Self::Header(value)
@ -63,7 +52,7 @@ impl<'a> core::fmt::Display for Event<'a> {
write!(f, "{} {} {}", s.version, s.status_code, s.status_text) write!(f, "{} {} {}", s.version, s.status_code, s.status_text)
}, },
Event::Header(h) => match h { Event::Header(h) => match h {
Header::Other(HeaderOther { name, value }) => { Header::Other { name, value } => {
write!( write!(
f, f,
"{}: {}", "{}: {}",
@ -71,13 +60,11 @@ impl<'a> core::fmt::Display for Event<'a> {
core::str::from_utf8(value).unwrap_or("<binary>") core::str::from_utf8(value).unwrap_or("<binary>")
) )
}, },
Header::Special(h) => match h { Header::ContentLength(cl) => write!(f, "Content-Length: {cl}"),
HeaderSpecial::ContentLength(cl) => write!(f, "Content-Length: {cl}"), Header::TransferEncodingChunked => {
HeaderSpecial::TransferEncodingChunked => {
write!(f, "Transfer-Encoding: chunked") write!(f, "Transfer-Encoding: chunked")
}, },
}, },
},
Event::BodyChunk(b) => { Event::BodyChunk(b) => {
write!(f, "{}", core::str::from_utf8(b).unwrap_or("<binary>")) write!(f, "{}", core::str::from_utf8(b).unwrap_or("<binary>"))
}, },
@ -142,7 +129,7 @@ impl Connection {
matches!(self.state, StateConnection::Recv(_)) matches!(self.state, StateConnection::Recv(_))
} }
pub fn handle_recv<'a>(&mut self, bytes: &'a [u8]) -> Parse<'a, Event<'a>> { pub fn poll_recv<'a>(&mut self, bytes: &'a [u8]) -> Parse<'a, Event<'a>> {
let recv = match &self.state { let recv = match &self.state {
StateConnection::Recv(r) => r, StateConnection::Recv(r) => r,
_ => { _ => {
@ -180,12 +167,8 @@ impl Connection {
} else { } else {
parse::header(bytes).map2(|h| { parse::header(bytes).map2(|h| {
let b = match h { let b = match h {
Header::Special(HeaderSpecial::TransferEncodingChunked) => { Header::TransferEncodingChunked => Some(BodyState::Chunked),
Some(BodyState::Chunked) Header::ContentLength(c) => Some(BodyState::ContentLength(c)),
},
Header::Special(HeaderSpecial::ContentLength(c)) => {
Some(BodyState::ContentLength(c))
},
_ => *body_state, _ => *body_state,
}; };
@ -265,13 +248,9 @@ impl Connection {
write::header(h, w)?; write::header(h, w)?;
let bs = match h { let bs = match h {
Header::Other(_) => body_state, Header::Other { .. } => body_state,
Header::Special(h) => match h { Header::TransferEncodingChunked => Some(BodyState::Chunked),
HeaderSpecial::TransferEncodingChunked => Some(BodyState::Chunked), Header::ContentLength(cl) => Some(BodyState::ContentLength(*cl)),
HeaderSpecial::ContentLength(cl) => {
Some(BodyState::ContentLength(*cl))
},
},
}; };
Ok(StateConnection::Send(StateSend::Headers(bs))) Ok(StateConnection::Send(StateSend::Headers(bs)))

View file

@ -1,10 +1,30 @@
use crate::{ use crate::{
fail_details, fail_details,
parts::{RequestLine, Version}, parts::{RequestLine, Version},
Error, ErrorKind, Header, StatusLine, Tup, Error, ErrorKind, Header, Method, StatusLine, Tup,
}; };
pub type Parse<'a, T> = Result<(&'a [u8], T), (&'a [u8], Error)>; pub type Parse<'a, T> = Result<(&'a [u8], T), (&'a [u8], Error)>;
pub trait NeedsMoreData {
fn needs_more_data(&self) -> bool;
}
impl<'a, T> NeedsMoreData for Parse<'a, T> {
fn needs_more_data(&self) -> bool {
self.as_ref()
.err()
.map(|e| e.1.kind)
.is_some_and(|e| e == ErrorKind::NeedMoreData)
}
}
impl<T> NeedsMoreData for Result<T, Error> {
fn needs_more_data(&self) -> bool {
self.as_ref()
.err()
.map(|e| e.kind)
.is_some_and(|e| e == ErrorKind::NeedMoreData)
}
}
pub fn split_crlf(d: &[u8]) -> Option<(&[u8], &[u8])> { pub fn split_crlf(d: &[u8]) -> Option<(&[u8], &[u8])> {
let p = d.windows(2).position(|w| w == b"\r\n")?; let p = d.windows(2).position(|w| w == b"\r\n")?;
@ -36,6 +56,7 @@ pub fn request_line(d: &[u8]) -> Parse<RequestLine> {
return fail_details(ErrorKind::Parse, "expected method to be ascii"); 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 target = match core::str::from_utf8(target) {
Ok(m) => m, Ok(m) => m,

View file

@ -10,9 +10,60 @@ impl core::fmt::Display for Version {
} }
} }
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Method<'a> {
Get,
Head,
Options,
Trace,
Put,
Delete,
Post,
Patch,
Connect,
Other(&'a str),
}
impl<'a> Method<'a> {
pub fn new_from_str(s: &'a str) -> Self {
match s {
_ if s.eq_ignore_ascii_case("get") => Method::Get,
_ if s.eq_ignore_ascii_case("head") => Method::Head,
_ if s.eq_ignore_ascii_case("options") => Method::Options,
_ if s.eq_ignore_ascii_case("trace") => Method::Trace,
_ if s.eq_ignore_ascii_case("put") => Method::Put,
_ if s.eq_ignore_ascii_case("delete") => Method::Delete,
_ if s.eq_ignore_ascii_case("post") => Method::Post,
_ if s.eq_ignore_ascii_case("patch") => Method::Patch,
_ if s.eq_ignore_ascii_case("connect") => Method::Connect,
s => Method::Other(s),
}
}
}
impl<'a> core::fmt::Display for Method<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
use Method::*;
let s = match self {
Get => "GET",
Head => "HEAD",
Options => "OPTIONS",
Trace => "TRACE",
Put => "PUT",
Delete => "DELETE",
Post => "POST",
Patch => "PATCH",
Connect => "CONNECT",
Other(s) => s,
};
write!(f, "{s}")
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct RequestLine<'a> { pub struct RequestLine<'a> {
pub method: &'a str, pub method: Method<'a>,
pub target: &'a str, pub target: &'a str,
pub version: Version, pub version: Version,
} }
@ -24,49 +75,11 @@ pub struct StatusLine<'a> {
pub status_text: &'a str, pub status_text: &'a str,
} }
/// Headers that impact the state of the connection
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum HeaderSpecial {
TransferEncodingChunked,
ContentLength(usize),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct HeaderOther<'a> {
pub name: &'a str,
pub value: &'a [u8],
}
impl<'a, Name, Value> From<(&'a Name, &'a Value)> for HeaderOther<'a>
where
Name: AsRef<str> + ?Sized,
Value: AsRef<[u8]> + ?Sized,
{
fn from(value: (&'a Name, &'a Value)) -> Self {
let (name, value) = value;
let name = name.as_ref();
let value = value.as_ref();
Self { name, value }
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Header<'a> { pub enum Header<'a> {
Special(HeaderSpecial), TransferEncodingChunked,
Other(HeaderOther<'a>), ContentLength(usize),
} Other { name: &'a str, value: &'a [u8] },
impl<'a> From<HeaderSpecial> for Header<'a> {
fn from(value: HeaderSpecial) -> Self {
Self::Special(value)
}
}
impl<'a> From<HeaderOther<'a>> for Header<'a> {
fn from(value: HeaderOther<'a>) -> Self {
Self::Other(value)
}
} }
impl<'a, Name, Value> From<(&'a Name, &'a Value)> for Header<'a> impl<'a, Name, Value> From<(&'a Name, &'a Value)> for Header<'a>
@ -83,18 +96,18 @@ where
_ if name.eq_ignore_ascii_case("transfer-encoding") _ if name.eq_ignore_ascii_case("transfer-encoding")
&& value.eq_ignore_ascii_case(b"chunked") => && value.eq_ignore_ascii_case(b"chunked") =>
{ {
Header::Special(HeaderSpecial::TransferEncodingChunked) Header::TransferEncodingChunked
}, },
_ if name.eq_ignore_ascii_case("content-length") => { _ if name.eq_ignore_ascii_case("content-length") => {
match 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())
{ {
Some(v) => Header::Special(HeaderSpecial::ContentLength(v)), Some(v) => Header::ContentLength(v),
_ => Header::Other(HeaderOther { name, value }), _ => Header::Other { name, value },
} }
}, },
_ => Header::Other(HeaderOther { name, value }), _ => Header::Other { name, value },
} }
} }
} }

View file

@ -1,3 +1,5 @@
use crate::{ErrorKind, Write, Written};
pub trait ResultTupExt<A, T, B, E> { pub trait ResultTupExt<A, T, B, E> {
fn map2<U>(self, f: impl FnOnce(T) -> U) -> Result<(A, U), (B, E)>; fn map2<U>(self, f: impl FnOnce(T) -> U) -> Result<(A, U), (B, E)>;
} }
@ -33,3 +35,133 @@ impl<V, T, E> Lift<V, T, E> for Result<(V, T), (V, E)> {
} }
} }
} }
pub struct NotEnoughSpace;
pub struct Buf<T, U> {
written_len: usize,
data: T,
_dt: core::marker::PhantomData<U>,
}
impl<T, U> Buf<T, U> {
pub fn new(data: T) -> Self {
Self {
data,
written_len: 0,
_dt: Default::default(),
}
}
}
impl<T, U> Buf<T, U>
where
T: AsRef<[U]>,
{
pub fn remaining(&self) -> &[U] {
&self.data.as_ref()[self.written_len..]
}
pub fn filled(&self) -> &[U] {
&self.data.as_ref()[..self.written_len]
}
}
impl<T, U> Buf<T, U>
where
T: AsRef<[U]>,
T: AsMut<[U]>,
{
pub fn remaining_mut(&mut self) -> &mut [U] {
&mut self.data.as_mut()[self.written_len..]
}
pub fn extend_from_slice(&mut self, s: &[U]) -> Result<(), NotEnoughSpace>
where
U: Copy,
{
if self.remaining().len() < s.len() {
return Err(NotEnoughSpace);
}
self.remaining_mut()[..s.len()].copy_from_slice(s);
self.written_len += s.len();
Ok(())
}
pub fn clear(&mut self) {
self.written_len = 0;
}
pub fn pop_front(&mut self, amt: usize)
where
U: Copy,
{
if amt > self.filled().len() {
panic!("Filled not big enough");
}
let data = self.data.as_mut();
let src = &data[amt..];
let count = data.len() - amt;
// SAFETY:
// - src comes from data
// - U is copy
// - count is within bounds of data
unsafe { core::ptr::copy(src.as_ptr(), data.as_mut_ptr(), count) }
self.written_len -= amt;
}
}
#[cfg(feature = "std")]
mod buf_std_impls {
use crate::Buf;
use std::io::Read;
use std::io::Result as IOResult;
impl<T> Buf<T, u8>
where
T: AsRef<[u8]> + AsMut<[u8]>,
{
pub fn read_from(&mut self, mut r: impl Read) -> IOResult<usize> {
let remaining = self.remaining_mut();
let amt = r.read(remaining)?;
self.written_len += amt;
Ok(amt)
}
}
impl<T, U> Buf<T, U>
where
T: AsRef<[U]>,
T: AsMut<[U]>,
{
pub fn pop_front_alloc(&mut self, amt: usize)
where
U: Copy + Default,
{
let to_copy = &self.filled()[amt..];
let buf_sz = to_copy.len();
let mut buf = vec![Default::default(); buf_sz].into_boxed_slice();
buf.copy_from_slice(to_copy);
self.clear();
let _ = self.extend_from_slice(&buf);
}
}
}
#[cfg(feature = "std")]
pub use buf_std_impls::*;
impl<T> Write for Buf<T, u8>
where
T: AsRef<[u8]> + AsMut<[u8]>,
{
fn write(&mut self, buf: &[u8]) -> Written {
self.extend_from_slice(buf)
.map_err(|_| ErrorKind::BufNotBigEnough.into())
}
}

View file

@ -1,6 +1,6 @@
use core::fmt::Arguments; use core::fmt::Arguments;
use crate::{Error, ErrorKind, Header, HeaderOther, HeaderSpecial, RequestLine, StatusLine}; use crate::{Error, ErrorKind, Header, RequestLine, StatusLine};
pub struct FmtWriteAdapter<T> { pub struct FmtWriteAdapter<T> {
inner: T, inner: T,
@ -108,11 +108,9 @@ pub fn request_line(rl: &RequestLine, mut w: impl Write) -> Written {
pub fn header(h: &Header, mut w: impl Write) -> Written { pub fn header(h: &Header, mut w: impl Write) -> Written {
match h { match h {
Header::Special(h) => match h { Header::TransferEncodingChunked => write!(w, "Transfer-Encoding: Chunked"),
HeaderSpecial::TransferEncodingChunked => write!(w, "Transfer-Encoding: Chunked"), Header::ContentLength(cl) => write!(w, "Content-Length: {}", cl),
HeaderSpecial::ContentLength(cl) => write!(w, "Content-Length: {}", cl), Header::Other { name, value } => {
},
Header::Other(HeaderOther { name, value }) => {
write!(w, "{name}: ")?; write!(w, "{name}: ")?;
w.write(value) w.write(value)
}, },