summaryrefslogtreecommitdiffstats
path: root/third_party/rust/bhttp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /third_party/rust/bhttp
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/bhttp')
-rw-r--r--third_party/rust/bhttp/.cargo-checksum.json1
-rw-r--r--third_party/rust/bhttp/Cargo.toml41
-rw-r--r--third_party/rust/bhttp/README.md28
-rw-r--r--third_party/rust/bhttp/src/err.rs70
-rw-r--r--third_party/rust/bhttp/src/lib.rs762
-rw-r--r--third_party/rust/bhttp/src/parse.rs75
-rw-r--r--third_party/rust/bhttp/src/rw.rs82
-rw-r--r--third_party/rust/bhttp/tests/test.rs210
8 files changed, 1269 insertions, 0 deletions
diff --git a/third_party/rust/bhttp/.cargo-checksum.json b/third_party/rust/bhttp/.cargo-checksum.json
new file mode 100644
index 0000000000..05397a2f34
--- /dev/null
+++ b/third_party/rust/bhttp/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"c0467a1857c040076f2a4be2da9f34b52bbceda73ace86ccef35acad98d0ae7b","README.md":"329e4ce4fbfabd037f6202418fbb381b6f6c77728dbe39545dae7c4f4265bae7","src/err.rs":"f0861ce8656de7b714c99fbfaea8e60e516bc8e85d4a4969c413525a1b1394b7","src/lib.rs":"112aae36a450530635dc36fcc920ebfeb52527ec1349d5a57461c36c6b5acc1e","src/parse.rs":"b4691c1e39e42ffa4a4d1a56f8c6e7b1b7a38d8cf5a1786fdcee4d7d48be1ff6","src/rw.rs":"3de0c74d4bc669918ebd5e0cfe8c134278224627db7e69c68d2c0675795adb86","tests/test.rs":"f0ed8cdfef30253ae34a9bcd3c3990ce4fda8084f3c4253e458b75ef5c1640cc"},"package":"9a561f43fe82923605345b977ebd5951126f0a1b4575e3c3d53e5954e5822de4"} \ No newline at end of file
diff --git a/third_party/rust/bhttp/Cargo.toml b/third_party/rust/bhttp/Cargo.toml
new file mode 100644
index 0000000000..d7ae9a182f
--- /dev/null
+++ b/third_party/rust/bhttp/Cargo.toml
@@ -0,0 +1,41 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+name = "bhttp"
+version = "0.2.3"
+authors = ["Martin Thomson <mt@lowentropy.net>"]
+description = "Binary HTTP messages (draft-ietf-httpbis-binary-message)"
+readme = "README.md"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/martinthomson/ohttp"
+
+[dependencies.url]
+version = "2"
+
+[dev-dependencies.hex]
+version = "0.4"
+
+[features]
+bhttp = [
+ "read-bhttp",
+ "write-bhttp",
+]
+default = ["bhttp"]
+http = [
+ "read-http",
+ "write-http",
+]
+read-bhttp = []
+read-http = []
+write-bhttp = []
+write-http = []
diff --git a/third_party/rust/bhttp/README.md b/third_party/rust/bhttp/README.md
new file mode 100644
index 0000000000..44e86164a9
--- /dev/null
+++ b/third_party/rust/bhttp/README.md
@@ -0,0 +1,28 @@
+# Binary HTTP Messages
+
+This is a rust implementation of [Binary HTTP
+Messages](https://httpwg.org/http-extensions/draft-ietf-httpbis-binary-message.html).
+
+This work is undergoing active revision in the IETF and so are these
+implementations. Use at your own risk.
+
+## Using
+
+The API documentation is currently sparse, but the API is fairly small and
+descriptive.
+
+The `bhttp` crate has the following features:
+
+- `read-bhttp` enables parsing of binary HTTP messages. This is enabled by
+ default.
+
+- `write-bhttp` enables writing of binary HTTP messages. This is enabled by
+ default.
+
+- `read-http` enables a simple HTTP/1.1 message parser. This parser is fairly
+ basic and is not recommended for production use. Getting an HTTP/1.1 parser
+ right is a massive enterprise; this one only does the basics. This is
+ disabled by default.
+
+- `write-http` enables writing of HTTP/1.1 messages. This is disabled by
+ default.
diff --git a/third_party/rust/bhttp/src/err.rs b/third_party/rust/bhttp/src/err.rs
new file mode 100644
index 0000000000..b37fa88698
--- /dev/null
+++ b/third_party/rust/bhttp/src/err.rs
@@ -0,0 +1,70 @@
+#[derive(Debug)]
+pub enum Error {
+ /// A request used the CONNECT method.
+ ConnectUnsupported,
+ /// A field contained invalid Unicode.
+ CharacterEncoding(std::string::FromUtf8Error),
+ /// A field contained an integer value that was out of range.
+ IntRange(std::num::TryFromIntError),
+ /// The mode of the message was invalid.
+ InvalidMode,
+ /// An IO error.
+ Io(std::io::Error),
+ /// A field or line was missing a necessary character.
+ Missing(u8),
+ /// A URL was missing a key component.
+ MissingUrlComponent,
+ /// An obs-fold line was the first line of a field section.
+ ObsFold,
+ /// A field contained a non-integer value.
+ ParseInt(std::num::ParseIntError),
+ /// A field was truncated.
+ Truncated,
+ /// A message included the Upgrade field.
+ UpgradeUnsupported,
+ /// A URL could not be parsed into components.
+ UrlParse(url::ParseError),
+}
+
+macro_rules! forward_errors {
+ {$($(#[$a:meta])* $t:path => $v:ident),* $(,)?} => {
+ $(
+ impl From<$t> for Error {
+ fn from(e: $t) -> Self {
+ Self::$v(e)
+ }
+ }
+ )*
+
+ impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ $( $(#[$a])* Self::$v(e) => Some(e), )*
+ _ => None,
+ }
+ }
+ }
+ };
+}
+
+forward_errors! {
+ std::io::Error => Io,
+ std::string::FromUtf8Error => CharacterEncoding,
+ std::num::ParseIntError => ParseInt,
+ std::num::TryFromIntError => IntRange,
+ url::ParseError => UrlParse,
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ write!(f, "{:?}", self)
+ }
+}
+
+#[cfg(any(
+ feature = "read-http",
+ feature = "write-http",
+ feature = "read-bhttp",
+ feature = "write-bhttp"
+))]
+pub type Res<T> = Result<T, Error>;
diff --git a/third_party/rust/bhttp/src/lib.rs b/third_party/rust/bhttp/src/lib.rs
new file mode 100644
index 0000000000..2bffd23923
--- /dev/null
+++ b/third_party/rust/bhttp/src/lib.rs
@@ -0,0 +1,762 @@
+#![deny(warnings, clippy::pedantic)]
+#![allow(clippy::missing_errors_doc)] // Too lazy to document these.
+
+#[cfg(feature = "read-bhttp")]
+use std::convert::TryFrom;
+#[cfg(any(
+ feature = "read-http",
+ feature = "write-http",
+ feature = "read-bhttp",
+ feature = "write-bhttp"
+))]
+use std::io;
+#[cfg(feature = "read-http")]
+use url::Url;
+
+mod err;
+mod parse;
+#[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))]
+mod rw;
+
+pub use err::Error;
+#[cfg(any(
+ feature = "read-http",
+ feature = "write-http",
+ feature = "read-bhttp",
+ feature = "write-bhttp"
+))]
+use err::Res;
+#[cfg(feature = "read-http")]
+use parse::{downcase, is_ows, read_line, split_at, COLON, SEMICOLON, SLASH, SP};
+use parse::{index_of, trim_ows, COMMA};
+#[cfg(feature = "read-bhttp")]
+use rw::{read_varint, read_vec};
+#[cfg(feature = "write-bhttp")]
+use rw::{write_len, write_varint, write_vec};
+
+#[cfg(feature = "read-http")]
+const CONTENT_LENGTH: &[u8] = b"content-length";
+#[cfg(feature = "read-bhttp")]
+const COOKIE: &[u8] = b"cookie";
+const TRANSFER_ENCODING: &[u8] = b"transfer-encoding";
+const CHUNKED: &[u8] = b"chunked";
+
+pub type StatusCode = u16;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))]
+pub enum Mode {
+ KnownLength,
+ IndefiniteLength,
+}
+
+pub struct Field {
+ name: Vec<u8>,
+ value: Vec<u8>,
+}
+
+impl Field {
+ #[must_use]
+ pub fn new(name: Vec<u8>, value: Vec<u8>) -> Self {
+ Self { name, value }
+ }
+
+ #[must_use]
+ pub fn name(&self) -> &[u8] {
+ &self.name
+ }
+
+ #[must_use]
+ pub fn value(&self) -> &[u8] {
+ &self.value
+ }
+
+ #[cfg(feature = "write-http")]
+ pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> {
+ w.write_all(&self.name)?;
+ w.write_all(b": ")?;
+ w.write_all(&self.value)?;
+ w.write_all(b"\r\n")?;
+ Ok(())
+ }
+
+ #[cfg(feature = "write-bhttp")]
+ pub fn write_bhttp(&self, w: &mut impl io::Write) -> Res<()> {
+ write_vec(&self.name, w)?;
+ write_vec(&self.value, w)?;
+ Ok(())
+ }
+
+ #[cfg(feature = "read-http")]
+ pub fn obs_fold(&mut self, extra: &[u8]) {
+ self.value.push(SP);
+ self.value.extend(trim_ows(extra));
+ }
+}
+
+#[derive(Default)]
+pub struct FieldSection(Vec<Field>);
+impl FieldSection {
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+
+ /// Gets the value from the first instance of the field.
+ #[must_use]
+ pub fn get(&self, n: &[u8]) -> Option<&[u8]> {
+ for f in &self.0 {
+ if &f.name[..] == n {
+ return Some(&f.value);
+ }
+ }
+ None
+ }
+
+ pub fn put(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) {
+ self.0.push(Field::new(name.into(), value.into()));
+ }
+
+ pub fn iter(&self) -> impl Iterator<Item = &Field> {
+ self.0.iter()
+ }
+
+ #[must_use]
+ pub fn fields(&self) -> &[Field] {
+ &self.0
+ }
+
+ #[must_use]
+ pub fn is_chunked(&self) -> bool {
+ // Look at the last symbol in Transfer-Encoding.
+ // This is very primitive decoding; structured field this is not.
+ if let Some(te) = self.get(TRANSFER_ENCODING) {
+ let mut slc = te;
+ while let Some(i) = index_of(COMMA, slc) {
+ slc = trim_ows(&slc[i + 1..]);
+ }
+ slc == CHUNKED
+ } else {
+ false
+ }
+ }
+
+ /// As required by the HTTP specification, remove the Connection header
+ /// field, everything it refers to, and a few extra fields.
+ #[cfg(feature = "read-http")]
+ fn strip_connection_headers(&mut self) {
+ const CONNECTION: &[u8] = b"connection";
+ const PROXY_CONNECTION: &[u8] = b"proxy-connection";
+ const SHOULD_REMOVE: &[&[u8]] = &[
+ CONNECTION,
+ PROXY_CONNECTION,
+ b"keep-alive",
+ b"te",
+ b"trailer",
+ b"transfer-encoding",
+ b"upgrade",
+ ];
+ let mut listed = Vec::new();
+ let mut track = |n| {
+ let mut name = Vec::from(trim_ows(n));
+ downcase(&mut name);
+ if !listed.contains(&name) {
+ listed.push(name);
+ }
+ };
+
+ for f in self
+ .0
+ .iter()
+ .filter(|f| f.name() == CONNECTION || f.name == PROXY_CONNECTION)
+ {
+ let mut v = f.value();
+ while let Some(i) = index_of(COMMA, v) {
+ track(&v[..i]);
+ v = &v[i + 1..];
+ }
+ track(v);
+ }
+
+ self.0.retain(|f| {
+ !SHOULD_REMOVE.contains(&f.name()) && listed.iter().all(|x| &x[..] != f.name())
+ });
+ }
+
+ #[cfg(feature = "read-http")]
+ fn parse_line(fields: &mut Vec<Field>, line: Vec<u8>) -> Res<()> {
+ // obs-fold is helpful in specs, so support it here too
+ let f = if is_ows(line[0]) {
+ let mut e = fields.pop().ok_or(Error::ObsFold)?;
+ e.obs_fold(&line);
+ e
+ } else if let Some((n, v)) = split_at(COLON, line) {
+ let mut name = Vec::from(trim_ows(&n));
+ downcase(&mut name);
+ let value = Vec::from(trim_ows(&v));
+ Field::new(name, value)
+ } else {
+ return Err(Error::Missing(COLON));
+ };
+ fields.push(f);
+ Ok(())
+ }
+
+ #[cfg(feature = "read-http")]
+ pub fn read_http(r: &mut impl io::BufRead) -> Res<Self> {
+ let mut fields = Vec::new();
+ loop {
+ let line = read_line(r)?;
+ if trim_ows(&line).is_empty() {
+ return Ok(Self(fields));
+ }
+ Self::parse_line(&mut fields, line)?;
+ }
+ }
+
+ #[cfg(feature = "read-bhttp")]
+ fn read_bhttp_fields(terminator: bool, r: &mut impl io::BufRead) -> Res<Vec<Field>> {
+ let mut fields = Vec::new();
+ let mut cookie_index: Option<usize> = None;
+ loop {
+ if let Some(n) = read_vec(r)? {
+ if n.is_empty() {
+ if terminator {
+ return Ok(fields);
+ }
+ return Err(Error::Truncated);
+ }
+ let mut v = read_vec(r)?.ok_or(Error::Truncated)?;
+ if n == COOKIE {
+ if let Some(i) = &cookie_index {
+ fields[*i].value.extend_from_slice(b"; ");
+ fields[*i].value.append(&mut v);
+ continue;
+ }
+ cookie_index = Some(fields.len());
+ }
+ fields.push(Field::new(n, v));
+ } else if terminator {
+ return Err(Error::Truncated);
+ } else {
+ return Ok(fields);
+ }
+ }
+ }
+
+ #[cfg(feature = "read-bhttp")]
+ pub fn read_bhttp(mode: Mode, r: &mut impl io::BufRead) -> Res<Self> {
+ let fields = if mode == Mode::KnownLength {
+ if let Some(buf) = read_vec(r)? {
+ Self::read_bhttp_fields(false, &mut io::BufReader::new(&buf[..]))?
+ } else {
+ Vec::new()
+ }
+ } else {
+ Self::read_bhttp_fields(true, r)?
+ };
+ Ok(Self(fields))
+ }
+
+ #[cfg(feature = "write-bhttp")]
+ fn write_bhttp_headers(&self, w: &mut impl io::Write) -> Res<()> {
+ for f in &self.0 {
+ f.write_bhttp(w)?;
+ }
+ Ok(())
+ }
+
+ #[cfg(feature = "write-bhttp")]
+ pub fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> {
+ if mode == Mode::KnownLength {
+ let mut buf = Vec::new();
+ self.write_bhttp_headers(&mut buf)?;
+ write_vec(&buf, w)?;
+ } else {
+ self.write_bhttp_headers(w)?;
+ write_len(0, w)?;
+ }
+ Ok(())
+ }
+
+ #[cfg(feature = "write-http")]
+ pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> {
+ for f in &self.0 {
+ f.write_http(w)?;
+ }
+ w.write_all(b"\r\n")?;
+ Ok(())
+ }
+}
+
+pub enum ControlData {
+ Request {
+ method: Vec<u8>,
+ scheme: Vec<u8>,
+ authority: Vec<u8>,
+ path: Vec<u8>,
+ },
+ Response(StatusCode),
+}
+
+impl ControlData {
+ #[must_use]
+ pub fn is_request(&self) -> bool {
+ matches!(self, Self::Request { .. })
+ }
+
+ #[must_use]
+ pub fn method(&self) -> Option<&[u8]> {
+ if let Self::Request { method, .. } = self {
+ Some(method)
+ } else {
+ None
+ }
+ }
+
+ #[must_use]
+ pub fn scheme(&self) -> Option<&[u8]> {
+ if let Self::Request { scheme, .. } = self {
+ Some(scheme)
+ } else {
+ None
+ }
+ }
+
+ #[must_use]
+ pub fn authority(&self) -> Option<&[u8]> {
+ if let Self::Request { authority, .. } = self {
+ if authority.is_empty() {
+ None
+ } else {
+ Some(authority)
+ }
+ } else {
+ None
+ }
+ }
+
+ #[must_use]
+ pub fn path(&self) -> Option<&[u8]> {
+ if let Self::Request { path, .. } = self {
+ if path.is_empty() {
+ None
+ } else {
+ Some(path)
+ }
+ } else {
+ None
+ }
+ }
+
+ #[must_use]
+ pub fn status(&self) -> Option<StatusCode> {
+ if let Self::Response(code) = self {
+ Some(*code)
+ } else {
+ None
+ }
+ }
+
+ #[cfg(feature = "read-http")]
+ pub fn read_http(line: Vec<u8>) -> Res<Self> {
+ // request-line = method SP request-target SP HTTP-version
+ // status-line = HTTP-version SP status-code SP [reason-phrase]
+ let (a, r) = split_at(SP, line).ok_or(Error::Missing(SP))?;
+ let (b, _) = split_at(SP, r).ok_or(Error::Missing(SP))?;
+ if index_of(SLASH, &a).is_some() {
+ // Probably a response, so treat it as such.
+ let status_str = String::from_utf8(b)?;
+ let code = status_str.parse::<u16>()?;
+ Ok(Self::Response(code))
+ } else if index_of(COLON, &b).is_some() {
+ // Now try to parse the URL.
+ let url_str = String::from_utf8(b)?;
+ let parsed = Url::parse(&url_str)?;
+ let authority = parsed.host_str().map_or_else(String::new, |host| {
+ let mut authority = String::from(host);
+ if let Some(port) = parsed.port() {
+ authority.push(':');
+ authority.push_str(&port.to_string());
+ }
+ authority
+ });
+ let mut path = String::from(parsed.path());
+ if let Some(q) = parsed.query() {
+ path.push('?');
+ path.push_str(q);
+ }
+ Ok(Self::Request {
+ method: a,
+ scheme: Vec::from(parsed.scheme().as_bytes()),
+ authority: Vec::from(authority.as_bytes()),
+ path: Vec::from(path.as_bytes()),
+ })
+ } else {
+ if a == b"CONNECT" {
+ return Err(Error::ConnectUnsupported);
+ }
+ Ok(Self::Request {
+ method: a,
+ scheme: Vec::from(&b"https"[..]),
+ authority: Vec::new(),
+ path: b,
+ })
+ }
+ }
+
+ #[cfg(feature = "read-bhttp")]
+ pub fn read_bhttp(request: bool, r: &mut impl io::BufRead) -> Res<Self> {
+ let v = if request {
+ let method = read_vec(r)?.ok_or(Error::Truncated)?;
+ let scheme = read_vec(r)?.ok_or(Error::Truncated)?;
+ let authority = read_vec(r)?.ok_or(Error::Truncated)?;
+ let path = read_vec(r)?.ok_or(Error::Truncated)?;
+ Self::Request {
+ method,
+ scheme,
+ authority,
+ path,
+ }
+ } else {
+ Self::Response(u16::try_from(read_varint(r)?.ok_or(Error::Truncated)?)?)
+ };
+ Ok(v)
+ }
+
+ /// If this is an informational response.
+ #[cfg(any(feature = "read-bhttp", feature = "read-http"))]
+ #[must_use]
+ fn informational(&self) -> Option<StatusCode> {
+ match self {
+ Self::Response(v) if *v >= 100 && *v < 200 => Some(*v),
+ _ => None,
+ }
+ }
+
+ #[cfg(feature = "write-bhttp")]
+ #[must_use]
+ fn code(&self, mode: Mode) -> u64 {
+ match (self, mode) {
+ (Self::Request { .. }, Mode::KnownLength) => 0,
+ (Self::Response(_), Mode::KnownLength) => 1,
+ (Self::Request { .. }, Mode::IndefiniteLength) => 2,
+ (Self::Response(_), Mode::IndefiniteLength) => 3,
+ }
+ }
+
+ #[cfg(feature = "write-bhttp")]
+ pub fn write_bhttp(&self, w: &mut impl io::Write) -> Res<()> {
+ match self {
+ Self::Request {
+ method,
+ scheme,
+ authority,
+ path,
+ } => {
+ write_vec(method, w)?;
+ write_vec(scheme, w)?;
+ write_vec(authority, w)?;
+ write_vec(path, w)?;
+ }
+ Self::Response(status) => write_varint(*status, w)?,
+ }
+ Ok(())
+ }
+
+ #[cfg(feature = "write-http")]
+ pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> {
+ match self {
+ Self::Request {
+ method,
+ scheme,
+ authority,
+ path,
+ } => {
+ w.write_all(method)?;
+ w.write_all(b" ")?;
+ if !authority.is_empty() {
+ w.write_all(scheme)?;
+ w.write_all(b"://")?;
+ w.write_all(authority)?;
+ }
+ w.write_all(path)?;
+ w.write_all(b" HTTP/1.1\r\n")?;
+ }
+ Self::Response(status) => {
+ let buf = format!("HTTP/1.1 {} Reason\r\n", *status);
+ w.write_all(buf.as_bytes())?;
+ }
+ }
+ Ok(())
+ }
+}
+
+pub struct InformationalResponse {
+ status: StatusCode,
+ fields: FieldSection,
+}
+
+impl InformationalResponse {
+ #[must_use]
+ pub fn new(status: StatusCode, fields: FieldSection) -> Self {
+ Self { status, fields }
+ }
+
+ #[must_use]
+ pub fn status(&self) -> StatusCode {
+ self.status
+ }
+
+ #[must_use]
+ pub fn fields(&self) -> &FieldSection {
+ &self.fields
+ }
+
+ #[cfg(feature = "write-bhttp")]
+ fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> {
+ write_varint(self.status, w)?;
+ self.fields.write_bhttp(mode, w)?;
+ Ok(())
+ }
+}
+
+pub struct Message {
+ informational: Vec<InformationalResponse>,
+ control: ControlData,
+ header: FieldSection,
+ content: Vec<u8>,
+ trailer: FieldSection,
+}
+
+impl Message {
+ #[must_use]
+ pub fn request(method: Vec<u8>, scheme: Vec<u8>, authority: Vec<u8>, path: Vec<u8>) -> Self {
+ Self {
+ informational: Vec::new(),
+ control: ControlData::Request {
+ method,
+ scheme,
+ authority,
+ path,
+ },
+ header: FieldSection::default(),
+ content: Vec::new(),
+ trailer: FieldSection::default(),
+ }
+ }
+
+ #[must_use]
+ pub fn response(status: StatusCode) -> Self {
+ Self {
+ informational: Vec::new(),
+ control: ControlData::Response(status),
+ header: FieldSection::default(),
+ content: Vec::new(),
+ trailer: FieldSection::default(),
+ }
+ }
+
+ pub fn put_header(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) {
+ self.header.put(name, value);
+ }
+
+ pub fn put_trailer(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) {
+ self.trailer.put(name, value);
+ }
+
+ pub fn write_content(&mut self, d: impl AsRef<[u8]>) {
+ self.content.extend_from_slice(d.as_ref());
+ }
+
+ #[must_use]
+ pub fn informational(&self) -> &[InformationalResponse] {
+ &self.informational
+ }
+
+ #[must_use]
+ pub fn control(&self) -> &ControlData {
+ &self.control
+ }
+
+ #[must_use]
+ pub fn header(&self) -> &FieldSection {
+ &self.header
+ }
+
+ #[must_use]
+ pub fn content(&self) -> &[u8] {
+ &self.content
+ }
+
+ #[must_use]
+ pub fn trailer(&self) -> &FieldSection {
+ &self.trailer
+ }
+
+ #[cfg(feature = "read-http")]
+ fn read_chunked(r: &mut impl io::BufRead) -> Res<Vec<u8>> {
+ let mut content = Vec::new();
+ loop {
+ let mut line = read_line(r)?;
+ if let Some(i) = index_of(SEMICOLON, &line) {
+ std::mem::drop(line.split_off(i));
+ }
+ let count_str = String::from_utf8(line)?;
+ let count = usize::from_str_radix(&count_str, 16)?;
+ if count == 0 {
+ return Ok(content);
+ }
+ let mut buf = vec![0; count];
+ r.read_exact(&mut buf)?;
+ assert!(read_line(r)?.is_empty());
+ content.append(&mut buf);
+ }
+ }
+
+ #[cfg(feature = "read-http")]
+ #[allow(clippy::read_zero_byte_vec)] // https://github.com/rust-lang/rust-clippy/issues/9274
+ pub fn read_http(r: &mut impl io::BufRead) -> Res<Self> {
+ let line = read_line(r)?;
+ let mut control = ControlData::read_http(line)?;
+ let mut informational = Vec::new();
+ while let Some(status) = control.informational() {
+ let fields = FieldSection::read_http(r)?;
+ informational.push(InformationalResponse::new(status, fields));
+ let line = read_line(r)?;
+ control = ControlData::read_http(line)?;
+ }
+
+ let mut header = FieldSection::read_http(r)?;
+
+ let (content, trailer) = if matches!(control.status(), Some(204) | Some(304)) {
+ // 204 and 304 have no body, no matter what Content-Length says.
+ // Unfortunately, we can't do the same for responses to HEAD.
+ (Vec::new(), FieldSection::default())
+ } else if header.is_chunked() {
+ let content = Self::read_chunked(r)?;
+ let trailer = FieldSection::read_http(r)?;
+ (content, trailer)
+ } else {
+ let mut content = Vec::new();
+ if let Some(cl) = header.get(CONTENT_LENGTH) {
+ let cl_str = String::from_utf8(Vec::from(cl))?;
+ let cl_int = cl_str.parse::<usize>()?;
+ if cl_int > 0 {
+ content.resize(cl_int, 0);
+ r.read_exact(&mut content)?;
+ }
+ } else {
+ // Note that for a request, the spec states that the content is
+ // empty, but this just reads all input like for a response.
+ r.read_to_end(&mut content)?;
+ }
+ (content, FieldSection::default())
+ };
+
+ header.strip_connection_headers();
+ Ok(Self {
+ informational,
+ control,
+ header,
+ content,
+ trailer,
+ })
+ }
+
+ #[cfg(feature = "write-http")]
+ pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> {
+ for info in &self.informational {
+ ControlData::Response(info.status()).write_http(w)?;
+ info.fields().write_http(w)?;
+ }
+ self.control.write_http(w)?;
+ if !self.content.is_empty() {
+ if self.trailer.is_empty() {
+ write!(w, "Content-Length: {}\r\n", self.content.len())?;
+ } else {
+ w.write_all(b"Transfer-Encoding: chunked\r\n")?;
+ }
+ }
+ self.header.write_http(w)?;
+
+ if self.header.is_chunked() {
+ write!(w, "{:x}\r\n", self.content.len())?;
+ w.write_all(&self.content)?;
+ w.write_all(b"\r\n0\r\n")?;
+ self.trailer.write_http(w)?;
+ } else {
+ w.write_all(&self.content)?;
+ }
+
+ Ok(())
+ }
+
+ /// Read a BHTTP message.
+ #[cfg(feature = "read-bhttp")]
+ pub fn read_bhttp(r: &mut impl io::BufRead) -> Res<Self> {
+ let t = read_varint(r)?.ok_or(Error::Truncated)?;
+ let request = t == 0 || t == 2;
+ let mode = match t {
+ 0 | 1 => Mode::KnownLength,
+ 2 | 3 => Mode::IndefiniteLength,
+ _ => return Err(Error::InvalidMode),
+ };
+
+ let mut control = ControlData::read_bhttp(request, r)?;
+ let mut informational = Vec::new();
+ while let Some(status) = control.informational() {
+ let fields = FieldSection::read_bhttp(mode, r)?;
+ informational.push(InformationalResponse::new(status, fields));
+ control = ControlData::read_bhttp(request, r)?;
+ }
+ let header = FieldSection::read_bhttp(mode, r)?;
+
+ let mut content = read_vec(r)?.unwrap_or_default();
+ if mode == Mode::IndefiniteLength && !content.is_empty() {
+ loop {
+ let mut extra = read_vec(r)?.unwrap_or_default();
+ if extra.is_empty() {
+ break;
+ }
+ content.append(&mut extra);
+ }
+ }
+
+ let trailer = FieldSection::read_bhttp(mode, r)?;
+
+ Ok(Self {
+ informational,
+ control,
+ header,
+ content,
+ trailer,
+ })
+ }
+
+ #[cfg(feature = "write-bhttp")]
+ pub fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> {
+ write_varint(self.control.code(mode), w)?;
+ for info in &self.informational {
+ info.write_bhttp(mode, w)?;
+ }
+ self.control.write_bhttp(w)?;
+ self.header.write_bhttp(mode, w)?;
+
+ write_vec(&self.content, w)?;
+ if mode == Mode::IndefiniteLength && !self.content.is_empty() {
+ write_len(0, w)?;
+ }
+ self.trailer.write_bhttp(mode, w)?;
+ Ok(())
+ }
+}
+
+#[cfg(feature = "write-http")]
+impl std::fmt::Debug for Message {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ let mut buf = Vec::new();
+ self.write_http(&mut buf).map_err(|_| std::fmt::Error)?;
+ write!(f, "{:?}", String::from_utf8_lossy(&buf))
+ }
+}
diff --git a/third_party/rust/bhttp/src/parse.rs b/third_party/rust/bhttp/src/parse.rs
new file mode 100644
index 0000000000..a2b8889808
--- /dev/null
+++ b/third_party/rust/bhttp/src/parse.rs
@@ -0,0 +1,75 @@
+#[cfg(feature = "read-http")]
+use crate::{Error, Res};
+
+pub const HTAB: u8 = 0x09;
+#[cfg(feature = "read-http")]
+pub const NL: u8 = 0x0a;
+#[cfg(feature = "read-http")]
+pub const CR: u8 = 0x0d;
+pub const SP: u8 = 0x20;
+pub const COMMA: u8 = 0x2c;
+#[cfg(feature = "read-http")]
+pub const SLASH: u8 = 0x2f;
+#[cfg(feature = "read-http")]
+pub const COLON: u8 = 0x3a;
+#[cfg(feature = "read-http")]
+pub const SEMICOLON: u8 = 0x3b;
+
+pub fn is_ows(x: u8) -> bool {
+ x == SP || x == HTAB
+}
+
+pub fn trim_ows(v: &[u8]) -> &[u8] {
+ for s in 0..v.len() {
+ if !is_ows(v[s]) {
+ for e in (s..v.len()).rev() {
+ if !is_ows(v[e]) {
+ return &v[s..=e];
+ }
+ }
+ }
+ }
+ &v[..0]
+}
+
+#[cfg(feature = "read-http")]
+pub fn downcase(n: &mut [u8]) {
+ for i in n {
+ if *i >= 0x41 && *i <= 0x5a {
+ *i += 0x20;
+ }
+ }
+}
+
+pub fn index_of(v: u8, line: &[u8]) -> Option<usize> {
+ for (i, x) in line.iter().enumerate() {
+ if *x == v {
+ return Some(i);
+ }
+ }
+ None
+}
+
+#[cfg(feature = "read-http")]
+pub fn split_at(v: u8, mut line: Vec<u8>) -> Option<(Vec<u8>, Vec<u8>)> {
+ index_of(v, &line).map(|i| {
+ let tail = line.split_off(i + 1);
+ let _ = line.pop();
+ (line, tail)
+ })
+}
+
+#[cfg(feature = "read-http")]
+pub fn read_line(r: &mut impl std::io::BufRead) -> Res<Vec<u8>> {
+ let mut buf = Vec::new();
+ r.read_until(NL, &mut buf)?;
+ let tail = buf.pop();
+ if tail != Some(NL) {
+ return Err(Error::Truncated);
+ }
+ if buf.pop().ok_or(Error::Missing(CR))? == CR {
+ Ok(buf)
+ } else {
+ Err(Error::Missing(CR))
+ }
+}
diff --git a/third_party/rust/bhttp/src/rw.rs b/third_party/rust/bhttp/src/rw.rs
new file mode 100644
index 0000000000..db42244351
--- /dev/null
+++ b/third_party/rust/bhttp/src/rw.rs
@@ -0,0 +1,82 @@
+#[cfg(feature = "read-bhttp")]
+use crate::err::Error;
+use crate::err::Res;
+use std::convert::TryFrom;
+use std::io;
+
+#[cfg(feature = "write-bhttp")]
+#[allow(clippy::cast_possible_truncation)]
+fn write_uint(n: u8, v: impl Into<u64>, w: &mut impl io::Write) -> Res<()> {
+ let v = v.into();
+ assert!(n > 0 && usize::from(n) < std::mem::size_of::<u64>());
+ for i in 0..n {
+ w.write_all(&[((v >> (8 * (n - i - 1))) & 0xff) as u8])?;
+ }
+ Ok(())
+}
+
+#[cfg(feature = "write-bhttp")]
+pub fn write_varint(v: impl Into<u64>, w: &mut impl io::Write) -> Res<()> {
+ let v = v.into();
+ match () {
+ _ if v < (1 << 6) => write_uint(1, v, w),
+ _ if v < (1 << 14) => write_uint(2, v | (1 << 14), w),
+ _ if v < (1 << 30) => write_uint(4, v | (2 << 30), w),
+ _ if v < (1 << 62) => write_uint(8, v | (3 << 62), w),
+ _ => panic!("Varint value too large"),
+ }
+}
+
+#[cfg(feature = "write-bhttp")]
+pub fn write_len(len: usize, w: &mut impl io::Write) -> Res<()> {
+ write_varint(u64::try_from(len).unwrap(), w)
+}
+
+#[cfg(feature = "write-bhttp")]
+pub fn write_vec(v: &[u8], w: &mut impl io::Write) -> Res<()> {
+ write_len(v.len(), w)?;
+ w.write_all(v)?;
+ Ok(())
+}
+
+#[cfg(feature = "read-bhttp")]
+fn read_uint(n: usize, r: &mut impl io::BufRead) -> Res<Option<u64>> {
+ let mut buf = [0; 7];
+ let count = r.read(&mut buf[..n])?;
+ if count == 0 {
+ return Ok(None);
+ } else if count < n {
+ return Err(Error::Truncated);
+ }
+ let mut v = 0;
+ for i in &buf[..n] {
+ v = (v << 8) | u64::from(*i);
+ }
+ Ok(Some(v))
+}
+
+#[cfg(feature = "read-bhttp")]
+pub fn read_varint(r: &mut impl io::BufRead) -> Res<Option<u64>> {
+ if let Some(b1) = read_uint(1, r)? {
+ Ok(Some(match b1 >> 6 {
+ 0 => b1 & 0x3f,
+ 1 => ((b1 & 0x3f) << 8) | read_uint(1, r)?.ok_or(Error::Truncated)?,
+ 2 => ((b1 & 0x3f) << 24) | read_uint(3, r)?.ok_or(Error::Truncated)?,
+ 3 => ((b1 & 0x3f) << 56) | read_uint(7, r)?.ok_or(Error::Truncated)?,
+ _ => unreachable!(),
+ }))
+ } else {
+ Ok(None)
+ }
+}
+
+#[cfg(feature = "read-bhttp")]
+pub fn read_vec(r: &mut impl io::BufRead) -> Res<Option<Vec<u8>>> {
+ if let Some(len) = read_varint(r)? {
+ let mut v = vec![0; usize::try_from(len).unwrap()];
+ r.read_exact(&mut v)?;
+ Ok(Some(v))
+ } else {
+ Ok(None)
+ }
+}
diff --git a/third_party/rust/bhttp/tests/test.rs b/third_party/rust/bhttp/tests/test.rs
new file mode 100644
index 0000000000..d9c9780fc9
--- /dev/null
+++ b/third_party/rust/bhttp/tests/test.rs
@@ -0,0 +1,210 @@
+// Rather than grapple with #[cfg(...)] for every variable and import.
+#![cfg(all(feature = "http", feature = "bhttp"))]
+
+use bhttp::{Error, Message, Mode};
+use std::io::BufReader;
+use std::mem::drop;
+
+const CHUNKED_HTTP: &[u8] = b"HTTP/1.1 200 OK\r\n\
+ Transfer-Encoding: camel, chunked\r\n\
+ \r\n\
+ 4\r\n\
+ This\r\n\
+ 6\r\n \
+ conte\r\n\
+ 13;chunk-extension=foo\r\n\
+ nt contains CRLF.\r\n\
+ \r\n\
+ 0\r\n\
+ Trailer: text\r\n\
+ \r\n";
+const TRANSFER_ENCODING: &[u8] = b"transfer-encoding";
+const CHUNKED_KNOWN: &[u8] = &[
+ 0x01, 0x40, 0xc8, 0x00, 0x1d, 0x54, 0x68, 0x69, 0x73, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
+ 0x74, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, 0x20, 0x43, 0x52, 0x4c, 0x46, 0x2e,
+ 0x0d, 0x0a, 0x0d, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x04, 0x74, 0x65, 0x78, 0x74,
+];
+const CHUNKED_INDEFINITE: &[u8] = &[
+ 0x03, 0x40, 0xc8, 0x00, 0x1d, 0x54, 0x68, 0x69, 0x73, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
+ 0x74, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, 0x20, 0x43, 0x52, 0x4c, 0x46, 0x2e,
+ 0x0d, 0x0a, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x04, 0x74, 0x65, 0x78, 0x74,
+ 0x00,
+];
+
+const REQUEST: &[u8] = b"GET /hello.txt HTTP/1.1\r\n\
+ user-agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3\r\n\
+ host: www.example.com\r\n\
+ accept-language: en, mi\r\n\
+ \r\n";
+const REQUEST_KNOWN: &[u8] = &[
+ 0x00, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x00, 0x0a, 0x2f, 0x68, 0x65,
+ 0x6c, 0x6c, 0x6f, 0x2e, 0x74, 0x78, 0x74, 0x40, 0x6c, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x61,
+ 0x67, 0x65, 0x6e, 0x74, 0x34, 0x63, 0x75, 0x72, 0x6c, 0x2f, 0x37, 0x2e, 0x31, 0x36, 0x2e, 0x33,
+ 0x20, 0x6c, 0x69, 0x62, 0x63, 0x75, 0x72, 0x6c, 0x2f, 0x37, 0x2e, 0x31, 0x36, 0x2e, 0x33, 0x20,
+ 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x53, 0x4c, 0x2f, 0x30, 0x2e, 0x39, 0x2e, 0x37, 0x6c, 0x20, 0x7a,
+ 0x6c, 0x69, 0x62, 0x2f, 0x31, 0x2e, 0x32, 0x2e, 0x33, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x0f, 0x77,
+ 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x0f, 0x61,
+ 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x06, 0x65,
+ 0x6e, 0x2c, 0x20, 0x6d, 0x69, 0x00, 0x00,
+];
+
+#[test]
+fn chunked_read() {
+ drop(Message::read_http(&mut BufReader::new(CHUNKED_HTTP)).unwrap());
+}
+
+#[test]
+fn chunked_read_known() {
+ drop(Message::read_bhttp(&mut BufReader::new(CHUNKED_KNOWN)).unwrap());
+}
+
+#[test]
+fn chunked_read_indefinite() {
+ drop(Message::read_bhttp(&mut BufReader::new(CHUNKED_INDEFINITE)).unwrap());
+}
+
+#[test]
+fn chunked_to_known() {
+ let m = Message::read_http(&mut BufReader::new(CHUNKED_HTTP)).unwrap();
+ assert!(m.header().get(TRANSFER_ENCODING).is_none());
+
+ let mut buf = Vec::new();
+ m.write_bhttp(Mode::KnownLength, &mut buf).unwrap();
+ println!("result: {}", hex::encode(&buf));
+ assert_eq!(&buf[..], CHUNKED_KNOWN);
+}
+
+#[test]
+fn chunked_to_indefinite() {
+ let m = Message::read_http(&mut BufReader::new(CHUNKED_HTTP)).unwrap();
+ assert!(m.header().get(TRANSFER_ENCODING).is_none());
+
+ let mut buf = Vec::new();
+ m.write_bhttp(Mode::IndefiniteLength, &mut buf).unwrap();
+ println!("result: {}", hex::encode(&buf));
+ assert_eq!(&buf[..], CHUNKED_INDEFINITE);
+}
+
+#[test]
+fn convert_request() {
+ let m = Message::read_http(&mut BufReader::new(REQUEST)).unwrap();
+ let mut buf = Vec::new();
+ m.write_bhttp(Mode::KnownLength, &mut buf).unwrap();
+ println!("result: {}", hex::encode(&buf));
+ assert_eq!(&buf[..], REQUEST_KNOWN);
+}
+
+#[test]
+fn padded_to_http() {
+ let mut padded = Vec::from(REQUEST_KNOWN);
+ padded.resize(padded.len() + 100, 0);
+ let m = Message::read_bhttp(&mut BufReader::new(&padded[..])).unwrap();
+ let mut buf = Vec::new();
+ m.write_http(&mut buf).unwrap();
+ assert_eq!(&buf[..], REQUEST);
+}
+
+#[test]
+fn truncated_to_http() {
+ let mut padded = Vec::from(REQUEST_KNOWN);
+ assert_eq!(2, padded.iter().rev().take_while(|&x| *x == 0).count());
+ padded.truncate(padded.len() - 2);
+
+ let m = Message::read_bhttp(&mut BufReader::new(&padded[..])).unwrap();
+ let mut buf = Vec::new();
+ m.write_http(&mut buf).unwrap();
+ assert_eq!(&buf[..], REQUEST);
+}
+
+#[test]
+fn tiny_request() {
+ const REQUEST: &[u8] = &[
+ 0x00, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x0b, 0x65, 0x78, 0x61,
+ 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x01, 0x2f,
+ ];
+ let m = Message::read_bhttp(&mut BufReader::new(REQUEST)).unwrap();
+ assert_eq!(m.control().method().unwrap(), b"GET");
+ assert_eq!(m.control().scheme().unwrap(), b"https");
+ assert_eq!(m.control().authority().unwrap(), b"example.com");
+ assert_eq!(m.control().path().unwrap(), b"/");
+ assert!(m.control().status().is_none());
+ assert!(m.header().is_empty());
+ assert!(m.content().is_empty());
+ assert!(m.trailer().is_empty());
+}
+
+#[test]
+fn tiny_response() {
+ const RESPONSE: &[u8] = &[0x01, 0x40, 0xc8];
+ let m = Message::read_bhttp(&mut BufReader::new(RESPONSE)).unwrap();
+ assert!(m.informational().is_empty());
+ assert_eq!(m.control().status().unwrap(), 200);
+ assert!(m.control().method().is_none());
+ assert!(m.control().scheme().is_none());
+ assert!(m.control().authority().is_none());
+ assert!(m.control().path().is_none());
+ assert!(m.header().is_empty());
+ assert!(m.content().is_empty());
+ assert!(m.trailer().is_empty());
+}
+
+#[test]
+fn connect_request() {
+ const REQUEST: &[u8] = b"CONNECT test.example HTTP/1.1\r\n\
+ Host: example.com\r\n\
+ \r\n";
+ let err = Message::read_http(&mut BufReader::new(REQUEST)).unwrap_err();
+ assert!(matches!(err, Error::ConnectUnsupported));
+}
+
+/// Verify that Connection and Proxy-Connection are stripped out properly.
+#[test]
+fn connection_header() {
+ const REQUEST: &[u8] = b"POST test.example HTTP/1.1\r\n\
+ Host: example.com\r\n\
+ other: test\r\n\
+ Connection: sample\r\n\
+ Connection: other, garbage\r\n\
+ sample: test2\r\n\
+ px: test3\r\n\
+ proXy-connection: px\r\n\
+ \r\n";
+
+ let m = Message::read_http(&mut BufReader::new(REQUEST)).unwrap();
+ assert!(m.header().get(b"other").is_none());
+ assert!(m.header().get(b"sample").is_none());
+ assert!(m.header().get(b"garbage").is_none());
+ assert!(m.header().get(b"connection").is_none());
+ assert!(m.header().get(b"proxy-connection").is_none());
+ assert!(m.header().get(b"px").is_none());
+}
+
+/// Verify that hop-by-hop headers (other than transfer-encoding) are stripped out properly.
+#[test]
+fn hop_by_hop() {
+ const REQUEST: &[u8] = b"POST test.example HTTP/1.1\r\n\
+ Host: example.com\r\n\
+ keep-alive: 1\r\n\
+ te: trailers\r\n\
+ trailer: te\r\n\
+ upgrade: h2c\r\n\
+ \r\n";
+
+ let m = Message::read_http(&mut BufReader::new(REQUEST)).unwrap();
+ assert!(m.header().get(b"keep-alive").is_none());
+ assert!(m.header().get(b"te").is_none());
+ assert!(m.header().get(b"trailer").is_none());
+ assert!(m.header().get(b"transfer-encoding").is_none());
+ assert!(m.header().get(b"upgrade").is_none());
+}
+
+/// Verify that very bad chunked encoding produces a result.
+#[test]
+fn bad_chunked() {
+ const REQUEST: &[u8] = b"POST test.example HTTP/1.1\r\n\
+ Transfer-Encoding: chunked\r\n\
+ \r\n";
+
+ let e = Message::read_http(&mut BufReader::new(REQUEST)).unwrap_err();
+ assert!(matches!(e, Error::Truncated));
+}