diff --git a/Cargo.lock b/Cargo.lock index 64fb825..614cf90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,8 +7,19 @@ name = "shelves" version = "0.1.0" dependencies = [ "uhttp", + "uhttp-ext", ] [[package]] name = "uhttp" version = "0.1.0" +dependencies = [ + "uhttp-ext", +] + +[[package]] +name = "uhttp-ext" +version = "0.1.0" +dependencies = [ + "uhttp", +] diff --git a/Cargo.toml b/Cargo.toml index c21e4ec..4b42423 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ - "crates/uhttp" + "crates/uhttp", + "crates/uhttp/crates/uhttp-ext", ] resolver = "2" @@ -11,3 +12,4 @@ edition = "2021" [dependencies] uhttp = { path = "crates/uhttp" } +uhttp-ext = { path = "crates/uhttp/crates/uhttp-ext" } diff --git a/crates/uhttp/Cargo.toml b/crates/uhttp/Cargo.toml index 2057426..4a4bb87 100644 --- a/crates/uhttp/Cargo.toml +++ b/crates/uhttp/Cargo.toml @@ -4,3 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] + +[dev-dependencies] +uhttp-ext = { path = "./crates/uhttp-ext" } \ No newline at end of file diff --git a/crates/uhttp/crates/uhttp-ext/Cargo.toml b/crates/uhttp/crates/uhttp-ext/Cargo.toml new file mode 100644 index 0000000..5bdf4e4 --- /dev/null +++ b/crates/uhttp/crates/uhttp-ext/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "uhttp-ext" +version = "0.1.0" +edition = "2021" + +[dependencies] +uhttp = { path = "../.." } diff --git a/crates/uhttp/crates/uhttp-ext/src/lib.rs b/crates/uhttp/crates/uhttp-ext/src/lib.rs new file mode 100644 index 0000000..c135c02 --- /dev/null +++ b/crates/uhttp/crates/uhttp-ext/src/lib.rs @@ -0,0 +1,99 @@ +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); + + Ok(()) + } + + 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 uhttp::Write for Buf +where + T: AsRef<[u8]> + AsMut<[u8]>, +{ + fn write(&mut self, buf: &[u8]) -> uhttp::Written { + self.extend_from_slice(buf) + .map_err(|_| uhttp::ErrorKind::BufNotBigEnough.into()) + } +} diff --git a/crates/uhttp/examples/echo.rs b/crates/uhttp/examples/echo.rs new file mode 100644 index 0000000..f480b64 --- /dev/null +++ b/crates/uhttp/examples/echo.rs @@ -0,0 +1,82 @@ +//! A simple echo server that listens on 127.0.0.1:8089 + +use std::{error::Error, io::Write, net::TcpListener}; + +use uhttp::Lift; + +fn main() -> Result<(), Box> { + let listener = TcpListener::bind("127.0.0.1:8089")?; + loop { + let (mut stream, _) = listener.accept()?; + let mut conn = uhttp::Connection::new(uhttp::Role::Server); + + let mut body: Vec = Vec::new(); + let mut buf = vec![0; 1024].into_boxed_slice(); + let mut buf = uhttp_ext::Buf::new(&mut buf); + + let mut method_not_allowed = false; + + loop { + let data = buf.filled(); + let (remaining, r) = conn.handle_recv(data).lift(); + match r.map_err(|e| e.kind) { + Err(uhttp::ErrorKind::NeedMoreData) => { + buf.read_from(&mut stream)?; + continue; + }, + Err(e) => panic!("{e:?}"), + Ok(event) => { + match event { + uhttp::Event::RequestLine(r) => { + if !r.method.eq_ignore_ascii_case("post") { + method_not_allowed = true; + } + }, + uhttp::Event::RecvDone => break, + uhttp::Event::BodyChunk(b) => body.extend_from_slice(b), + _ => (), + }; + }, + }; + + let len = data.len() - remaining.len(); + buf.pop_front(len); + } + + let parts: &[uhttp::Event] = if method_not_allowed { + &[ + uhttp::Event::StatusLine(uhttp::StatusLine { + version: uhttp::Version::HTTP1_1, + status_code: 405, + status_text: "Method not allowed", + }), + uhttp::Event::HeadersEnd, + uhttp::Event::SendDone, + ] + } else { + &[ + uhttp::Event::StatusLine(uhttp::StatusLine { + version: uhttp::Version::HTTP1_1, + status_code: 200, + status_text: "OK", + }), + uhttp::Event::Header(uhttp::Header::Special( + uhttp::HeaderSpecial::ContentLength(body.len()), + )), + uhttp::Event::HeadersEnd, + uhttp::Event::BodyChunk(body.as_slice()), + uhttp::Event::SendDone, + ] + }; + + let buf = vec![0; 1024]; + let mut cursor = uhttp::WriteCursor::new(buf); + for p in parts { + if let Err(e) = conn.handle_send(p, &mut cursor) { + panic!("{e:?}") + }; + stream.write_all(cursor.written())?; + cursor.reset(); + } + } +} diff --git a/crates/uhttp/src/common.rs b/crates/uhttp/src/common.rs index fa34fed..7e1460b 100644 --- a/crates/uhttp/src/common.rs +++ b/crates/uhttp/src/common.rs @@ -1,41 +1,38 @@ -use crate::Error; - -pub enum Eat { +#[derive(Debug)] +pub enum ErrorKind { NeedMoreData, - Consumed { - amt: usize, - result: Result, - }, + InvalidConnectionState, + Parse, + TrailingBytes, + BufNotBigEnough, + InvalidEventForConnectionState, + BodySizeMismatch, } -impl Eat { - pub fn map(self, f: impl FnOnce(T) -> U) -> Eat { - match self { - Eat::NeedMoreData => Eat::NeedMoreData, - Eat::Consumed { - amt, - result: Err(e), - } => Eat::Consumed { - amt, - result: Err(e), - }, - Eat::Consumed { amt, result: Ok(v) } => Eat::Consumed { - amt, - result: Ok(f(v)), - }, - } + +#[derive(Debug)] +pub struct Error { + pub kind: ErrorKind, + pub details: &'static str, +} +impl Error { + pub fn new(kind: ErrorKind) -> Self { + Self::with_details(kind, "") } - pub fn and_then(self, f: impl FnOnce(usize, T) -> Eat) -> Eat { - match self { - Eat::NeedMoreData => Eat::NeedMoreData, - Eat::Consumed { - amt, - result: Err(e), - } => Eat::Consumed { - amt, - result: Err(e), - }, - Eat::Consumed { amt, result: Ok(v) } => f(amt, v), - } + pub fn with_details(kind: ErrorKind, details: &'static str) -> Self { + Error { kind, details } } } +impl From for Error { + fn from(value: ErrorKind) -> Self { + Self::new(value) + } +} + +pub fn fail(error_kind: ErrorKind) -> Result { + fail_details(error_kind, "") +} + +pub fn fail_details(error_kind: ErrorKind, details: &'static str) -> Result { + Err(Error::with_details(error_kind, details)) +} diff --git a/crates/uhttp/src/lib.rs b/crates/uhttp/src/lib.rs index dbd45dc..5f06cf6 100644 --- a/crates/uhttp/src/lib.rs +++ b/crates/uhttp/src/lib.rs @@ -2,18 +2,16 @@ pub mod common; pub mod parse; pub mod parts; +pub mod util; +pub mod write; pub use common::*; +pub use parse::Parse; pub use parts::*; +pub use util::*; +pub use write::{Write, WriteCursor, Written}; -#[derive(Debug)] -pub enum Error { - InvalidConnectionState, - Parse(&'static str), - TrailingBytes, -} - -#[derive(Default, Debug)] +#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Event<'a> { #[default] Empty, @@ -21,7 +19,9 @@ pub enum Event<'a> { Header(Header<'a>), HeadersEnd, BodyChunk(&'a [u8]), - Done, + RecvDone, + StatusLine(StatusLine<'a>), + SendDone, } #[derive(Debug)] @@ -46,7 +46,10 @@ enum StateRecv { #[derive(Debug, Copy, Clone)] enum StateSend { - TODO, + StatusLine, + Headers(Option), + Body(BodyState), + ValidateDone, } #[derive(Debug, Copy, Clone)] @@ -64,7 +67,7 @@ impl Connection { pub fn new(role: Role) -> Self { let state = match role { Role::Server => StateConnection::Recv(StateRecv::RequestLine), - Role::Client => StateConnection::Send(StateSend::TODO), + Role::Client => StateConnection::Send(StateSend::StatusLine), }; Self { state, role } @@ -78,47 +81,43 @@ impl Connection { matches!(self.state, StateConnection::Recv(_)) } - pub fn handle_recv<'a>(&mut self, bytes: &'a [u8]) -> Eat> { + pub fn handle_recv<'a>(&mut self, bytes: &'a [u8]) -> Parse<'a, Event<'a>> { let recv = match &self.state { StateConnection::Recv(r) => r, _ => { - return Eat::Consumed { - amt: 0, - result: Err(Error::InvalidConnectionState), - } + return Err((bytes, ErrorKind::InvalidConnectionState.into())); }, }; let n = match recv { - StateRecv::RequestLine => parse::request_line(bytes).map(|rl| { + StateRecv::RequestLine => parse::request_line(bytes).map2(|rl| { ( Event::RequestLine(rl), StateConnection::Recv(StateRecv::Headers(None)), ) }), + StateRecv::Headers(body_state) => { if bytes.starts_with(b"\r\n") { - Eat::Consumed { - amt: 2, - result: Ok(( + Ok(( + &bytes[2..], + ( Event::HeadersEnd, match body_state { - None => StateConnection::Send(StateSend::TODO), - Some(b) => { - StateConnection::Recv(StateRecv::Body(*b)) - }, + None => StateConnection::Recv(StateRecv::ValidateDone), + Some(b) => StateConnection::Recv(StateRecv::Body(*b)), }, - )), - } + ), + )) } else { - parse::header(bytes).map(|h| { + 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::Special(HeaderSpecial::TransferEncodingChunked) => { + Some(BodyState::Chunked) + }, + Header::Special(HeaderSpecial::ContentLength(c)) => { + Some(BodyState::ContentLength(c)) + }, _ => *body_state, }; @@ -129,54 +128,120 @@ impl Connection { }) } }, + StateRecv::Body(body_state) => match body_state { BodyState::ContentLength(remaining) => { if bytes.len() < *remaining { - Eat::Consumed { - amt: bytes.len(), - result: Ok(( + Ok(( + &[] as &[u8], + ( Event::BodyChunk(bytes), StateConnection::Recv(StateRecv::Body( - BodyState::ContentLength( - remaining - bytes.len(), - ), + BodyState::ContentLength(remaining - bytes.len()), )), - )), - } + ), + )) } else { - Eat::Consumed { - amt: *remaining, - result: Ok(( - Event::BodyChunk(bytes), + Ok(( + &bytes[*remaining..], + ( + Event::BodyChunk(&bytes[..*remaining]), StateConnection::Recv(StateRecv::ValidateDone), - )), - } + ), + )) } }, _ => todo!(), }, + StateRecv::ValidateDone => { if bytes.is_empty() { - Eat::Consumed { - amt: 0, - result: Ok(( - Event::Done, - StateConnection::Send(StateSend::TODO), - )), - } + Ok(( + bytes, + ( + Event::RecvDone, + StateConnection::Send(StateSend::StatusLine), + ), + )) } else { - return Eat::Consumed { - amt: 0, - result: Err(Error::TrailingBytes), - }; + fail(ErrorKind::TrailingBytes).tup(bytes) } }, }; - n.map(|(ev, next_state)| { + n.map2(move |(ev, next_state)| { self.state = next_state; 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 (state, event) { + (StateSend::StatusLine, Event::StatusLine(sl)) => write::status_line(sl, w) + .map(|_| StateConnection::Send(StateSend::Headers(None))), + + (StateSend::Headers(body_state), Event::Header(h)) => { + 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)) + }, + }, + }; + + Ok(StateConnection::Send(StateSend::Headers(bs))) + }, + + (StateSend::Headers(body_state), Event::HeadersEnd) => { + 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::RequestLine)) + }, + + _ => return Err(Error::from(ErrorKind::InvalidEventForConnectionState)), + }?; + + self.state = next; + + Ok(()) + } } diff --git a/crates/uhttp/src/parse.rs b/crates/uhttp/src/parse.rs index f577f45..834b423 100644 --- a/crates/uhttp/src/parse.rs +++ b/crates/uhttp/src/parse.rs @@ -1,133 +1,113 @@ use crate::{ - common::Eat, + fail_details, parts::{RequestLine, Version}, - Error, Header, HeaderOther, HeaderSpecial, + Error, ErrorKind, Header, HeaderOther, HeaderSpecial, Tup, }; -pub type Parse = Eat; +pub type Parse<'a, T> = Result<(&'a [u8], T), (&'a [u8], Error)>; -pub fn split_crlf(d: &[u8]) -> Option<(&[u8], usize)> { +pub fn split_crlf(d: &[u8]) -> Option<(&[u8], &[u8])> { let p = d.windows(2).position(|w| w == b"\r\n")?; - Some((&d[..p], p + 2)) + Some((&d[..p], &d[p + 2..])) } pub fn request_line(d: &[u8]) -> Parse { - let (line, amt) = match split_crlf(d) { - Some(l) => l, - None => { - return Parse::NeedMoreData; - }, - }; + let (line, rest) = split_crlf(d).ok_or((d, ErrorKind::NeedMoreData.into()))?; - let mut it = line - .split(|b| b.is_ascii_whitespace()) - .filter(|bs| !bs.is_empty()); + let go = || { + let mut it = 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 Parse::Consumed { - amt, - result: Err(Error::Parse( + 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 = match core::str::from_utf8(method) { - Ok(m) => m, - _ => { - return Parse::Consumed { - amt, - result: Err(Error::Parse("expected method to be ascii")), - } - }, - }; + let method = match core::str::from_utf8(method) { + Ok(m) => m, + _ => { + return fail_details(ErrorKind::Parse, "expected method to be ascii"); + }, + }; - let target = match core::str::from_utf8(target) { - Ok(m) => m, - _ => { - return Parse::Consumed { - amt, - result: Err(Error::Parse("expected target to be ascii")), - } - }, - }; + let target = match core::str::from_utf8(target) { + Ok(m) => m, + _ => { + return fail_details(ErrorKind::Parse, "expected target to be ascii"); + }, + }; - let version = match () { - _ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1, - _ => { - return Parse::Consumed { - amt, - result: Err(Error::Parse("unknown http 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"); + }, + }; - Parse::Consumed { - amt, - result: Ok(RequestLine { + Ok(RequestLine { method, target, version, - }), - } + }) + }; + + go().tup(rest) } pub fn header(d: &[u8]) -> Parse
{ - let (line, amt) = match split_crlf(d) { - Some(l) => l, - None => { - return Parse::NeedMoreData; - }, - }; + let (line, rest) = split_crlf(d).ok_or((d, ErrorKind::NeedMoreData.into()))?; - let mut it = line - .split(|b| b.is_ascii_whitespace()) - .filter(|bs| !bs.is_empty()); + let go = || { + let mut it = line + .split(|b| b.is_ascii_whitespace()) + .filter(|bs| !bs.is_empty()); - let (name, value) = match (it.next(), it.next()) { - (Some(n), Some(v)) => (n, v), - _ => { - return Parse::Consumed { - amt, - result: Err(Error::Parse( + let (name, value) = match (it.next(), it.next()) { + (Some(n), Some(v)) => (n, v), + _ => { + return fail_details( + ErrorKind::Parse, "header doesn't have required number of elements", - )), - } - }, - }; + ); + }, + }; - let name = match core::str::from_utf8(name) { - Ok(m) => m, - _ => { - return Parse::Consumed { - amt, - result: Err(Error::Parse("expected target to be ascii")), - } - }, - }; - let name = name.split_once(":").map(|(f, _)| f).unwrap_or(name); + let name = match core::str::from_utf8(name) { + Ok(m) => m, + _ => return fail_details(ErrorKind::Parse, "expected target to be ascii"), + }; + let name = name + .split_once(":") + .map(|(f, _)| f) + .ok_or(Error::with_details(ErrorKind::Parse, "invalid header name"))?; - let h = match () { - _ if name.eq_ignore_ascii_case("transfer-encoding") - && value.eq_ignore_ascii_case(b"chunked") => - { - Header::Special(HeaderSpecial::TransferEncodingChunked) - }, - _ if name.eq_ignore_ascii_case("content-length") => { - match core::str::from_utf8(value) - .ok() - .and_then(|s| s.parse().ok()) + let h = match () { + _ if name.eq_ignore_ascii_case("transfer-encoding") + && value.eq_ignore_ascii_case(b"chunked") => { - Some(v) => Header::Special(HeaderSpecial::ContentLength(v)), - _ => Header::Other(HeaderOther { name, value }), - } - }, - _ => Header::Other(HeaderOther { name, value }), + Header::Special(HeaderSpecial::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 }), + } + }, + _ => Header::Other(HeaderOther { name, value }), + }; + + Ok(h) }; - Parse::Consumed { amt, result: Ok(h) } + go().tup(rest) } diff --git a/crates/uhttp/src/parts.rs b/crates/uhttp/src/parts.rs index 0005879..6513b04 100644 --- a/crates/uhttp/src/parts.rs +++ b/crates/uhttp/src/parts.rs @@ -1,29 +1,43 @@ -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Version { HTTP1_1, } +impl core::fmt::Display for Version { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Version::HTTP1_1 => write!(f, "HTTP/1.1"), + } + } +} -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct RequestLine<'a> { pub method: &'a str, pub target: &'a str, pub version: Version, } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct StatusLine<'a> { + pub version: Version, + pub status_code: u16, + pub status_text: &'a str, +} + /// Headers that impact the state of the connection -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum HeaderSpecial { TransferEncodingChunked, ContentLength(usize), } -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct HeaderOther<'a> { pub name: &'a str, pub value: &'a [u8], } -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Header<'a> { Special(HeaderSpecial), Other(HeaderOther<'a>), diff --git a/crates/uhttp/src/util.rs b/crates/uhttp/src/util.rs new file mode 100644 index 0000000..31414a6 --- /dev/null +++ b/crates/uhttp/src/util.rs @@ -0,0 +1,37 @@ +use crate::{ErrorKind, Write}; + +pub trait ResultTupExt { + fn map2(self, f: impl FnOnce(T) -> U) -> Result<(A, U), (B, E)>; +} +impl ResultTupExt for Result<(A, T), (B, E)> { + fn map2(self, f: impl FnOnce(T) -> U) -> Result<(A, U), (B, E)> { + match self { + Ok((a, t)) => Ok((a, f(t))), + Err((b, e)) => Err((b, e)), + } + } +} + +pub trait Tup { + fn tup(self, v: V) -> Result<(V, T), (V, E)>; +} +impl Tup for Result { + fn tup(self, v: V) -> Result<(V, T), (V, E)> { + match self { + Ok(t) => Ok((v, t)), + Err(e) => Err((v, e)), + } + } +} + +pub trait Lift { + fn lift(self) -> (V, Result); +} +impl Lift for Result<(V, T), (V, E)> { + fn lift(self) -> (V, Result) { + match self { + Ok((v, t)) => (v, Ok(t)), + Err((v, e)) => (v, Err(e)), + } + } +} diff --git a/crates/uhttp/src/write.rs b/crates/uhttp/src/write.rs new file mode 100644 index 0000000..b972ec6 --- /dev/null +++ b/crates/uhttp/src/write.rs @@ -0,0 +1,118 @@ +use core::fmt::Arguments; + +use crate::{Error, ErrorKind, Header, HeaderOther, HeaderSpecial, StatusLine}; + +pub struct FmtWriteAdapter { + inner: T, + err: Written, +} +impl FmtWriteAdapter { + pub fn new(inner: T) -> Self { + Self { inner, err: Ok(()) } + } +} + +impl core::fmt::Write for FmtWriteAdapter +where + T: Write + Sized, +{ + fn write_str(&mut self, s: &str) -> core::fmt::Result { + self.inner.write(s.as_bytes()).map_err(|e| { + self.err = Err(e); + core::fmt::Error + }) + } +} + +pub trait Write { + fn write(&mut self, buf: &[u8]) -> Written; + + fn write_fmt(&mut self, args: Arguments<'_>) -> Written + where + Self: Sized, + { + let mut adp = FmtWriteAdapter::new(self); + let _ = core::fmt::Write::write_fmt(&mut adp, args); + + adp.err + } +} + +impl Write for &mut T +where + T: Write, +{ + fn write(&mut self, buf: &[u8]) -> Written { + (**self).write(buf) + } +} + +pub struct WriteCursor { + at: usize, + buf: T, +} +impl WriteCursor { + pub fn new(buf: T) -> Self { + Self { at: 0, buf } + } + + pub fn reset(&mut self) { + self.at = 0; + } + + pub fn remaining_mut(&mut self) -> &mut [u8] + where + T: AsMut<[u8]>, + { + &mut self.buf.as_mut()[self.at..] + } + + pub fn written(&mut self) -> &[u8] + where + T: AsRef<[u8]>, + { + &self.buf.as_ref()[..self.at] + } +} + +impl Write for WriteCursor +where + T: AsMut<[u8]>, +{ + fn write(&mut self, buf: &[u8]) -> Written { + let remaining = self.remaining_mut(); + if buf.len() > remaining.len() { + return Err(Error::from(ErrorKind::BufNotBigEnough)); + } + + remaining[..buf.len()].copy_from_slice(buf); + self.at += buf.len(); + + Ok(()) + } +} + +pub type Written = Result<(), Error>; + +pub fn status_line(sl: &StatusLine, mut w: impl Write) -> Written { + write!( + w, + "{} {} {}\r\n", + sl.version, sl.status_code, sl.status_text + ) +} + +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 }) => { + write!(w, "{name}: ")?; + w.write(value) + }, + }?; + + write!(w, "\r\n") +} diff --git a/rustfmt.toml b/rustfmt.toml index 7000591..cb6146a 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,5 +1,5 @@ edition = "2021" hard_tabs = true match_block_trailing_comma = true -max_width = 80 +max_width = 95 empty_item_single_line = false diff --git a/src/main.rs b/src/main.rs index 8477007..09486cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,34 +1,3 @@ -use std::{error::Error, io::Read, net::TcpListener}; - -fn main() -> Result<(), Box> { - let listener = TcpListener::bind("127.0.0.1:8089")?; - loop { - let (mut stream, _) = listener.accept()?; - let mut buf = vec![0; 1024]; - - let mut conn = uhttp::Connection::new(uhttp::Role::Server); - - let end = stream.read(&mut buf)?; - let mut data = &buf[..end]; - - loop { - dbg!(&conn, data); - match conn.handle_recv(data) { - uhttp::Eat::NeedMoreData => todo!(), - uhttp::Eat::Consumed { amt, result } => match result { - Ok(ev) => match ev { - uhttp::Event::Done => break, - _ => { - dbg!(ev); - data = &data[amt..]; - }, - }, - - Err(e) => { - dbg!(e); - }, - }, - } - } - } +fn main() { + println!("hello, world"); }