1use http::header::{HeaderMap, HeaderName, HeaderValue};
4
5use crate::error::{Error, Result};
6
7const RESERVED: &[&str] = &[
9 "host",
10 "content-length",
11 "connection",
12 "transfer-encoding",
13 "te",
14 "trailer",
15 "upgrade",
16 "keep-alive",
17];
18
19fn steer(name: &str) -> Option<&'static str> {
21 match name {
22 "user-agent" => Some("the user-agent option"),
23 "cookie" => Some("the cookies option"),
24 _ => None,
25 }
26}
27
28pub fn from_pairs<I, K, V>(pairs: I) -> Result<HeaderMap>
30where
31 I: IntoIterator<Item = (K, V)>,
32 K: AsRef<str>,
33 V: AsRef<str>,
34{
35 let mut headers = HeaderMap::new();
36 for (name, value) in pairs {
37 let (name, value) = validate(name.as_ref(), value.as_ref())?;
38 headers.append(name, value);
39 }
40 Ok(headers)
41}
42
43pub fn parse_lines<I, S>(lines: I) -> Result<HeaderMap>
46where
47 I: IntoIterator<Item = S>,
48 S: AsRef<str>,
49{
50 let mut headers = HeaderMap::new();
51 for line in lines {
52 let (name, value) = split_line(line.as_ref())?;
53 let (name, value) = validate(&name, &value)?;
54 headers.append(name, value);
55 }
56 Ok(headers)
57}
58
59fn validate(name: &str, value: &str) -> Result<(HeaderName, HeaderValue)> {
60 let name = HeaderName::from_bytes(name.trim().as_bytes())
61 .map_err(|_| Error::invalid_header(format!("invalid header name '{}'", name.trim())))?;
62 if let Some(hint) = steer(name.as_str()) {
63 return Err(Error::invalid_header(format!(
64 "header '{name}' cannot be set as a custom header; use {hint} instead"
65 )));
66 }
67 if RESERVED.contains(&name.as_str()) {
68 return Err(Error::invalid_header(format!(
69 "header '{name}' is managed by the engine and cannot be overridden"
70 )));
71 }
72 let value = HeaderValue::from_str(value)
73 .map_err(|_| Error::invalid_header(format!("invalid value for header '{name}'")))?;
74 Ok((name, value))
75}
76
77fn split_line(line: &str) -> Result<(String, String)> {
78 match line.split_once(':') {
79 Some((name, value)) => {
80 let value = value.trim_start();
81 if value.is_empty() {
82 return Err(malformed(line));
83 }
84 Ok((name.to_owned(), value.to_owned()))
85 }
86 None => match line.strip_suffix(';') {
87 Some(name) if !name.is_empty() => Ok((name.to_owned(), String::new())),
88 _ => Err(malformed(line)),
89 },
90 }
91}
92
93fn malformed(line: &str) -> Error {
94 Error::invalid_header(format!(
95 "invalid header '{line}': expected 'Name: Value' (use 'Name;' for an empty value)"
96 ))
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn parses_and_appends_lines() {
105 let h = parse_lines(["X-A: 1", "X-B: two"]).unwrap();
106 assert_eq!(h.get("x-a").unwrap(), "1", "first header");
107 assert_eq!(h.get("x-b").unwrap(), "two", "second header");
108 }
109
110 #[test]
111 fn empty_value_via_trailing_semicolon() {
112 let h = parse_lines(["X-Empty;"]).unwrap();
113 assert_eq!(h.get("x-empty").unwrap(), "", "semicolon sends empty value");
114 }
115
116 #[test]
117 fn from_pairs_builds_map() {
118 let h = from_pairs([("X-A", "1"), ("X-A", "2")]).unwrap();
119 let values: Vec<_> = h.get_all("x-a").iter().collect();
120 assert_eq!(values.len(), 2, "duplicate names are appended");
121 }
122
123 #[test]
124 fn rejects_crlf_injection() {
125 assert!(parse_lines(["X-A: a\r\nEvil: b"]).is_err(), "CRLF must be rejected");
126 assert!(from_pairs([("X-A", "a\r\nEvil: b")]).is_err(), "CRLF must be rejected");
127 }
128
129 #[test]
130 fn rejects_reserved_and_steered() {
131 assert!(parse_lines(["Host: x"]).is_err(), "framing header rejected");
132 assert!(parse_lines(["User-Agent: x"]).is_err(), "user-agent steered");
133 assert!(parse_lines(["Cookie: a=1"]).is_err(), "cookie steered");
134 }
135
136 #[test]
137 fn rejects_malformed_lines() {
138 assert!(parse_lines(["no-colon"]).is_err(), "missing colon");
139 assert!(parse_lines(["X-Foo:"]).is_err(), "empty value needs 'Name;'");
140 }
141}