xocomil 0.3.0

A lightweight, zero-allocation HTTP/1.1 request parser and response writer
Documentation
//! Unified error type for all xocomil operations.
//!
//! [`Error`] wraps every failure mode — I/O errors, parse errors, and body
//! errors — into a single type so callers can use one `?` chain for an
//! entire request–response cycle.
//!
//! Pattern-match on [`Error`] directly to distinguish error
//! classes via the inner kind. The [`ParseErrorKind`] and [`BodyErrorKind`] enums are
//! `#[non_exhaustive]` so new variants can be added without breaking
//! downstream code.

use std::fmt;
use std::io;

// ---------------------------------------------------------------------------
// ParseErrorKind
// ---------------------------------------------------------------------------

/// What went wrong when parsing an HTTP request.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseErrorKind {
    /// Headers exceed the maximum allowed size.
    HeadersTooLarge,
    /// Connection closed before headers were complete.
    ConnectionClosed,
    /// No request line found.
    NoRequestLine,
    /// Headers not terminated by `\r\n\r\n`.
    IncompleteHeaders,
    /// Request line is malformed.
    MalformedRequestLine,
    /// Request target contains invalid bytes.
    MalformedRequestTarget,
    /// A header line is malformed.
    MalformedHeader,
    /// HTTP method is not supported.
    UnsupportedMethod,
    /// HTTP version is not supported.
    UnsupportedHttpVersion,
    /// Too many headers.
    TooManyHeaders,
    /// Missing required Host header (HTTP/1.1).
    MissingHostHeader,
    /// Duplicate header that must be unique.
    DuplicateHeader,
    /// Conflicting Transfer-Encoding and Content-Length headers.
    ConflictingHeaders,
    /// Content-Length value is not a valid integer.
    InvalidContentLength,
    /// Transfer-Encoding value is not supported.
    UnsupportedTransferEncoding,
}

impl fmt::Display for ParseErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::HeadersTooLarge => f.write_str("headers too large"),
            Self::ConnectionClosed => f.write_str("connection closed before headers complete"),
            Self::NoRequestLine => f.write_str("no request line"),
            Self::IncompleteHeaders => f.write_str("headers not terminated by \\r\\n\\r\\n"),
            Self::MalformedRequestLine => f.write_str("malformed request line"),
            Self::MalformedRequestTarget => f.write_str("request target contains invalid byte"),
            Self::MalformedHeader => f.write_str("malformed header line"),
            Self::UnsupportedMethod => f.write_str("unsupported method"),
            Self::UnsupportedHttpVersion => f.write_str("unsupported HTTP version"),
            Self::TooManyHeaders => f.write_str("too many headers"),
            Self::MissingHostHeader => f.write_str("missing required Host header"),
            Self::DuplicateHeader => f.write_str("duplicate header not allowed"),
            Self::ConflictingHeaders => {
                f.write_str("conflicting Transfer-Encoding and Content-Length")
            }
            Self::InvalidContentLength => f.write_str("invalid Content-Length value"),
            Self::UnsupportedTransferEncoding => {
                f.write_str("unsupported Transfer-Encoding (only chunked is supported)")
            }
        }
    }
}

// ---------------------------------------------------------------------------
// BodyErrorKind
// ---------------------------------------------------------------------------

/// What went wrong when reading or decoding a request body.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BodyErrorKind {
    /// Body exceeds the maximum allowed size.
    BodyTooLarge,
    /// Connection closed before the complete body was received.
    UnexpectedEof,
    /// Chunk size line contains invalid hex digits.
    InvalidChunkSize,
    /// Chunked encoding is structurally invalid.
    MalformedChunkedEncoding,
}

impl fmt::Display for BodyErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::BodyTooLarge => f.write_str("body exceeds buffer size"),
            Self::UnexpectedEof => f.write_str("connection closed before body complete"),
            Self::InvalidChunkSize => f.write_str("invalid chunk size"),
            Self::MalformedChunkedEncoding => f.write_str("malformed chunked encoding"),
        }
    }
}

// ---------------------------------------------------------------------------
// PctErrorKind
// ---------------------------------------------------------------------------

/// What went wrong when percent-decoding a byte slice.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PctErrorKind {
    /// A `%` was not followed by two hex digits.
    InvalidEscape,
    /// The output buffer was too small to hold the decoded result.
    BufferTooSmall,
}

impl fmt::Display for PctErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidEscape => f.write_str("invalid percent-encoded escape"),
            Self::BufferTooSmall => f.write_str("decode buffer too small"),
        }
    }
}

// ---------------------------------------------------------------------------
// MediaErrorKind
// ---------------------------------------------------------------------------

/// What went wrong when parsing a media-type header value.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MediaErrorKind {
    /// The input was empty or whitespace-only.
    Empty,
    /// The input did not contain a `/` separating type and subtype.
    MissingSlash,
    /// The type or subtype was empty after trimming whitespace.
    InvalidToken,
}

