1use crate::sip_header_addr::{ParseSipHeaderAddrError, SipHeaderAddr};
8use crate::variables::{HistoryInfo, HistoryInfoError, SipCallInfo, SipCallInfoError};
9
10#[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 pub enum SipHeader {
30 CallInfo => "Call-Info",
32 HistoryInfo => "History-Info",
34 PAssertedIdentity => "P-Asserted-Identity",
36 }
37}
38
39impl SipHeader {
40 pub fn extract_from(&self, message: &str) -> Option<String> {
45 crate::sip_message::extract_header(message, self.as_str())
46 }
47}
48
49pub trait SipHeaderLookup {
75 fn sip_header_str(&self, name: &str) -> Option<&str>;
77
78 fn sip_header(&self, name: SipHeader) -> Option<&str> {
80 self.sip_header_str(name.as_str())
81 }
82
83 fn call_info_raw(&self) -> Option<&str> {
85 self.sip_header(SipHeader::CallInfo)
86 }
87
88 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 fn history_info_raw(&self) -> Option<&str> {
100 self.sip_header(SipHeader::HistoryInfo)
101 }
102
103 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 fn p_asserted_identity_raw(&self) -> Option<&str> {
115 self.sip_header(SipHeader::PAssertedIdentity)
116 }
117
118 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}