xocomil 0.3.0

A lightweight, zero-allocation HTTP/1.1 request parser and response writer
Documentation
use std::io::{self, BufRead};

use crate::bytes::ByteSearch;
use crate::error::{Error, ParseErrorKind, ReadError};

use super::{DEFAULT_MAX_HEADERS, Request};

/// Result of [`Request::read`]: the parsed request plus metadata about
/// how many bytes were consumed from the reader.
///
/// Body bytes that arrived in the same read as the headers are stored
/// internally — pass them (along with the original reader) to
/// [`stream_body_to`](Self::stream_body_to) to write the decoded body
/// to any [`Write`](std::io::Write) destination.
#[derive(Debug)]
pub struct ReadRequest<'buf, const MAX_HDRS: usize = DEFAULT_MAX_HEADERS> {
    pub(super) request: Request<'buf, MAX_HDRS>,
    pub(super) prefetch: &'buf [u8],
    pub(super) body_offset: usize,
    pub(super) bytes_read: usize,
}

impl<'buf, const MAX_HDRS: usize> ReadRequest<'buf, MAX_HDRS> {
    /// The parsed HTTP request.
    #[inline]
    #[must_use]
    pub const fn request(&self) -> &Request<'buf, MAX_HDRS> {
        &self.request
    }

    /// Body bytes already in the header buffer (from the read overshoot).
    ///
    /// When using [`stream_body_to`](Self::stream_body_to), these bytes are
    /// consumed automatically. This accessor is useful for manual body
    /// handling or inspection.
    #[inline]
    #[must_use]
    pub const fn prefetch(&self) -> &'buf [u8] {
        self.prefetch
    }

    /// Byte offset in the buffer where the request body begins
    /// (immediately after the `\r\n\r\n` header terminator).
    #[inline]
    #[must_use]
    pub const fn body_offset(&self) -> usize {
        self.body_offset
    }

    /// Total number of bytes read into the buffer.
    #[inline]
    #[must_use]
    pub const fn bytes_read(&self) -> usize {
        self.bytes_read
    }

    /// Stream the request body from `reader` to `writer`.
    ///
    /// Uses [`DEFAULT_MAX_BODY_SIZE`](crate::body::DEFAULT_MAX_BODY_SIZE)
    /// (8 MiB) as the body size limit. Use [`stream_body_to_limited`](Self::stream_body_to_limited)
    /// to specify a custom limit.
    ///
    /// Writes any body bytes already in the header buffer (the prefetch),
    /// then continues reading from `reader` and writing decoded body data
    /// to `writer`. For chunked Transfer-Encoding, chunk framing is
    /// stripped on the fly — only decoded data reaches the writer.
    ///
    /// For best performance, wrap unbuffered readers (e.g. raw `TcpStream`)
    /// in a [`BufReader`](std::io::BufReader) before calling this method.
    ///
    /// Returns the number of decoded body bytes written.
    ///
    /// # Errors
    ///
    /// Returns [`Error`] if the body is malformed, exceeds the default
    /// limit, or the connection closes prematurely.
    ///
    /// # Examples
    ///
    /// ```
    /// use xocomil::request::Request;
    ///
    /// let raw = b"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 5\r\n\r\nhello";
    /// let mut buf = [0u8; 1024];
    /// let rr = Request::<32>::read(&mut &raw[..], &mut buf).unwrap();
    ///
    /// let mut body = Vec::new();
    /// let n = rr.stream_body_to(&mut &b""[..], &mut body).unwrap();
    /// assert_eq!(body, b"hello");
    /// assert_eq!(n, 5);
    /// ```
    pub fn stream_body_to(
        &self,
        reader: &mut impl io::BufRead,
        writer: &mut impl io::Write,
    ) -> Result<u64, Error> {
        self.stream_body_to_limited(reader, writer, crate::body::DEFAULT_MAX_BODY_SIZE)
    }

    /// Stream the request body with an explicit size limit.
    ///
    /// Like [`stream_body_to`](Self::stream_body_to) but with a
    /// caller-specified maximum body size. Pass
    /// [`UNLIMITED_BODY_SIZE`](crate::body::UNLIMITED_BODY_SIZE) to
    /// disable the limit.
    ///
    /// # Errors
    ///
    /// Returns <code>[Error::Body]([BodyErrorKind::BodyTooLarge])</code>
    /// if the body exceeds `max_body_size`.
    pub fn stream_body_to_limited(
        &self,
        reader: &mut impl io::BufRead,
        writer: &mut impl io::Write,
        max_body_size: u64,
    ) -> Result<u64, Error> {
        crate::body::stream_body(
            reader,
            writer,
            self.prefetch,
            self.request.body_kind(),
            max_body_size,
        )
    }

    /// Stream the request body with compile-time configuration.
    ///
    /// Like [`stream_body_to`](Self::stream_body_to) but all tunables
    /// are const generics:
    ///
    /// - `CHUNK_LINE_BUF` — stack buffer size for chunk size lines
    /// - `MAX_BODY_SIZE` — maximum decoded body bytes
    /// - `MAX_TRAILER_SIZE` — maximum bytes consumed while skipping
    ///   trailer headers; pass
    ///   [`DEFAULT_MAX_TRAILER_SIZE`](crate::body::DEFAULT_MAX_TRAILER_SIZE)
    ///   for the standard 8 KiB cap
    ///
    /// See [`stream_body_with`](crate::body::stream_body_with) for details.
    ///
    /// # Errors
    ///
    /// Returns [`Error`] if the body is malformed, exceeds `MAX_BODY_SIZE`,
    /// or the connection closes prematurely.
    pub fn stream_body_to_with<
        const CHUNK_LINE_BUF: usize,
        const MAX_BODY_SIZE: u64,
        const MAX_TRAILER_SIZE: usize,
    >(
        &self,
        reader: &mut impl io::BufRead,
        writer: &mut impl io::Write,
    ) -> Result<u64, Error> {
        crate::body::stream_body_with::<CHUNK_LINE_BUF, MAX_BODY_SIZE, MAX_TRAILER_SIZE>(
            reader,
            writer,
            self.prefetch,
            self.request.body_kind(),
        )
    }

    /// Construct a [`BodyReader`](crate::body::BodyReader) over the
    /// request body.
    ///
    /// The returned `Read` adapter yields decoded body bytes — for
    /// chunked Transfer-Encoding, framing is stripped on the fly. The
    /// prefetch (body bytes that arrived in the same buffer as the
    /// headers) is consumed first; subsequent reads are pulled from
    /// `reader`.
    ///
    /// Uses
    /// [`DEFAULT_MAX_BODY_SIZE`](crate::body::DEFAULT_MAX_BODY_SIZE) as
    /// the body-size cap. For a custom limit, construct
    /// [`BodyReader`](crate::body::BodyReader) directly with
    /// [`prefetch`](Self::prefetch) and
    /// [`body_kind`](crate::request::Request::body_kind).
    ///
    /// # Example
    ///
    /// ```
    /// use std::io::Read;
    /// use xocomil::request::Request;
    ///
    /// let raw = b"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 5\r\n\r\nhello";
    /// let mut buf = [0u8; 1024];
    /// let rr = Request::<32>::read(&mut &raw[..], &mut buf).unwrap();
    ///
    /// let mut body = String::new();
    /// rr.body_reader(&mut &b""[..]).read_to_string(&mut body).unwrap();
    /// assert_eq!(body, "hello");
    /// ```
    pub fn body_reader<R: io::BufRead>(&self, reader: R) -> crate::body::BodyReader<'_, R> {
        crate::body::BodyReader::new(
            reader,
            self.prefetch,
            self.request.body_kind(),
            crate::body::DEFAULT_MAX_BODY_SIZE,
        )
    }
}

