Skip to main content

freeswitch_types/
sip_header.rs

1//! Standard SIP header names and typed lookup trait (RFC 3261 and extensions).
2//!
3//! Protocol-agnostic catalog of SIP header names with canonical wire casing,
4//! plus a [`SipHeaderLookup`] trait providing typed convenience accessors for
5//! any key-value store that can look up headers by name.
6
7use crate::sip_header_addr::{ParseSipHeaderAddrError, SipHeaderAddr};
8use crate::variables::{SipCallInfo, SipCallInfoError};
9
10/// Error returned when parsing an unrecognized SIP header name.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ParseSipHeaderError(pub String);
13
14impl std::fmt::Display for ParseSipHeaderError {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        write!(f, "unknown SIP header: {}", self.0)
17    }
18}
19
20impl std::error::Error for ParseSipHeaderError {}
21
22define_header_enum! {
23    error_type: ParseSipHeaderError,
24    /// Standard SIP header names with canonical wire casing.
25    ///
26    /// Each variant maps to the header's canonical form as defined in the
27    /// relevant RFC. `FromStr` is case-insensitive; `Display` always emits
28    /// the canonical form.
29    pub enum SipHeader {
30        /// `Call-Info` (RFC 3261 section 20.9).
31        CallInfo => "Call-Info",
32        /// `P-Asserted-Identity` (RFC 3325).
33        PAssertedIdentity => "P-Asserted-Identity",
34    }
35}
36
37impl SipHeader {
38    /// Extract this header's value from a raw SIP message.
39    ///
40    /// Delegates to [`extract_header`](crate::sip_message::extract_header)
41    /// using the canonical wire name from [`as_str()`](Self::as_str).
42    pub fn extract_from(&self, message: &str) -> Option<String> {
43        crate::sip_message::extract_header(message, self.as_str())
44    }
45}
46
47/// Trait for looking up standard SIP headers from any key-value store.
48///
49/// Implementors provide `sip_header_str()` and get all typed accessors as
50/// default implementations. A blanket implementation bridges
51/// [`HeaderLookup`](crate::HeaderLookup) implementors automatically.
52///
53/// # Example
54///
55/// ```
56/// use std::collections::HashMap;
57/// use freeswitch_types::{SipHeaderLookup, SipHeader};
58///
59/// let mut headers = HashMap::new();
60/// headers.insert(
61///     "Call-Info".to_string(),
62///     "<urn:emergency:uid:callid:abc>;purpose=emergency-CallId".to_string(),
63/// );
64///
65/// assert_eq!(
66///     headers.sip_header(SipHeader::CallInfo),
67///     Some("<urn:emergency:uid:callid:abc>;purpose=emergency-CallId"),
68/// );
69/// let ci = headers.call_info().unwrap().unwrap();
70/// assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
71/// ```
72pub trait SipHeaderLookup {
73    /// Look up a SIP header by its raw wire name (e.g. `"Call-Info"`).
74    fn sip_header_str(&self, name: &str) -> Option<&str>;
75
76    /// Look up a SIP header by its [`SipHeader`] enum variant.
77    fn sip_header(&self, name: SipHeader) -> Option<&str> {
78        self.sip_header_str(name.as_str())
79    }
80
81    /// Raw `Call-Info` header value (RFC 3261 section 20.9).
82    fn call_info_raw(&self) -> Option<&str> {
83        self.sip_header(SipHeader::CallInfo)
84    }
85
86    /// Parse the `Call-Info` header into a [`SipCallInfo`].
87    ///
88    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
89    fn call_info(&self) -> Result<Option<SipCallInfo>, SipCallInfoError> {
90        match self.call_info_raw() {
91            Some(s) => SipCallInfo::parse(s).map(Some),
92            None => Ok(None),
93        }
94    }
95
96    /// Raw `P-Asserted-Identity` header value (RFC 3325).
97    fn p_asserted_identity_raw(&self) -> Option<&str> {
98        self.sip_header(SipHeader::PAssertedIdentity)
99    }
100
101    /// Parse the `P-Asserted-Identity` header into a [`SipHeaderAddr`].
102    ///
103    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
104    fn p_asserted_identity(&self) -> Result<Option<SipHeaderAddr>, ParseSipHeaderAddrError> {
105        match self.p_asserted_identity_raw() {
106            Some(s) => s
107                .parse::<SipHeaderAddr>()
108                .map(Some),
109            None => Ok(None),
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use std::collections::HashMap;
118
119    #[test]
120    fn display_round_trip() {
121        assert_eq!(SipHeader::CallInfo.to_string(), "Call-Info");
122        assert_eq!(
123            SipHeader::PAssertedIdentity.to_string(),
124            "P-Asserted-Identity"
125        );
126    }
127
128    #[test]
129    fn as_ref_str() {
130        let h: &str = SipHeader::CallInfo.as_ref();
131        assert_eq!(h, "Call-Info");
132    }
133
134    #[test]
135    fn from_str_case_insensitive() {
136        assert_eq!("call-info".parse::<SipHeader>(), Ok(SipHeader::CallInfo));
137        assert_eq!("CALL-INFO".parse::<SipHeader>(), Ok(SipHeader::CallInfo));
138        assert_eq!(
139            "p-asserted-identity".parse::<SipHeader>(),
140            Ok(SipHeader::PAssertedIdentity)
141        );
142        assert_eq!(
143            "P-ASSERTED-IDENTITY".parse::<SipHeader>(),
144            Ok(SipHeader::PAssertedIdentity)
145        );
146    }
147
148    #[test]
149    fn from_str_unknown() {
150        assert!("X-Custom"
151            .parse::<SipHeader>()
152            .is_err());
153    }
154
155    #[test]
156    fn from_str_round_trip_all() {
157        let variants = [SipHeader::CallInfo, SipHeader::PAssertedIdentity];
158        for v in variants {
159            let wire = v.to_string();
160            let parsed: SipHeader = wire
161                .parse()
162                .unwrap();
163            assert_eq!(parsed, v, "round-trip failed for {wire}");
164        }
165    }
166
167    fn headers_with(pairs: &[(&str, &str)]) -> HashMap<String, String> {
168        pairs
169            .iter()
170            .map(|(k, v)| (k.to_string(), v.to_string()))
171            .collect()
172    }
173
174    #[test]
175    fn sip_header_by_enum() {
176        let h = headers_with(&[("Call-Info", "<urn:x>;purpose=icon")]);
177        assert_eq!(
178            h.sip_header(SipHeader::CallInfo),
179            Some("<urn:x>;purpose=icon")
180        );
181    }
182
183    #[test]
184    fn call_info_raw_lookup() {
185        let h = headers_with(&[(
186            "Call-Info",
187            "<urn:emergency:uid:callid:test:bcf.example.com>;purpose=emergency-CallId",
188        )]);
189        assert_eq!(
190            h.call_info_raw(),
191            Some("<urn:emergency:uid:callid:test:bcf.example.com>;purpose=emergency-CallId")
192        );
193    }
194
195    #[test]
196    fn call_info_typed() {
197        let h = headers_with(&[(
198            "Call-Info",
199            "<urn:emergency:uid:callid:test:bcf.example.com>;purpose=emergency-CallId",
200        )]);
201        let ci = h
202            .call_info()
203            .unwrap()
204            .unwrap();
205        assert_eq!(ci.len(), 1);
206        assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
207    }
208
209    #[test]
210    fn call_info_absent() {
211        let h = headers_with(&[]);
212        assert_eq!(
213            h.call_info()
214                .unwrap(),
215            None
216        );
217    }
218
219    #[test]
220    fn p_asserted_identity_raw_lookup() {
221        let h = headers_with(&[(
222            "P-Asserted-Identity",
223            r#""EXAMPLE CO" <sip:+15551234567@198.51.100.1>"#,
224        )]);
225        assert_eq!(
226            h.p_asserted_identity_raw(),
227            Some(r#""EXAMPLE CO" <sip:+15551234567@198.51.100.1>"#)
228        );
229    }
230
231    #[test]
232    fn p_asserted_identity_typed() {
233        let h = headers_with(&[(
234            "P-Asserted-Identity",
235            r#""EXAMPLE CO" <sip:+15551234567@198.51.100.1>"#,
236        )]);
237        let pai = h
238            .p_asserted_identity()
239            .unwrap()
240            .unwrap();
241        assert_eq!(pai.display_name(), Some("EXAMPLE CO"));
242    }
243
244    #[test]
245    fn p_asserted_identity_absent() {
246        let h = headers_with(&[]);
247        assert_eq!(
248            h.p_asserted_identity()
249                .unwrap(),
250            None
251        );
252    }
253
254    #[test]
255    fn extract_from_sip_message() {
256        let msg = concat!(
257            "INVITE sip:bob@host SIP/2.0\r\n",
258            "Call-Info: <urn:emergency:uid:callid:abc>;purpose=emergency-CallId\r\n",
259            "P-Asserted-Identity: \"Corp\" <sip:+15551234567@198.51.100.1>\r\n",
260            "\r\n",
261        );
262        assert_eq!(
263            SipHeader::CallInfo.extract_from(msg),
264            Some("<urn:emergency:uid:callid:abc>;purpose=emergency-CallId".into())
265        );
266        assert_eq!(
267            SipHeader::PAssertedIdentity.extract_from(msg),
268            Some("\"Corp\" <sip:+15551234567@198.51.100.1>".into())
269        );
270    }
271
272    #[test]
273    fn extract_from_missing() {
274        let msg = concat!(
275            "INVITE sip:bob@host SIP/2.0\r\n",
276            "From: Alice <sip:alice@host>\r\n",
277            "\r\n",
278        );
279        assert_eq!(SipHeader::CallInfo.extract_from(msg), None);
280        assert_eq!(SipHeader::PAssertedIdentity.extract_from(msg), None);
281    }
282
283    #[test]
284    fn missing_headers_return_none() {
285        let h = headers_with(&[]);
286        assert_eq!(h.call_info_raw(), None);
287        assert_eq!(
288            h.call_info()
289                .unwrap(),
290            None
291        );
292        assert_eq!(h.p_asserted_identity_raw(), None);
293        assert_eq!(
294            h.p_asserted_identity()
295                .unwrap(),
296            None
297        );
298    }
299}