1#[derive(Debug, Clone, Default)]
13pub struct Headers {
14 entries: Vec<(String, String)>,
15}
16
17#[must_use]
19pub fn is_valid_name(name: &str) -> bool {
20 !name.is_empty()
21 && name.bytes().all(|b| {
22 matches!(b,
23 b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+'
24 | b'-' | b'.' | b'^' | b'_' | b'`' | b'|' | b'~'
25 | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z')
26 })
27}
28
29#[must_use]
32pub fn is_valid_value(value: &str) -> bool {
33 value
34 .chars()
35 .all(|c| c == '\t' || c == ' ' || ('\u{21}'..='\u{7E}').contains(&c) || c == '\u{0C}' || c == '\u{00A0}')
36}
37
38#[must_use]
43pub fn normalize_value(text: &str) -> String {
44 let input = text.as_bytes();
45 let mut out: Vec<u8> = Vec::with_capacity(input.len());
46 let mut read = 0;
47 while read < input.len() && (input[read] == b' ' || input[read] == b'\t') {
48 read += 1;
49 }
50 let mut pending: Option<u8> = None;
51 while read < input.len() {
52 match input[read] {
53 b'\r'
54 if read + 2 < input.len()
55 && input[read + 1] == b'\n'
56 && (input[read + 2] == b' ' || input[read + 2] == b'\t') =>
57 {
58 pending = Some(input[read + 2]);
59 read += 3;
60 },
61 b'\r' | b'\n' => read += 1,
62 b' ' | b'\t' => {
63 pending = Some(input[read]);
64 read += 1;
65 },
66 byte => {
67 if let Some(ws) = pending.take()
68 && !out.is_empty()
69 {
70 out.push(ws);
71 }
72 out.push(byte);
73 read += 1;
74 },
75 }
76 }
77 while matches!(out.last(), Some(b' ' | b'\t')) {
78 out.pop();
79 }
80 String::from_utf8_lossy(&out).into_owned()
81}
82
83impl Headers {
84 #[must_use]
85 pub fn new() -> Self {
86 Self::default()
87 }
88
89 #[must_use]
92 pub fn from_pairs(entries: Vec<(String, String)>) -> Self {
93 Self { entries }
94 }
95
96 fn position(&self, name: &str) -> Option<usize> {
97 self.entries.iter().position(|(k, _)| k.eq_ignore_ascii_case(name))
98 }
99
100 #[must_use]
103 pub fn get(&self, name: &str) -> Option<String> {
104 let values = self.get_all(name);
105 if values.is_empty() {
106 return None;
107 }
108 let sep = if name.eq_ignore_ascii_case("set-cookie") {
109 "\n"
110 } else {
111 ", "
112 };
113 Some(values.join(sep))
114 }
115
116 #[must_use]
118 pub fn get_first(&self, name: &str) -> Option<&str> {
119 self.position(name).map(|i| self.entries[i].1.as_str())
120 }
121
122 #[must_use]
124 pub fn get_all(&self, name: &str) -> Vec<&str> {
125 self
126 .entries
127 .iter()
128 .filter(|(k, _)| k.eq_ignore_ascii_case(name))
129 .map(|(_, v)| v.as_str())
130 .collect()
131 }
132
133 #[must_use]
135 pub fn get_set_cookie(&self) -> Vec<&str> {
136 self.get_all("set-cookie")
137 }
138
139 #[must_use]
140 pub fn contains(&self, name: &str) -> bool {
141 self.position(name).is_some()
142 }
143
144 pub fn set(&mut self, name: &str, value: impl Into<String>) {
147 let value = value.into();
148 match self.position(name) {
149 Some(i) => {
150 self.entries[i].1 = value;
151 let lower = name.to_ascii_lowercase();
153 let mut seen = false;
154 self.entries.retain(|(k, _)| {
155 if k.eq_ignore_ascii_case(&lower) {
156 let keep = !seen;
157 seen = true;
158 keep
159 } else {
160 true
161 }
162 });
163 },
164 None => self.entries.push((name.to_string(), value)),
165 }
166 }
167
168 pub fn set_if_absent(&mut self, name: &str, value: impl Into<String>) {
170 if !self.contains(name) {
171 self.entries.push((name.to_string(), value.into()));
172 }
173 }
174
175 pub fn append(&mut self, name: impl Into<String>, value: impl Into<String>) {
178 self.entries.push((name.into(), value.into()));
179 }
180
181 pub fn append_combined(&mut self, name_lc: String, value: String) {
186 if name_lc == "set-cookie" {
187 self.entries.push((name_lc, value));
188 return;
189 }
190 match self.position(&name_lc) {
191 Some(i) => self.entries[i].1 = format!("{}, {value}", self.entries[i].1),
192 None => self.entries.push((name_lc, value)),
193 }
194 }
195
196 #[must_use]
201 pub fn get_joined(&self, name: &str) -> Option<String> {
202 let values = self.get_all(name);
203 (!values.is_empty()).then(|| values.join(", "))
204 }
205
206 #[must_use]
210 pub fn sorted_entries(&self) -> Vec<(String, String)> {
211 let mut sorted = self.entries.clone();
212 sorted.sort_by(|a, b| a.0.cmp(&b.0));
213 sorted
214 }
215
216 pub fn remove(&mut self, name: &str) {
218 self.entries.retain(|(k, _)| !k.eq_ignore_ascii_case(name));
219 }
220
221 pub fn iter(&self) -> impl Iterator<Item = &(String, String)> {
223 self.entries.iter()
224 }
225
226 #[must_use]
227 pub fn entries(&self) -> &[(String, String)] {
228 &self.entries
229 }
230
231 #[must_use]
232 pub fn into_pairs(self) -> Vec<(String, String)> {
233 self.entries
234 }
235
236 #[must_use]
241 pub fn to_object(&self) -> Vec<(String, String)> {
242 let mut out: Vec<(String, String)> = Vec::new();
243 for (name, _) in &self.entries {
244 let lower = name.to_ascii_lowercase();
245 if out.iter().any(|(k, _)| *k == lower) {
246 continue;
247 }
248 let combined = self.get(&lower).unwrap_or_default();
249 out.push((lower, combined));
250 }
251 out
252 }
253
254 #[must_use]
255 pub fn is_empty(&self) -> bool {
256 self.entries.is_empty()
257 }
258}
259
260impl From<Vec<(String, String)>> for Headers {
261 fn from(entries: Vec<(String, String)>) -> Self {
262 Self::from_pairs(entries)
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn combined_get_and_set_cookie_split() {
272 let mut h = Headers::new();
273 h.append("Accept", "a");
274 h.append("accept", "b");
275 assert_eq!(h.get("accept").as_deref(), Some("a, b"));
276 h.append("Set-Cookie", "x=1");
277 h.append("set-cookie", "y=2");
278 assert_eq!(h.get("set-cookie").as_deref(), Some("x=1\ny=2"));
279 assert_eq!(h.get_set_cookie(), vec!["x=1", "y=2"]);
280 }
281
282 #[test]
283 fn to_object_lowercases_combines_and_keeps_first_appearance_order() {
284 let h = Headers::from_pairs(vec![
285 ("Content-Type".into(), "text/plain".into()),
286 ("Set-Cookie".into(), "a=1".into()),
287 ("X-Dup".into(), "one".into()),
288 ("set-cookie".into(), "b=2".into()),
289 ("x-dup".into(), "two".into()),
290 ]);
291 assert_eq!(
292 h.to_object(),
293 vec![
294 ("content-type".to_string(), "text/plain".to_string()),
295 ("set-cookie".to_string(), "a=1\nb=2".to_string()),
296 ("x-dup".to_string(), "one, two".to_string()),
297 ]
298 );
299 }
300
301 #[test]
302 fn set_replaces_all_same_name() {
303 let mut h = Headers::from_pairs(vec![
304 ("x".into(), "1".into()),
305 ("y".into(), "2".into()),
306 ("X".into(), "3".into()),
307 ]);
308 h.set("x", "9");
309 assert_eq!(h.get_all("x"), vec!["9"]);
310 assert_eq!(h.entries()[0], ("x".into(), "9".into()));
312 assert_eq!(h.get_first("y"), Some("2"));
313 }
314
315 #[test]
316 fn set_if_absent_and_remove() {
317 let mut h = Headers::new();
318 h.set_if_absent("content-type", "application/json");
319 h.set_if_absent("content-type", "text/plain");
320 assert_eq!(h.get_first("content-type"), Some("application/json"));
321 h.remove("content-type");
322 assert!(!h.contains("content-type"));
323 }
324}