Skip to main content

proxima_primitives/pipe/
header_list.rs

1#![cfg(feature = "alloc")]
2
3use core::ops::{Deref, DerefMut};
4
5use alloc::string::String;
6use alloc::vec::Vec;
7use bytes::Bytes;
8use serde::{Deserialize, Serialize};
9
10/// Insertion-ordered header/query pairs backing `Request.headers`,
11/// `Request.query`, `Response.headers`. Bytes storage; case-insensitive
12/// `&str` lookups. Serializes as `Vec<(String, String)>` for jsonl
13/// compat — non-UTF-8 lossily converted on the way out.
14#[derive(Clone, Default, Debug, PartialEq, Eq)]
15pub struct HeaderList {
16    entries: Vec<(Bytes, Bytes)>,
17}
18
19impl HeaderList {
20    #[must_use]
21    pub fn new() -> Self {
22        Self {
23            entries: Vec::new(),
24        }
25    }
26
27    #[must_use]
28    pub fn with_capacity(capacity: usize) -> Self {
29        Self {
30            entries: Vec::with_capacity(capacity),
31        }
32    }
33
34    #[must_use]
35    pub fn from_pairs<I, K, V>(pairs: I) -> Self
36    where
37        I: IntoIterator<Item = (K, V)>,
38        K: IntoHeaderBytes,
39        V: IntoHeaderBytes,
40    {
41        let mut list = Self::new();
42        for (key, value) in pairs {
43            list.insert(key.into_header_bytes(), value.into_header_bytes());
44        }
45        list
46    }
47
48    #[must_use]
49    pub fn len(&self) -> usize {
50        self.entries.len()
51    }
52
53    #[must_use]
54    pub fn is_empty(&self) -> bool {
55        self.entries.is_empty()
56    }
57
58    #[must_use]
59    pub fn get(&self, name: &str) -> Option<&Bytes> {
60        let needle = name.as_bytes();
61        self.entries
62            .iter()
63            .find(|(key, _)| key.as_ref().eq_ignore_ascii_case(needle))
64            .map(|(_, value)| value)
65    }
66
67    /// `None` if absent OR if the bytes aren't valid UTF-8.
68    #[must_use]
69    pub fn get_str(&self, name: &str) -> Option<&str> {
70        self.get(name)
71            .and_then(|value| core::str::from_utf8(value).ok())
72    }
73
74    /// Case-insensitive replace-or-append. Returns the prior value when
75    /// a same-name entry already existed.
76    pub fn insert<N: IntoHeaderBytes, V: IntoHeaderBytes>(
77        &mut self,
78        name: N,
79        value: V,
80    ) -> Option<Bytes> {
81        let name_bytes = name.into_header_bytes();
82        let value_bytes = value.into_header_bytes();
83        let needle = name_bytes.as_ref();
84        if let Some(slot) = self
85            .entries
86            .iter_mut()
87            .find(|(key, _)| key.as_ref().eq_ignore_ascii_case(needle))
88        {
89            return Some(core::mem::replace(&mut slot.1, value_bytes));
90        }
91        self.entries.push((name_bytes, value_bytes));
92        None
93    }
94
95    /// case-insensitive presence check.
96    #[must_use]
97    pub fn contains_key(&self, name: &str) -> bool {
98        let needle = name.as_bytes();
99        self.entries
100            .iter()
101            .any(|(key, _)| key.as_ref().eq_ignore_ascii_case(needle))
102    }
103
104    pub fn insert_if_absent<N: IntoHeaderBytes, V: IntoHeaderBytes>(&mut self, name: N, value: V) {
105        let name_bytes = name.into_header_bytes();
106        if !self.contains_key_bytes(name_bytes.as_ref()) {
107            self.entries.push((name_bytes, value.into_header_bytes()));
108        }
109    }
110
111    fn contains_key_bytes(&self, needle: &[u8]) -> bool {
112        self.entries
113            .iter()
114            .any(|(key, _)| key.as_ref().eq_ignore_ascii_case(needle))
115    }
116
117    pub fn iter(&self) -> core::slice::Iter<'_, (Bytes, Bytes)> {
118        self.entries.iter()
119    }
120
121    pub fn keys(&self) -> impl Iterator<Item = &Bytes> {
122        self.entries.iter().map(|(name, _)| name)
123    }
124
125    pub fn retain<F: FnMut(&Bytes, &Bytes) -> bool>(&mut self, mut predicate: F) {
126        self.entries.retain(|(name, value)| predicate(name, value));
127    }
128
129    pub fn remove(&mut self, name: &str) -> Option<Bytes> {
130        let needle = name.as_bytes();
131        let position = self
132            .entries
133            .iter()
134            .position(|(key, _)| key.as_ref().eq_ignore_ascii_case(needle))?;
135        Some(self.entries.swap_remove(position).1)
136    }
137
138    #[must_use]
139    pub fn into_inner(self) -> Vec<(Bytes, Bytes)> {
140        self.entries
141    }
142
143    #[must_use]
144    pub fn as_slice(&self) -> &[(Bytes, Bytes)] {
145        &self.entries
146    }
147}
148
149/// Conversion shim — exists because `Bytes::From` only covers `&'static`
150/// slices; we need to accept non-static `&str` / `&[u8]` (with a copy).
151pub trait IntoHeaderBytes {
152    fn into_header_bytes(self) -> Bytes;
153}
154
155impl IntoHeaderBytes for Bytes {
156    fn into_header_bytes(self) -> Bytes {
157        self
158    }
159}
160
161impl IntoHeaderBytes for &Bytes {
162    fn into_header_bytes(self) -> Bytes {
163        Bytes::clone(self)
164    }
165}
166
167impl IntoHeaderBytes for String {
168    fn into_header_bytes(self) -> Bytes {
169        Bytes::from(self)
170    }
171}
172
173impl IntoHeaderBytes for Vec<u8> {
174    fn into_header_bytes(self) -> Bytes {
175        Bytes::from(self)
176    }
177}
178
179impl IntoHeaderBytes for &str {
180    fn into_header_bytes(self) -> Bytes {
181        Bytes::copy_from_slice(self.as_bytes())
182    }
183}
184
185impl IntoHeaderBytes for &[u8] {
186    fn into_header_bytes(self) -> Bytes {
187        Bytes::copy_from_slice(self)
188    }
189}
190
191impl Deref for HeaderList {
192    type Target = [(Bytes, Bytes)];
193    fn deref(&self) -> &Self::Target {
194        &self.entries
195    }
196}
197
198impl DerefMut for HeaderList {
199    fn deref_mut(&mut self) -> &mut Self::Target {
200        &mut self.entries
201    }
202}
203
204impl<'a> IntoIterator for &'a HeaderList {
205    type Item = &'a (Bytes, Bytes);
206    type IntoIter = core::slice::Iter<'a, (Bytes, Bytes)>;
207    fn into_iter(self) -> Self::IntoIter {
208        self.entries.iter()
209    }
210}
211
212impl IntoIterator for HeaderList {
213    type Item = (Bytes, Bytes);
214    type IntoIter = alloc::vec::IntoIter<(Bytes, Bytes)>;
215    fn into_iter(self) -> Self::IntoIter {
216        self.entries.into_iter()
217    }
218}
219
220impl FromIterator<(Bytes, Bytes)> for HeaderList {
221    fn from_iter<I: IntoIterator<Item = (Bytes, Bytes)>>(iter: I) -> Self {
222        Self {
223            entries: iter.into_iter().collect(),
224        }
225    }
226}
227
228impl From<Vec<(Bytes, Bytes)>> for HeaderList {
229    fn from(entries: Vec<(Bytes, Bytes)>) -> Self {
230        Self { entries }
231    }
232}
233
234// serde lens: edge-form `Vec<(String, String)>` for wire compat with
235// jsonl recording schemas already in the wild. invalid UTF-8 in a
236// header value renders as a replacement-char string on serialize —
237// preserves round-trip-ability rather than dropping the entry.
238impl Serialize for HeaderList {
239    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
240        use serde::ser::SerializeSeq;
241        let mut sequence = serializer.serialize_seq(Some(self.entries.len()))?;
242        for (name, value) in &self.entries {
243            let name_str = String::from_utf8_lossy(name);
244            let value_str = String::from_utf8_lossy(value);
245            sequence.serialize_element(&(name_str.as_ref(), value_str.as_ref()))?;
246        }
247        sequence.end()
248    }
249}
250
251impl<'de> Deserialize<'de> for HeaderList {
252    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
253        let pairs: Vec<(String, String)> = Vec::deserialize(deserializer)?;
254        Ok(Self {
255            entries: pairs
256                .into_iter()
257                .map(|(name, value)| (Bytes::from(name), Bytes::from(value)))
258                .collect(),
259        })
260    }
261}
262
263#[cfg(test)]
264#[allow(clippy::unwrap_used, clippy::expect_used)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn insert_replaces_case_insensitive_match_and_returns_prior() {
270        let mut list = HeaderList::new();
271        let prior = list.insert(
272            Bytes::from_static(b"Content-Type"),
273            Bytes::from_static(b"application/json"),
274        );
275        assert!(prior.is_none());
276        let prior = list.insert(
277            Bytes::from_static(b"content-type"),
278            Bytes::from_static(b"text/plain"),
279        );
280        assert_eq!(prior.as_deref(), Some(b"application/json".as_slice()));
281        assert_eq!(
282            list.len(),
283            1,
284            "case-insensitive replace should not duplicate"
285        );
286        assert_eq!(list.get_str("CONTENT-TYPE"), Some("text/plain"));
287    }
288
289    #[test]
290    fn insert_if_absent_skips_existing_case_insensitive_match() {
291        let mut list = HeaderList::new();
292        list.insert(Bytes::from_static(b"X-Trace"), Bytes::from_static(b"first"));
293        list.insert_if_absent("x-trace", "second");
294        assert_eq!(list.get_str("x-trace"), Some("first"));
295    }
296
297    #[test]
298    fn retain_drops_entries_matching_predicate() {
299        let mut list = HeaderList::from_pairs([
300            ("authorization", "bearer"),
301            ("x-token", "abc"),
302            ("x-trace", "trace"),
303        ]);
304        list.retain(|name, _| !name.as_ref().eq_ignore_ascii_case(b"authorization"));
305        assert_eq!(list.len(), 2);
306        assert!(list.get("authorization").is_none());
307    }
308
309    #[test]
310    fn iter_preserves_insertion_order() {
311        let list = HeaderList::from_pairs([("z-last", "1"), ("a-first", "2"), ("m-middle", "3")]);
312        let names: Vec<&str> = list
313            .iter()
314            .map(|(name, _)| core::str::from_utf8(name).expect("ascii"))
315            .collect();
316        assert_eq!(names, vec!["z-last", "a-first", "m-middle"]);
317    }
318
319    #[test]
320    fn serde_round_trip_preserves_string_form() {
321        let list = HeaderList::from_pairs([
322            ("content-type", "application/json"),
323            ("x-trace", "trace-deadbeef"),
324        ]);
325        let json = serde_json::to_string(&list).expect("serialize");
326        let restored: HeaderList = serde_json::from_str(&json).expect("deserialize");
327        assert_eq!(list, restored);
328    }
329
330    #[test]
331    fn into_header_bytes_static_str_does_not_copy_payload() {
332        // sanity: &'static str via `Bytes::from` is the same allocation
333        // as the static slice. owning callers can rely on this for
334        // zero-copy of compile-time-known header names.
335        let bytes = Bytes::from_static(b"traceparent");
336        let other = "traceparent".into_header_bytes();
337        assert_eq!(bytes, other);
338    }
339}