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