sark-http 0.29.1

HTTP wire types, semantics, codecs, and ownership invariants for Sark
pub mod common;

use std::iter;

use common::head::{self, Request as _, method};
use dope_test::alloc::Tracker;
use sark_http::{
    field,
    h1::{
        head::line::{Request, Version, error},
        target,
    },
    method::{Error, Known, Method},
    uri::path,
};

#[global_allocator]
static ALLOCATOR: Tracker = Tracker::new();

#[test]
fn parses_complete_line() {
    let raw = b"GET /items?ignored=yes HTTP/1.1\r\nHost: example\r\n\r\n";
    let line = (Request::parse(raw))
        .expect("complete request line must parse")
        .expect("complete request line must parse");

    assert_eq!(line.method(), b"GET");
    assert_eq!(line.target(), b"/items?ignored=yes");
    assert_eq!(line.path(), Some(b"/items".as_slice()));
    assert_eq!(line.query(), Some(b"ignored=yes".as_slice()));
    assert!((line.version()) == (Version::Eleven));
    assert_eq!(&raw[line.end()..], b"Host: example\r\n\r\n");
}

#[test]
fn classifies_failures() {
    assert!(matches!(Request::parse(b"GET / HTTP/1.1\r"), Ok(None)));
    assert!(Request::parse(b"GET / HTTP/1.1\rx").is_err());
    assert!(Request::parse(b"GET / HTTP/2.0\r\n").is_err());
}

#[test]
fn prefixes_stay_partial() {
    let raw = head::ESCAPED_REQUEST_LINE;
    for end in 0..raw.len() {
        assert!(
            matches!(Request::parse(&raw[..end]), Ok(None)),
            "prefix {end} was classified as complete or malformed",
        );
    }
    let line = (Request::parse(raw))
        .expect("complete request line must parse")
        .expect("complete request line must parse");
    assert_eq!(line.method(), b"OPTIONS");
    assert_eq!(line.path(), Some(head::ESCAPED_PATH));
    assert!((line.version()) == (Version::Ten));
    assert_eq!(line.end(), raw.len());
}

#[test]
fn checks_separator_lanes() {
    for len in 1..=32 {
        let mut raw = b"GET ".to_vec();
        let start = raw.len();
        raw.push(b'/');
        raw.extend(iter::repeat_n(b'a', len - 1));
        raw.extend_from_slice(b" HTTP/1.1\r\n");

        let line = (Request::parse(&raw))
            .expect("complete request line must parse")
            .expect("complete request line must parse");
        assert_eq!(line.target(), &raw[start..start + len]);

        raw[start + len - 1] = b'\r';
        assert!(Request::parse(&raw).is_err());
    }
}

#[test]
fn keeps_absolute_spans() {
    let raw = b"GET https://origin.example/items/42?q=rust HTTP/1.1\r\n";
    let line = (Request::parse(raw))
        .expect("complete request line must parse")
        .expect("complete request line must parse");
    assert_eq!(line.path(), Some(b"/items/42".as_slice()));
    assert_eq!(line.authority(), Some(b"origin.example".as_slice()));
}

#[test]
fn rejects_bad_shapes() {
    for raw in [
        b" / HTTP/1.1\r\n".as_slice(),
        b"GET  HTTP/1.1\r\n",
        b"GET /\r\n",
        b"GET / HTTP/1.1 extra\r\n",
        b"GET / HTTP/1.2\r\n",
        b"GET / HTTP/1.1\n\r\n",
        b"G\rET / HTTP/1.1\r\n",
        b"GET /\rhidden HTTP/1.1\r\n",
        b"GET /bad/%zz HTTP/1.1\r\n",
        b"GET * HTTP/1.1\r\n",
        head::CONNECT_ORIGIN,
        head::INVALID_ESCAPE_PREFIX,
    ] {
        assert!(Request::parse(raw).is_err(), "{raw:?}");
    }
}

#[test]
fn admits_rfc3986_bytes() {
    for byte in u8::MIN..=u8::MAX {
        let ordinary = byte.is_ascii_alphanumeric() || b"-._~!$&'()*+,;=:@/".contains(&byte);
        for context in *b"g?" {
            let short = [b'/', context, byte];
            let mut wide = [b'g'; 128];
            wide[0] = b'/';
            wide[1] = context;
            wide[65] = byte;
            for target in [short.as_slice(), wide.as_slice()] {
                assert_eq!(
                    matches!(Request::parse(&target.request()), Ok(Some(_))),
                    ordinary || byte == b'?',
                    "byte {byte:#04x}, context {context:#04x}, length {}",
                    target.len(),
                );
            }
        }
    }
}

