use crate::NovaStr;
pub(crate) const MAX_HEAD_BYTES: usize = 8 * 1024;
pub(crate) const MAX_HEADER_COUNT: usize = 100;
pub(crate) const ERR_HEAD_TOO_LARGE: i64 = 1;
pub(crate) const ERR_TOO_MANY_HEADERS: i64 = 2;
pub(crate) const ERR_BAD_REQUEST_LINE: i64 = 3;
pub(crate) const ERR_BAD_HEADER: i64 = 4;
pub(crate) const ERR_BAD_NEWLINE: i64 = 5;
pub(crate) const ERR_INCOMPLETE_HEAD_FIELDS: i64 = 6;
pub(crate) const ERR_SLICE_OUT_OF_BUFFER: i64 = 7;
fn status_only(status: i64) -> *mut u8 {
let block = crate::gc::alloc(8 + 8, true);
let words = block as *mut i64;
unsafe {
*words = 1;
*words.add(1) = status;
}
block
}
fn offset_within(base: &[u8], part: &[u8]) -> Option<i64> {
let base_start = base.as_ptr() as usize;
let base_end = base_start + base.len();
let part_start = part.as_ptr() as usize;
let part_end = part_start + part.len();
if part_start < base_start || part_end > base_end {
return None;
}
i64::try_from(part_start - base_start).ok()
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_http_parse_request(b: *const NovaStr) -> *mut u8 {
let buf = unsafe { crate::bytes::as_bytes(b) };
if buf.len() > MAX_HEAD_BYTES {
return status_only(-ERR_HEAD_TOO_LARGE);
}
let mut storage = [httparse::EMPTY_HEADER; MAX_HEADER_COUNT];
let mut req = httparse::Request::new(&mut storage);
let consumed = match req.parse(buf) {
Ok(httparse::Status::Complete(n)) => n,
Ok(httparse::Status::Partial) => return status_only(1),
Err(httparse::Error::TooManyHeaders) => return status_only(-ERR_TOO_MANY_HEADERS),
Err(httparse::Error::NewLine) => return status_only(-ERR_BAD_NEWLINE),
Err(httparse::Error::HeaderName) | Err(httparse::Error::HeaderValue) => {
return status_only(-ERR_BAD_HEADER)
}
Err(_) => return status_only(-ERR_BAD_REQUEST_LINE),
};
let (Some(method), Some(path), Some(minor)) = (req.method, req.path, req.version) else {
return status_only(-ERR_INCOMPLETE_HEAD_FIELDS);
};
let headers = &req.headers[..];
let n = headers.len();
if n > MAX_HEADER_COUNT {
return status_only(-ERR_TOO_MANY_HEADERS);
}
let Some(method_start) = offset_within(buf, method.as_bytes()) else {
return status_only(-ERR_SLICE_OUT_OF_BUFFER);
};
let Some(path_start) = offset_within(buf, path.as_bytes()) else {
return status_only(-ERR_SLICE_OUT_OF_BUFFER);
};
for h in headers {
if offset_within(buf, h.name.as_bytes()).is_none() || offset_within(buf, h.value).is_none()
{
return status_only(-ERR_SLICE_OUT_OF_BUFFER);
}
}
let count = 8 + 4 * n;
let block = crate::gc::alloc(8 + 8 * count, true);
let words = block as *mut i64;
unsafe {
*words = count as i64;
*words.add(1) = 0;
*words.add(2) = method_start;
*words.add(3) = method.len() as i64;
*words.add(4) = path_start;
*words.add(5) = path.len() as i64;
*words.add(6) = i64::from(minor);
*words.add(7) = n as i64;
for (i, h) in headers.iter().enumerate() {
let at = 7 + 4 * i;
let (Some(name_start), Some(value_start)) = (
offset_within(buf, h.name.as_bytes()),
offset_within(buf, h.value),
) else {
*words = 1;
*words.add(1) = -ERR_SLICE_OUT_OF_BUFFER;
return block;
};
*words.add(1 + at) = name_start;
*words.add(2 + at) = h.name.len() as i64;
*words.add(3 + at) = value_start;
*words.add(4 + at) = h.value.len() as i64;
}
*words.add(1 + 8 + 4 * n - 1) = consumed as i64;
}
block
}
#[cfg(test)]
mod tests {
use super::*;
fn read_block(block: *mut u8) -> Vec<i64> {
let words = block as *const i64;
unsafe {
let n = *words;
(0..n).map(|i| *words.add(1 + i as usize)).collect()
}
}
fn parse(src: &[u8]) -> Vec<i64> {
let b = crate::gc_bytes_for_test(src);
read_block(unsafe { nova_rt_http_parse_request(b) })
}
#[test]
fn parses_a_request_with_two_headers() {
let src = b"GET /hi HTTP/1.1\r\nHost: a.example\r\nX-K: v\r\n\r\nbody";
let t = parse(src);
assert_eq!(t.len(), 8 + 4 * 2, "8 fixed words plus four per header");
assert_eq!(t[0], 0, "status: complete");
assert_eq!((t[1], t[2]), (0, 3), "method `GET`");
assert_eq!((t[3], t[4]), (4, 3), "path `/hi`");
assert_eq!(t[5], 1, "minor version");
assert_eq!(t[6], 2, "header count");
assert_eq!((t[7], t[8]), (18, 4), "name `Host`");
assert_eq!((t[9], t[10]), (24, 9), "value `a.example`");
assert_eq!((t[11], t[12]), (35, 3), "name `X-K`");
assert_eq!((t[13], t[14]), (40, 1), "value `v`");
assert_eq!(t[15], 45, "body_start, just past the terminating CRLF CRLF");
assert_eq!(&src[t[15] as usize..], b"body");
}
#[test]
fn a_partial_head_is_status_one_and_carries_nothing_else() {
let t = parse(b"GET /hi HTTP/1.1\r\nHost: a.exam");
assert_eq!(t, vec![1], "status 1 alone: the caller must read more");
}
#[test]
fn a_head_over_the_byte_ceiling_is_rejected_before_parsing() {
let mut src = b"GET /".to_vec();
src.resize(MAX_HEAD_BYTES + 1, b'x');
let t = parse(&src);
assert_eq!(t, vec![-ERR_HEAD_TOO_LARGE]);
}
#[test]
fn a_head_at_the_byte_ceiling_is_not_rejected_for_being_too_large() {
let mut src = b"GET /".to_vec();
src.resize(MAX_HEAD_BYTES, b'x');
let t = parse(&src);
assert_ne!(
t,
vec![-ERR_HEAD_TOO_LARGE],
"the ceiling is inclusive: exactly MAX_HEAD_BYTES is allowed in"
);
}
#[test]
fn more_headers_than_the_ceiling_is_its_own_error_kind() {
let mut src = b"GET / HTTP/1.1\r\n".to_vec();
for i in 0..=MAX_HEADER_COUNT {
src.extend_from_slice(b"H");
src.extend_from_slice(i.to_string().as_bytes());
src.extend_from_slice(b": v\r\n");
}
src.extend_from_slice(b"\r\n");
assert!(
src.len() <= MAX_HEAD_BYTES,
"this input tests headers, not bytes"
);
let t = parse(&src);
assert_eq!(t, vec![-ERR_TOO_MANY_HEADERS]);
}
#[test]
fn exactly_the_header_ceiling_parses() {
let mut src = b"GET / HTTP/1.1\r\n".to_vec();
for i in 0..MAX_HEADER_COUNT {
src.extend_from_slice(b"H");
src.extend_from_slice(i.to_string().as_bytes());
src.extend_from_slice(b": v\r\n");
}
src.extend_from_slice(b"\r\n");
let t = parse(&src);
assert_eq!(t[0], 0, "status: complete");
assert_eq!(t[6], MAX_HEADER_COUNT as i64, "header count at the ceiling");
assert_eq!(t.len(), 8 + 4 * MAX_HEADER_COUNT);
}
#[test]
fn a_malformed_request_line_is_a_negative_status_of_length_one() {
let t = parse(b"GET\r\n\r\n");
assert_eq!(t.len(), 1, "an error carries the status and nothing else");
assert!(t[0] < 0, "an error status is negative, got {}", t[0]);
}
#[test]
fn an_empty_buffer_is_partial_rather_than_an_error() {
assert_eq!(parse(b""), vec![1]);
}
#[test]
fn a_request_with_no_headers_still_reports_body_start() {
let src = b"GET / HTTP/1.1\r\n\r\n";
let t = parse(src);
assert_eq!(t[0], 0);
assert_eq!(t[6], 0, "no headers");
assert_eq!(t.len(), 8, "8 fixed words and no header quadruples");
assert_eq!(t[7], src.len() as i64, "body_start is the end of the head");
}
#[test]
fn http_one_zero_reports_minor_version_zero() {
let t = parse(b"GET / HTTP/1.0\r\n\r\n");
assert_eq!(t[0], 0);
assert_eq!(t[5], 0);
}
#[test]
fn no_http_intrinsic_can_panic() {
let source = include_str!("http.rs");
let production = source.split("mod tests {").next().unwrap_or(source);
for needle in [
".borrow_mut()",
".borrow()",
"unwrap()",
".expect(",
"panic!",
"format!",
] {
assert!(
!production.contains(needle),
"a std/http intrinsic must not panic: `{needle}` found in this \
file's production code, which is reachable from a compiled \
Nova frame with no landing pad to unwind through"
);
}
}
}