Skip to main content

base_http/
headers.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! Explicit HTTP interpretation of raw fields. Other application protocols can interpret `Fields` independently or configure their own combination policy.
5//!
6//! [`Fields`] stores ordered raw pairs with token names and opaque values, preserving original name spelling, duplicate pairs and byte-valued content. It has no trailer marker, combination policy or cached index. [`HttpHeaders::new`] provides explicit HTTP interpretation: lazy lookup combines lists and cookies, preserves separate `set-cookie` values, and checks singleton and trailer rules. [`Policy`] supports application overrides. [`HttpHeaders::append_trailers`] accepts an explicit trailing section, including an empty one, and [`HttpHeaders::into_parts`] recovers the initial and trailing sections without losing provenance. Protocol codecs independently enforce mandatory wire restrictions.
7//!
8//! A received body `Headers` frame can contribute trailers to an interpreter without changing the message's initial fields:
9//!
10//! ```rust
11//! use base_http::{Fields, HttpHeaders, Response, Status, body::EmptyBody};
12//!
13//! let mut response = Response::new(Status::OK, EmptyBody);
14//! response.fields.add("Content-Type", "text/plain")?;
15//!
16//! let mut headers = HttpHeaders::new(response.fields.clone());
17//! headers.append_trailers(Fields::try_from_pairs([("x-checksum", "example")])?)?;
18//! assert_eq!(headers.headers().len(), 1);
19//! assert_eq!(headers.trailers().len(), 1);
20//! assert_eq!(response.fields.len(), 1);
21//! # Ok::<(), base_http::FieldError>(())
22//! ```
23use crate::{
24    Field, FieldError, Fields,
25    syntax::{is_token, trim_ows},
26};
27use std::collections::HashMap;
28
29/// How repeated raw values contribute to the interpreted value.
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub enum Combination {
32    Singleton,
33    Comma,
34    LowercaseComma,
35    Cookie,
36    Separate,
37    Last,
38}
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct FieldPolicy {
42    pub combination: Combination,
43    pub trailer: bool,
44}
45
46impl FieldPolicy {
47    pub const fn new(combination: Combination, trailer: bool) -> Self {
48        Self {
49            combination,
50            trailer,
51        }
52    }
53}
54
55/// Combination/trailer defaults following Socketry, with case-insensitive
56/// overrides. Repeated credentials are rejected rather than concatenated.
57#[derive(Clone, Debug, Default)]
58pub struct Policy {
59    // Most messages use only the built-in policy. Keep that case pointer-sized;
60    // allocate the override table itself only when a caller supplies overrides.
61    #[allow(clippy::box_collection)]
62    overrides: Option<Box<HashMap<String, FieldPolicy>>>,
63}
64
65impl Policy {
66    pub fn set(&mut self, name: &str, policy: FieldPolicy) -> Result<(), FieldError> {
67        if !is_token(name.as_bytes()) {
68            return Err(FieldError::InvalidName);
69        }
70        self.overrides
71            .get_or_insert_with(Default::default)
72            .insert(name.to_ascii_lowercase(), policy);
73        Ok(())
74    }
75
76    pub fn get(&self, name: &str) -> FieldPolicy {
77        let name = name.to_ascii_lowercase();
78        if let Some(policy) = self
79            .overrides
80            .as_ref()
81            .and_then(|overrides| overrides.get(&name))
82        {
83            return *policy;
84        }
85        use Combination::*;
86        match name.as_str() {
87            "content-disposition"
88            | "content-length"
89            | "content-type"
90            | "expect"
91            | "from"
92            | "host"
93            | "location"
94            | "max-forwards"
95            | "referer"
96            | "retry-after"
97            | "server"
98            | "user-agent"
99            | "if-range"
100            | "content-range" => FieldPolicy::new(Singleton, false),
101            "connection" => FieldPolicy::new(LowercaseComma, false),
102            "cookie" => FieldPolicy::new(Cookie, false),
103            "set-cookie" | "www-authenticate" | "proxy-authenticate" => {
104                FieldPolicy::new(Separate, false)
105            }
106            "authorization" | "proxy-authorization" => FieldPolicy::new(Singleton, false),
107            "date"
108            | "expires"
109            | "last-modified"
110            | "if-modified-since"
111            | "if-unmodified-since"
112            | "etag" => FieldPolicy::new(Last, true),
113            "range" | "transfer-encoding" | "trailer" | "upgrade" | "cache-control" | "te"
114            | "vary" | "priority" | "via" | "x-forwarded-for" | "if-match" | "if-none-match"
115            | "accept" | "accept-ranges" | "accept-charset" | "accept-encoding"
116            | "accept-language" | "content-encoding" => FieldPolicy::new(Comma, false),
117            _ => FieldPolicy::new(Comma, true),
118        }
119    }
120}
121
122/// Interpreted components. Raw spelling/spacing always remains in `Fields::pairs`.
123#[derive(Clone, Debug, Eq, PartialEq)]
124pub struct FieldValue {
125    combination: Combination,
126    parts: Vec<Vec<u8>>,
127}
128
129impl FieldValue {
130    pub fn combination(&self) -> Combination {
131        self.combination
132    }
133    pub fn parts(&self) -> &[Vec<u8>] {
134        &self.parts
135    }
136
137    /// A single component, when present. This does not join a list or cookies.
138    pub fn single(&self) -> Option<&[u8]> {
139        if self.parts.len() == 1 {
140            Some(&self.parts[0])
141        } else {
142            None
143        }
144    }
145
146    /// Combine into one value when policy permits. Separate fields such as
147    /// `set-cookie` must be emitted individually and return `None`.
148    pub fn combined(&self) -> Option<Vec<u8>> {
149        let separator: &[u8] = match self.combination {
150            Combination::Separate => return None,
151            Combination::Cookie => b"; ",
152            _ => b",",
153        };
154        Some(self.parts.join(separator))
155    }
156
157    fn parse(name: &str, value: &[u8], combination: Combination) -> Result<Self, FieldError> {
158        let mut parts = match combination {
159            Combination::Comma | Combination::LowercaseComma => split_list(name, value)?,
160            _ => vec![value.to_vec()],
161        };
162        if combination == Combination::LowercaseComma {
163            for part in &mut parts {
164                part.make_ascii_lowercase();
165            }
166        }
167        Ok(Self { combination, parts })
168    }
169}
170
171fn split_list(name: &str, value: &[u8]) -> Result<Vec<Vec<u8>>, FieldError> {
172    let mut parts = Vec::new();
173    let (mut start, mut quoted, mut escaped) = (0, false, false);
174    for (position, byte) in value.iter().enumerate() {
175        if escaped {
176            escaped = false;
177        } else if quoted && *byte == b'\\' {
178            escaped = true;
179        } else if *byte == b'"' {
180            quoted = !quoted;
181        } else if !quoted && *byte == b',' {
182            let part = trim_ows(&value[start..position]);
183            if !part.is_empty() {
184                parts.push(part.to_vec());
185            }
186            start = position + 1;
187        }
188    }
189    if quoted {
190        return Err(FieldError::InvalidList(name.to_owned()));
191    }
192    let part = trim_ows(&value[start..]);
193    if !part.is_empty() {
194        parts.push(part.to_vec());
195    }
196    Ok(parts)
197}
198
199pub type Index = HashMap<String, FieldValue>;
200
201/// An owning HTTP header interpreter with lazy lookup and explicit trailer
202/// accumulation. Initial and trailing sections remain distinguishable; raw
203/// `Fields` never carries an interpreter or a hidden trailer marker.
204#[derive(Clone, Debug, Default)]
205pub struct HttpHeaders {
206    fields: Fields,
207    tail: Option<usize>,
208    indexed: Option<Box<Index>>,
209    policy: Policy,
210}
211
212impl HttpHeaders {
213    pub fn new(fields: Fields) -> Self {
214        Self {
215            fields,
216            ..Self::default()
217        }
218    }
219    pub fn try_from_pairs<N: AsRef<str>, V: AsRef<[u8]>>(
220        pairs: impl IntoIterator<Item = (N, V)>,
221    ) -> Result<Self, FieldError> {
222        Ok(Self::new(Fields::try_from_pairs(pairs)?))
223    }
224    pub fn with_policy(fields: Fields, policy: Policy) -> Self {
225        Self {
226            fields,
227            policy,
228            ..Self::default()
229        }
230    }
231    /// Recover the initial and trailing sections without losing their provenance.
232    pub fn into_parts(mut self) -> (Fields, Option<Fields>) {
233        let trailers = self.tail.map(|tail| self.fields.split_off(tail));
234        (self.fields, trailers)
235    }
236    pub fn pairs(&self) -> &[Field] {
237        &self.fields.pairs
238    }
239    pub fn len(&self) -> usize {
240        self.fields.pairs.len()
241    }
242    pub fn is_empty(&self) -> bool {
243        self.fields.pairs.is_empty()
244    }
245    pub fn tail(&self) -> Option<usize> {
246        self.tail
247    }
248    pub fn has_trailers(&self) -> bool {
249        self.tail.is_some()
250    }
251    pub fn headers(&self) -> &[Field] {
252        &self.fields.pairs[..self.tail.unwrap_or(self.fields.pairs.len())]
253    }
254    pub fn trailers(&self) -> &[Field] {
255        &self.fields.pairs[self.tail.unwrap_or(self.fields.pairs.len())..]
256    }
257    pub fn policy(&self) -> &Policy {
258        &self.policy
259    }
260
261    /// Changing policy invalidates interpreted values; validation is deferred.
262    pub fn set_policy(&mut self, policy: Policy) {
263        self.policy = policy;
264        self.indexed = None;
265    }
266
267    /// Explicitly append a received trailing section. Failed validation leaves
268    /// this interpreter unchanged, including its cached index and section split.
269    pub fn append_trailers(&mut self, fields: Fields) -> Result<(), FieldError> {
270        for field in fields.pairs() {
271            if !self.policy.get(field.name()).trailer {
272                return Err(FieldError::InvalidTrailer(
273                    field.name().to_ascii_lowercase(),
274                ));
275            }
276        }
277        // Validate combinations before moving any pairs into the collection.
278        if let Some(index) = &self.indexed {
279            let mut next = (**index).clone();
280            for field in fields.pairs() {
281                merge_into(&mut next, field, self.policy.get(field.name()))?;
282            }
283            self.indexed = Some(Box::new(next));
284        }
285        self.tail.get_or_insert(self.fields.len());
286        self.fields.extend(fields);
287        Ok(())
288    }
289
290    pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a [u8]> + 'a {
291        self.fields
292            .pairs
293            .iter()
294            .filter(move |field| field.name().eq_ignore_ascii_case(name))
295            .map(Field::value)
296    }
297
298    pub fn contains(&self, name: &str) -> bool {
299        self.fields
300            .pairs
301            .iter()
302            .any(|field| field.name().eq_ignore_ascii_case(name))
303    }
304
305    #[inline]
306    pub fn add(
307        &mut self,
308        name: impl AsRef<str>,
309        value: impl AsRef<[u8]>,
310    ) -> Result<(), FieldError> {
311        self.append(Field::new(name, value)?)
312    }
313
314    #[inline]
315    pub fn append(&mut self, field: Field) -> Result<(), FieldError> {
316        self.check_trailer(&field)?;
317        if let Some(index) = &mut self.indexed {
318            merge_into(index, &field, self.policy.get(field.name()))?;
319        }
320        self.fields.pairs.push(field);
321        Ok(())
322    }
323
324    /// Replace all matching pairs and append in the current section. Validation
325    /// failures leave the original collection and its cached index unchanged.
326    pub fn set(
327        &mut self,
328        name: impl AsRef<str>,
329        value: impl AsRef<[u8]>,
330    ) -> Result<(), FieldError> {
331        let field = Field::new(name, value)?;
332        self.check_trailer(&field)?;
333        let parsed = if self.indexed.is_some() {
334            Some(FieldValue::parse(
335                field.name(),
336                field.value(),
337                self.policy.get(field.name()).combination,
338            )?)
339        } else {
340            None
341        };
342        self.remove(field.name());
343        if let Some(parsed) = parsed {
344            self.indexed
345                .as_mut()
346                .unwrap()
347                .insert(field.name().to_ascii_lowercase(), parsed);
348        }
349        self.fields.pairs.push(field);
350        Ok(())
351    }
352
353    /// Remove matching pairs in both sections, preserving the trailer boundary.
354    pub fn remove(&mut self, name: &str) -> usize {
355        let mut position = 0;
356        let mut removed_headers = 0;
357        let original_length = self.fields.pairs.len();
358        self.fields.pairs.retain(|field| {
359            let keep = !field.name().eq_ignore_ascii_case(name);
360            if !keep && position < self.tail.unwrap_or(original_length) {
361                removed_headers += 1;
362            }
363            position += 1;
364            keep
365        });
366        if let Some(tail) = &mut self.tail {
367            *tail -= removed_headers;
368        }
369        if let Some(index) = &mut self.indexed {
370            index.remove(&name.to_ascii_lowercase());
371        }
372        original_length - self.fields.pairs.len()
373    }
374
375    pub fn clear(&mut self) {
376        self.fields.pairs.clear();
377        self.tail = None;
378        self.indexed = None;
379    }
380
381    pub fn get(&mut self, name: &str) -> Result<Option<&FieldValue>, FieldError> {
382        Ok(self.index()?.get(&name.to_ascii_lowercase()))
383    }
384
385    /// Validate and cache the whole semantic index. Failed builds are not cached,
386    /// so removing an offending pair or changing policy can repair the collection.
387    pub fn index(&mut self) -> Result<&Index, FieldError> {
388        if self.indexed.is_none() {
389            let mut index = Index::new();
390            for (position, field) in self.fields.pairs.iter().enumerate() {
391                if self.tail.is_some_and(|tail| position >= tail) {
392                    self.check_trailer(field)?;
393                }
394                merge_into(&mut index, field, self.policy.get(field.name()))?;
395            }
396            self.indexed = Some(Box::new(index));
397        }
398        Ok(self.indexed.as_ref().unwrap())
399    }
400
401    fn check_trailer(&self, field: &Field) -> Result<(), FieldError> {
402        if self.tail.is_some() && !self.policy.get(field.name()).trailer {
403            return Err(FieldError::InvalidTrailer(
404                field.name().to_ascii_lowercase(),
405            ));
406        }
407        Ok(())
408    }
409}
410
411fn merge_into(index: &mut Index, field: &Field, policy: FieldPolicy) -> Result<(), FieldError> {
412    let name = field.name().to_ascii_lowercase();
413    if policy.combination == Combination::Singleton && index.contains_key(&name) {
414        return Err(FieldError::Duplicate(name));
415    }
416    let parsed = FieldValue::parse(&name, field.value(), policy.combination)?;
417    match index.get_mut(&name) {
418        Some(current) if policy.combination != Combination::Last => {
419            current.parts.extend(parsed.parts)
420        }
421        _ => {
422            index.insert(name, parsed);
423        }
424    }
425    Ok(())
426}