base-http 0.2.0

Runtime-independent HTTP semantics and bodies for Base
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
// Released under the MIT License.
// Copyright, 2026, by Samuel Williams.

//! Explicit HTTP interpretation of raw fields. Other application protocols can interpret `Fields` independently or configure their own combination policy.
//!
//! [`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.
//!
//! A received body `Headers` frame can contribute trailers to an interpreter without changing the message's initial fields:
//!
//! ```rust
//! use base_http::{Fields, HttpHeaders, Response, Status, body::EmptyBody};
//!
//! let mut response = Response::new(Status::OK, EmptyBody);
//! response.fields.add("Content-Type", "text/plain")?;
//!
//! let mut headers = HttpHeaders::new(response.fields.clone());
//! headers.append_trailers(Fields::try_from_pairs([("x-checksum", "example")])?)?;
//! assert_eq!(headers.headers().len(), 1);
//! assert_eq!(headers.trailers().len(), 1);
//! assert_eq!(response.fields.len(), 1);
//! # Ok::<(), base_http::FieldError>(())
//! ```
use crate::{
    Field, FieldError, Fields,
    syntax::{is_token, trim_ows},
};
use std::collections::HashMap;

/// How repeated raw values contribute to the interpreted value.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Combination {
    Singleton,
    Comma,
    LowercaseComma,
    Cookie,
    Separate,
    Last,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FieldPolicy {
    pub combination: Combination,
    pub trailer: bool,
}

impl FieldPolicy {
    pub const fn new(combination: Combination, trailer: bool) -> Self {
        Self {
            combination,
            trailer,
        }
    }
}

/// Combination/trailer defaults following Socketry, with case-insensitive
/// overrides. Repeated credentials are rejected rather than concatenated.
#[derive(Clone, Debug, Default)]
pub struct Policy {
    // Most messages use only the built-in policy. Keep that case pointer-sized;
    // allocate the override table itself only when a caller supplies overrides.
    #[allow(clippy::box_collection)]
    overrides: Option<Box<HashMap<String, FieldPolicy>>>,
}

impl Policy {
    pub fn set(&mut self, name: &str, policy: FieldPolicy) -> Result<(), FieldError> {
        if !is_token(name.as_bytes()) {
            return Err(FieldError::InvalidName);
        }
        self.overrides
            .get_or_insert_with(Default::default)
            .insert(name.to_ascii_lowercase(), policy);
        Ok(())
    }

    pub fn get(&self, name: &str) -> FieldPolicy {
        let name = name.to_ascii_lowercase();
        if let Some(policy) = self
            .overrides
            .as_ref()
            .and_then(|overrides| overrides.get(&name))
        {
            return *policy;
        }
        use Combination::*;
        match name.as_str() {
            "content-disposition"
            | "content-length"
            | "content-type"
            | "expect"
            | "from"
            | "host"
            | "location"
            | "max-forwards"
            | "referer"
            | "retry-after"
            | "server"
            | "user-agent"
            | "if-range"
            | "content-range" => FieldPolicy::new(Singleton, false),
            "connection" => FieldPolicy::new(LowercaseComma, false),
            "cookie" => FieldPolicy::new(Cookie, false),
            "set-cookie" | "www-authenticate" | "proxy-authenticate" => {
                FieldPolicy::new(Separate, false)
            }
            "authorization" | "proxy-authorization" => FieldPolicy::new(Singleton, false),
            "date"
            | "expires"
            | "last-modified"
            | "if-modified-since"
            | "if-unmodified-since"
            | "etag" => FieldPolicy::new(Last, true),
            "range" | "transfer-encoding" | "trailer" | "upgrade" | "cache-control" | "te"
            | "vary" | "priority" | "via" | "x-forwarded-for" | "if-match" | "if-none-match"
            | "accept" | "accept-ranges" | "accept-charset" | "accept-encoding"
            | "accept-language" | "content-encoding" => FieldPolicy::new(Comma, false),
            _ => FieldPolicy::new(Comma, true),
        }
    }
}

/// Interpreted components. Raw spelling/spacing always remains in `Fields::pairs`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FieldValue {
    combination: Combination,
    parts: Vec<Vec<u8>>,
}

impl FieldValue {
    pub fn combination(&self) -> Combination {
        self.combination
    }
    pub fn parts(&self) -> &[Vec<u8>] {
        &self.parts
    }

    /// A single component, when present. This does not join a list or cookies.
    pub fn single(&self) -> Option<&[u8]> {
        if self.parts.len() == 1 {
            Some(&self.parts[0])
        } else {
            None
        }
    }

