Fetchplz!
This commit is contained in:
parent
523bbd2e4c
commit
da94c17fe8
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -8,6 +8,15 @@ version = "1.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fetchplz"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
"httplz",
|
||||||
|
"httplzx",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
|
|
||||||
10
Cargo.toml
10
Cargo.toml
|
|
@ -6,12 +6,18 @@ license-file = ".gitignore"
|
||||||
description = "A sans-io, no-std HTTP implementation"
|
description = "A sans-io, no-std HTTP implementation"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
"crates/fetchplz",
|
||||||
"crates/httplzx"
|
"crates/httplzx"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
httplz = { path = ".", features = ["std"] }
|
||||||
|
httplzx = { path = "./crates/httplzx" }
|
||||||
|
fetchplz = { path = "./crates/fetchplz" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
std = []
|
std = []
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
httplzx = { path = "./crates/httplzx" }
|
httplzx = { path = "./crates/httplzx" }
|
||||||
|
|
|
||||||
9
crates/fetchplz/Cargo.toml
Normal file
9
crates/fetchplz/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "fetchplz"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
httplz = { workspace = true }
|
||||||
|
httplzx = { workspace = true }
|
||||||
|
http = { version = "1.1.0" }
|
||||||
181
crates/fetchplz/src/lib.rs
Normal file
181
crates/fetchplz/src/lib.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
use std::io::{Error as IOError, Read, Write};
|
||||||
|
use std::net::TcpStream;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use httplz::NeedsMoreData;
|
||||||
|
use httplzx::ToEvents;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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,
|
||||||
|
InvalidStatusCode,
|
||||||
|
InvalidHeaderValue,
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.write_all(buf.filled())?;
|
||||||
|
|
||||||
|
assert!(conn.is_recving());
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
let (mut parts, _) = http::Response::new(()).into_parts();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(http::Response::from_parts(
|
||||||
|
parts,
|
||||||
|
Body {
|
||||||
|
buf,
|
||||||
|
stream,
|
||||||
|
conn,
|
||||||
|
chunk_end: 0,
|
||||||
|
read_amt: 0,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_fetch {
|
||||||
|
use crate::fetch;
|
||||||
|
|
||||||
|
#[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")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
crates/fetchplz/testing/snapshots/httpbin_get_empty.txt
Normal file
11
crates/fetchplz/testing/snapshots/httpbin_get_empty.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"args": {},
|
||||||
|
"headers": {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Host": "httpbin.org",
|
||||||
|
"User-Agent": "curl/8.7.1",
|
||||||
|
"X-Amzn-Trace-Id": "Root=1-6711c17e-1fdd277564daedff276ec60a"
|
||||||
|
},
|
||||||
|
"origin": "72.94.94.124",
|
||||||
|
"url": "https://httpbin.org/get"
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use httplz::{Event, Header, StatusLine};
|
use httplz::{Event, Header, RequestLine, StatusLine};
|
||||||
|
|
||||||
use crate::ToEvents;
|
use crate::ToEvents;
|
||||||
|
|
||||||
|
|
@ -20,3 +20,26 @@ impl ToEvents for http::response::Parts {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToEvents for http::request::Parts {
|
||||||
|
fn to_events(&self) -> Vec<Event> {
|
||||||
|
let mut out = vec![RequestLine {
|
||||||
|
version: httplz::Version::HTTP1_1,
|
||||||
|
method: httplz::Method::new_from_str(self.method.as_str()),
|
||||||
|
target: self
|
||||||
|
.uri
|
||||||
|
.path_and_query()
|
||||||
|
.map(|pq| pq.as_str())
|
||||||
|
.unwrap_or_else(|| self.uri.path()),
|
||||||
|
}
|
||||||
|
.into()];
|
||||||
|
|
||||||
|
out.extend(
|
||||||
|
self.headers
|
||||||
|
.iter()
|
||||||
|
.map(|(n, v)| Event::from(Header::from((n, v)))),
|
||||||
|
);
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
use std::{error::Error, io::Write, net::TcpListener};
|
use std::{error::Error, io::Write, net::TcpListener};
|
||||||
|
|
||||||
use httplz::Lift;
|
use httplz::NeedsMoreData;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = std::env::args().skip(1);
|
||||||
|
|
@ -21,38 +21,34 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let mut conn = httplz::Connection::new(httplz::Role::Server);
|
let mut conn = httplz::Connection::new(httplz::Role::Server);
|
||||||
|
|
||||||
let mut body: Vec<u8> = Vec::new();
|
let mut body: Vec<u8> = Vec::new();
|
||||||
let mut buf = vec![0; 1024].into_boxed_slice();
|
let mut buf = httplz::Buf::new(vec![0; 1024].into_boxed_slice());
|
||||||
let mut buf = httplz_ext::Buf::new(&mut buf);
|
|
||||||
|
|
||||||
let mut method_not_allowed = false;
|
let mut method_not_allowed = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let data = buf.filled();
|
let data = buf.filled();
|
||||||
let (remaining, r) = conn.poll_recv(data).lift();
|
let r = conn.poll_recv(data);
|
||||||
match r.map_err(|e| e.kind) {
|
if r.needs_more_data() {
|
||||||
Err(httplz::ErrorKind::NeedMoreData) => {
|
buf.read_from(&mut stream)?;
|
||||||
buf.read_from(&mut stream)?;
|
continue;
|
||||||
continue;
|
}
|
||||||
},
|
|
||||||
Err(e) => panic!("{e:?}"),
|
let (amt, event) = r.unwrap();
|
||||||
Ok(event) => {
|
match event {
|
||||||
match event {
|
httplz::Event::RequestLine(r) => {
|
||||||
httplz::Event::RequestLine(r) => {
|
if r.method != httplz::Method::Post {
|
||||||
if !r.method.eq_ignore_ascii_case("post") {
|
method_not_allowed = true;
|
||||||
method_not_allowed = true;
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
httplz::Event::RecvDone => break,
|
|
||||||
httplz::Event::BodyChunk(b) => body.extend_from_slice(b),
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
httplz::Event::RecvDone => break,
|
||||||
|
httplz::Event::BodyChunk(b) => body.extend_from_slice(b),
|
||||||
|
_ => (),
|
||||||
};
|
};
|
||||||
|
|
||||||
let len = data.len() - remaining.len();
|
buf.pop_front(amt);
|
||||||
buf.pop_front(len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let body_len = format!("{}", body.len());
|
||||||
let parts: &[httplz::Event] = if method_not_allowed {
|
let parts: &[httplz::Event] = if method_not_allowed {
|
||||||
&[
|
&[
|
||||||
httplz::Event::StatusLine(httplz::StatusLine {
|
httplz::Event::StatusLine(httplz::StatusLine {
|
||||||
|
|
@ -70,9 +66,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
status_code: 200,
|
status_code: 200,
|
||||||
status_text: "OK",
|
status_text: "OK",
|
||||||
}),
|
}),
|
||||||
httplz::Event::Header(httplz::Header::Special(
|
httplz::Header {
|
||||||
httplz::HeaderSpecial::ContentLength(body.len()),
|
name: "Content-Length",
|
||||||
)),
|
value: body_len.as_bytes(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
httplz::Event::HeadersDone,
|
httplz::Event::HeadersDone,
|
||||||
httplz::Event::BodyChunk(body.as_slice()),
|
httplz::Event::BodyChunk(body.as_slice()),
|
||||||
httplz::Event::SendDone,
|
httplz::Event::SendDone,
|
||||||
|
|
|
||||||
58
src/lib.rs
58
src/lib.rs
|
|
@ -51,20 +51,12 @@ impl<'a> core::fmt::Display for Event<'a> {
|
||||||
Event::StatusLine(s) => {
|
Event::StatusLine(s) => {
|
||||||
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(Header { name, value }) => write!(
|
||||||
Header::Other { name, value } => {
|
f,
|
||||||
write!(
|
"{}: {}",
|
||||||
f,
|
name,
|
||||||
"{}: {}",
|
core::str::from_utf8(value).unwrap_or("<binary>")
|
||||||
name,
|
),
|
||||||
core::str::from_utf8(value).unwrap_or("<binary>")
|
|
||||||
)
|
|
||||||
},
|
|
||||||
Header::ContentLength(cl) => write!(f, "Content-Length: {cl}"),
|
|
||||||
Header::TransferEncodingChunked => {
|
|
||||||
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>"))
|
||||||
},
|
},
|
||||||
|
|
@ -133,7 +125,7 @@ impl Connection {
|
||||||
let recv = match &self.state {
|
let recv = match &self.state {
|
||||||
StateConnection::Recv(r) => r,
|
StateConnection::Recv(r) => r,
|
||||||
_ => {
|
_ => {
|
||||||
return Err((bytes, ErrorKind::InvalidConnectionState.into()));
|
return Err(ErrorKind::InvalidConnectionState.into());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -155,7 +147,7 @@ impl Connection {
|
||||||
(_, StateRecv::Headers(body_state)) => {
|
(_, StateRecv::Headers(body_state)) => {
|
||||||
if bytes.starts_with(b"\r\n") {
|
if bytes.starts_with(b"\r\n") {
|
||||||
Ok((
|
Ok((
|
||||||
&bytes[2..],
|
2,
|
||||||
(
|
(
|
||||||
Event::HeadersDone,
|
Event::HeadersDone,
|
||||||
match body_state {
|
match body_state {
|
||||||
|
|
@ -166,11 +158,13 @@ impl Connection {
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
parse::header(bytes).map2(|h| {
|
parse::header(bytes).map2(|h| {
|
||||||
let b = match h {
|
let b = h
|
||||||
Header::TransferEncodingChunked => Some(BodyState::Chunked),
|
.special()
|
||||||
Header::ContentLength(c) => Some(BodyState::ContentLength(c)),
|
.map(|h| match h {
|
||||||
_ => *body_state,
|
HeaderSpecial::TransferEncodingChunked => BodyState::Chunked,
|
||||||
};
|
HeaderSpecial::ContentLength(c) => BodyState::ContentLength(c),
|
||||||
|
})
|
||||||
|
.or(*body_state);
|
||||||
|
|
||||||
(
|
(
|
||||||
Event::Header(h),
|
Event::Header(h),
|
||||||
|
|
@ -183,11 +177,11 @@ impl Connection {
|
||||||
(_, StateRecv::Body(body_state)) => match body_state {
|
(_, StateRecv::Body(body_state)) => match body_state {
|
||||||
BodyState::ContentLength(remaining) => {
|
BodyState::ContentLength(remaining) => {
|
||||||
if bytes.is_empty() && *remaining != 0 {
|
if bytes.is_empty() && *remaining != 0 {
|
||||||
return fail(ErrorKind::NeedMoreData).tup(bytes);
|
return fail(ErrorKind::NeedMoreData);
|
||||||
}
|
}
|
||||||
if bytes.len() < *remaining {
|
if bytes.len() < *remaining {
|
||||||
Ok((
|
Ok((
|
||||||
&[] as &[u8],
|
bytes.len(),
|
||||||
(
|
(
|
||||||
Event::BodyChunk(bytes),
|
Event::BodyChunk(bytes),
|
||||||
StateConnection::Recv(StateRecv::Body(
|
StateConnection::Recv(StateRecv::Body(
|
||||||
|
|
@ -197,7 +191,7 @@ impl Connection {
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok((
|
Ok((
|
||||||
&bytes[*remaining..],
|
*remaining,
|
||||||
(
|
(
|
||||||
Event::BodyChunk(&bytes[..*remaining]),
|
Event::BodyChunk(&bytes[..*remaining]),
|
||||||
StateConnection::Recv(StateRecv::ValidateDone),
|
StateConnection::Recv(StateRecv::ValidateDone),
|
||||||
|
|
@ -211,11 +205,11 @@ impl Connection {
|
||||||
(_, StateRecv::ValidateDone) => {
|
(_, StateRecv::ValidateDone) => {
|
||||||
if bytes.is_empty() {
|
if bytes.is_empty() {
|
||||||
Ok((
|
Ok((
|
||||||
bytes,
|
0,
|
||||||
(Event::RecvDone, StateConnection::Send(StateSend::StartLine)),
|
(Event::RecvDone, StateConnection::Send(StateSend::StartLine)),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
fail(ErrorKind::TrailingBytes).tup(bytes)
|
fail(ErrorKind::TrailingBytes)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -247,11 +241,13 @@ impl Connection {
|
||||||
(_, StateSend::Headers(body_state), Event::Header(h)) => {
|
(_, StateSend::Headers(body_state), Event::Header(h)) => {
|
||||||
write::header(h, w)?;
|
write::header(h, w)?;
|
||||||
|
|
||||||
let bs = match h {
|
let bs = h
|
||||||
Header::Other { .. } => body_state,
|
.special()
|
||||||
Header::TransferEncodingChunked => Some(BodyState::Chunked),
|
.map(|h| match h {
|
||||||
Header::ContentLength(cl) => Some(BodyState::ContentLength(*cl)),
|
HeaderSpecial::TransferEncodingChunked => BodyState::Chunked,
|
||||||
};
|
HeaderSpecial::ContentLength(cl) => BodyState::ContentLength(cl),
|
||||||
|
})
|
||||||
|
.or(body_state);
|
||||||
|
|
||||||
Ok(StateConnection::Send(StateSend::Headers(bs)))
|
Ok(StateConnection::Send(StateSend::Headers(bs)))
|
||||||
},
|
},
|
||||||
|
|
|
||||||
197
src/parse.rs
197
src/parse.rs
|
|
@ -1,23 +1,14 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
fail_details,
|
fail_details,
|
||||||
parts::{RequestLine, Version},
|
parts::{RequestLine, Version},
|
||||||
Error, ErrorKind, Header, Method, StatusLine, Tup,
|
Error, ErrorKind, Header, Method, StatusLine,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type Parse<'a, T> = Result<(&'a [u8], T), (&'a [u8], Error)>;
|
pub type Parse<'a, T> = Result<(usize, T), Error>;
|
||||||
pub trait NeedsMoreData {
|
pub trait NeedsMoreData {
|
||||||
fn needs_more_data(&self) -> bool;
|
fn needs_more_data(&self) -> bool;
|
||||||
}
|
}
|
||||||
impl<'a, T> NeedsMoreData for Parse<'a, T> {
|
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 {
|
fn needs_more_data(&self) -> bool {
|
||||||
self.as_ref()
|
self.as_ref()
|
||||||
.err()
|
.err()
|
||||||
|
|
@ -26,132 +17,126 @@ impl<T> NeedsMoreData for Result<T, Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn split_crlf(d: &[u8]) -> Option<(&[u8], &[u8])> {
|
pub fn split_crlf(d: &[u8]) -> Option<(&[u8], usize)> {
|
||||||
let p = d.windows(2).position(|w| w == b"\r\n")?;
|
let p = d.windows(2).position(|w| w == b"\r\n")?;
|
||||||
|
|
||||||
Some((&d[..p], &d[p + 2..]))
|
Some((&d[..p], p + 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_line(d: &[u8]) -> Parse<RequestLine> {
|
pub fn request_line(d: &[u8]) -> Parse<RequestLine> {
|
||||||
let (line, rest) = split_crlf(d).ok_or((d, ErrorKind::NeedMoreData.into()))?;
|
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?;
|
||||||
|
|
||||||
let go = || {
|
let mut it = line
|
||||||
let mut it = line
|
.split(|b| b.is_ascii_whitespace())
|
||||||
.split(|b| b.is_ascii_whitespace())
|
.filter(|bs| !bs.is_empty());
|
||||||
.filter(|bs| !bs.is_empty());
|
|
||||||
|
|
||||||
let (method, target, version) = match (it.next(), it.next(), it.next()) {
|
let (method, target, version) = match (it.next(), it.next(), it.next()) {
|
||||||
(Some(m), Some(t), Some(v)) => (m, t, v),
|
(Some(m), Some(t), Some(v)) => (m, t, v),
|
||||||
_ => {
|
_ => {
|
||||||
return fail_details(
|
return fail_details(
|
||||||
ErrorKind::Parse,
|
ErrorKind::Parse,
|
||||||
"request line doesn't have required number of elements",
|
"request line doesn't have required number of elements",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let method = match core::str::from_utf8(method) {
|
let method = match core::str::from_utf8(method) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
_ => {
|
_ => {
|
||||||
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 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,
|
||||||
_ => {
|
_ => {
|
||||||
return fail_details(ErrorKind::Parse, "expected target to be ascii");
|
return fail_details(ErrorKind::Parse, "expected target to be ascii");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let version = match () {
|
let version = match () {
|
||||||
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
|
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
|
||||||
_ => {
|
_ => {
|
||||||
return fail_details(ErrorKind::Parse, "unknown http version");
|
return fail_details(ErrorKind::Parse, "unknown http version");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(RequestLine {
|
Ok((
|
||||||
|
amt,
|
||||||
|
RequestLine {
|
||||||
method,
|
method,
|
||||||
target,
|
target,
|
||||||
version,
|
version,
|
||||||
})
|
},
|
||||||
};
|
))
|
||||||
|
|
||||||
go().tup(rest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status_line(d: &[u8]) -> Parse<StatusLine> {
|
pub fn status_line(d: &[u8]) -> Parse<StatusLine> {
|
||||||
let (line, rest) = split_crlf(d).ok_or((d, ErrorKind::NeedMoreData.into()))?;
|
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?;
|
||||||
|
|
||||||
let go = || {
|
let mut it = line
|
||||||
let mut it = line
|
.split(|b| b.is_ascii_whitespace())
|
||||||
.split(|b| b.is_ascii_whitespace())
|
.filter(|bs| !bs.is_empty());
|
||||||
.filter(|bs| !bs.is_empty());
|
|
||||||
|
|
||||||
let (version, status_code, status_text) = match (it.next(), it.next(), it.next()) {
|
let (version, status_code, status_text) = match (it.next(), it.next(), it.next()) {
|
||||||
(Some(m), Some(t), Some(v)) => (m, t, v),
|
(Some(m), Some(t), Some(v)) => (m, t, v),
|
||||||
_ => {
|
_ => {
|
||||||
return fail_details(
|
return fail_details(
|
||||||
ErrorKind::Parse,
|
ErrorKind::Parse,
|
||||||
"status line doesn't have required number of elements",
|
"status line doesn't have required number of elements",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let version = match () {
|
let version = match () {
|
||||||
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
|
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
|
||||||
_ => {
|
_ => {
|
||||||
return fail_details(ErrorKind::Parse, "unknown http version");
|
return fail_details(ErrorKind::Parse, "unknown http version");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let status_code = core::str::from_utf8(status_code)
|
let status_code = core::str::from_utf8(status_code)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
.ok_or_else(|| Error::with_details(ErrorKind::Parse, "invalid status code"))?;
|
.ok_or_else(|| Error::with_details(ErrorKind::Parse, "invalid status code"))?;
|
||||||
|
|
||||||
let status_text = core::str::from_utf8(status_text)
|
let status_text = core::str::from_utf8(status_text)
|
||||||
.ok()
|
.ok()
|
||||||
.ok_or_else(|| Error::with_details(ErrorKind::Parse, "invalid status text"))?;
|
.ok_or_else(|| Error::with_details(ErrorKind::Parse, "invalid status text"))?;
|
||||||
|
|
||||||
Ok(StatusLine {
|
Ok((
|
||||||
|
amt,
|
||||||
|
StatusLine {
|
||||||
version,
|
version,
|
||||||
status_code,
|
status_code,
|
||||||
status_text,
|
status_text,
|
||||||
})
|
},
|
||||||
};
|
))
|
||||||
|
|
||||||
go().tup(rest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn header(d: &[u8]) -> Parse<Header> {
|
pub fn header(d: &[u8]) -> Parse<Header> {
|
||||||
let (line, rest) = split_crlf(d).ok_or((d, ErrorKind::NeedMoreData.into()))?;
|
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?;
|
||||||
|
|
||||||
let go = || {
|
let mut it = line.split(|b| *b == b':').filter(|bs| !bs.is_empty());
|
||||||
let mut it = line.split(|b| *b == b':').filter(|bs| !bs.is_empty());
|
|
||||||
|
|
||||||
let (name, value) = match (it.next(), it.next()) {
|
let (name, value) = match (it.next(), it.next()) {
|
||||||
(Some(n), Some(v)) => (n, v),
|
(Some(n), Some(v)) => (n, v),
|
||||||
_ => {
|
_ => {
|
||||||
return fail_details(
|
return fail_details(
|
||||||
ErrorKind::Parse,
|
ErrorKind::Parse,
|
||||||
"header doesn't have required number of elements",
|
"header doesn't have required number of elements",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
|
||||||
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.trim_ascii();
|
|
||||||
|
|
||||||
Ok(Header::from((name, value)))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
go().tup(rest)
|
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.trim_ascii();
|
||||||
|
|
||||||
|
Ok((amt, Header::from((name, value))))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
57
src/parts.rs
57
src/parts.rs
|
|
@ -76,10 +76,38 @@ pub struct StatusLine<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
pub enum Header<'a> {
|
pub struct Header<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub value: &'a [u8],
|
||||||
|
}
|
||||||
|
impl<'a> Header<'a> {
|
||||||
|
pub fn special(&self) -> Option<HeaderSpecial> {
|
||||||
|
let Self { name, value } = self;
|
||||||
|
|
||||||
|
match () {
|
||||||
|
_ if name.eq_ignore_ascii_case("transfer-encoding")
|
||||||
|
&& value.eq_ignore_ascii_case(b"chunked") =>
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
|
pub enum HeaderSpecial {
|
||||||
TransferEncodingChunked,
|
TransferEncodingChunked,
|
||||||
ContentLength(usize),
|
ContentLength(usize),
|
||||||
Other { name: &'a str, value: &'a [u8] },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Name, Value> From<(&'a Name, &'a Value)> for Header<'a>
|
impl<'a, Name, Value> From<(&'a Name, &'a Value)> for Header<'a>
|
||||||
|
|
@ -87,27 +115,10 @@ where
|
||||||
Name: AsRef<str> + ?Sized,
|
Name: AsRef<str> + ?Sized,
|
||||||
Value: AsRef<[u8]> + ?Sized,
|
Value: AsRef<[u8]> + ?Sized,
|
||||||
{
|
{
|
||||||
fn from(value: (&'a Name, &'a Value)) -> Self {
|
fn from((name, value): (&'a Name, &'a Value)) -> Self {
|
||||||
let (name, value) = value;
|
Header {
|
||||||
let name = name.as_ref();
|
name: name.as_ref(),
|
||||||
let value = value.as_ref();
|
value: value.as_ref(),
|
||||||
|
|
||||||
match () {
|
|
||||||
_ if name.eq_ignore_ascii_case("transfer-encoding")
|
|
||||||
&& value.eq_ignore_ascii_case(b"chunked") =>
|
|
||||||
{
|
|
||||||
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::ContentLength(v),
|
|
||||||
_ => Header::Other { name, value },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => Header::Other { name, value },
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
src/util.rs
34
src/util.rs
|
|
@ -1,37 +1,13 @@
|
||||||
use crate::{ErrorKind, Write, Written};
|
use crate::{ErrorKind, Write, Written};
|
||||||
|
|
||||||
pub trait ResultTupExt<A, T, B, E> {
|
pub trait ResultTupExt<A, T, 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), E>;
|
||||||
}
|
}
|
||||||
impl<A, T, B, E> ResultTupExt<A, T, B, E> for Result<(A, T), (B, E)> {
|
impl<A, T, E> ResultTupExt<A, T, E> for Result<(A, T), 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), E> {
|
||||||
match self {
|
match self {
|
||||||
Ok((a, t)) => Ok((a, f(t))),
|
Ok((a, t)) => Ok((a, f(t))),
|
||||||
Err((b, e)) => Err((b, e)),
|
Err(e) => Err(e),
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Tup<T, E> {
|
|
||||||
fn tup<V>(self, v: V) -> Result<(V, T), (V, E)>;
|
|
||||||
}
|
|
||||||
impl<T, E> Tup<T, E> for Result<T, E> {
|
|
||||||
fn tup<V>(self, v: V) -> Result<(V, T), (V, E)> {
|
|
||||||
match self {
|
|
||||||
Ok(t) => Ok((v, t)),
|
|
||||||
Err(e) => Err((v, e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Lift<V, T, E> {
|
|
||||||
fn lift(self) -> (V, Result<T, E>);
|
|
||||||
}
|
|
||||||
impl<V, T, E> Lift<V, T, E> for Result<(V, T), (V, E)> {
|
|
||||||
fn lift(self) -> (V, Result<T, E>) {
|
|
||||||
match self {
|
|
||||||
Ok((v, t)) => (v, Ok(t)),
|
|
||||||
Err((v, e)) => (v, Err(e)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
src/write.rs
11
src/write.rs
|
|
@ -107,14 +107,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 {
|
let Header { name, value } = h;
|
||||||
Header::TransferEncodingChunked => write!(w, "Transfer-Encoding: Chunked"),
|
write!(w, "{name}: ")?;
|
||||||
Header::ContentLength(cl) => write!(w, "Content-Length: {}", cl),
|
w.write(value)?;
|
||||||
Header::Other { name, value } => {
|
|
||||||
write!(w, "{name}: ")?;
|
|
||||||
w.write(value)
|
|
||||||
},
|
|
||||||
}?;
|
|
||||||
|
|
||||||
write!(w, "\r\n")
|
write!(w, "\r\n")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue