saddle-framework 0.2.0

The single business-facing facade for Saddle applications
Documentation
const HEAD_BYTES: usize = 1_024;
const ROUTE_BYTES: usize = 64;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ParserError {
    HeadTooLarge,
    InvalidSyntax,
    Unsupported,
    InvalidRoute,
    InvalidHost,
    InvalidContentLength,
    AmbiguousFraming,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct FrozenHead {
    route: [u8; ROUTE_BYTES],
    route_len: usize,
    content_length: usize,
}

impl FrozenHead {
    pub(crate) fn route(&self) -> &[u8] {
        &self.route[..self.route_len]
    }

    pub(crate) const fn content_length(&self) -> usize {
        self.content_length
    }
}

pub(crate) struct HeadParser {
    bytes: [u8; HEAD_BYTES],
    length: usize,
    complete: bool,
}

impl HeadParser {
    pub(crate) const fn new() -> Self {
        Self {
            bytes: [0; HEAD_BYTES],
            length: 0,
            complete: false,
        }
    }

    /// Pushes exactly one byte so Transport never materializes body bytes
    /// before the frozen route has been looked up and admitted.
    pub(crate) fn push_byte(&mut self, byte: u8) -> Result<Option<FrozenHead>, ParserError> {
        if self.complete {
            return Err(ParserError::InvalidSyntax);
        }
        let target = self
            .bytes
            .get_mut(self.length)
            .ok_or(ParserError::HeadTooLarge)?;
        *target = byte;
        self.length += 1;
        if !self.bytes[..self.length].ends_with(b"\r\n\r\n") {
            return Ok(None);
        }
        self.complete = true;
        parse_frozen(&self.bytes[..self.length]).map(Some)
    }
}

fn parse_frozen(head: &[u8]) -> Result<FrozenHead, ParserError> {
    if head.iter().enumerate().any(|(index, byte)| {
        (*byte == b'\n' && (index == 0 || head[index - 1] != b'\r'))
            || (*byte == b'\r' && head.get(index + 1) != Some(&b'\n'))
    }) {
        return Err(ParserError::InvalidSyntax);
    }
    let mut lines = head.split(|byte| *byte == b'\n');
    let request = trim_cr(lines.next().ok_or(ParserError::InvalidSyntax)?)?;
    let route = parse_request_line(request)?;
    let mut host_seen = false;
    let mut length = None;
    let mut ended = false;
    for raw in lines {
        let line = trim_cr(raw)?;
        if line.is_empty() {
            ended = true;
            break;
        }
        if line[0] == b' ' || line[0] == b'\t' {
            return Err(ParserError::InvalidSyntax);
        }
        let colon = line
            .iter()
            .position(|byte| *byte == b':')
            .ok_or(ParserError::InvalidSyntax)?;
        let name = &line[..colon];
        let value = &line[colon + 1..];
        if name.eq_ignore_ascii_case(b"host") {
            if host_seen || !canonical_field_value(value) || !canonical_host(&value[1..]) {
                return Err(ParserError::InvalidHost);
            }
            host_seen = true;
        } else if name.eq_ignore_ascii_case(b"content-length") {
            if length.is_some() || !canonical_field_value(value) {
                return Err(ParserError::InvalidContentLength);
            }
            length = Some(parse_decimal(&value[1..])?);
        } else if name.eq_ignore_ascii_case(b"transfer-encoding")
            || name.eq_ignore_ascii_case(b"upgrade")
            || name.eq_ignore_ascii_case(b"expect")
            || name.eq_ignore_ascii_case(b"trailer")
            || name.eq_ignore_ascii_case(b"proxy-connection")
        {
            return Err(ParserError::AmbiguousFraming);
        } else if name.eq_ignore_ascii_case(b"connection")
            && (!canonical_field_value(value) || &value[1..] != b"close")
        {
            return Err(ParserError::Unsupported);
        }
    }
    if !ended || !host_seen {
        return Err(ParserError::InvalidHost);
    }
    let content_length = length.ok_or(ParserError::InvalidContentLength)?;
    let mut frozen_route = [0; ROUTE_BYTES];
    frozen_route[..route.len()].copy_from_slice(route);
    Ok(FrozenHead {
        route: frozen_route,
        route_len: route.len(),
        content_length,
    })
}

fn trim_cr(line: &[u8]) -> Result<&[u8], ParserError> {
    line.strip_suffix(b"\r").ok_or(ParserError::InvalidSyntax)
}

fn parse_request_line(line: &[u8]) -> Result<&[u8], ParserError> {
    let prefix = b"POST ";
    let suffix = b" HTTP/1.1";
    if !line.starts_with(prefix) || !line.ends_with(suffix) {
        return Err(ParserError::Unsupported);
    }
    let route = &line[prefix.len()..line.len() - suffix.len()];
    if route.len() < 2 || route.len() > ROUTE_BYTES || route[0] != b'/' {
        return Err(ParserError::InvalidRoute);
    }
    let mut segment = 0;
    for byte in &route[1..] {
        match byte {
            b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' => segment += 1,
            b'/' if segment > 0 => segment = 0,
            _ => return Err(ParserError::InvalidRoute),
        }
    }
    if segment == 0 {
        return Err(ParserError::InvalidRoute);
    }
    Ok(route)
}

fn canonical_field_value(value: &[u8]) -> bool {
    value.len() > 1
        && value[0] == b' '
        && !value[1..]
            .iter()
            .any(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | 0))
}

