Skip to main content

proxima_primitives/pipe/
header_name.rs

1use bytes::Bytes;
2
3// HeaderName's own enum + inherent methods are core-tier (only bytes::Bytes,
4// which compiles without alloc); only the IntoHeaderBytes bridge below needs
5// header_list, which is alloc-gated.
6#[cfg(feature = "alloc")]
7use crate::pipe::header_list::IntoHeaderBytes;
8
9/// An HTTP header name. A name we already know is a unit variant that resolves
10/// to a `'static` canonical (lowercase) byte string — zero allocation, integer
11/// compare. An unknown name carries its bytes in [`HeaderName::Custom`].
12///
13/// `HeaderName` implements [`IntoHeaderBytes`], so `headers.insert(HeaderName::
14/// ContentLength, ..)` stores `Bytes::from_static(b"content-length")` with no
15/// copy — the outbound construction win — while `HeaderList` keeps its `Bytes`
16/// storage and case-insensitive `&str` lookups unchanged. HTTP names are
17/// case-insensitive, so `from_bytes` matches any casing and canonicalizes to
18/// the lowercase static form (what `get`'s `eq_ignore_ascii_case` already finds).
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum HeaderName {
21    Host,
22    ContentType,
23    ContentLength,
24    ContentEncoding,
25    TransferEncoding,
26    Connection,
27    Accept,
28    AcceptEncoding,
29    Authorization,
30    UserAgent,
31    CacheControl,
32    Location,
33    Date,
34    Server,
35    Cookie,
36    SetCookie,
37    Traceparent,
38    Tracestate,
39    Baggage,
40    /// A non-standard header name, holding its bytes.
41    Custom(Bytes),
42}
43
44impl HeaderName {
45    /// The known names paired with their canonical lowercase wire bytes. The
46    /// single source of truth for `from_bytes` / `as_bytes`.
47    const KNOWN: &'static [(HeaderName, &'static [u8])] = &[
48        (HeaderName::Host, b"host"),
49        (HeaderName::ContentType, b"content-type"),
50        (HeaderName::ContentLength, b"content-length"),
51        (HeaderName::ContentEncoding, b"content-encoding"),
52        (HeaderName::TransferEncoding, b"transfer-encoding"),
53        (HeaderName::Connection, b"connection"),
54        (HeaderName::Accept, b"accept"),
55        (HeaderName::AcceptEncoding, b"accept-encoding"),
56        (HeaderName::Authorization, b"authorization"),
57        (HeaderName::UserAgent, b"user-agent"),
58        (HeaderName::CacheControl, b"cache-control"),
59        (HeaderName::Location, b"location"),
60        (HeaderName::Date, b"date"),
61        (HeaderName::Server, b"server"),
62        (HeaderName::Cookie, b"cookie"),
63        (HeaderName::SetCookie, b"set-cookie"),
64        (HeaderName::Traceparent, b"traceparent"),
65        (HeaderName::Tracestate, b"tracestate"),
66        (HeaderName::Baggage, b"baggage"),
67    ];
68
69    /// Classify raw bytes case-insensitively. A known name returns a unit
70    /// variant (no allocation); an unknown name copies into `Custom`.
71    #[must_use]
72    pub fn from_bytes(bytes: &[u8]) -> Self {
73        for (name, canonical) in Self::KNOWN {
74            if bytes.eq_ignore_ascii_case(canonical) {
75                return name.clone();
76            }
77        }
78        Self::Custom(Bytes::copy_from_slice(bytes))
79    }
80
81    /// Take ownership of wire `Bytes`: a known name drops the bytes and returns
82    /// a unit variant; an unknown name keeps the bytes **zero-copy** in `Custom`.
83    #[must_use]
84    pub fn from_wire(bytes: Bytes) -> Self {
85        for (name, canonical) in Self::KNOWN {
86            if bytes.as_ref().eq_ignore_ascii_case(canonical) {
87                return name.clone();
88            }
89        }
90        Self::Custom(bytes)
91    }
92
93    /// The canonical lowercase wire bytes. Known names return a `'static`
94    /// slice; `Custom` returns its carried bytes. No allocation.
95    #[must_use]
96    pub fn as_bytes(&self) -> &[u8] {
97        if let Self::Custom(bytes) = self {
98            return bytes.as_ref();
99        }
100        for (name, canonical) in Self::KNOWN {
101            if name == self {
102                return canonical;
103            }
104        }
105        // unreachable: every non-Custom variant is in KNOWN.
106        b""
107    }
108
109    /// The name as `Bytes` for storage/wire. Known names are
110    /// `Bytes::from_static` (zero allocation); `Custom` clones its `Bytes`
111    /// (a refcount bump).
112    #[must_use]
113    pub fn to_bytes(&self) -> Bytes {
114        if let Self::Custom(bytes) = self {
115            return Bytes::clone(bytes);
116        }
117        for (name, canonical) in Self::KNOWN {
118            if name == self {
119                return Bytes::from_static(canonical);
120            }
121        }
122        // unreachable: every non-Custom variant is in KNOWN.
123        Bytes::new()
124    }
125
126    /// The name as `&str` (always valid UTF-8 for known names).
127    #[must_use]
128    pub fn as_str(&self) -> Option<&str> {
129        core::str::from_utf8(self.as_bytes()).ok()
130    }
131}
132
133#[cfg(feature = "alloc")]
134impl IntoHeaderBytes for HeaderName {
135    fn into_header_bytes(self) -> Bytes {
136        self.to_bytes()
137    }
138}
139
140impl From<&[u8]> for HeaderName {
141    fn from(bytes: &[u8]) -> Self {
142        Self::from_bytes(bytes)
143    }
144}
145
146impl From<&str> for HeaderName {
147    fn from(value: &str) -> Self {
148        Self::from_bytes(value.as_bytes())
149    }
150}
151
152impl From<Bytes> for HeaderName {
153    fn from(bytes: Bytes) -> Self {
154        Self::from_wire(bytes)
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn known_names_match_case_insensitively_to_unit_variants() {
164        assert_eq!(
165            HeaderName::from_bytes(b"Content-Length"),
166            HeaderName::ContentLength
167        );
168        assert_eq!(HeaderName::from("CONTENT-TYPE"), HeaderName::ContentType);
169        assert_eq!(HeaderName::from_bytes(b"host"), HeaderName::Host);
170    }
171
172    #[test]
173    fn known_name_to_bytes_is_static_lowercase_no_alloc() {
174        // a known name yields the canonical lowercase static form.
175        assert_eq!(
176            HeaderName::ContentLength.to_bytes(),
177            Bytes::from_static(b"content-length")
178        );
179        assert_eq!(HeaderName::ContentLength.as_bytes(), b"content-length");
180    }
181
182    #[test]
183    fn from_wire_keeps_unknown_name_zero_copy() {
184        let wire = Bytes::from_static(b"x-custom-thing");
185        let name = HeaderName::from_wire(Bytes::clone(&wire));
186        match name {
187            HeaderName::Custom(bytes) => assert_eq!(bytes.as_ptr(), wire.as_ptr()),
188            other => panic!("expected Custom, got {other:?}"),
189        }
190    }
191
192    #[test]
193    fn into_header_bytes_uses_static_for_known() {
194        let bytes = HeaderName::ContentLength.into_header_bytes();
195        assert_eq!(bytes, Bytes::from_static(b"content-length"));
196    }
197}