httplz 0.0.0

A sans-io, no-std HTTP implementation
Documentation
#![no_std]
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(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,
}

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> {
	fn from(value: Header<'a>) -> Self {
		Self::Header(value)
	}
}

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(h) => match h {
				Header::Other(HeaderOther { name, value }) => {
					write!(
						f,
						"{}: {}",
						name,
						core::str::from_utf8(value).unwrap_or("<binary>")
					)
				},
				Header::Special(h) => match h {
					HeaderSpecial::ContentLength(cl) => write!(f, "Content-Length: {cl}"),
					HeaderSpecial::TransferEncodingChunked => {
						write!(f, "Transfer-Encoding: chunked")
					},
				},
			},
			Event::BodyChunk(b) => {
				write!(f, "{}", core::str::from_utf8(b).unwrap_or("<binary>"))
			},
		}
	}
}

#[derive(Debug, Copy, Clone)]
pub enum Role {
	Client,
	Server,
}

#[derive(Debug, Copy, Clone)]
enum BodyState {
	ContentLength(usize),
	Chunked,
}

#[derive(Debug, Copy, Clone)]
enum StateRecv {
	StartLine,
	Headers(Option<BodyState>),
	Body(BodyState),
	ValidateDone,
}

#[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)]
pub struct Connection {
	role: Role,
	state: StateConnection,
}
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 }
	}

	pub fn is_sending(&self) -> bool {
		matches!(self.state, StateConnection::Send(_))
	}

	pub fn is_recving(&self) -> bool {
		matches!(self.state, StateConnection::Recv(_))
	}

	pub fn handle_recv<'a>(&mut self, bytes: &'a [u8]) -> Parse<'a, Event<'a>> {
		let recv = match &self.state {
			StateConnection::Recv(r) => r,
			_ => {
				return Err((bytes, ErrorKind::InvalidConnectionState.into()));
			},
		};

		let n = match (self.role, recv) {
			(Role::Server, StateRecv::StartLine) => parse::request_line(bytes).map2(|rl| {
				(
					Event::RequestLine(rl),
					StateConnection::Recv(StateRecv::Headers(None)),
				)
			}),

			(Role::Client, StateRecv::StartLine) => parse::status_line(bytes).map2(|rl| {
				(
					Event::StatusLine(rl),
					StateConnection::Recv(StateRecv::Headers(None)),
				)
			}),

			(_, StateRecv::Headers(body_state)) => {
				if bytes.starts_with(b"\r\n") {
					Ok((
						&bytes[2..],
						(
							Event::HeadersDone,
							match body_state {
								None => StateConnection::Recv(StateRecv::ValidateDone),
								Some(b) => StateConnection::Recv(StateRecv::Body(*b)),
							},
						),
					))
				} 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))
							},
							_ => *body_state,
						};

						(
							Event::Header(h),
							StateConnection::Recv(StateRecv::Headers(b)),
						)
					})
				}
			},

			(_, StateRecv::Body(body_state)) => match body_state {
				BodyState::ContentLength(remaining) => {
					if bytes.is_empty() && *remaining != 0 {
						return fail(ErrorKind::NeedMoreData).tup(bytes);
					}
					if bytes.len() < *remaining {
						Ok((
							&[] as &[u8],
							(
								Event::BodyChunk(bytes),
								StateConnection::Recv(StateRecv::Body(
									BodyState::ContentLength(remaining - bytes.len()),
								)),
							),
						))
					} else {
						Ok((
							&bytes[*remaining..],
							(
								Event::BodyChunk(&bytes[..*remaining]),
								StateConnection::Recv(StateRecv::ValidateDone),
							),
						))
					}
				},
				_ => todo!(),
			},

			(_, StateRecv::ValidateDone) => {
				if bytes.is_empty() {
					Ok((
						bytes,
						(Event::RecvDone, StateConnection::Send(StateSend::StartLine)),
					))
				} else {
					fail(ErrorKind::TrailingBytes).tup(bytes)
				}
			},
		};

		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 (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 = 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::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(())
	}
}