Skip to main content

trillium_http/headers/
field_section.rs

1//! Protocol-agnostic representations of an HTTP field section.
2//!
3//! An HTTP field section is the combined pseudo-headers + regular headers payload of a
4//! request or response. The same structure is decoded from HPACK (HTTP/2) and QPACK
5//! (HTTP/3).
6//!
7//! [`FieldLineValue`] tracks provenance (static / borrowed / owned) of a value slice so the
8//! encoder and dynamic-table code paths can defer cloning until the last possible moment.
9
10use super::{
11    Headers,
12    entry_name::{EntryName, PseudoHeaderName},
13    header_value::HeaderValueInner,
14    shared_value::SharedValue,
15};
16use crate::{HeaderValue, Method, Status, compact_cow::CompactCow};
17use fieldwork::Fieldwork;
18use smallvec::SmallVec;
19use std::{
20    borrow::{Borrow, Cow},
21    fmt::{self, Display, Formatter},
22    hash,
23    ops::Deref,
24};
25
26/// The six defined HTTP pseudo-header fields.
27///
28/// Unlike regular headers, pseudo-headers are a fixed set — unknown pseudo-headers are a
29/// protocol error. Each may appear at most once.
30#[derive(Debug, Default, Clone, PartialEq, Eq, Fieldwork)]
31#[fieldwork(
32    get,
33    get_mut(option_borrow_inner = false),
34    take,
35    with,
36    without,
37    set,
38    into
39)]
40pub struct PseudoHeaders<'a> {
41    /// `:method` pseudo-header
42    #[field(copy)]
43    pub(in crate::headers) method: Option<Method>,
44
45    /// `:status` pseudo-header
46    #[field(copy)]
47    pub(in crate::headers) status: Option<Status>,
48
49    /// `:path` pseudo-header
50    pub(in crate::headers) path: Option<Cow<'a, str>>,
51
52    /// `:scheme` pseudo-header
53    pub(in crate::headers) scheme: Option<Cow<'a, str>>,
54
55    /// `:authority` pseudo-header
56    pub(in crate::headers) authority: Option<Cow<'a, str>>,
57
58    /// `:protocol` pseudo-header
59    pub(in crate::headers) protocol: Option<Cow<'a, str>>,
60}
61
62impl PseudoHeaders<'_> {
63    /// `true` when no pseudo-header fields are set.
64    pub fn is_empty(&self) -> bool {
65        self.method.is_none()
66            && self.status.is_none()
67            && self.path.is_none()
68            && self.scheme.is_none()
69            && self.authority.is_none()
70            && self.protocol.is_none()
71    }
72
73    /// Convert into a `PseudoHeaders<'static>` by allocating any borrowed string fields.
74    #[allow(
75        dead_code,
76        reason = "consumed by external callers; not visible in this crate's build"
77    )]
78    pub fn into_owned(self) -> PseudoHeaders<'static> {
79        PseudoHeaders {
80            method: self.method,
81            status: self.status,
82            path: self.path.map(|c| Cow::Owned(c.into_owned())),
83            scheme: self.scheme.map(|c| Cow::Owned(c.into_owned())),
84            authority: self.authority.map(|c| Cow::Owned(c.into_owned())),
85            protocol: self.protocol.map(|c| Cow::Owned(c.into_owned())),
86        }
87    }
88}
89
90impl Display for PseudoHeaders<'_> {
91    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
92        if let Some(method) = &self.method {
93            writeln!(f, ":method: {method}")?;
94        }
95
96        if let Some(status) = &self.status {
97            writeln!(f, ":status: {status}")?;
98        }
99
100        if let Some(path) = &self.path {
101            writeln!(f, ":path: {path}")?;
102        }
103        if let Some(scheme) = &self.scheme {
104            writeln!(f, ":scheme: {scheme}")?;
105        }
106
107        if let Some(authority) = &self.authority {
108            writeln!(f, ":authority: {authority}")?;
109        }
110
111        if let Some(protocol) = &self.protocol {
112            writeln!(f, ":protocol: {protocol}")?;
113        }
114
115        Ok(())
116    }
117}
118
119/// Combined [`PseudoHeaders`] and [`Headers`] — one HTTP field section.
120#[derive(Debug, Clone, Fieldwork)]
121#[fieldwork(get, get_mut, into_field)]
122pub struct FieldSection<'a> {
123    /// pseudo-headers
124    pseudo_headers: PseudoHeaders<'a>,
125
126    /// headers
127    headers: Cow<'a, Headers>,
128}
129
130impl<'a> FieldSection<'a> {
131    /// Construct a new borrowed `FieldSection` for encoding.
132    pub fn new(pseudo_headers: PseudoHeaders<'a>, headers: &'a Headers) -> Self {
133        Self {
134            pseudo_headers,
135            headers: Cow::Borrowed(headers),
136        }
137    }
138
139    /// Construct a `FieldSection` owning its headers — used by decoders that produce a
140    /// fresh [`Headers`] from the wire.
141    pub(in crate::headers) fn from_owned(
142        pseudo_headers: PseudoHeaders<'static>,
143        headers: Headers,
144    ) -> FieldSection<'static> {
145        FieldSection {
146            pseudo_headers,
147            headers: Cow::Owned(headers),
148        }
149    }
150
151    /// Flatten this field section into an ordered list of `(name, value, never_indexed)`
152    /// triples suitable for feeding to a compression-aware encoder.
153    ///
154    /// Pseudo-headers come first in RFC-mandated order; regular headers follow.
155    /// `FieldLineValue` provenance is preserved so a downstream encoder can elide
156    /// allocations for already-static slices. The `never_indexed` flag carries the
157    /// HPACK / QPACK N bit per value; pseudo-headers are always `false` because they
158    /// round-trip through typed `Conn` fields, not the `Headers` map.
159    pub(in crate::headers) fn field_lines(&self) -> FieldLines<'_> {
160        fn field_line_value_from(v: &HeaderValue) -> FieldLineValue<'_> {
161            match &v.inner {
162                HeaderValueInner::Utf8(CompactCow::Borrowed(b)) => {
163                    FieldLineValue::Static(b.as_bytes())
164                }
165                HeaderValueInner::Utf8(CompactCow::Shared(s)) => {
166                    FieldLineValue::Shared(SharedValue::Utf8(s.clone()))
167                }
168                HeaderValueInner::Bytes(b) => FieldLineValue::Shared(SharedValue::Bytes(b.clone())),
169                HeaderValueInner::Utf8(CompactCow::Owned(_)) => {
170                    FieldLineValue::Borrowed(v.as_ref())
171                }
172            }
173        }
174
175        // Inline capacity covers a typical response (`:status` + a handful of headers) with no
176        // heap allocation; larger sections (e.g. proxied responses forwarding many headers)
177        // spill to a single right-sized heap allocation via `with_capacity`.
178        let mut lines = SmallVec::with_capacity(self.headers.len() + 6);
179        if let Some(method) = &self.pseudo_headers.method {
180            lines.push((
181                PseudoHeaderName::Method.into(),
182                FieldLineValue::Static(method.as_str().as_bytes()),
183                false,
184            ));
185        }
186
187        if let Some(status) = &self.pseudo_headers.status {
188            lines.push((
189                PseudoHeaderName::Status.into(),
190                FieldLineValue::Static(status.code().as_bytes()),
191                false,
192            ));
193        }
194
195        if let Some(path) = &self.pseudo_headers.path {
196            lines.push((
197                PseudoHeaderName::Path.into(),
198                FieldLineValue::Borrowed(path.as_bytes()),
199                false,
200            ));
201        }
202        if let Some(scheme) = &self.pseudo_headers.scheme {
203            lines.push((
204                PseudoHeaderName::Scheme.into(),
205                FieldLineValue::Borrowed(scheme.as_bytes()),
206                false,
207            ));
208        }
209
210        if let Some(authority) = &self.pseudo_headers.authority {
211            lines.push((
212                PseudoHeaderName::Authority.into(),
213                FieldLineValue::Borrowed(authority.as_bytes()),
214                false,
215            ));
216        }
217
218        if let Some(protocol) = &self.pseudo_headers.protocol {
219            lines.push((
220                PseudoHeaderName::Protocol.into(),
221                FieldLineValue::Borrowed(protocol.as_bytes()),
222                false,
223            ));
224        }
225
226        // Iterate the inner maps directly (rather than the public `Iter`) so the
227        // `UnknownHeaderName<'static>` inner lifetime is preserved on each item;
228        // `Iter` erases it to the iterator's borrow lifetime, which would prevent
229        // calling `into_lower_static`.
230        for (k, hv) in &self.headers.known {
231            for v in hv {
232                let value = field_line_value_from(v);
233                lines.push((EntryName::Known(*k), value, v.is_never_indexed()));
234            }
235        }
236
237        for (uhn, hv) in &self.headers.unknown {
238            for v in hv {
239                let value = field_line_value_from(v);
240                // Route the clone through the lowercase interner so any recoverable
241                // `&'static str` survives lifetime erasure via the `UnknownStatic`
242                // variant tag.
243                let lowered = uhn.clone().into_lower_static();
244                let name = match lowered.as_static_str() {
245                    Some(s) => EntryName::UnknownStatic(s),
246                    None => EntryName::Unknown(lowered),
247                };
248                lines.push((name, value, v.is_never_indexed()));
249            }
250        }
251
252        lines
253    }
254
255    /// Decompose a `FieldSection` into its pseudo-headers and headers.
256    pub fn into_parts(self) -> (PseudoHeaders<'a>, Headers) {
257        (self.pseudo_headers, self.headers.into_owned())
258    }
259
260    /// The *uncompressed* size of this field section: the sum, over every field line, of the
261    /// name's length in bytes, the value's length in bytes, and a 32-byte per-field overhead.
262    /// Pseudo-header names count their leading colon (`:method` is 7).
263    ///
264    /// This is the metric both HTTP/2's `SETTINGS_MAX_HEADER_LIST_SIZE` ([RFC 7540 §6.5.2]) and
265    /// HTTP/3's `SETTINGS_MAX_FIELD_SECTION_SIZE` ([RFC 9114 §4.2.2]) are defined in — the 32-byte
266    /// overhead and the formula are identical, both deriving from the HPACK entry size
267    /// ([RFC 7541 §4.1]). It is independent of HPACK/QPACK compression, which is why it can't be
268    /// read off the encoded byte length.
269    ///
270    /// [RFC 7540 §6.5.2]: https://www.rfc-editor.org/rfc/rfc7540#section-6.5.2
271    /// [RFC 9114 §4.2.2]: https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2
272    /// [RFC 7541 §4.1]: https://www.rfc-editor.org/rfc/rfc7541#section-4.1
273    pub(crate) fn uncompressed_len(&self) -> u64 {
274        self.field_lines()
275            .iter()
276            .map(|(name, value, _)| name.len() as u64 + value.len() as u64 + 32)
277            .sum()
278    }
279}
280
281impl Display for FieldSection<'_> {
282    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
283        write!(f, "{}", self.pseudo_headers)?;
284        for (n, v) in &*self.headers {
285            for v in v {
286                writeln!(f, "{n}: {v}")?;
287            }
288        }
289        Ok(())
290    }
291}
292
293/// An ordered list of `(name, value, never_indexed)` field-line triples, as produced by
294/// [`FieldSection::field_lines`] for a compression-aware encoder.
295///
296/// Inline storage holds 16 lines — enough for a typical response field section to stay on
297/// the stack — and spills to the heap beyond that.
298pub(in crate::headers) type FieldLines<'a> =
299    SmallVec<[(EntryName<'a>, FieldLineValue<'a>, bool); 16]>;
300
301/// A byte-slice value that tracks its provenance — static, externally borrowed, or owned.
302///
303/// Serves the same purpose as `Cow<'a, Cow<'static, [u8]>>` but with a cleaner surface. The
304/// `Static` variant lets us keep static literals cheap through the whole encode path;
305/// `Borrowed` lets a decoder yield zero-copy slices into the frame buffer; `Owned` is the
306/// escape hatch for Huffman-decoded bytes and similar transforms; `Shared` is a value that
307/// lives in a dynamic table and is handed out by refcount bump.
308///
309/// `PartialEq` / `Eq` / `Hash` delegate to the underlying bytes — provenance is a storage
310/// detail, not a semantic distinction.
311#[derive(Debug, Clone)]
312pub(crate) enum FieldLineValue<'a> {
313    Static(&'static [u8]),
314    Borrowed(&'a [u8]),
315    Owned(Vec<u8>),
316    Shared(SharedValue),
317}
318
319impl Deref for FieldLineValue<'_> {
320    type Target = [u8];
321
322    fn deref(&self) -> &Self::Target {
323        self.as_bytes()
324    }
325}
326
327impl PartialEq for FieldLineValue<'_> {
328    fn eq(&self, other: &Self) -> bool {
329        self.as_bytes() == other.as_bytes()
330    }
331}
332
333impl Eq for FieldLineValue<'_> {}
334
335/// `Hash` and `Eq` go through `as_bytes`, so a map keyed by `FieldLineValue` can be probed
336/// with a bare `&[u8]`.
337impl Borrow<[u8]> for FieldLineValue<'_> {
338    fn borrow(&self) -> &[u8] {
339        self.as_bytes()
340    }
341}
342
343impl hash::Hash for FieldLineValue<'_> {
344    fn hash<H: hash::Hasher>(&self, state: &mut H) {
345        self.as_bytes().hash(state);
346    }
347}
348
349impl FieldLineValue<'_> {
350    /// Convert to a form suitable for storage in a dynamic table: `Static` stays static
351    /// (already free to copy), everything else becomes `Shared`.
352    pub(in crate::headers) fn into_shared(self) -> FieldLineValue<'static> {
353        match self {
354            FieldLineValue::Static(b) => FieldLineValue::Static(b),
355            other => FieldLineValue::Shared(other.into()),
356        }
357    }
358
359    pub(in crate::headers) fn reborrow(&self) -> FieldLineValue<'_> {
360        match self {
361            FieldLineValue::Static(items) => FieldLineValue::Static(items),
362            FieldLineValue::Borrowed(items) => FieldLineValue::Borrowed(items),
363            FieldLineValue::Owned(items) => FieldLineValue::Borrowed(items),
364            FieldLineValue::Shared(shared) => FieldLineValue::Shared(shared.clone()),
365        }
366    }
367
368    pub(in crate::headers) fn as_bytes(&self) -> &[u8] {
369        match self {
370            FieldLineValue::Static(items) | FieldLineValue::Borrowed(items) => items,
371            FieldLineValue::Owned(items) => items,
372            FieldLineValue::Shared(shared) => shared.as_bytes(),
373        }
374    }
375}
376
377impl From<Cow<'static, [u8]>> for FieldLineValue<'static> {
378    fn from(value: Cow<'static, [u8]>) -> Self {
379        match value {
380            Cow::Borrowed(b) => FieldLineValue::Static(b),
381            Cow::Owned(v) => FieldLineValue::Owned(v),
382        }
383    }
384}
385
386/// Borrowed bytes are copied straight into a `HeaderValue`'s inline storage when they fit,
387/// so the non-Huffman literal path never round-trips through a `Vec`. Owned bytes hand their
388/// allocation over instead of copying.
389impl From<FieldLineValue<'_>> for HeaderValue {
390    fn from(value: FieldLineValue<'_>) -> Self {
391        match value {
392            FieldLineValue::Static(b) => HeaderValue::from(b),
393            FieldLineValue::Borrowed(b) => HeaderValue::parse(b),
394            FieldLineValue::Owned(v) => HeaderValue::from(v),
395            FieldLineValue::Shared(s) => HeaderValue::from(s),
396        }
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::{KnownHeaderName, Method};
404
405    #[test]
406    fn uncompressed_len_sums_name_value_plus_32_per_field() {
407        // :method GET  -> 7 + 3 + 32 = 42
408        // accept: */*  -> 6 + 3 + 32 = 41
409        let mut headers = Headers::new();
410        headers.insert(KnownHeaderName::Accept, "*/*");
411        let pseudo = PseudoHeaders::default().with_method(Method::Get);
412        let field_section = FieldSection::new(pseudo, &headers);
413        assert_eq!(field_section.uncompressed_len(), 42 + 41);
414    }
415
416    #[test]
417    fn uncompressed_len_counts_repeated_values_separately() {
418        // two set-cookie values each count as their own field line
419        let mut headers = Headers::new();
420        headers.append(KnownHeaderName::SetCookie, "a=1"); // 10 + 3 + 32 = 45
421        headers.append(KnownHeaderName::SetCookie, "bb=22"); // 10 + 5 + 32 = 47
422        let field_section = FieldSection::new(PseudoHeaders::default(), &headers);
423        assert_eq!(field_section.uncompressed_len(), 45 + 47);
424    }
425}