1use std::str;
24
25use thiserror::Error;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct FrameHeader {
36 pub content_length: u64,
39 pub content_type: Option<String>,
42}
43
44#[derive(Debug, Error, PartialEq, Eq)]
46pub enum FrameError {
47 #[error("missing Content-Length header")]
51 MissingContentLength,
52 #[error("invalid Content-Length value: {0:?}")]
55 InvalidContentLength(String),
56 #[error("malformed header line: {0:?}")]
59 MalformedHeader(String),
60 #[error("non-ascii bytes in header block")]
63 NonAsciiHeader,
64 #[error("duplicate Content-Length header")]
69 DuplicateContentLength,
70 #[error("Content-Length {got} exceeds ceiling {limit}")]
74 OverlargeMessage { got: u64, limit: u64 },
75}
76
77pub const MAX_MESSAGE_BYTES: u64 = 64 * 1024 * 1024;
82
83pub fn parse_header_block(block: &[u8]) -> Result<FrameHeader, FrameError> {
89 parse_header_block_with_limit(block, MAX_MESSAGE_BYTES)
90}
91
92pub fn parse_header_block_with_limit(block: &[u8], limit: u64) -> Result<FrameHeader, FrameError> {
96 let text = str::from_utf8(block).map_err(|_| FrameError::NonAsciiHeader)?;
97 if !text.is_ascii() {
98 return Err(FrameError::NonAsciiHeader);
99 }
100
101 let mut content_length: Option<u64> = None;
102 let mut content_type: Option<String> = None;
103
104 for line in text.split("\r\n") {
105 if line.is_empty() {
106 continue;
111 }
112 let (name, value) = line
113 .split_once(':')
114 .ok_or_else(|| FrameError::MalformedHeader(line.to_string()))?;
115 let name = name.trim();
116 let value = value.trim();
117 if name.eq_ignore_ascii_case("content-length") {
118 if content_length.is_some() {
119 return Err(FrameError::DuplicateContentLength);
120 }
121 let parsed: u64 = value
122 .parse()
123 .map_err(|_| FrameError::InvalidContentLength(value.to_string()))?;
124 if parsed > limit {
125 return Err(FrameError::OverlargeMessage { got: parsed, limit });
126 }
127 content_length = Some(parsed);
128 } else if name.eq_ignore_ascii_case("content-type") {
129 content_type = Some(value.to_string());
130 }
131 }
134
135 let content_length = content_length.ok_or(FrameError::MissingContentLength)?;
136 Ok(FrameHeader {
137 content_length,
138 content_type,
139 })
140}
141
142pub fn encode_header(body_len: usize) -> String {
147 format!("Content-Length: {}\r\n\r\n", body_len)
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn parses_minimal_header() {
159 let h = parse_header_block(b"Content-Length: 42").unwrap();
160 assert_eq!(h.content_length, 42);
161 assert_eq!(h.content_type, None);
162 }
163
164 #[test]
165 fn parses_header_with_content_type() {
166 let block =
167 b"Content-Length: 13\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8";
168 let h = parse_header_block(block).unwrap();
169 assert_eq!(h.content_length, 13);
170 assert_eq!(
171 h.content_type.as_deref(),
172 Some("application/vscode-jsonrpc; charset=utf-8")
173 );
174 }
175
176 #[test]
177 fn header_name_is_case_insensitive() {
178 let h = parse_header_block(b"content-length: 7").unwrap();
179 assert_eq!(h.content_length, 7);
180 let h = parse_header_block(b"CONTENT-LENGTH: 7").unwrap();
181 assert_eq!(h.content_length, 7);
182 }
183
184 #[test]
185 fn tolerates_whitespace_around_value() {
186 let h = parse_header_block(b"Content-Length: 99 ").unwrap();
187 assert_eq!(h.content_length, 99);
188 }
189
190 #[test]
191 fn ignores_unknown_headers() {
192 let h = parse_header_block(b"X-Trace-Id: abc\r\nContent-Length: 1").unwrap();
193 assert_eq!(h.content_length, 1);
194 }
195
196 #[test]
197 fn missing_content_length_is_error() {
198 let err = parse_header_block(b"Content-Type: application/json").unwrap_err();
199 assert_eq!(err, FrameError::MissingContentLength);
200 }
201
202 #[test]
203 fn invalid_content_length_is_error() {
204 let err = parse_header_block(b"Content-Length: not-a-number").unwrap_err();
205 assert!(matches!(err, FrameError::InvalidContentLength(_)));
206 }
207
208 #[test]
209 fn malformed_line_is_error() {
210 let err = parse_header_block(b"Content-Length 42").unwrap_err();
211 assert!(matches!(err, FrameError::MalformedHeader(_)));
212 }
213
214 #[test]
215 fn duplicate_content_length_is_error() {
216 let err = parse_header_block(b"Content-Length: 5\r\nContent-Length: 6").unwrap_err();
217 assert_eq!(err, FrameError::DuplicateContentLength);
218 }
219
220 #[test]
221 fn non_ascii_header_is_error() {
222 let mut block = b"Content-Length: 5\r\nX-Note: ".to_vec();
226 block.push(0xFF);
227 let err = parse_header_block(&block).unwrap_err();
228 assert_eq!(err, FrameError::NonAsciiHeader);
229 }
230
231 #[test]
232 fn body_size_ceiling_is_enforced() {
233 assert!(parse_header_block(b"Content-Length: 1024").is_ok());
235 let err = parse_header_block_with_limit(b"Content-Length: 1024", 1023).unwrap_err();
237 assert!(matches!(
238 err,
239 FrameError::OverlargeMessage {
240 got: 1024,
241 limit: 1023
242 }
243 ));
244 }
245
246 #[test]
247 fn encode_header_roundtrips() {
248 let h = encode_header(1234);
252 assert_eq!(h, "Content-Length: 1234\r\n\r\n");
253 let block = h.trim_end_matches("\r\n\r\n");
254 let parsed = parse_header_block(block.as_bytes()).unwrap();
255 assert_eq!(parsed.content_length, 1234);
256 }
257
258 #[test]
259 fn empty_block_is_missing_content_length() {
260 let err = parse_header_block(b"").unwrap_err();
261 assert_eq!(err, FrameError::MissingContentLength);
262 }
263}