Skip to main content

eggserve_core/primitives/
header_block.rs

1//! Duplicate-preserving HTTP header block.
2//!
3//! [`HeaderBlock`] stores HTTP headers as an ordered list of name/value pairs,
4//! preserving duplicates and original field-name casing. Case-insensitive
5//! lookup is provided by field name.
6
7use std::fmt;
8
9/// Errors from header validation.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum HeaderError {
12    /// The header name is empty or contains invalid characters.
13    InvalidName,
14    /// The header value contains a carriage return, line feed, or NUL byte.
15    InvalidValue,
16    /// The header name is too long.
17    NameTooLong,
18}
19
20impl fmt::Display for HeaderError {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::InvalidName => write!(f, "invalid header name"),
24            Self::InvalidValue => write!(f, "invalid header value (contains CR/LF/NUL)"),
25            Self::NameTooLong => write!(f, "header name too long"),
26        }
27    }
28}
29
30impl std::error::Error for HeaderError {}
31
32/// Error returned by [`HeaderBlock::get_unique`] when duplicates exist.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct DuplicateHeaderError {
35    /// The header name that had duplicates.
36    name: String,
37    /// The number of values found.
38    count: usize,
39}
40
41impl DuplicateHeaderError {
42    /// Returns the header name.
43    pub fn name(&self) -> &str {
44        &self.name
45    }
46
47    /// Returns the number of duplicate values.
48    pub fn count(&self) -> usize {
49        self.count
50    }
51}
52
53impl fmt::Display for DuplicateHeaderError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(
56            f,
57            "header '{}' has {} values; use get_all() to access all",
58            self.name, self.count
59        )
60    }
61}
62
63impl std::error::Error for DuplicateHeaderError {}
64
65/// A validated HTTP header name.
66#[derive(Debug, Clone, PartialEq, Eq, Hash)]
67pub struct HeaderName(String);
68
69impl HeaderName {
70    /// Create a validated header name.
71    ///
72    /// # Errors
73    ///
74    /// Returns [`HeaderError::InvalidName`] if the name is empty or contains
75    /// characters outside the visible ASCII range (0x21–0x7E excluding
76    /// separators).
77    pub fn new(name: impl Into<String>) -> Result<Self, HeaderError> {
78        let s = name.into();
79        if s.is_empty() {
80            return Err(HeaderError::InvalidName);
81        }
82        if s.len() > 256 {
83            return Err(HeaderError::NameTooLong);
84        }
85        if !is_valid_header_name(&s) {
86            return Err(HeaderError::InvalidName);
87        }
88        Ok(Self(s))
89    }
90
91    /// Returns the header name as a string slice.
92    pub fn as_str(&self) -> &str {
93        &self.0
94    }
95}
96
97impl fmt::Display for HeaderName {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.write_str(&self.0)
100    }
101}
102
103/// A validated HTTP header value.
104#[derive(Debug, Clone, PartialEq, Eq, Hash)]
105pub struct HeaderValue(String);
106
107impl HeaderValue {
108    /// Create a validated header value.
109    ///
110    /// # Errors
111    ///
112    /// Returns [`HeaderError::InvalidValue`] if the value contains a
113    /// carriage return (CR), line feed (LF), or NUL byte.
114    pub fn new(value: impl Into<String>) -> Result<Self, HeaderError> {
115        let s = value.into();
116        if s.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0) {
117            return Err(HeaderError::InvalidValue);
118        }
119        Ok(Self(s.trim_matches([' ', '\t']).to_owned()))
120    }
121
122    /// Returns the header value as a string slice.
123    pub fn as_str(&self) -> &str {
124        &self.0
125    }
126}
127
128impl fmt::Display for HeaderValue {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        f.write_str(&self.0)
131    }
132}
133
134/// A single header field as a name/value pair.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct HeaderField {
137    pub name: HeaderName,
138    pub value: HeaderValue,
139}
140
141/// An ordered, duplicate-preserving collection of HTTP headers.
142///
143/// Headers are stored as an ordered list of name/value pairs. Duplicate
144/// field names are preserved. Case-insensitive lookup by field name is
145/// provided.
146///
147/// # Canonical representation
148///
149/// This type is normatively a list, not a map. Dictionary conversion
150/// (via `Into<HashMap>`) is lossy and should only be used as an explicit
151/// convenience.
152#[derive(Debug, Clone, Default, PartialEq, Eq)]
153pub struct HeaderBlock {
154    fields: Vec<HeaderField>,
155}
156
157impl HeaderBlock {
158    /// Create an empty header block.
159    pub fn new() -> Self {
160        Self { fields: Vec::new() }
161    }
162
163    /// Create a header block with a pre-allocated capacity.
164    pub fn with_capacity(capacity: usize) -> Self {
165        Self {
166            fields: Vec::with_capacity(capacity),
167        }
168    }
169
170    /// Push a validated header field.
171    pub fn push(&mut self, name: HeaderName, value: HeaderValue) {
172        self.fields.push(HeaderField { name, value });
173    }
174
175    /// Push a header field from string slices.
176    ///
177    /// # Errors
178    ///
179    /// Returns [`HeaderError`] if the name or value is invalid.
180    pub fn push_str(
181        &mut self,
182        name: impl Into<String>,
183        value: impl Into<String>,
184    ) -> Result<(), HeaderError> {
185        let name = HeaderName::new(name)?;
186        let value = HeaderValue::new(value)?;
187        self.push(name, value);
188        Ok(())
189    }
190
191    /// Returns the first value for the given header name (case-insensitive).
192    pub fn get_first(&self, name: &str) -> Option<&HeaderValue> {
193        self.fields
194            .iter()
195            .find(|f| f.name.as_str().eq_ignore_ascii_case(name))
196            .map(|f| &f.value)
197    }
198
199    /// Returns all values for the given header name (case-insensitive),
200    /// in order.
201    pub fn get_all(&self, name: &str) -> Vec<&HeaderValue> {
202        self.fields
203            .iter()
204            .filter(|f| f.name.as_str().eq_ignore_ascii_case(name))
205            .map(|f| &f.value)
206            .collect()
207    }
208
209    /// Returns the unique value for the given header name (case-insensitive).
210    ///
211    /// # Errors
212    ///
213    /// Returns [`DuplicateHeaderError`] if the header appears more than once.
214    /// Returns `Ok(None)` if the header is absent.
215    pub fn get_unique(&self, name: &str) -> Result<Option<&HeaderValue>, DuplicateHeaderError> {
216        let values = self.get_all(name);
217        match values.len() {
218            0 => Ok(None),
219            1 => Ok(Some(values[0])),
220            _ => Err(DuplicateHeaderError {
221                name: name.to_string(),
222                count: values.len(),
223            }),
224        }
225    }
226
227    /// Returns `true` if a header with the given name exists
228    /// (case-insensitive).
229    pub fn contains(&self, name: &str) -> bool {
230        self.fields
231            .iter()
232            .any(|f| f.name.as_str().eq_ignore_ascii_case(name))
233    }
234
235    /// Returns an iterator over all header fields.
236    pub fn iter(&self) -> impl Iterator<Item = &HeaderField> {
237        self.fields.iter()
238    }
239
240    /// Returns the number of header fields.
241    pub fn len(&self) -> usize {
242        self.fields.len()
243    }
244
245    /// Returns `true` if there are no header fields.
246    pub fn is_empty(&self) -> bool {
247        self.fields.is_empty()
248    }
249
250    /// Retains only the elements specified by the predicate.
251    pub fn retain<F>(&mut self, f: F)
252    where
253        F: FnMut(&HeaderField) -> bool,
254    {
255        self.fields.retain(f);
256    }
257}
258
259impl fmt::Display for HeaderBlock {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        for field in &self.fields {
262            writeln!(f, "{}: {}", field.name, field.value)?;
263        }
264        Ok(())
265    }
266}
267
268/// Check if a character is a valid HTTP header name character (RFC 9110
269/// section 5.6.2, token).
270fn is_valid_header_name(s: &str) -> bool {
271    s.bytes().all(|b| matches!(b, 0x21 | 0x23..=0x27 | 0x2A | 0x2B | 0x2D..=0x2E | 0x30..=0x39 | 0x41..=0x5A | 0x5E..=0x7A | 0x7C | 0x7E))
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn header_name_valid() {
280        assert!(HeaderName::new("Content-Type").is_ok());
281        assert!(HeaderName::new("x-custom-header").is_ok());
282        assert!(HeaderName::new("X").is_ok());
283    }
284
285    #[test]
286    fn header_name_empty_rejected() {
287        assert_eq!(HeaderName::new("").unwrap_err(), HeaderError::InvalidName);
288    }
289
290    #[test]
291    fn header_name_invalid_chars_rejected() {
292        assert_eq!(
293            HeaderName::new("foo bar").unwrap_err(),
294            HeaderError::InvalidName
295        );
296        assert_eq!(
297            HeaderName::new("foo\tbar").unwrap_err(),
298            HeaderError::InvalidName
299        );
300    }
301
302    #[test]
303    fn header_name_too_long_rejected() {
304        let long_name = "x".repeat(257);
305        assert_eq!(
306            HeaderName::new(long_name).unwrap_err(),
307            HeaderError::NameTooLong
308        );
309    }
310
311    #[test]
312    fn header_value_valid() {
313        assert!(HeaderValue::new("text/html").is_ok());
314        assert!(HeaderValue::new("").is_ok());
315        assert!(HeaderValue::new("hello world").is_ok());
316    }
317
318    #[test]
319    fn header_value_trims_optional_whitespace() {
320        assert_eq!(HeaderValue::new(" \tvalue\t ").unwrap().as_str(), "value");
321    }
322
323    #[test]
324    fn header_value_cr_rejected() {
325        assert_eq!(
326            HeaderValue::new("foo\rbar").unwrap_err(),
327            HeaderError::InvalidValue
328        );
329    }
330
331    #[test]
332    fn header_value_lf_rejected() {
333        assert_eq!(
334            HeaderValue::new("foo\nbar").unwrap_err(),
335            HeaderError::InvalidValue
336        );
337    }
338
339    #[test]
340    fn header_value_nul_rejected() {
341        assert_eq!(
342            HeaderValue::new("foo\0bar").unwrap_err(),
343            HeaderError::InvalidValue
344        );
345    }
346
347    #[test]
348    fn empty_header_block() {
349        let block = HeaderBlock::new();
350        assert!(block.is_empty());
351        assert_eq!(block.len(), 0);
352        assert!(block.get_first("foo").is_none());
353    }
354
355    #[test]
356    fn push_and_get_first() {
357        let mut block = HeaderBlock::new();
358        block.push_str("content-type", "text/html").unwrap();
359        assert_eq!(
360            block.get_first("content-type").unwrap().as_str(),
361            "text/html"
362        );
363        assert_eq!(
364            block.get_first("Content-Type").unwrap().as_str(),
365            "text/html"
366        );
367        assert!(block.get_first("missing").is_none());
368    }
369
370    #[test]
371    fn duplicates_preserved() {
372        let mut block = HeaderBlock::new();
373        block.push_str("set-cookie", "a=1").unwrap();
374        block.push_str("set-cookie", "b=2").unwrap();
375        assert_eq!(block.len(), 2);
376        assert_eq!(block.get_first("Set-Cookie").unwrap().as_str(), "a=1");
377    }
378
379    #[test]
380    fn get_all_returns_all_values() {
381        let mut block = HeaderBlock::new();
382        block.push_str("set-cookie", "a=1").unwrap();
383        block.push_str("set-cookie", "b=2").unwrap();
384        block.push_str("set-cookie", "c=3").unwrap();
385        let all = block.get_all("set-cookie");
386        assert_eq!(all.len(), 3);
387        assert_eq!(all[0].as_str(), "a=1");
388        assert_eq!(all[1].as_str(), "b=2");
389        assert_eq!(all[2].as_str(), "c=3");
390    }
391
392    #[test]
393    fn get_unique_single() {
394        let mut block = HeaderBlock::new();
395        block.push_str("content-type", "text/html").unwrap();
396        let result = block.get_unique("content-type").unwrap();
397        assert_eq!(result.unwrap().as_str(), "text/html");
398    }
399
400    #[test]
401    fn get_unique_absent() {
402        let block = HeaderBlock::new();
403        assert!(block.get_unique("content-type").unwrap().is_none());
404    }
405
406    #[test]
407    fn get_unique_duplicate_error() {
408        let mut block = HeaderBlock::new();
409        block.push_str("set-cookie", "a=1").unwrap();
410        block.push_str("set-cookie", "b=2").unwrap();
411        let err = block.get_unique("set-cookie").unwrap_err();
412        assert_eq!(err.name(), "set-cookie");
413        assert_eq!(err.count(), 2);
414    }
415
416    #[test]
417    fn contains_case_insensitive() {
418        let mut block = HeaderBlock::new();
419        block.push_str("Content-Type", "text/html").unwrap();
420        assert!(block.contains("content-type"));
421        assert!(block.contains("CONTENT-TYPE"));
422        assert!(block.contains("Content-Type"));
423        assert!(!block.contains("missing"));
424    }
425
426    #[test]
427    fn iteration_order() {
428        let mut block = HeaderBlock::new();
429        block.push_str("a", "1").unwrap();
430        block.push_str("b", "2").unwrap();
431        block.push_str("c", "3").unwrap();
432        let names: Vec<&str> = block.iter().map(|f| f.name.as_str()).collect();
433        assert_eq!(names, vec!["a", "b", "c"]);
434    }
435
436    #[test]
437    fn display() {
438        let mut block = HeaderBlock::new();
439        block.push_str("a", "1").unwrap();
440        let display = format!("{}", block);
441        assert!(display.contains("a: 1"));
442    }
443
444    #[test]
445    fn header_name_display() {
446        assert_eq!(format!("{}", HeaderName::new("foo").unwrap()), "foo");
447    }
448
449    #[test]
450    fn header_value_display() {
451        assert_eq!(format!("{}", HeaderValue::new("bar").unwrap()), "bar");
452    }
453
454    #[test]
455    fn duplicate_header_error_display() {
456        let err = DuplicateHeaderError {
457            name: "set-cookie".to_string(),
458            count: 3,
459        };
460        let msg = err.to_string();
461        assert!(msg.contains("set-cookie"));
462        assert!(msg.contains("3"));
463    }
464
465    #[test]
466    fn duplicate_header_error_is_error() {
467        let err: &dyn std::error::Error = &DuplicateHeaderError {
468            name: "x".to_string(),
469            count: 2,
470        };
471        assert!(!err.to_string().is_empty());
472    }
473
474    #[test]
475    fn header_error_display() {
476        assert!(!HeaderError::InvalidName.to_string().is_empty());
477        assert!(!HeaderError::InvalidValue.to_string().is_empty());
478        assert!(!HeaderError::NameTooLong.to_string().is_empty());
479    }
480
481    #[test]
482    fn header_error_is_error() {
483        let err: &dyn std::error::Error = &HeaderError::InvalidName;
484        assert!(!err.to_string().is_empty());
485    }
486
487    #[test]
488    fn with_capacity() {
489        let block = HeaderBlock::with_capacity(10);
490        assert!(block.is_empty());
491    }
492}