    /// Combine into one value when policy permits. Separate fields such as
    /// `set-cookie` must be emitted individually and return `None`.
    pub fn combined(&self) -> Option<Vec<u8>> {
        let separator: &[u8] = match self.combination {
            Combination::Separate => return None,
            Combination::Cookie => b"; ",
            _ => b",",
        };
        Some(self.parts.join(separator))
    }

    fn parse(name: &str, value: &[u8], combination: Combination) -> Result<Self, FieldError> {
        let mut parts = match combination {
            Combination::Comma | Combination::LowercaseComma => split_list(name, value)?,
            _ => vec![value.to_vec()],
        };
        if combination == Combination::LowercaseComma {
            for part in &mut parts {
                part.make_ascii_lowercase();
            }
        }
        Ok(Self { combination, parts })
    }
}

fn split_list(name: &str, value: &[u8]) -> Result<Vec<Vec<u8>>, FieldError> {
    let mut parts = Vec::new();
    let (mut start, mut quoted, mut escaped) = (0, false, false);
    for (position, byte) in value.iter().enumerate() {
        if escaped {
            escaped = false;
        } else if quoted && *byte == b'\\' {
            escaped = true;
        } else if *byte == b'"' {
            quoted = !quoted;
        } else if !quoted && *byte == b',' {
            let part = trim_ows(&value[start..position]);
            if !part.is_empty() {
                parts.push(part.to_vec());
            }
            start = position + 1;
        }
    }
    if quoted {
        return Err(FieldError::InvalidList(name.to_owned()));
    }
    let part = trim_ows(&value[start..]);
    if !part.is_empty() {
        parts.push(part.to_vec());
    }
    Ok(parts)
}

pub type Index = HashMap<String, FieldValue>;

/// An owning HTTP header interpreter with lazy lookup and explicit trailer
/// accumulation. Initial and trailing sections remain distinguishable; raw
/// `Fields` never carries an interpreter or a hidden trailer marker.
#[derive(Clone, Debug, Default)]
pub struct HttpHeaders {
    fields: Fields,
    tail: Option<usize>,
    indexed: Option<Box<Index>>,
    policy: Policy,
}

impl HttpHeaders {
    pub fn new(fields: Fields) -> Self {
        Self {
            fields,
            ..Self::default()
        }
    }
    pub fn try_from_pairs<N: AsRef<str>, V: AsRef<[u8]>>(
        pairs: impl IntoIterator<Item = (N, V)>,
    ) -> Result<Self, FieldError> {
        Ok(Self::new(Fields::try_from_pairs(pairs)?))
    }
    pub fn with_policy(fields: Fields, policy: Policy) -> Self {
        Self {
            fields,
            policy,
            ..Self::default()
        }
    }
    /// Recover the initial and trailing sections without losing their provenance.
    pub fn into_parts(mut self) -> (Fields, Option<Fields>) {
        let trailers = self.tail.map(|tail| self.fields.split_off(tail));
        (self.fields, trailers)
    }
    pub fn pairs(&self) -> &[Field] {
        &self.fields.pairs
    }
    pub fn len(&self) -> usize {
        self.fields.pairs.len()
    }
    pub fn is_empty(&self) -> bool {
        self.fields.pairs.is_empty()
    }
    pub fn tail(&self) -> Option<usize> {
        self.tail
    }
    pub fn has_trailers(&self) -> bool {
        self.tail.is_some()
    }
    pub fn headers(&self) -> &[Field] {
        &self.fields.pairs[..self.tail.unwrap_or(self.fields.pairs.len())]
    }
    pub fn trailers(&self) -> &[Field] {
        &self.fields.pairs[self.tail.unwrap_or(self.fields.pairs.len())..]
    }
    pub fn policy(&self) -> &Policy {
        &self.policy
    }

    /// Changing policy invalidates interpreted values; validation is deferred.
    pub fn set_policy(&mut self, policy: Policy) {
        self.policy = policy;
        self.indexed = None;
    }

    /// Explicitly append a received trailing section. Failed validation leaves
    /// this interpreter unchanged, including its cached index and section split.
    pub fn append_trailers(&mut self, fields: Fields) -> Result<(), FieldError> {
        for field in fields.pairs() {
            if !self.policy.get(field.name()).trailer {
                return Err(FieldError::InvalidTrailer(
                    field.name().to_ascii_lowercase(),
                ));
            }
        }
        // Validate combinations before moving any pairs into the collection.
        if let Some(index) = &self.indexed {
            let mut next = (**index).clone();
            for field in fields.pairs() {
                merge_into(&mut next, field, self.policy.get(field.name()))?;
            }
            self.indexed = Some(Box::new(next));
        }
        self.tail.get_or_insert(self.fields.len());
        self.fields.extend(fields);
        Ok(())
    }

    pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a [u8]> + 'a {
        self.fields
            .pairs
            .iter()
            .filter(move |field| field.name().eq_ignore_ascii_case(name))
            .map(Field::value)
    }

    pub fn contains(&self, name: &str) -> bool {
        self.fields
            .pairs
            .iter()
            .any(|field| field.name().eq_ignore_ascii_case(name))
    }

    #[inline]
    pub fn add(
        &mut self,
        name: impl AsRef<str>,
        value: impl AsRef<[u8]>,
    ) -> Result<(), FieldError> {
        self.append(Field::new(name, value)?)
    }

    #[inline]
    pub fn append(&mut self, field: Field) -> Result<(), FieldError> {
        self.check_trailer(&field)?;
        if let Some(index) = &mut self.indexed {
            merge_into(index, &field, self.policy.get(field.name()))?;
        }
        self.fields.pairs.push(field);
        Ok(())
    }

    /// Replace all matching pairs and append in the current section. Validation
    /// failures leave the original collection and its cached index unchanged.
    pub fn set(
        &mut self,
        name: impl AsRef<str>,
        value: impl AsRef<[u8]>,
    ) -> Result<(), FieldError> {
        let field = Field::new(name, value)?;
        self.check_trailer(&field)?;
        let parsed = if self.indexed.is_some() {
            Some(FieldValue::parse(
                field.name(),
                field.value(),
                self.policy.get(field.name()).combination,
            )?)
        } else {
            None
        };
        self.remove(field.name());
        if let Some(parsed) = parsed {
            self.indexed
                .as_mut()
                .unwrap()
                .insert(field.name().to_ascii_lowercase(), parsed);
        }
        self.fields.pairs.push(field);
        Ok(())
    }

    /// Remove matching pairs in both sections, preserving the trailer boundary.
    pub fn remove(&mut self, name: &str) -> usize {
        let mut position = 0;
        let mut removed_headers = 0;
        let original_length = self.fields.pairs.len();
        self.fields.pairs.retain(|field| {
            let keep = !field.name().eq_ignore_ascii_case(name);
            if !keep && position < self.tail.unwrap_or(original_length) {
                removed_headers += 1;
            }
            position += 1;
            keep
        });
        if let Some(tail) = &mut self.tail {
            *tail -= removed_headers;
        }
        if let Some(index) = &mut self.indexed {
            index.remove(&name.to_ascii_lowercase());
        }
        original_length - self.fields.pairs.len()
    }

    pub fn clear(&mut self) {
        self.fields.pairs.clear();
        self.tail = None;
        self.indexed = None;
    }

    pub fn get(&mut self, name: &str) -> Result<Option<&FieldValue>, FieldError> {
        Ok(self.index()?.get(&name.to_ascii_lowercase()))
    }

    /// Validate and cache the whole semantic index. Failed builds are not cached,
    /// so removing an offending pair or changing policy can repair the collection.
    pub fn index(&mut self) -> Result<&Index, FieldError> {
        if self.indexed.is_none() {
            let mut index = Index::new();
            for (position, field) in self.fields.pairs.iter().enumerate() {
                if self.tail.is_some_and(|tail| position >= tail) {
                    self.check_trailer(field)?;
                }
                merge_into(&mut index, field, self.policy.get(field.name()))?;
            }
            self.indexed = Some(Box::new(index));
        }
        Ok(self.indexed.as_ref().unwrap())
    }

    fn check_trailer(&self, field: &Field) -> Result<(), FieldError> {
        if self.tail.is_some() && !self.policy.get(field.name()).trailer {
            return Err(FieldError::InvalidTrailer(
                field.name().to_ascii_lowercase(),
            ));
        }
        Ok(())
    }
}

fn merge_into(index: &mut Index, field: &Field, policy: FieldPolicy) -> Result<(), FieldError> {
    let name = field.name().to_ascii_lowercase();
    if policy.combination == Combination::Singleton && index.contains_key(&name) {
        return Err(FieldError::Duplicate(name));
    }
    let parsed = FieldValue::parse(&name, field.value(), policy.combination)?;
    match index.get_mut(&name) {
        Some(current) if policy.combination != Combination::Last => {
            current.parts.extend(parsed.parts)
        }
        _ => {
            index.insert(name, parsed);
        }
    }
    Ok(())
}