Skip to main content

sipx_sip/
name.rs

1//! Header field names (RFC 3261 §7.3, §20).
2//!
3//! Names compare case-insensitively, and a compact form is the same header as its long form.
4//! Both facts are load-bearing: RFC 4475 §3.1.1.1 spells `Max-Forwards` as `MaX-fOrWaRdS` and
5//! writes `Content-Length` as `l` and `Subject` as `s` in the same message.
6//!
7//! Resolving a name does not lose the original spelling. The entry that holds a header keeps
8//! the bytes as they arrived so a forwarded message is re-emitted unchanged; this type is
9//! only how the *meaning* of a name is decided.
10
11use 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        /// A header field name.
20        #[derive(Debug, Clone)]
21        #[non_exhaustive]
22        pub enum HeaderName {
23            $(
24                #[doc = concat!("`", $canonical, "`")]
25                $variant,
26            )*
27            /// A header sipx does not model. Compared case-insensitively; its bytes are kept.
28            Other(Bytes),
29        }
30
31        impl HeaderName {
32            /// The canonical spelling, as sipx writes it in messages it constructs.
33            #[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            /// The single-letter compact form, where the header has one.
42            #[must_use]
43            pub fn compact(&self) -> Option<u8> {
44                match self {
45                    $( $( Self::$variant => Some($compact), )? )*
46                    _ => None,
47                }
48            }
49
50            /// Resolve a name as it appeared on the wire.
51            ///
52            /// Never fails: an unrecognized name is [`HeaderName::Other`], because a proxy has
53            /// to forward headers it does not know.
54            #[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    // RFC 3261 §20, in the order the RFC lists them. Compact forms from §20 and, for the
75    // extension headers, from the RFCs that define them.
76    Accept              => "Accept";
77    AcceptContact       => "Accept-Contact" | b'a';       // RFC 3841
78    AcceptEncoding      => "Accept-Encoding";
79    AcceptLanguage      => "Accept-Language";
80    AlertInfo           => "Alert-Info";
81    Allow               => "Allow";
82    AllowEvents         => "Allow-Events" | b'u';         // RFC 6665
83    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';                // RFC 6665
97    Expires             => "Expires";
98    FeatureCaps         => "Feature-Caps";                // RFC 6809
99    From                => "From" | b'f';
100    Identity            => "Identity" | b'y';             // RFC 4474
101    IdentityInfo        => "Identity-Info" | b'n';        // RFC 4474
102    FlowTimer           => "Flow-Timer";                  // RFC 5626
103    InReplyTo           => "In-Reply-To";
104    HistoryInfo         => "History-Info";                // RFC 7044
105    MaxForwards         => "Max-Forwards";
106    MimeVersion         => "MIME-Version";
107    MinExpires          => "Min-Expires";
108    MinSe               => "Min-SE";                      // RFC 4028
109    Organization        => "Organization";
110    Path                => "Path";                        // RFC 3327
111    PAssertedIdentity   => "P-Asserted-Identity";         // RFC 3325
112    PPreferredIdentity  => "P-Preferred-Identity";        // RFC 3325
113    Priority            => "Priority";
114    Privacy             => "Privacy";                     // RFC 3323
115    ProxyAuthenticate   => "Proxy-Authenticate";
116    ProxyAuthorization  => "Proxy-Authorization";
117    ProxyRequire        => "Proxy-Require";
118    RAck                => "RAck";                        // RFC 3262
119    Reason              => "Reason";                      // RFC 3326
120    RecordRoute         => "Record-Route";
121    ReferSub            => "Refer-Sub";                   // RFC 4488
122    ReferTo             => "Refer-To" | b'r';             // RFC 3515
123    ReferredBy          => "Referred-By" | b'b';          // RFC 3892
124    RejectContact       => "Reject-Contact" | b'j';       // RFC 3841
125    Replaces            => "Replaces";                    // RFC 3891
126    ReplyTo             => "Reply-To";
127    RequestDisposition  => "Request-Disposition" | b'd';  // RFC 3841
128    Require             => "Require";
129    RetryAfter          => "Retry-After";
130    Route               => "Route";
131    RSeq                => "RSeq";                        // RFC 3262
132    Server              => "Server";
133    ServiceRoute        => "Service-Route";               // RFC 3608
134    SessionExpires      => "Session-Expires" | b'x';      // RFC 4028
135    SipETag             => "SIP-ETag";                    // RFC 3903
136    SipIfMatch          => "SIP-If-Match";                // RFC 3903
137    Subject             => "Subject" | b's';
138    SubscriptionState   => "Subscription-State";          // RFC 6665
139    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    /// Whether this header's grammar is a comma-separated list, so that repeated header lines
151    /// and one line of comma-separated values mean the same thing (RFC 3261 §7.3.1).
152    ///
153    /// The authentication headers are the exception the RFC calls out by name: their values
154    /// contain commas of their own, so splitting on commas would corrupt them.
155    #[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                // RFC 6809 §4: `Feature-Caps = "Feature-Caps" HCOLON fc-value *(COMMA fc-value)`.
172                | 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    /// Whether the RFC permits at most one of this header in a message.
194    ///
195    /// RFC 4475 §3.3.8 turns on this: a message with two `To` headers parses, and must be
196    /// rejected by validation rather than by silently using the first.
197    #[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            // A known name never equals an `Other`: `parse` resolves every known spelling,
227            // including compact forms, so an `Other` cannot hold one.
228            (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        // Hash the lowercased canonical form so that hashing agrees with case-insensitive
239        // equality. Getting this wrong would make a `HashMap<HeaderName, _>` lose entries.
240        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        // RFC 4475 3.1.1.1 spells it exactly this way.
270        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    /// A `HashMap` keyed by header name would silently lose entries if hashing disagreed with
315    /// equality — the classic way case-insensitive keys go wrong.
316    #[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        // 'z' is not a registered compact form.
331        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        // RFC 3327 §4 gives `Path` the same `route-param *(COMMA route-param)` grammar the
339        // other route headers have. Nothing in the crate branches on this predicate today —
340        // the address-list decoder splits on commas itself — but it is public API, and a
341        // caller asking whether it may join two `Path` rows deserves the right answer.
342        assert!(HeaderName::Path.is_comma_separated_list());
343        // RFC 3608 §5: `Service-Route = "Service-Route" HCOLON sr-value *( COMMA sr-value )`.
344        assert!(HeaderName::ServiceRoute.is_comma_separated_list());
345        // The RFC names the authentication headers as the exception: their values contain
346        // commas of their own.
347        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}