fn canonical_host(value: &[u8]) -> bool {
    if value.is_empty() || value.len() > 128 {
        return false;
    }
    let (host, port) = match value.iter().rposition(|byte| *byte == b':') {
        Some(colon) if !value[..colon].contains(&b':') => {
            (&value[..colon], Some(&value[colon + 1..]))
        }
        Some(_) => return false,
        None => (value, None),
    };
    if host.is_empty()
        || host.split(|byte| *byte == b'.').any(|label| {
            label.is_empty()
                || label.len() > 63
                || !(label[0].is_ascii_lowercase() || label[0].is_ascii_digit())
                || !(label[label.len() - 1].is_ascii_lowercase()
                    || label[label.len() - 1].is_ascii_digit())
                || label.iter().any(|byte| {
                    !byte.is_ascii_lowercase() && !byte.is_ascii_digit() && *byte != b'-'
                })
        })
    {
        return false;
    }
    match port {
        None => true,
        Some(port)
            if !port.is_empty()
                && port.iter().all(u8::is_ascii_digit)
                && (port.len() == 1 || port[0] != b'0') =>
        {
            port.iter()
                .try_fold(0_u32, |value, digit| {
                    value.checked_mul(10)?.checked_add(u32::from(digit - b'0'))
                })
                .is_some_and(|port| (1..=u32::from(u16::MAX)).contains(&port))
        }
        Some(_) => false,
    }
}

fn parse_decimal(value: &[u8]) -> Result<usize, ParserError> {
    if value.is_empty()
        || !value.iter().all(u8::is_ascii_digit)
        || (value.len() > 1 && value[0] == b'0')
    {
        return Err(ParserError::InvalidContentLength);
    }
    value.iter().try_fold(0_usize, |total, digit| {
        total
            .checked_mul(10)
            .and_then(|value| value.checked_add(usize::from(digit - b'0')))
            .ok_or(ParserError::InvalidContentLength)
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    fn parse(value: &[u8]) -> Result<FrozenHead, ParserError> {
        let mut parser = HeadParser::new();
        for byte in value {
            if let Some(head) = parser.push_byte(*byte)? {
                return Ok(head);
            }
        }
        Err(ParserError::InvalidSyntax)
    }

    #[test]
    fn freezes_only_after_canonical_head() {
        let head = parse(
            b"POST /orders/create HTTP/1.1\r\nHost: api.example\r\nContent-Length: 5\r\nConnection: close\r\n\r\n",
        )
        .unwrap();
        assert_eq!(head.route(), b"/orders/create");
        assert_eq!(head.content_length(), 5);
    }

    #[test]
    fn rejects_ambiguous_framing_and_normalization() {
        assert_eq!(
            parse(b"POST /x HTTP/1.1\r\nHost: api\r\nContent-Length: 1\r\nTransfer-Encoding: chunked\r\n\r\n")
                .unwrap_err(),
            ParserError::AmbiguousFraming
        );
        assert_eq!(
            parse(b"POST /X HTTP/1.1\r\nHost: api\r\nContent-Length: 1\r\n\r\n").unwrap_err(),
            ParserError::InvalidRoute
        );
        assert_eq!(
            parse(b"POST /x HTTP/1.1\r\nHost: api\r\nContent-Length: 01\r\n\r\n").unwrap_err(),
            ParserError::InvalidContentLength
        );
    }

    #[test]
    fn host_matches_the_approved_authority_subset() {
        for host in [
            b"Api.example".as_slice(),
            b"-api.example".as_slice(),
            b"api-.example".as_slice(),
            b"api..example".as_slice(),
            b"api.example:0".as_slice(),
            b"api.example:080".as_slice(),
            b"api.example:65536".as_slice(),
            b"api.example:1:2".as_slice(),
        ] {
            assert!(!canonical_host(host), "{host:?}");
        }
        for host in [
            b"api.example".as_slice(),
            b"127.0.0.1".as_slice(),
            b"api-1.example:443".as_slice(),
        ] {
            assert!(canonical_host(host), "{host:?}");
        }
    }
}