noggin_parser/
header_parser.rs

1use memchr::memmem;
2
3#[derive(thiserror::Error, PartialEq, Debug)]
4pub enum Error {
5    #[error("the http head was not complete")]
6    IncompleteHead,
7    #[error("the http head contained non-ascii characters")]
8    NonAscii,
9    #[error("missing http header: {0}")]
10    MissingHeader(&'static str),
11    #[error("malformed http header")]
12    MalformedHeader,
13    #[error("invalid http header value: {0}")]
14    InvalidHeaderValue(&'static str),
15}
16
17/// The `HeadParser` trait provides a way to parse HTTP headers and potentially
18/// returns the parsed headers and the remaining body of an HTTP message.
19///
20/// This trait is intended to be automatically implemented by the `noggin::Noggin`
21/// procedural macro for suitable structs. As such, users shouldn't typically
22/// need to implement it manually.
23pub trait HeadParser<'de>: Sized {
24    /// Parse the HTTP headers from a string slice representing the head section
25    /// of an HTTP message.
26    ///
27    /// # Parameters
28    ///
29    /// * `head`: A string slice containing the head section of an HTTP message.
30    ///
31    /// # Returns
32    ///
33    /// * `Result<Self, Error>`: Returns the parsed headers if successful, or
34    ///   an error if parsing fails.
35    fn parse_head_section(head: &'de str) -> Result<Self, Error>;
36
37    /// Parse the HTTP headers and returns both the parsed headers and the
38    /// remaining body from a byte slice containing both head and body sections
39    /// of an HTTP message.
40    ///
41    /// This function first locates the boundary between the head and body sections
42    /// (denoted by the sequence `\r\n\r\n`), then validates the ASCII nature of the
43    /// head, and finally calls the `parse_head_section` function to parse the headers.
44    ///
45    /// # Parameters
46    ///
47    /// * `head_and_body`: A byte slice containing both the head and body sections
48    ///   of an HTTP message.
49    ///
50    /// # Returns
51    ///
52    /// * `Result<(Self, &'de [u8]), Error>`: Returns a tuple containing the parsed
53    ///   headers and the remaining body if successful, or an error if parsing fails.
54    fn parse_headers(head_and_body: &'de [u8]) -> Result<(Self, &'de [u8]), Error> {
55        let head_end = memmem::find(head_and_body, b"\r\n\r\n").ok_or(Error::IncompleteHead)?;
56        let head_bytes = &head_and_body[..head_end];
57        if !head_bytes.is_ascii() {
58            return Err(Error::NonAscii);
59        }
60        // this is safe because we just checked if the bytes contained valid
61        // ascii and ascii is strict subset of utf-8
62        let head = unsafe { std::str::from_utf8_unchecked(head_bytes) };
63        let headers = Self::parse_head_section(head)?;
64        let body = &head_and_body[head_end + 4..];
65        Ok((headers, body))
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[derive(Debug, PartialEq)]
74    pub struct SimpleHeaders {
75        pub content_length: usize,
76        // Add additional headers as fields here...
77    }
78
79    impl<'de> HeadParser<'de> for SimpleHeaders {
80        fn parse_head_section(head: &'de str) -> Result<Self, Error> {
81            // A simple parsing implementation, for illustration.
82            let content_length_str = head
83                .split("\r\n")
84                .find(|line| line.starts_with("Content-Length:"))
85                .ok_or(Error::MissingHeader("Content-Length"))?
86                .split(':')
87                .nth(1)
88                .ok_or(Error::MalformedHeader)?
89                .trim();
90
91            let content_length = content_length_str
92                .parse::<usize>()
93                .map_err(|_| Error::InvalidHeaderValue("Content-Length"))?;
94
95            Ok(SimpleHeaders {
96                content_length,
97                // ... initialize other fields here
98            })
99        }
100    }
101
102    #[test]
103    fn parse_valid_head() {
104        let input_head = b"Content-Length: 5\r\nAnother-Header: value\r\n\r\nBodyHere";
105        let (headers, body) = SimpleHeaders::parse_headers(input_head).unwrap();
106
107        assert_eq!(headers, SimpleHeaders { content_length: 5 });
108        assert_eq!(body, b"BodyHere");
109    }
110
111    #[test]
112    fn error_on_non_ascii_head() {
113        let input_head = b"Content-Length: 5\r\nNon-Ascii: \x80\x81\x82\r\n\r\nBodyHere";
114        let result = SimpleHeaders::parse_headers(input_head);
115
116        assert_eq!(result, Err(Error::NonAscii));
117    }
118
119    #[test]
120    fn error_on_incomplete_head() {
121        let input_head = b"Content-Length: 5\r\nAnother-Header: value\r\nBodyWithoutHeadDelimiter";
122        let result = SimpleHeaders::parse_headers(input_head);
123
124        assert_eq!(result, Err(Error::IncompleteHead));
125    }
126
127    #[test]
128    fn error_on_missing_header() {
129        let input_head = b"Wrong-Header: 5\r\nAnother-Header: value\r\n\r\nBodyHere";
130        let result = SimpleHeaders::parse_headers(input_head);
131
132        assert_eq!(result, Err(Error::MissingHeader("Content-Length")));
133    }
134
135    #[test]
136    fn error_on_invalid_header_value() {
137        let input_head = b"Content-Length: invalid_value\r\nAnother-Header: value\r\n\r\nBodyHere";
138        let result = SimpleHeaders::parse_headers(input_head);
139
140        assert_eq!(result, Err(Error::InvalidHeaderValue("Content-Length")));
141    }
142}