net-wire 0.1.0

Allocation-free no_std wire views and codecs for network protocols.
Documentation
use net_wire::http1::{
    Http1BodyFraming, Http1FieldRef, Http1ParseError, Http1RequestHead, Http1RequestHeadBuilder,
    Http1ResponseHead, Http1ResponseHeadBuilder, Http1Version,
};

#[test]
fn content_length_uses_all_occurrences_and_comma_members() {
    let equal = Http1RequestHead::parse(
        b"POST / HTTP/1.1\r\nContent-Length: 42, 42\r\ncontent-length:\t42 \r\n\r\n",
    )
    .unwrap();
    assert_eq!(
        equal.body_framing(),
        Ok(Http1BodyFraming::ContentLength(42))
    );

    let conflicting = Http1RequestHead::parse(
        b"POST / HTTP/1.1\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\n",
    )
    .unwrap();
    assert_eq!(
        conflicting.body_framing(),
        Err(Http1ParseError::ConflictingContentLength)
    );

    for value in [b"" as &[u8], b"4,", b"+4", b"4 4"] {
        let mut wire = [0u8; 96];
        let prefix = b"POST / HTTP/1.1\r\nContent-Length:";
        wire[..prefix.len()].copy_from_slice(prefix);
        wire[prefix.len()..prefix.len() + value.len()].copy_from_slice(value);
        let end = prefix.len() + value.len();
        wire[end..end + 4].copy_from_slice(b"\r\n\r\n");
        let head = Http1RequestHead::parse(&wire[..end + 4]).unwrap();
        assert_eq!(
            head.body_framing(),
            Err(Http1ParseError::InvalidContentLength)
        );
    }

    let overflow =
        Http1RequestHead::parse(b"POST / HTTP/1.1\r\nContent-Length: 18446744073709551616\r\n\r\n")
            .unwrap();
    if usize::BITS == 64 {
        assert_eq!(
            overflow.body_framing(),
            Err(Http1ParseError::ContentLengthOverflow)
        );
    }
}

#[test]
fn transfer_encoding_is_ordered_and_security_strict() {
    let valid = Http1RequestHead::parse(
        b"POST / HTTP/1.1\r\nTransfer-Encoding: gzip; level=\"a,b\", chunked\r\n\r\n",
    )
    .unwrap();
    assert_eq!(valid.body_framing(), Ok(Http1BodyFraming::Chunked));

    let split = Http1RequestHead::parse(
        b"POST / HTTP/1.1\r\nTransfer-Encoding: gzip\r\nTransfer-Encoding: chunked\r\n\r\n",
    )
    .unwrap();
    assert_eq!(split.body_framing(), Ok(Http1BodyFraming::Chunked));

    let escaped = Http1RequestHead::parse(
        b"POST / HTTP/1.1\r\nTransfer-Encoding: gzip; note=\"a\\\"b\", chunked\r\n\r\n",
    )
    .unwrap();
    assert_eq!(escaped.body_framing(), Ok(Http1BodyFraming::Chunked));
    let dangling_escape = Http1RequestHead::parse(
        b"POST / HTTP/1.1\r\nTransfer-Encoding: gzip; note=\"bad\\\r\n\r\n",
    )
    .unwrap();
    assert_eq!(
        dangling_escape.body_framing(),
        Err(Http1ParseError::InvalidTransferEncoding)
    );

    let response =
        Http1ResponseHead::parse(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip; level=fast\r\n\r\n")
            .unwrap();
    assert_eq!(
        response.body_framing(None),
        Ok(Http1BodyFraming::CloseDelimited)
    );

    let request =
        Http1RequestHead::parse(b"POST / HTTP/1.1\r\nTransfer-Encoding: gzip\r\n\r\n").unwrap();
    assert_eq!(
        request.body_framing(),
        Err(Http1ParseError::InvalidTransferEncoding)
    );

    for value in [
        b"chunked, gzip" as &[u8],
        b"chunked; q=1",
        b"gzip, chunked, chunked",
        b"gzip,,chunked",
        b"gzip; q=\"unterminated",
    ] {
        let fields = [Http1FieldRef::new(b"Transfer-Encoding", value)];
        let mut output = [0u8; 128];
        let head =
            Http1RequestHeadBuilder::new(&mut output, b"POST", b"/", Http1Version::Http11, &fields)
                .build()
                .unwrap();
        assert!(head.body_framing().is_err(), "{value:?}");
    }

    let conflict = Http1RequestHead::parse(
        b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nContent-Length: bogus\r\n\r\n",
    )
    .unwrap();
    assert_eq!(
        conflict.body_framing(),
        Err(Http1ParseError::TransferEncodingWithContentLength)
    );
}

#[test]
fn response_framing_applies_rfc_precedence_and_context() {
    for status in [100, 199, 204, 304] {
        let mut wire = [0u8; 96];
        let status = [
            b'0' + (status / 100) as u8,
            b'0' + ((status / 10) % 10) as u8,
            b'0' + (status % 10) as u8,
        ];
        let fields = [
            Http1FieldRef::new(b"Transfer-Encoding", b"chunked"),
            Http1FieldRef::new(b"Content-Length", b"5"),
        ];
        let head = Http1ResponseHeadBuilder::new(
            &mut wire,
            Http1Version::Http11,
            u16::from(status[0] - b'0') * 100
                + u16::from(status[1] - b'0') * 10
                + u16::from(status[2] - b'0'),
            b"x",
            &fields,
        )
        .build()
        .unwrap();
        assert_eq!(head.body_framing(None), Ok(Http1BodyFraming::None));
    }

    let conflict = Http1ResponseHead::parse(
        b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n",
    )
    .unwrap();
    assert_eq!(
        conflict.body_framing(Some(b"HEAD")),
        Ok(Http1BodyFraming::None)
    );
    assert_eq!(
        conflict.body_framing(Some(b"CONNECT")),
        Ok(Http1BodyFraming::Tunnel)
    );
    assert_eq!(
        conflict.body_framing(None),
        Err(Http1ParseError::TransferEncodingWithContentLength)
    );

    let connect_299 = Http1ResponseHead::parse(b"HTTP/1.1 299 x\r\n\r\n").unwrap();
    assert_eq!(
        connect_299.body_framing(Some(b"CONNECT")),
        Ok(Http1BodyFraming::Tunnel)
    );
    let connect_300 = Http1ResponseHead::parse(b"HTTP/1.1 300 x\r\n\r\n").unwrap();
    assert_eq!(
        connect_300.body_framing(Some(b"CONNECT")),
        Ok(Http1BodyFraming::CloseDelimited)
    );

    let no_fields = Http1ResponseHead::parse(b"HTTP/1.1 200 OK\r\n\r\n").unwrap();
    assert_eq!(
        no_fields.body_framing(None),
        Ok(Http1BodyFraming::CloseDelimited)
    );
    assert_eq!(
        Http1RequestHead::parse(b"GET / HTTP/1.1\r\n\r\n")
            .unwrap()
            .body_framing(),
        Ok(Http1BodyFraming::None)
    );
}