impl fmt::Display for MediaErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => f.write_str("empty media type"),
            Self::MissingSlash => f.write_str("media type missing '/'"),
            Self::InvalidToken => f.write_str("media type has empty token"),
        }
    }
}

// ---------------------------------------------------------------------------
// ResponseErrorKind
// ---------------------------------------------------------------------------

/// What went wrong when building an HTTP response.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResponseErrorKind {
    /// Response header capacity exceeded.
    HeaderCapacityExceeded,
    /// Response header name contains invalid bytes.
    InvalidHeaderName,
    /// Response header value contains forbidden bytes (NUL, CR, or LF).
    InvalidHeaderValue,
    /// Duplicate Content-Length header.
    DuplicateContentLength,
}

impl fmt::Display for ResponseErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::HeaderCapacityExceeded => f.write_str("response header capacity exceeded"),
            Self::InvalidHeaderName => f.write_str("response header name contains invalid byte"),
            Self::InvalidHeaderValue => {
                f.write_str("response header value contains forbidden byte (NUL, CR, or LF)")
            }
            Self::DuplicateContentLength => f.write_str("duplicate Content-Length header"),
        }
    }
}

// ---------------------------------------------------------------------------
// Error
// ---------------------------------------------------------------------------

/// Unified error type for all xocomil operations.
///
/// Wraps I/O errors, request-parse errors, and body errors into a single
/// enum so callers need only one error type in their signatures.
///
/// # Matching specific error kinds
///
/// ```
/// use xocomil::error::{Error, ParseErrorKind};
///
/// fn status_for(err: &Error) -> u16 {
///     match err {
///         Error::Parse(ParseErrorKind::HeadersTooLarge) => 431,
///         Error::Parse(_) => 400,
///         Error::Body(_) => 400,
///         Error::Response(_) => 400,
///         Error::Pct(_) => 400,
///         Error::Media(_) => 400,
///         Error::Io(_) => 500,
///     }
/// }
/// ```
#[derive(Debug)]
pub enum Error {
    /// An I/O error from the underlying reader or writer.
    Io(io::Error),
    /// A request parsing error.
    Parse(ParseErrorKind),
    /// A body reading or decoding error.
    Body(BodyErrorKind),
    /// A response building error.
    Response(ResponseErrorKind),
    /// A percent-decoding error.
    Pct(PctErrorKind),
    /// A media-type parsing error.
    Media(MediaErrorKind),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io(e) => fmt::Display::fmt(e, f),
            Self::Parse(k) => fmt::Display::fmt(k, f),
            Self::Body(k) => fmt::Display::fmt(k, f),
            Self::Response(k) => fmt::Display::fmt(k, f),
            Self::Pct(k) => fmt::Display::fmt(k, f),
            Self::Media(k) => fmt::Display::fmt(k, f),
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Io(e) => Some(e),
            _ => None,
        }
    }
}

impl From<io::Error> for Error {
    fn from(e: io::Error) -> Self {
        Self::Io(e)
    }
}

impl From<ParseErrorKind> for Error {
    fn from(k: ParseErrorKind) -> Self {
        Self::Parse(k)
    }
}

impl From<BodyErrorKind> for Error {
    fn from(k: BodyErrorKind) -> Self {
        Self::Body(k)
    }
}

impl From<ResponseErrorKind> for Error {
    fn from(k: ResponseErrorKind) -> Self {
        Self::Response(k)
    }
}

impl From<PctErrorKind> for Error {
    fn from(k: PctErrorKind) -> Self {
        Self::Pct(k)
    }
}

impl From<MediaErrorKind> for Error {
    fn from(k: MediaErrorKind) -> Self {
        Self::Media(k)
    }
}

impl From<Error> for io::Error {
    fn from(e: Error) -> Self {
        match e {
            Error::Io(e) => e,
            Error::Response(_) | Error::Pct(_) | Error::Media(_) => {
                Self::new(io::ErrorKind::InvalidInput, e)
            }
            other => Self::new(io::ErrorKind::InvalidData, other),
        }
    }
}

// ---------------------------------------------------------------------------
// ReadError
// ---------------------------------------------------------------------------

/// Error from [`Request::read`](crate::request::Request::read), preserving
/// how many bytes were read into the buffer before the failure.
///
/// This allows callers to recover partial data (e.g. for logging or
/// connection reuse).
#[derive(Debug)]
pub struct ReadError {
    /// The underlying error.
    pub error: Error,
    /// Number of bytes written into the buffer before the error.
    /// The buffer contents `buf[..bytes_read]` are valid but may
    /// represent an incomplete or malformed request.
    pub bytes_read: usize,
}

impl fmt::Display for ReadError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.error.fmt(f)
    }
}

impl std::error::Error for ReadError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.error.source()
    }
}

impl From<ReadError> for io::Error {
    fn from(e: ReadError) -> Self {
        e.error.into()
    }
}