1use std::hash::{Hash, Hasher};
12
13use bytes::Bytes;
14
15use crate::escape;
16
17macro_rules! header_names {
18 ($( $variant:ident => $canonical:literal $(| $compact:literal)? ; )*) => {
19 #[derive(Debug, Clone)]
21 #[non_exhaustive]
22 pub enum HeaderName {
23 $(
24 #[doc = concat!("`", $canonical, "`")]
25 $variant,
26 )*
27 Other(Bytes),
29 }
30
31 impl HeaderName {
32 #[must_use]
34 pub fn canonical(&self) -> &[u8] {
35 match self {
36 $( Self::$variant => $canonical.as_bytes(), )*
37 Self::Other(raw) => raw,
38 }
39 }
40
41 #[must_use]
43 pub fn compact(&self) -> Option<u8> {
44 match self {
45 $( $( Self::$variant => Some($compact), )? )*
46 _ => None,
47 }
48 }
49
50 #[must_use]
55 pub fn parse(raw: &Bytes) -> Self {
56 if raw.len() == 1 {
57 if let Some(&b) = raw.first() {
58 let lower = b.to_ascii_lowercase();
59 $( $( if lower == $compact { return Self::$variant; } )? )*
60 }
61 }
62 $(
63 if escape::eq_ignore_ascii_case(raw, $canonical.as_bytes()) {
64 return Self::$variant;
65 }
66 )*
67 Self::Other(raw.clone())
68 }
69 }
70 };
71}
72
73header_names! {
74 Accept => "Accept";
77 AcceptContact => "Accept-Contact" | b'a'; AcceptEncoding => "Accept-Encoding";
79 AcceptLanguage => "Accept-Language";
80 AlertInfo => "Alert-Info";
81 Allow => "Allow";
82 AllowEvents => "Allow-Events" | b'u'; AuthenticationInfo => "Authentication-Info";
84 Authorization => "Authorization";
85 CallId => "Call-ID" | b'i';
86 CallInfo => "Call-Info";
87 Contact => "Contact" | b'm';
88 ContentDisposition => "Content-Disposition";
89 ContentEncoding => "Content-Encoding" | b'e';
90 ContentLanguage => "Content-Language";
91 ContentLength => "Content-Length" | b'l';
92 ContentType => "Content-Type" | b'c';
93 CSeq => "CSeq";
94 Date => "Date";
95 ErrorInfo => "Error-Info";
96 Event => "Event" | b'o'; Expires => "Expires";
98 FeatureCaps => "Feature-Caps"; From => "From" | b'f';
100 Identity => "Identity" | b'y'; IdentityInfo => "Identity-Info" | b'n'; FlowTimer => "Flow-Timer"; InReplyTo => "In-Reply-To";
104 HistoryInfo => "History-Info"; MaxForwards => "Max-Forwards";
106 MimeVersion => "MIME-Version";
107 MinExpires => "Min-Expires";
108 MinSe => "Min-SE"; Organization => "Organization";
110 Path => "Path"; PAssertedIdentity => "P-Asserted-Identity"; PPreferredIdentity => "P-Preferred-Identity"; Priority => "Priority";
114 Privacy => "Privacy"; ProxyAuthenticate => "Proxy-Authenticate";
116 ProxyAuthorization => "Proxy-Authorization";
117 ProxyRequire => "Proxy-Require";
118 RAck => "RAck"; Reason => "Reason"; RecordRoute => "Record-Route";
121 ReferSub => "Refer-Sub"; ReferTo => "Refer-To" | b'r'; ReferredBy => "Referred-By" | b'b'; RejectContact => "Reject-Contact" | b'j'; Replaces => "Replaces"; ReplyTo => "Reply-To";
127 RequestDisposition => "Request-Disposition" | b'd'; Require => "Require";
129 RetryAfter => "Retry-After";
130 Route => "Route";
131 RSeq => "RSeq"; Server => "Server";
133 ServiceRoute => "Service-Route"; SessionExpires => "Session-Expires" | b'x'; SipETag => "SIP-ETag"; SipIfMatch => "SIP-If-Match"; Subject => "Subject" | b's';
138 SubscriptionState => "Subscription-State"; Supported => "Supported" | b'k';
140 Timestamp => "Timestamp";
141 To => "To" | b't';
142 Unsupported => "Unsupported";
143 UserAgent => "User-Agent";
144 Via => "Via" | b'v';
145 Warning => "Warning";
146 WwwAuthenticate => "WWW-Authenticate";
147}
148
149impl HeaderName {
150 #[must_use]
156 pub fn is_comma_separated_list(&self) -> bool {
157 matches!(
158 self,
159 Self::Accept
160 | Self::AcceptContact
161 | Self::AcceptEncoding
162 | Self::AcceptLanguage
163 | Self::AlertInfo
164 | Self::Allow
165 | Self::AllowEvents
166 | Self::CallInfo
167 | Self::Contact
168 | Self::ContentEncoding
169 | Self::ContentLanguage
170 | Self::ErrorInfo
171 | Self::FeatureCaps
173 | Self::HistoryInfo
174 | Self::InReplyTo
175 | Self::Path
176 | Self::PAssertedIdentity
177 | Self::PPreferredIdentity
178 | Self::Privacy
179 | Self::ProxyRequire
180 | Self::RecordRoute
181 | Self::Reason
182 | Self::RejectContact
183 | Self::Require
184 | Self::Route
185 | Self::ServiceRoute
186 | Self::Supported
187 | Self::Unsupported
188 | Self::Via
189 | Self::Warning
190 )
191 }
192
193 #[must_use]
198 pub fn is_single_value(&self) -> bool {
199 matches!(
200 self,
201 Self::CallId
202 | Self::ContentLength
203 | Self::ContentType
204 | Self::CSeq
205 | Self::Date
206 | Self::Expires
207 | Self::From
208 | Self::MaxForwards
209 | Self::MinExpires
210 | Self::Organization
211 | Self::Server
212 | Self::SipETag
213 | Self::SipIfMatch
214 | Self::Subject
215 | Self::Timestamp
216 | Self::To
217 | Self::UserAgent
218 )
219 }
220}
221
222impl PartialEq for HeaderName {
223 fn eq(&self, other: &Self) -> bool {
224 match (self, other) {
225 (Self::Other(a), Self::Other(b)) => escape::eq_ignore_ascii_case(a, b),
226 (Self::Other(_), _) | (_, Self::Other(_)) => false,
229 _ => std::mem::discriminant(self) == std::mem::discriminant(other),
230 }
231 }
232}
233
234impl Eq for HeaderName {}
235
236impl Hash for HeaderName {
237 fn hash<H: Hasher>(&self, state: &mut H) {
238 for b in self.canonical() {
241 state.write_u8(b.to_ascii_lowercase());
242 }
243 }
244}
245
246impl std::fmt::Display for HeaderName {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 write!(f, "{}", String::from_utf8_lossy(self.canonical()))
249 }
250}
251
252#[cfg(test)]
253#[allow(
254 clippy::unwrap_used,
255 clippy::expect_used,
256 clippy::panic,
257 clippy::indexing_slicing
258)]
259mod tests {
260 use super::*;
261 use std::collections::HashSet;
262
263 fn name(s: &str) -> HeaderName {
264 HeaderName::parse(&Bytes::from(s.to_owned()))
265 }
266
267 #[test]
268 fn names_resolve_case_insensitively() {
269 assert_eq!(name("MaX-fOrWaRdS"), HeaderName::MaxForwards);
271 assert_eq!(name("content-length"), HeaderName::ContentLength);
272 assert_eq!(name("WWW-Authenticate"), HeaderName::WwwAuthenticate);
273 assert_eq!(name("sip-etag"), HeaderName::SipETag);
274 assert_eq!(name("SIP-IF-MATCH"), HeaderName::SipIfMatch);
275 }
276
277 #[test]
278 fn compact_forms_are_the_same_header() {
279 for (compact, long) in [
280 ("i", "Call-ID"),
281 ("m", "Contact"),
282 ("e", "Content-Encoding"),
283 ("l", "Content-Length"),
284 ("c", "Content-Type"),
285 ("f", "From"),
286 ("s", "Subject"),
287 ("k", "Supported"),
288 ("t", "To"),
289 ("v", "Via"),
290 ("r", "Refer-To"),
291 ("o", "Event"),
292 ] {
293 assert_eq!(name(compact), name(long), "{compact} should be {long}");
294 assert_eq!(name(&compact.to_uppercase()), name(long));
295 }
296 }
297
298 #[test]
299 fn compact_form_is_reported_for_headers_that_have_one() {
300 assert_eq!(HeaderName::Via.compact(), Some(b'v'));
301 assert_eq!(HeaderName::CSeq.compact(), None);
302 }
303
304 #[test]
305 fn unknown_names_are_preserved_and_compare_case_insensitively() {
306 let a = name("NewFangledHeader");
307 let b = name("newfangledheader");
308 assert_eq!(a, b);
309 assert_eq!(a.canonical(), b"NewFangledHeader");
310 assert_ne!(a, name("UnknownHeaderWithUnusualValue"));
311 assert_ne!(a, HeaderName::Via);
312 }
313
314 #[test]
317 fn hashing_agrees_with_equality() {
318 let mut set = HashSet::new();
319 set.insert(name("Via"));
320 assert!(set.contains(&name("v")));
321 assert!(set.contains(&name("VIA")));
322
323 set.insert(name("X-Custom"));
324 assert!(set.contains(&name("x-custom")));
325 assert_eq!(set.len(), 2);
326 }
327
328 #[test]
329 fn single_letter_names_that_are_not_compact_forms_stay_unknown() {
330 assert_eq!(name("z"), HeaderName::Other(Bytes::from_static(b"z")));
332 }
333
334 #[test]
335 fn list_and_single_value_headers_are_classified() {
336 assert!(HeaderName::Via.is_comma_separated_list());
337 assert!(HeaderName::Route.is_comma_separated_list());
338 assert!(HeaderName::Path.is_comma_separated_list());
343 assert!(HeaderName::ServiceRoute.is_comma_separated_list());
345 assert!(!HeaderName::WwwAuthenticate.is_comma_separated_list());
348 assert!(HeaderName::SipETag.is_single_value());
349 assert!(HeaderName::SipIfMatch.is_single_value());
350 assert!(!HeaderName::SipETag.is_comma_separated_list());
351 assert!(!HeaderName::Authorization.is_comma_separated_list());
352 assert!(!HeaderName::ProxyAuthenticate.is_comma_separated_list());
353
354 assert!(HeaderName::To.is_single_value());
355 assert!(HeaderName::CSeq.is_single_value());
356 assert!(!HeaderName::Via.is_single_value());
357 }
358}