Skip to main content

go_http/parse/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2
3pub mod chunk;
4pub mod request;
5pub mod response;
6pub mod transfer;
7
8use std::fmt;
9
10/// Errors produced by the HTTP/1.1 parser.
11/// Mirrors Go's internal parse error set.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum ParseError {
14    BadRequestLine,
15    BadStatusLine,
16    HeaderTooLarge,
17    InvalidChunkSize,
18    InvalidContentLength,
19    UnexpectedEof,
20    InvalidHeaderName,
21    InvalidHeaderValue,
22    Other(String),
23}
24
25impl fmt::Display for ParseError {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            Self::BadRequestLine       => write!(f, "malformed HTTP request line"),
29            Self::BadStatusLine        => write!(f, "malformed HTTP status line"),
30            Self::HeaderTooLarge       => write!(f, "header exceeds size limit"),
31            Self::InvalidChunkSize     => write!(f, "invalid chunk size"),
32            Self::InvalidContentLength => write!(f, "invalid Content-Length"),
33            Self::UnexpectedEof        => write!(f, "unexpected EOF"),
34            Self::InvalidHeaderName    => write!(f, "invalid header name"),
35            Self::InvalidHeaderValue   => write!(f, "invalid header value"),
36            Self::Other(s)             => write!(f, "{s}"),
37        }
38    }
39}
40
41impl std::error::Error for ParseError {}
42
43// ---------------------------------------------------------------------------
44// Shared line-reading helpers
45// ---------------------------------------------------------------------------
46
47/// Maximum number of bytes read looking for a single CRLF-terminated line.
48pub(crate) const MAX_LINE: usize = 4096;
49
50/// Read bytes from `r` until `\n`, returning the line **without** the trailing
51/// `\n` (or `\r\n`). Returns `ParseError::UnexpectedEof` if the reader closes
52/// before any byte is delivered, `ParseError::HeaderTooLarge` if `MAX_LINE` is
53/// exceeded.
54pub(crate) fn read_line(r: &mut impl std::io::Read) -> Result<String, ParseError> {
55    let mut buf = Vec::with_capacity(128);
56    let mut byte = [0u8; 1];
57    loop {
58        match r.read(&mut byte) {
59            Ok(0) => {
60                if buf.is_empty() {
61                    return Err(ParseError::UnexpectedEof);
62                }
63                break;
64            }
65            Ok(_) => {
66                if byte[0] == b'\n' {
67                    break;
68                }
69                buf.push(byte[0]);
70                if buf.len() > MAX_LINE {
71                    return Err(ParseError::HeaderTooLarge);
72                }
73            }
74            Err(_) => return Err(ParseError::UnexpectedEof),
75        }
76    }
77    // Strip trailing \r if present.
78    if buf.last() == Some(&b'\r') {
79        buf.pop();
80    }
81    String::from_utf8(buf).map_err(|_| ParseError::Other("non-UTF-8 in header line".into()))
82}
83
84/// Read all headers until the blank line, returning a `Header`.
85/// Enforces `max_bytes` across all header data read.
86pub(crate) fn read_headers(
87    r: &mut impl std::io::Read,
88    max_bytes: usize,
89) -> Result<crate::header::Header, ParseError> {
90    use crate::header::Header;
91    let mut h = Header::new();
92    let mut total = 0usize;
93
94    loop {
95        let line = read_line(r)?;
96        total += line.len() + 2; // approximate: line + CRLF
97        if total > max_bytes {
98            return Err(ParseError::HeaderTooLarge);
99        }
100        if line.is_empty() {
101            break; // blank line ends headers
102        }
103        // Folded headers (obs-fold) are treated as invalid per RFC 7230 §3.2.4.
104        if line.starts_with(' ') || line.starts_with('\t') {
105            return Err(ParseError::Other("obsolete header folding not supported".into()));
106        }
107        let colon = line.find(':').ok_or(ParseError::InvalidHeaderName)?;
108        let name  = line[..colon].trim();
109        let value = line[colon + 1..].trim();
110        if name.is_empty() {
111            return Err(ParseError::InvalidHeaderName);
112        }
113        // Validate header name characters (token per RFC 7230).
114        if !name.bytes().all(is_token_char) {
115            return Err(ParseError::InvalidHeaderName);
116        }
117        h.add(name, value);
118    }
119    Ok(h)
120}
121
122/// RFC 7230 §3.2.6 token character.
123pub(crate) fn is_token_char(b: u8) -> bool {
124    matches!(b,
125        b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
126        | b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+'
127        | b'-' | b'.' | b'^' | b'_' | b'`' | b'|' | b'~'
128    )
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use std::io::Cursor;
135
136    #[test]
137    fn read_line_crlf() {
138        let mut c = Cursor::new(b"Hello\r\n");
139        assert_eq!(read_line(&mut c).unwrap(), "Hello");
140    }
141
142    #[test]
143    fn read_line_lf_only() {
144        let mut c = Cursor::new(b"World\n");
145        assert_eq!(read_line(&mut c).unwrap(), "World");
146    }
147
148    #[test]
149    fn read_headers_basic() {
150        let raw = b"Content-Type: text/plain\r\nContent-Length: 5\r\n\r\n";
151        let mut c = Cursor::new(raw.as_ref());
152        let h = read_headers(&mut c, 8192).unwrap();
153        assert_eq!(h.get("content-type"), Some("text/plain"));
154        assert_eq!(h.get("content-length"), Some("5"));
155    }
156
157    #[test]
158    fn read_headers_too_large() {
159        let big: Vec<u8> = (0..5000).flat_map(|_| b"X-H: v\r\n".iter().copied()).collect();
160        let mut bytes = big;
161        bytes.extend_from_slice(b"\r\n");
162        let mut c = Cursor::new(bytes);
163        assert_eq!(read_headers(&mut c, 1024), Err(ParseError::HeaderTooLarge));
164    }
165}