diff --git a/Cargo.lock b/Cargo.lock index 01b3893..810a086 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,15 +3,45 @@ version = 3 [[package]] -name = "httplz" -version = "0.1.0" +name = "bytes" +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 = [ - "httplz-ext", + "bytes", + "fnv", + "itoa", ] [[package]] -name = "httplz-ext" -version = "0.1.0" +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" diff --git a/Cargo.toml b/Cargo.toml index e39e36f..c7c0ae6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,17 @@ [package] name = "httplz" -version = "0.1.0" +version = "0.0.0" edition = "2021" +license-file = ".gitignore" +description = "A sans-io, no-std HTTP implementation" -[dependencies] +[workspace] +members = [ + "crates/httplzx" +] + +[features] +std = [] [dev-dependencies] -httplz-ext = { path = "./crates/httplz-ext" } \ No newline at end of file +httplzx = { path = "./crates/httplzx" } \ No newline at end of file diff --git a/crates/httplz-ext/Cargo.toml b/crates/httplz-ext/Cargo.toml deleted file mode 100644 index ef51d96..0000000 --- a/crates/httplz-ext/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "httplz-ext" -version = "0.1.0" -edition = "2021" - -[dependencies] -httplz = { path = "../.." } diff --git a/crates/httplz-ext/src/lib.rs b/crates/httplz-ext/src/lib.rs deleted file mode 100644 index 490b844..0000000 --- a/crates/httplz-ext/src/lib.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::io::{Read, Result as IOResult}; - -pub struct NotEnoughSpace; -pub struct Buf { - written_len: usize, - data: T, - _dt: core::marker::PhantomData, -} -impl Buf { - pub fn new(data: T) -> Self { - Self { - data, - written_len: 0, - _dt: Default::default(), - } - } -} - -impl Buf -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 Buf -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 Buf -where - T: AsRef<[u8]> + AsMut<[u8]>, -{ - pub fn read_from(&mut self, mut r: impl Read) -> IOResult { - let remaining = self.remaining_mut(); - let amt = r.read(remaining)?; - - self.written_len += amt; - - Ok(amt) - } -} - -impl httplz::Write for Buf -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()) - } -} diff --git a/crates/httplzx/Cargo.toml b/crates/httplzx/Cargo.toml new file mode 100644 index 0000000..feb891a --- /dev/null +++ b/crates/httplzx/Cargo.toml @@ -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"] \ No newline at end of file diff --git a/crates/httplzx/src/compat_http.rs b/crates/httplzx/src/compat_http.rs new file mode 100644 index 0000000..bce2af7 --- /dev/null +++ b/crates/httplzx/src/compat_http.rs @@ -0,0 +1,22 @@ +use httplz::{Event, Header, StatusLine}; + +use crate::ToEvents; + +impl ToEvents for http::response::Parts { + fn to_events(&self) -> Vec { + 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 + } +} diff --git a/crates/httplzx/src/lib.rs b/crates/httplzx/src/lib.rs new file mode 100644 index 0000000..ccd0376 --- /dev/null +++ b/crates/httplzx/src/lib.rs @@ -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; +} diff --git a/crates/httplzx/src/utils/buf.rs b/crates/httplzx/src/utils/buf.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/httplzx/src/utils/buf.rs @@ -0,0 +1 @@ + diff --git a/crates/httplzx/src/utils/mod.rs b/crates/httplzx/src/utils/mod.rs new file mode 100644 index 0000000..ca892ea --- /dev/null +++ b/crates/httplzx/src/utils/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "utils-buf")] +pub mod buf; diff --git a/examples/client.rs b/examples/client.rs index 41111f1..2eadf2a 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -44,7 +44,7 @@ pub fn main() -> Result<(), Box> { buf.clear(); loop { let data = buf.filled(); - let (d, r) = conn.handle_recv(data).lift(); + let (d, r) = conn.poll_recv(data).lift(); match r { Err(httplz::Error { diff --git a/examples/echo.rs b/examples/echo.rs index 3cd5b19..528a3f2 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -28,7 +28,7 @@ fn main() -> Result<(), Box> { loop { 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) { Err(httplz::ErrorKind::NeedMoreData) => { buf.read_from(&mut stream)?; diff --git a/src/common.rs b/src/common.rs index 7e1460b..17d6900 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,4 +1,4 @@ -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] pub enum ErrorKind { NeedMoreData, InvalidConnectionState, @@ -9,7 +9,7 @@ pub enum ErrorKind { BodySizeMismatch, } -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] pub struct Error { pub kind: ErrorKind, pub details: &'static str, diff --git a/src/lib.rs b/src/lib.rs index fc27802..daaa1a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -#![no_std] +#![cfg_attr(not(feature = "std"), no_std)] + pub mod common; pub mod parse; pub mod parts; @@ -6,7 +7,7 @@ pub mod util; pub mod write; pub use common::*; -pub use parse::Parse; +pub use parse::{NeedsMoreData, Parse}; pub use parts::*; pub use util::*; pub use write::{Write, WriteCursor, Written}; @@ -24,18 +25,6 @@ pub enum Event<'a> { SendDone, } -impl<'a> From for Event<'a> { - fn from(value: HeaderSpecial) -> Self { - Self::Header(value.into()) - } -} - -impl<'a> From> for Event<'a> { - fn from(value: HeaderOther<'a>) -> Self { - Self::Header(value.into()) - } -} - impl<'a> From> for Event<'a> { fn from(value: Header<'a>) -> Self { 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) }, Event::Header(h) => match h { - Header::Other(HeaderOther { name, value }) => { + Header::Other { name, value } => { write!( f, "{}: {}", @@ -71,11 +60,9 @@ impl<'a> core::fmt::Display for Event<'a> { core::str::from_utf8(value).unwrap_or("") ) }, - Header::Special(h) => match h { - HeaderSpecial::ContentLength(cl) => write!(f, "Content-Length: {cl}"), - HeaderSpecial::TransferEncodingChunked => { - write!(f, "Transfer-Encoding: chunked") - }, + Header::ContentLength(cl) => write!(f, "Content-Length: {cl}"), + Header::TransferEncodingChunked => { + write!(f, "Transfer-Encoding: chunked") }, }, Event::BodyChunk(b) => { @@ -142,7 +129,7 @@ impl Connection { 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 { StateConnection::Recv(r) => r, _ => { @@ -180,12 +167,8 @@ impl Connection { } else { parse::header(bytes).map2(|h| { let b = match h { - Header::Special(HeaderSpecial::TransferEncodingChunked) => { - Some(BodyState::Chunked) - }, - Header::Special(HeaderSpecial::ContentLength(c)) => { - Some(BodyState::ContentLength(c)) - }, + Header::TransferEncodingChunked => Some(BodyState::Chunked), + Header::ContentLength(c) => Some(BodyState::ContentLength(c)), _ => *body_state, }; @@ -265,13 +248,9 @@ impl Connection { write::header(h, w)?; let bs = match h { - Header::Other(_) => body_state, - Header::Special(h) => match h { - HeaderSpecial::TransferEncodingChunked => Some(BodyState::Chunked), - HeaderSpecial::ContentLength(cl) => { - Some(BodyState::ContentLength(*cl)) - }, - }, + Header::Other { .. } => body_state, + Header::TransferEncodingChunked => Some(BodyState::Chunked), + Header::ContentLength(cl) => Some(BodyState::ContentLength(*cl)), }; Ok(StateConnection::Send(StateSend::Headers(bs))) diff --git a/src/parse.rs b/src/parse.rs index 319de87..ccdeef5 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,10 +1,30 @@ use crate::{ fail_details, 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 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 NeedsMoreData for Result { + 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])> { let p = d.windows(2).position(|w| w == b"\r\n")?; @@ -36,6 +56,7 @@ pub fn request_line(d: &[u8]) -> Parse { 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) { Ok(m) => m, diff --git a/src/parts.rs b/src/parts.rs index 84e0887..f14d330 100644 --- a/src/parts.rs +++ b/src/parts.rs @@ -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)] pub struct RequestLine<'a> { - pub method: &'a str, + pub method: Method<'a>, pub target: &'a str, pub version: Version, } @@ -24,49 +75,11 @@ pub struct StatusLine<'a> { 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 + ?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)] pub enum Header<'a> { - Special(HeaderSpecial), - Other(HeaderOther<'a>), -} - -impl<'a> From for Header<'a> { - fn from(value: HeaderSpecial) -> Self { - Self::Special(value) - } -} - -impl<'a> From> for Header<'a> { - fn from(value: HeaderOther<'a>) -> Self { - Self::Other(value) - } + TransferEncodingChunked, + ContentLength(usize), + Other { name: &'a str, value: &'a [u8] }, } 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") && value.eq_ignore_ascii_case(b"chunked") => { - Header::Special(HeaderSpecial::TransferEncodingChunked) + Header::TransferEncodingChunked }, _ if name.eq_ignore_ascii_case("content-length") => { match core::str::from_utf8(value) .ok() .and_then(|s| s.parse().ok()) { - Some(v) => Header::Special(HeaderSpecial::ContentLength(v)), - _ => Header::Other(HeaderOther { name, value }), + Some(v) => Header::ContentLength(v), + _ => Header::Other { name, value }, } }, - _ => Header::Other(HeaderOther { name, value }), + _ => Header::Other { name, value }, } } } diff --git a/src/util.rs b/src/util.rs index ebd7e5a..634c96b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,5 @@ +use crate::{ErrorKind, Write, Written}; + pub trait ResultTupExt { fn map2(self, f: impl FnOnce(T) -> U) -> Result<(A, U), (B, E)>; } @@ -33,3 +35,133 @@ impl Lift for Result<(V, T), (V, E)> { } } } + +pub struct NotEnoughSpace; +pub struct Buf { + written_len: usize, + data: T, + _dt: core::marker::PhantomData, +} +impl Buf { + pub fn new(data: T) -> Self { + Self { + data, + written_len: 0, + _dt: Default::default(), + } + } +} + +impl Buf +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 Buf +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 Buf + where + T: AsRef<[u8]> + AsMut<[u8]>, + { + pub fn read_from(&mut self, mut r: impl Read) -> IOResult { + let remaining = self.remaining_mut(); + let amt = r.read(remaining)?; + + self.written_len += amt; + + Ok(amt) + } + } + + impl Buf + 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 Write for Buf +where + T: AsRef<[u8]> + AsMut<[u8]>, +{ + fn write(&mut self, buf: &[u8]) -> Written { + self.extend_from_slice(buf) + .map_err(|_| ErrorKind::BufNotBigEnough.into()) + } +} diff --git a/src/write.rs b/src/write.rs index ac3d2d0..7f275a3 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1,6 +1,6 @@ use core::fmt::Arguments; -use crate::{Error, ErrorKind, Header, HeaderOther, HeaderSpecial, RequestLine, StatusLine}; +use crate::{Error, ErrorKind, Header, RequestLine, StatusLine}; pub struct FmtWriteAdapter { 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 { match h { - Header::Special(h) => match h { - HeaderSpecial::TransferEncodingChunked => write!(w, "Transfer-Encoding: Chunked"), - HeaderSpecial::ContentLength(cl) => write!(w, "Content-Length: {}", cl), - }, - Header::Other(HeaderOther { name, value }) => { + Header::TransferEncodingChunked => write!(w, "Transfer-Encoding: Chunked"), + Header::ContentLength(cl) => write!(w, "Content-Length: {}", cl), + Header::Other { name, value } => { write!(w, "{name}: ")?; w.write(value) },