impl<'buf, const MAX_HDRS: usize> Request<'buf, MAX_HDRS> {
    /// Read HTTP headers from a buffered reader into the caller-provided buffer.
    /// Returns a [`ReadRequest`] containing the parsed request, the byte
    /// offset where the body begins, and the total bytes read.
    ///
    /// Takes `impl BufRead` so the same buffered reader can be reused for
    /// body streaming without losing data buffered during header reading.
    ///
    /// Any body bytes that arrived in the same read as the headers are
    /// available in `buf[result.body_offset..result.bytes_read]`.
    ///
    /// `MAX_HDRS` can be customised via a const generic. Default is
    /// [`DEFAULT_MAX_HEADERS`]. The header buffer size is determined by
    /// the length of the slice you pass — typically
    /// [`DEFAULT_MAX_HEADER_SIZE`](super::DEFAULT_MAX_HEADER_SIZE).
    ///
    /// **Note:** This function does not enforce any timeout. Callers should
    /// set a read timeout on the underlying reader (e.g.
    /// [`TcpStream::set_read_timeout`](std::net::TcpStream::set_read_timeout))
    /// to defend against slowloris-style attacks where a client sends data
    /// very slowly to hold connections open.
    ///
    /// # Errors
    ///
    /// Returns [`ReadError`] which contains both the underlying
    /// [`ParseErrorKind`] and the number of bytes already read into `buf`.
    /// This allows callers to inspect partial data for logging or to
    /// reuse the connection after draining bad input.
    pub fn read(
        reader: &mut impl BufRead,
        buf: &'buf mut [u8],
    ) -> Result<ReadRequest<'buf, MAX_HDRS>, ReadError> {
        let buf_size = buf.len();
        let mut filled = 0;

        let header_end = loop {
            if filled >= buf_size {
                return Err(ReadError {
                    error: ParseErrorKind::HeadersTooLarge.into(),
                    bytes_read: filled,
                });
            }

            let available = match reader.fill_buf() {
                Ok([]) => {
                    return Err(ReadError {
                        error: ParseErrorKind::ConnectionClosed.into(),
                        bytes_read: filled,
                    });
                }
                Ok(b) => b,
                Err(e) => {
                    return Err(ReadError {
                        error: Error::Io(e),
                        bytes_read: filled,
                    });
                }
            };
            let n = available.len().min(buf_size - filled);
            buf[filled..filled + n].copy_from_slice(&available[..n]);
            reader.consume(n);
            filled += n;

            let search_start = filled.saturating_sub(n + 3);
            if let Some(pos) = buf[..filled].find_header_end(search_start) {
                break pos;
            }
        };

        let request = Self::parse_impl(&buf[..header_end]).map_err(|error| ReadError {
            error: error.into(),
            bytes_read: filled,
        })?;
        Ok(ReadRequest {
            request,
            prefetch: &buf[header_end..filled],
            body_offset: header_end,
            bytes_read: filled,
        })
    }
}