#[test]
fn scans_all_simd_lanes() {
    for pad in 0..=80 {
        let mut target = vec![b'/'];
        target.extend(iter::repeat_n(b'a', pad));
        target.extend_from_slice(b"%2f?q=%41??tail");
        let raw = target.request();
        let line = (Request::parse(&raw))
            .expect("complete request line must parse")
            .expect("complete request line must parse");
        assert_eq!(line.target(), target);

        let percent = 1 + pad;
        target[percent + 2] = b'g';
        assert!(Request::parse(&target.request()).is_err());
    }
}

#[test]
fn stops_before_headers() {
    let raw =
        b"GET /%2f?q=?? HTTP/1.1\r\nX-Ignored: %zz#????????????????????????????????\xff\r\n\r\n";
    let line = Request::parse(raw)
        .expect("valid request line")
        .expect("complete request line");
    assert_eq!(line.path(), Some(b"/%2f".as_slice()));
    assert_eq!(line.query(), Some(b"q=??".as_slice()));
    assert_eq!(line.end(), 24);
}

#[test]
fn keeps_first_path_error() {
    for at in [15, 16, 17, 31, 32, 33, 63, 64, 65, 79, 80, 81] {
        let mut target = [b'a'; 128];
        target[0] = b'/';
        target[at..at + 4].copy_from_slice(b"#%zz");
        assert_eq!(
            Request::parse(&target.request()).err(),
            Some(error::Request::Target(target::Error::Path(
                path::Error::Byte { at, byte: b'#' }
            ))),
        );

        target[at..at + 4].copy_from_slice(b"%zz#");
        assert_eq!(
            Request::parse(&target.request()).err(),
            Some(error::Request::Target(target::Error::Path(
                path::Error::Escape {
                    at,
                    first: Some(b'z'),
                    second: Some(b'z'),
                }
            ))),
        );
    }
}

#[test]
fn filters_method_keys() {
    let get = (Request::parse_with(b"GET / HTTP/1.1\r\n", Known::Get.bit()))
        .expect("complete request line must parse")
        .expect("complete request line must parse");
    assert!((get.matched()) == (Some(Known::Get)));

    let post = (Request::parse_with(b"POST / HTTP/1.1\r\n", Known::Get.bit()))
        .expect("complete request line must parse")
        .expect("complete request line must parse");
    assert!(post.matched().is_none());

    let ext = (Request::parse_with(b"SYNC / HTTP/1.1\r\n", 0))
        .expect("complete request line must parse")
        .expect("complete request line must parse");
    assert_eq!(ext.method(), b"SYNC");
    assert!(ext.matched().is_none());

    for (raw, known, form) in [
        (
            b"CONNECT example.test:443 HTTP/1.1\r\n".as_slice(),
            Known::Connect,
            target::Form::Authority,
        ),
        (
            b"OPTIONS * HTTP/1.1\r\n".as_slice(),
            Known::Options,
            target::Form::Asterisk,
        ),
    ] {
        for (methods, matched) in [(0, None), (known.bit(), Some(known))] {
            let line = Request::parse_with(raw, methods)
                .expect("admitted method")
                .expect("complete line");
            assert_eq!(line.method().as_ptr(), raw.as_ptr());
            assert_eq!(line.method(), known.as_str().as_bytes());
            assert_eq!(line.matched(), matched);
            assert!(line.plan().form() == form);
        }
    }
    assert_eq!(
        Request::parse_with(b"G\xffT /%zz HTTP/1.1\r\n", 0).err(),
        Some(error::Request::Syntax),
    );
}

#[test]
fn keeps_method_identity() {
    assert_eq!(Method::from(Known::Get), Method::GET);
    assert_eq!(
        Method::from_bytes(method::TRACE).expect("trace"),
        Method::TRACE
    );
    assert_eq!(
        Method::from_bytes(method::QUERY).expect("query"),
        Method::QUERY
    );

    let (short, usage) = Tracker::usage(|| Method::from_bytes(method::INLINE15.as_bytes()));
    let short = short.expect("inline extension");
    assert_eq!(usage, (0, 0));
    let (long, usage) = Tracker::usage(|| Method::from_bytes(method::OWNED16.as_bytes()));
    let long = long.expect("owned extension");
    assert_eq!(usage, (1, method::OWNED16.len()));
    assert_eq!(short.as_str(), method::INLINE15);
    assert_eq!(long.as_bytes(), method::OWNED16.as_bytes());
    assert_ne!(
        Method::from_bytes(method::LOWERCASE).expect("lowercase token"),
        Method::GET
    );
}

#[test]
fn retains_token_failure() {
    assert_eq!(
        Method::from_bytes(method::EMPTY),
        Err(Error::Syntax(field::Error::Empty))
    );
    assert_eq!(
        Method::from_bytes(method::SPACE),
        Err(Error::Syntax(field::Error::InvalidByte {
            index: 3,
            byte: b' '
        })),
    );
    assert_eq!(
        Method::from_bytes(method::NON_ASCII),
        Err(Error::Syntax(field::Error::InvalidByte {
            index: 7,
            byte: 255
        })),
    );
}