Compare commits
2 commits
da94c17fe8
...
879003024c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
879003024c | ||
|
|
21e8b9c8bb |
51
Cargo.lock
generated
51
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
10
Cargo.toml
10
Cargo.toml
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
))?;
|
||||
let mut conn = httplz::Connection::new(httplz::Role::Client);
|
||||
|
||||
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)?;
|
||||
impl From<http::status::InvalidStatusCode> for Error {
|
||||
fn from(_: http::status::InvalidStatusCode) -> Self {
|
||||
Self::InvalidStatusCode
|
||||
}
|
||||
}
|
||||
|
||||
stream.write_all(buf.filled())?;
|
||||
impl From<http::header::InvalidHeaderValue> for Error {
|
||||
fn from(_: http::header::InvalidHeaderValue) -> Self {
|
||||
Self::InvalidHeaderValue
|
||||
}
|
||||
}
|
||||
|
||||
assert!(conn.is_recving());
|
||||
impl From<http::header::InvalidHeaderName> for Error {
|
||||
fn from(_: http::header::InvalidHeaderName) -> Self {
|
||||
Self::InvalidHeaderName
|
||||
}
|
||||
}
|
||||
|
||||
buf.clear();
|
||||
let (mut parts, _) = http::Response::new(()).into_parts();
|
||||
pub type FetchResult<T> = Result<T, Error>;
|
||||
|
||||
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;
|
||||
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::Response::new(()).into_parts();
|
||||
|
||||
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() {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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>> {
|
|||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
10
flake.nix
10
flake.nix
|
|
@ -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
102
src/http.rs
Normal 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],
|
||||
}
|
||||
403
src/lib.rs
403
src/lib.rs
|
|
@ -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 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 role(&self) -> Role {
|
||||
self.role
|
||||
}
|
||||
|
||||
pub fn is_sending(&self) -> bool {
|
||||
matches!(self.state, StateConnection::Send(_))
|
||||
pub fn send(&self) -> State {
|
||||
self.send
|
||||
}
|
||||
|
||||
pub fn is_recving(&self) -> bool {
|
||||
matches!(self.state, StateConnection::Recv(_))
|
||||
pub fn recv(&self) -> State {
|
||||
self.recv
|
||||
}
|
||||
|
||||
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());
|
||||
},
|
||||
};
|
||||
|
||||
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((
|
||||
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 = h
|
||||
.special()
|
||||
.map(|h| match h {
|
||||
HeaderSpecial::TransferEncodingChunked => BodyState::Chunked,
|
||||
HeaderSpecial::ContentLength(c) => BodyState::ContentLength(c),
|
||||
})
|
||||
.or(*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);
|
||||
}
|
||||
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!(),
|
||||
},
|
||||
|
||||
(_, StateRecv::ValidateDone) => {
|
||||
if bytes.is_empty() {
|
||||
Ok((
|
||||
0,
|
||||
(Event::RecvDone, StateConnection::Send(StateSend::StartLine)),
|
||||
))
|
||||
} else {
|
||||
fail(ErrorKind::TrailingBytes)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
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 = 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(())
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
self.keep_alive
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub fn new(role: Role) -> Self {
|
||||
Self {
|
||||
keep_alive: true,
|
||||
role,
|
||||
send: State::StartLine,
|
||||
recv: State::StartLine,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_iter<'a>(&'a mut self, data: &'a [u8]) -> RecvIter<'a> {
|
||||
RecvIter {
|
||||
conn: self,
|
||||
data,
|
||||
read_amt: 0,
|
||||
err: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RecvIter<'a> {
|
||||
conn: &'a mut Connection,
|
||||
data: &'a [u8],
|
||||
read_amt: usize,
|
||||
err: Option<Error>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RecvIter<'a> {
|
||||
type Item = Event<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.err.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let data = &self.data[self.read_amt..];
|
||||
|
||||
let (res, next) = match (self.conn.role, self.conn.recv) {
|
||||
(Role::Client, State::StartLine) => (
|
||||
StatusLine::parse(data).map2(Event::from),
|
||||
State::Headers(None),
|
||||
),
|
||||
|
||||
(Role::Server, State::StartLine) => (
|
||||
RequestLine::parse(data).map2(Event::from),
|
||||
State::Headers(None),
|
||||
),
|
||||
|
||||
(_, State::Headers(body_state)) => {
|
||||
if data.starts_with(LINE_ENDING) {
|
||||
(
|
||||
Ok((LINE_ENDING.len(), Event::HeadersEnd)),
|
||||
State::Body(body_state),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Header::parse(data).map2(Event::from),
|
||||
State::Headers(body_state),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
(_, State::Body(None)) => (Ok((0, Event::MessageEnd)), State::StartLine),
|
||||
(_, State::Body(Some(body_state))) => todo!(),
|
||||
};
|
||||
|
||||
let (amt, event) = match res {
|
||||
Ok(v) => v,
|
||||
Err(Some(e)) => {
|
||||
self.err = Some(e.into());
|
||||
return None;
|
||||
},
|
||||
Err(None) => {
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
self.read_amt += amt;
|
||||
self.conn.recv = next;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
232
src/parse.rs
232
src/parse.rs
|
|
@ -1,142 +1,144 @@
|
|||
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))?;
|
||||
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 mut it = line
|
||||
.split(|b| b.is_ascii_whitespace())
|
||||
.filter(|bs| !bs.is_empty());
|
||||
let method = tokens.next().ok_or(ParseError {
|
||||
ctx: "request line",
|
||||
details: "missing method",
|
||||
})?;
|
||||
|
||||
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 target = tokens.next().ok_or(ParseError {
|
||||
ctx: "request line",
|
||||
details: "missing target",
|
||||
})?;
|
||||
|
||||
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 version = tokens.next().ok_or(ParseError {
|
||||
ctx: "request line",
|
||||
details: "missing version",
|
||||
})?;
|
||||
|
||||
let target = match core::str::from_utf8(target) {
|
||||
Ok(m) => m,
|
||||
_ => {
|
||||
return fail_details(ErrorKind::Parse, "expected target to be ascii");
|
||||
},
|
||||
};
|
||||
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)?;
|
||||
|
||||
let version = match () {
|
||||
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
|
||||
_ => {
|
||||
return fail_details(ErrorKind::Parse, "unknown http version");
|
||||
},
|
||||
};
|
||||
|
||||
Ok((
|
||||
amt,
|
||||
RequestLine {
|
||||
method,
|
||||
target,
|
||||
version,
|
||||
},
|
||||
))
|
||||
Ok((
|
||||
line_amt,
|
||||
RequestLine {
|
||||
method,
|
||||
target,
|
||||
version,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status_line(d: &[u8]) -> Parse<StatusLine> {
|
||||
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.is_ascii_whitespace())
|
||||
.filter(|bs| !bs.is_empty());
|
||||
let (version, status) =
|
||||
line.split_at(line.iter().position(|b| b.is_ascii_whitespace()).ok_or(
|
||||
ParseError {
|
||||
ctx: "status line",
|
||||
details: "missing status",
|
||||
},
|
||||
)?);
|
||||
|
||||
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) = Version::parse(version)?;
|
||||
let (_, status) = Status::parse(status.trim_ascii())?;
|
||||
|
||||
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,
|
||||
},
|
||||
))
|
||||
Ok((line_amt, Self { version, status }))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(d: &[u8]) -> Parse<Header> {
|
||||
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?;
|
||||
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 mut it = line.split(|b| *b == b':').filter(|bs| !bs.is_empty());
|
||||
let (name, value) =
|
||||
line.split_at(line.iter().position(|b| *b == b':').ok_or(ParseError {
|
||||
ctx: "header",
|
||||
details: "missing :",
|
||||
})?);
|
||||
|
||||
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 = from_utf8(name).map_err(|_| ParseError {
|
||||
ctx: "header",
|
||||
details: "name is not valid utf8",
|
||||
})?;
|
||||
|
||||
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 value = &value[1..].trim_ascii();
|
||||
|
||||
let value = value.trim_ascii();
|
||||
|
||||
Ok((amt, Header::from((name, value))))
|
||||
Ok((line_amt, Self { name, value }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
src/parts.rs
17
src/parts.rs
|
|
@ -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)
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
{
|
||||
Some(v) => Some(HeaderSpecial::ContentLength(v)),
|
||||
_ => None,
|
||||
}
|
||||
},
|
||||
_ if name.eq_ignore_ascii_case("content-length") => core::str::from_utf8(value)
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.map(HeaderSpecial::ContentLength),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
src/state.rs
Normal file
1
src/state.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
1
src/test/mod.rs
Normal file
1
src/test/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
mod recv;
|
||||
162
src/test/recv.rs
Normal file
162
src/test/recv.rs
Normal 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
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue