summaryrefslogtreecommitdiffstats
path: root/third_party/rust/bhttp/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/bhttp/src
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/bhttp/src')
-rw-r--r--third_party/rust/bhttp/src/err.rs38
-rw-r--r--third_party/rust/bhttp/src/lib.rs797
-rw-r--r--third_party/rust/bhttp/src/parse.rs81
-rw-r--r--third_party/rust/bhttp/src/rw.rs106
4 files changed, 1022 insertions, 0 deletions
diff --git a/third_party/rust/bhttp/src/err.rs b/third_party/rust/bhttp/src/err.rs
new file mode 100644
index 0000000000..5ee136f960
--- /dev/null
+++ b/third_party/rust/bhttp/src/err.rs
@@ -0,0 +1,38 @@
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum Error {
+ #[error("a request used the CONNECT method")]
+ ConnectUnsupported,
+ #[error("a field contained invalid Unicode: {0}")]
+ CharacterEncoding(#[from] std::string::FromUtf8Error),
+ #[error("a field contained an integer value that was out of range: {0}")]
+ IntRange(#[from] std::num::TryFromIntError),
+ #[error("the mode of the message was invalid")]
+ InvalidMode,
+ #[error("IO error {0}")]
+ Io(#[from] std::io::Error),
+ #[error("a field or line was missing a necessary character 0x{0:x}")]
+ Missing(u8),
+ #[error("a URL was missing a key component")]
+ MissingUrlComponent,
+ #[error("an obs-fold line was the first line of a field section")]
+ ObsFold,
+ #[error("a field contained a non-integer value: {0}")]
+ ParseInt(#[from] std::num::ParseIntError),
+ #[error("a field was truncated")]
+ Truncated,
+ #[error("a message included the Upgrade field")]
+ UpgradeUnsupported,
+ #[error("a URL could not be parsed into components: {0}")]
+ #[cfg(feature = "read-http")]
+ UrlParse(#[from] url::ParseError),
+}
+
+#[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..205cbf7202
--- /dev/null
+++ b/third_party/rust/bhttp/src/lib.rs
@@ -0,0 +1,797 @@
+#![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(any(feature = "read-http", feature = "read-bhttp",))]
+use std::borrow::BorrowMut;
+
+#[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;
+
+pub trait ReadSeek: io::BufRead + io::Seek {}
+impl<T> ReadSeek for io::Cursor<T> where T: AsRef<[u8]> {}
+impl<T> ReadSeek for io::BufReader<T> where T: io::Read + io::Seek {}
+
+#[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<T, R>(r: &mut T) -> Res<Self>
+ where
+ T: BorrowMut<R> + ?Sized,
+ R: ReadSeek + ?Sized,
+ {
+ 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<T, R>(terminator: bool, r: &mut T) -> Res<Vec<Field>>
+ where
+ T: BorrowMut<R> + ?Sized,
+ R: ReadSeek + ?Sized,
+ {
+ let r = r.borrow_mut();
+ 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<T, R>(mode: Mode, r: &mut T) -> Res<Self>
+ where
+ T: BorrowMut<R> + ?Sized,
+ R: ReadSeek + ?Sized,
+ {
+ let fields = if mode == Mode::KnownLength {
+ if let Some(buf) = read_vec(r)? {
+ Self::read_bhttp_fields(false, &mut io::Cursor::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<T, R>(request: bool, r: &mut T) -> Res<Self>
+ where
+ T: BorrowMut<R> + ?Sized,
+ R: ReadSeek + ?Sized,
+ {
+ 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<T, R>(r: &mut T) -> Res<Vec<u8>>
+ where
+ T: BorrowMut<R> + ?Sized,
+ R: ReadSeek + ?Sized,
+ {
+ 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.borrow_mut().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<T, R>(r: &mut T) -> Res<Self>
+ where
+ T: BorrowMut<R> + ?Sized,
+ R: ReadSeek + ?Sized,
+ {
+ 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.borrow_mut().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.borrow_mut().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<T, R>(r: &mut T) -> Res<Self>
+ where
+ T: BorrowMut<R> + ?Sized,
+ R: ReadSeek + ?Sized,
+ {
+ 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..42ba2b8b01
--- /dev/null
+++ b/third_party/rust/bhttp/src/parse.rs
@@ -0,0 +1,81 @@
+#[cfg(feature = "read-http")]
+use crate::{Error, ReadSeek, Res};
+#[cfg(feature = "read-http")]
+use std::borrow::BorrowMut;
+
+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<T, R>(r: &mut T) -> Res<Vec<u8>>
+where
+ T: BorrowMut<R> + ?Sized,
+ R: ReadSeek + ?Sized,
+{
+ let mut buf = Vec::new();
+ r.borrow_mut().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..8fee16daa9
--- /dev/null
+++ b/third_party/rust/bhttp/src/rw.rs
@@ -0,0 +1,106 @@
+use crate::err::Res;
+#[cfg(feature = "read-bhttp")]
+use crate::{err::Error, ReadSeek};
+#[cfg(feature = "read-bhttp")]
+use std::borrow::BorrowMut;
+use std::{convert::TryFrom, 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<T, R>(n: usize, r: &mut T) -> Res<Option<u64>>
+where
+ T: BorrowMut<R> + ?Sized,
+ R: ReadSeek + ?Sized,
+{
+ let mut buf = [0; 7];
+ let count = r.borrow_mut().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<T, R>(r: &mut T) -> Res<Option<u64>>
+where
+ T: BorrowMut<R> + ?Sized,
+ R: ReadSeek + ?Sized,
+{
+ 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<T, R>(r: &mut T) -> Res<Option<Vec<u8>>>
+where
+ T: BorrowMut<R> + ?Sized,
+ R: ReadSeek + ?Sized,
+{
+ use std::io::SeekFrom;
+
+ if let Some(len) = read_varint(r)? {
+ // Check that the input contains enough data. Before allocating.
+ let r = r.borrow_mut();
+ let pos = r.stream_position()?;
+ let end = r.seek(SeekFrom::End(0))?;
+ if end - pos < len {
+ return Err(Error::Truncated);
+ }
+ let _ = r.seek(SeekFrom::Start(pos))?;
+
+ let mut v = vec![0; usize::try_from(len)?];
+ r.read_exact(&mut v)?;
+ Ok(Some(v))
+ } else {
+ Ok(None)
+ }
+}