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::{HistoryInfo, HistoryInfoError, 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        /// `History-Info` (RFC 7044).
33        HistoryInfo => "History-Info",
34        /// `P-Asserted-Identity` (RFC 3325).
35        PAssertedIdentity => "P-Asserted-Identity",
36    }
37}
38
39impl SipHeader {
40    /// Extract this header's value from a raw SIP message.
41    ///
42    /// Delegates to [`extract_header`](crate::sip_message::extract_header)
43    /// using the canonical wire name from [`as_str()`](Self::as_str).
44    pub fn extract_from(&self, message: &str) -> Option<String> {
45        crate::sip_message::extract_header(message, self.as_str())
46    }
47}
48
49/// Trait for looking up standard SIP headers from any key-value store.
50///
51/// Implementors provide `sip_header_str()` and get all typed accessors as
52/// default implementations. A blanket implementation bridges
53/// [`HeaderLookup`](crate::HeaderLookup) implementors automatically.
54///
55/// # Example
56///
57/// ```
58/// use std::collections::HashMap;
59/// use freeswitch_types::{SipHeaderLookup, SipHeader};
60///
61/// let mut headers = HashMap::new();
62/// headers.insert(
63///     "Call-Info".to_string(),
64///     "<urn:emergency:uid:callid:abc>;purpose=emergency-CallId".to_string(),
65/// );
66///
67/// assert_eq!(
68///     headers.sip_header(SipHeader::CallInfo),
69///     Some("<urn:emergency:uid:callid:abc>;purpose=emergency-CallId"),
70/// );
71/// let ci = headers.call_info().unwrap().unwrap();
72/// assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
73/// ```
74pub trait SipHeaderLookup {
75    /// Look up a SIP header by its raw wire name (e.g. `"Call-Info"`).
76    fn sip_header_str(&self, name: &str) -> Option<&str>;
77
78    /// Look up a SIP header by its [`SipHeader`] enum variant.
79    fn sip_header(&self, name: SipHeader) -> Option<&str> {
80        self.sip_header_str(name.as_str())
81    }
82
83    /// Raw `Call-Info` header value (RFC 3261 section 20.9).
84    fn call_info_raw(&self) -> Option<&str> {
85        self.sip_header(SipHeader::CallInfo)
86    }
87
88    /// Parse the `Call-Info` header into a [`SipCallInfo`].
89    ///
90    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
91    fn call_info(&self) -> Result<Option<SipCallInfo>, SipCallInfoError> {
92        match self.call_info_raw() {
93            Some(s) => SipCallInfo::parse(s).map(Some),
94            None => Ok(None),
95        }
96    }
97
98    /// Raw `History-Info` header value (RFC 7044).
99    fn history_info_raw(&self) -> Option<&str> {
100        self.sip_header(SipHeader::HistoryInfo)
101    }
102
103    /// Parse the `History-Info` header into a [`HistoryInfo`].
104    ///
105    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
106    fn history_info(&self) -> Result<Option<HistoryInfo>, HistoryInfoError> {
107        match self.history_info_raw() {
108            Some(s) => HistoryInfo::parse(s).map(Some),
109            None => Ok(None),
110        }
111    }
112
113    /// Raw `P-Asserted-Identity` header value (RFC 3325).
114    fn p_asserted_identity_raw(&self) -> Option<&str> {
115        self.sip_header(SipHeader::PAssertedIdentity)
116    }
117
118    /// Parse the `P-Asserted-Identity` header into a [`SipHeaderAddr`].
119    ///
120    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
121    fn p_asserted_identity(&self) -> Result<Option<SipHeaderAddr>, ParseSipHeaderAddrError> {
122        match self.p_asserted_identity_raw() {
123            Some(s) => s
124                .parse::<SipHeaderAddr>()
125                .map(Some),
126            None => Ok(None),
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use std::collections::HashMap;
135
136    #[test]
137    fn display_round_trip() {
138        assert_eq!(SipHeader::CallInfo.to_string(), "Call-Info");
139        assert_eq!(SipHeader::HistoryInfo.to_string(), "History-Info");
140        assert_eq!(
141            SipHeader::PAssertedIdentity.to_string(),
142            "P-Asserted-Identity"
143        );
144    }
145
146    #[test]
147    fn as_ref_str() {
148        let h: &str = SipHeader::CallInfo.as_ref();
149        assert_eq!(h, "Call-Info");
150    }
151
152    #[test]
153    fn from_str_case_insensitive() {
154        assert_eq!("call-info".parse::<SipHeader>(), Ok(SipHeader::CallInfo));
155        assert_eq!("CALL-INFO".parse::<SipHeader>(), Ok(SipHeader::CallInfo));
156        assert_eq!(
157            "history-info".parse::<SipHeader>(),
158            Ok(SipHeader::HistoryInfo)
159        );
160        assert_eq!(
161            "p-asserted-identity".parse::<SipHeader>(),
162            Ok(SipHeader::PAssertedIdentity)
163        );
164        assert_eq!(
165            "P-ASSERTED-IDENTITY".parse::<SipHeader>(),
166            Ok(SipHeader::PAssertedIdentity)
167        );
168    }
169
170    #[test]
171    fn from_str_unknown() {
172        assert!("X-Custom"
173            .parse::<SipHeader>()
174            .is_err());
175    }
176
177    #[test]
178    fn from_str_round_trip_all() {
179        let variants = [
180            SipHeader::CallInfo,
181            SipHeader::HistoryInfo,
182            SipHeader::PAssertedIdentity,
183        ];
184        for v in variants {
185            let wire = v.to_string();
186            let parsed: SipHeader = wire
187                .parse()
188                .unwrap();
189            assert_eq!(parsed, v, "round-trip failed for {wire}");
190        }
191    }
192
193    fn headers_with(pairs: &[(&str, &str)]) -> HashMap<String, String> {
194        pairs
195            .iter()
196            .map(|(k, v)| (k.to_string(), v.to_string()))
197            .collect()
198    }
199
200    #[test]
201    fn sip_header_by_enum() {
202        let h = headers_with(&[("Call-Info", "<urn:x>;purpose=icon")]);
203        assert_eq!(
204            h.sip_header(SipHeader::CallInfo),
205            Some("<urn:x>;purpose=icon")
206        );
207    }
208
209    #[test]
210    fn call_info_raw_lookup() {
211        let h = headers_with(&[(
212            "Call-Info",
213            "<urn:emergency:uid:callid:test:bcf.example.com>;purpose=emergency-CallId",
214        )]);
215        assert_eq!(
216            h.call_info_raw(),
217            Some("<urn:emergency:uid:callid:test:bcf.example.com>;purpose=emergency-CallId")
218        );
219    }
220
221    #[test]
222    fn call_info_typed() {
223        let h = headers_with(&[(
224            "Call-Info",
225            "<urn:emergency:uid:callid:test:bcf.example.com>;purpose=emergency-CallId",
226        )]);
227        let ci = h
228            .call_info()
229            .unwrap()
230            .unwrap();
231        assert_eq!(ci.len(), 1);
232        assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
233    }
234
235    #[test]
236    fn call_info_absent() {
237        let h = headers_with(&[]);
238        assert_eq!(
239            h.call_info()
240                .unwrap(),
241            None
242        );
243    }
244
245    #[test]
246    fn p_asserted_identity_raw_lookup() {
247        let h = headers_with(&[(
248            "P-Asserted-Identity",
249            r#""EXAMPLE CO" <sip:+15551234567@198.51.100.1>"#,
250        )]);
251        assert_eq!(
252            h.p_asserted_identity_raw(),
253            Some(r#""EXAMPLE CO" <sip:+15551234567@198.51.100.1>"#)
254        );
255    }
256
257    #[test]
258    fn p_asserted_identity_typed() {
259        let h = headers_with(&[(
260            "P-Asserted-Identity",
261            r#""EXAMPLE CO" <sip:+15551234567@198.51.100.1>"#,
262        )]);
263        let pai = h
264            .p_asserted_identity()
265            .unwrap()
266            .unwrap();
267        assert_eq!(pai.display_name(), Some("EXAMPLE CO"));
268    }
269
270    #[test]
271    fn p_asserted_identity_absent() {
272        let h = headers_with(&[]);
273        assert_eq!(
274            h.p_asserted_identity()
275                .unwrap(),
276            None
277        );
278    }
279
280    #[test]
281    fn history_info_raw_lookup() {
282        let h = headers_with(&[(
283            "History-Info",
284            "<sip:alice@esrp.example.com>;index=1,<sip:sos@psap.example.com>;index=1.1",
285        )]);
286        assert!(h
287            .history_info_raw()
288            .unwrap()
289            .contains("esrp.example.com"));
290    }
291
292    #[test]
293    fn history_info_typed() {
294        let h = headers_with(&[(
295            "History-Info",
296            "<sip:alice@esrp.example.com>;index=1,<sip:sos@psap.example.com>;index=1.1",
297        )]);
298        let hi = h
299            .history_info()
300            .unwrap()
301            .unwrap();
302        assert_eq!(hi.len(), 2);
303        assert_eq!(hi.entries()[0].index(), Some("1"));
304        assert_eq!(hi.entries()[1].index(), Some("1.1"));
305    }
306
307    #[test]
308    fn history_info_absent() {
309        let h = headers_with(&[]);
310        assert_eq!(
311            h.history_info()
312                .unwrap(),
313            None
314        );
315    }
316
317    #[test]
318    fn extract_from_sip_message() {
319        let msg = concat!(
320            "INVITE sip:bob@host SIP/2.0\r\n",
321            "Call-Info: <urn:emergency:uid:callid:abc>;purpose=emergency-CallId\r\n",
322            "History-Info: <sip:esrp@example.com>;index=1\r\n",
323            "P-Asserted-Identity: \"Corp\" <sip:+15551234567@198.51.100.1>\r\n",
324            "\r\n",
325        );
326        assert_eq!(
327            SipHeader::CallInfo.extract_from(msg),
328            Some("<urn:emergency:uid:callid:abc>;purpose=emergency-CallId".into())
329        );
330        assert_eq!(
331            SipHeader::HistoryInfo.extract_from(msg),
332            Some("<sip:esrp@example.com>;index=1".into())
333        );
334        assert_eq!(
335            SipHeader::PAssertedIdentity.extract_from(msg),
336            Some("\"Corp\" <sip:+15551234567@198.51.100.1>".into())
337        );
338    }
339
340    #[test]
341    fn extract_from_missing() {
342        let msg = concat!(
343            "INVITE sip:bob@host SIP/2.0\r\n",
344            "From: Alice <sip:alice@host>\r\n",
345            "\r\n",
346        );
347        assert_eq!(SipHeader::CallInfo.extract_from(msg), None);
348        assert_eq!(SipHeader::PAssertedIdentity.extract_from(msg), None);
349    }
350
351    #[test]
352    fn missing_headers_return_none() {
353        let h = headers_with(&[]);
354        assert_eq!(h.call_info_raw(), None);
355        assert_eq!(
356            h.call_info()
357                .unwrap(),
358            None
359        );
360        assert_eq!(h.history_info_raw(), None);
361        assert_eq!(
362            h.history_info()
363                .unwrap(),
364            None
365        );
366        assert_eq!(h.p_asserted_identity_raw(), None);
367        assert_eq!(
368            h.p_asserted_identity()
369                .unwrap(),
370            None
371        );
372    }
373}