1use std::fmt;
4use std::str::Utf8Error;
5
6use percent_encoding::percent_decode_str;
7
8use crate::header_addr::{ParseSipHeaderAddrError, SipHeaderAddr};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum HistoryInfoError {
14 Empty,
16 InvalidEntry(ParseSipHeaderAddrError),
18 Malformed(String),
21}
22
23impl fmt::Display for HistoryInfoError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::Empty => write!(f, "empty History-Info header"),
27 Self::InvalidEntry(e) => write!(f, "invalid History-Info entry: {e}"),
28 Self::Malformed(reason) => write!(f, "malformed History-Info value: {reason}"),
29 }
30 }
31}
32
33impl std::error::Error for HistoryInfoError {
34 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
35 match self {
36 Self::InvalidEntry(e) => Some(e),
37 _ => None,
38 }
39 }
40}
41
42impl From<ParseSipHeaderAddrError> for HistoryInfoError {
43 fn from(e: ParseSipHeaderAddrError) -> Self {
44 Self::InvalidEntry(e)
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct HistoryInfoReason {
54 protocol: String,
55 cause: Option<u16>,
56 text: Option<String>,
57}
58
59impl HistoryInfoReason {
60 pub fn protocol(&self) -> &str {
62 &self.protocol
63 }
64
65 pub fn cause(&self) -> Option<u16> {
68 self.cause
69 }
70
71 pub fn text(&self) -> Option<&str> {
74 self.text
75 .as_deref()
76 }
77}
78
79fn parse_reason(decoded: &str) -> HistoryInfoReason {
81 let (protocol, rest) = decoded
82 .split_once(';')
83 .unwrap_or((decoded, ""));
84
85 let params = crate::parse_params(rest);
86 let find = |name: &str| {
87 params
88 .iter()
89 .find(|p| {
90 p.key
91 .eq_ignore_ascii_case(name)
92 })
93 };
94 let cause = find("cause")
95 .and_then(|p| p.value)
96 .and_then(|v| {
97 v.parse::<u16>()
98 .ok()
99 });
100 let text = find("text")
101 .and_then(|p| p.unquoted())
102 .map(|(text, _)| text);
103
104 HistoryInfoReason {
105 protocol: protocol
106 .trim()
107 .to_string(),
108 cause,
109 text,
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct HistoryInfoEntry {
119 addr: SipHeaderAddr,
120}
121
122impl HistoryInfoEntry {
123 pub fn addr(&self) -> &SipHeaderAddr {
125 &self.addr
126 }
127
128 pub fn uri(&self) -> &sip_uri::Uri {
130 self.addr
131 .uri()
132 }
133
134 pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
136 self.addr
137 .sip_uri()
138 }
139
140 pub fn index(&self) -> Option<&str> {
142 self.addr
143 .param_raw("index")
144 .flatten()
145 }
146
147 pub fn reason_raw(&self) -> Option<&str> {
151 self.addr
152 .sip_uri()?
153 .header("Reason")
154 }
155
156 pub fn reason(&self) -> Option<Result<HistoryInfoReason, Utf8Error>> {
165 let raw = self.reason_raw()?;
166 Some(
167 percent_decode_str(raw)
168 .decode_utf8()
169 .map(|decoded| parse_reason(&decoded)),
170 )
171 }
172}
173
174impl fmt::Display for HistoryInfoEntry {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 write!(f, "{}", self.addr)
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct HistoryInfo(Vec<HistoryInfoEntry>);
196
197impl HistoryInfo {
198 pub fn parse(raw: &str) -> Result<Self, HistoryInfoError> {
200 let raw = raw.trim();
201 if raw.is_empty() {
202 return Err(HistoryInfoError::Empty);
203 }
204 Self::from_entries(crate::split_comma_entries(raw))
205 }
206
207 pub fn from_entries<'a>(
212 entries: impl IntoIterator<Item = &'a str>,
213 ) -> Result<Self, HistoryInfoError> {
214 let entries: Vec<_> = entries
215 .into_iter()
216 .map(parse_entry)
217 .collect::<Result<_, _>>()?;
218 if entries.is_empty() {
219 return Err(HistoryInfoError::Empty);
220 }
221 Ok(Self(entries))
222 }
223
224 pub fn entries(&self) -> &[HistoryInfoEntry] {
226 &self.0
227 }
228
229 pub fn into_entries(self) -> Vec<HistoryInfoEntry> {
231 self.0
232 }
233
234 pub fn len(&self) -> usize {
236 self.0
237 .len()
238 }
239
240 pub fn is_empty(&self) -> bool {
242 self.0
243 .is_empty()
244 }
245}
246
247fn parse_entry(raw: &str) -> Result<HistoryInfoEntry, HistoryInfoError> {
248 let addr: SipHeaderAddr = raw
249 .trim()
250 .parse()?;
251 Ok(HistoryInfoEntry { addr })
252}
253
254impl fmt::Display for HistoryInfo {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 crate::fmt_joined(f, &self.0, ",")
257 }
258}
259
260impl<'a> IntoIterator for &'a HistoryInfo {
261 type Item = &'a HistoryInfoEntry;
262 type IntoIter = std::slice::Iter<'a, HistoryInfoEntry>;
263
264 fn into_iter(self) -> Self::IntoIter {
265 self.0
266 .iter()
267 }
268}
269
270impl IntoIterator for HistoryInfo {
271 type Item = HistoryInfoEntry;
272 type IntoIter = std::vec::IntoIter<HistoryInfoEntry>;
273
274 fn into_iter(self) -> Self::IntoIter {
275 self.0
276 .into_iter()
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 const EXAMPLE_1: &str = "\
285<sip:user1@esrp.example.com?Reason=RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22>;index=1,\
286<sip:sos@psap.example.com>;index=2";
287
288 const EXAMPLE_2: &str = "\
289<sip:lsrg.example.com?Reason=SIP%3Bcause%3D200%3Btext%3D%22Legacy+routing%22>;index=1,\
290<sip:user1@esrp2.example.com;lr;transport=udp?Reason=RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22>;index=1.1,\
291<sip:sos@psap.example.com>;index=1.2";
292
293 #[test]
296 fn parse_two_entries() {
297 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
298 assert_eq!(hi.len(), 2);
299 }
300
301 #[test]
302 fn parse_three_entries() {
303 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
304 assert_eq!(hi.len(), 3);
305 }
306
307 #[test]
308 fn parse_single_entry() {
309 let hi = HistoryInfo::parse("<sip:alice@example.com>;index=1").unwrap();
310 assert_eq!(hi.len(), 1);
311 }
312
313 #[test]
314 fn empty_input() {
315 assert!(matches!(
316 HistoryInfo::parse(""),
317 Err(HistoryInfoError::Empty)
318 ));
319 }
320
321 #[test]
324 fn index_simple() {
325 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
326 assert_eq!(hi.entries()[0].index(), Some("1"));
327 assert_eq!(hi.entries()[1].index(), Some("2"));
328 }
329
330 #[test]
331 fn index_hierarchical() {
332 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
333 assert_eq!(hi.entries()[0].index(), Some("1"));
334 assert_eq!(hi.entries()[1].index(), Some("1.1"));
335 assert_eq!(hi.entries()[2].index(), Some("1.2"));
336 }
337
338 #[test]
339 fn index_absent() {
340 let hi = HistoryInfo::parse("<sip:alice@example.com>").unwrap();
341 assert_eq!(hi.entries()[0].index(), None);
342 }
343
344 #[test]
347 fn uri_with_user() {
348 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
349 let sip = hi.entries()[0]
350 .sip_uri()
351 .unwrap();
352 assert_eq!(sip.user(), Some("user1"));
353 assert_eq!(
354 sip.host()
355 .to_string(),
356 "esrp.example.com"
357 );
358 }
359
360 #[test]
361 fn uri_without_user() {
362 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
363 let sip = hi.entries()[0]
364 .sip_uri()
365 .unwrap();
366 assert_eq!(sip.user(), None);
367 assert_eq!(
368 sip.host()
369 .to_string(),
370 "lsrg.example.com"
371 );
372 }
373
374 #[test]
375 fn uri_with_params() {
376 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
377 let sip = hi.entries()[1]
378 .sip_uri()
379 .unwrap();
380 assert_eq!(sip.user(), Some("user1"));
381 assert_eq!(
382 sip.host()
383 .to_string(),
384 "esrp2.example.com"
385 );
386 assert!(sip
387 .param("lr")
388 .is_some());
389 assert_eq!(sip.param("transport"), Some(&Some("udp".to_string())));
390 }
391
392 #[test]
395 fn reason_raw_present() {
396 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
397 assert_eq!(
398 hi.entries()[0].reason_raw(),
399 Some("RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22")
400 );
401 }
402
403 #[test]
404 fn reason_raw_absent() {
405 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
406 assert_eq!(hi.entries()[1].reason_raw(), None);
407 }
408
409 #[test]
410 fn reason_parsed_route_action() {
411 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
412 let reason = hi.entries()[0]
413 .reason()
414 .unwrap()
415 .unwrap();
416 assert_eq!(reason.protocol(), "RouteAction");
417 assert_eq!(reason.cause(), Some(200));
418 assert_eq!(reason.text(), Some("Normal+Next+Hop"));
419 }
420
421 #[test]
422 fn reason_parsed_sip() {
423 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
424 let reason = hi.entries()[0]
425 .reason()
426 .unwrap()
427 .unwrap();
428 assert_eq!(reason.protocol(), "SIP");
429 assert_eq!(reason.cause(), Some(200));
430 assert_eq!(reason.text(), Some("Legacy+routing"));
431 }
432
433 #[test]
434 fn reason_absent_returns_none() {
435 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
436 assert!(hi.entries()[2]
437 .reason()
438 .is_none());
439 }
440
441 #[test]
442 fn reason_multiple_entries() {
443 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
444 let r0 = hi.entries()[0]
445 .reason()
446 .unwrap()
447 .unwrap();
448 let r1 = hi.entries()[1]
449 .reason()
450 .unwrap()
451 .unwrap();
452 assert_eq!(r0.protocol(), "SIP");
453 assert_eq!(r1.protocol(), "RouteAction");
454 assert!(hi.entries()[2]
455 .reason()
456 .is_none());
457 }
458
459 #[test]
462 fn display_roundtrip_simple() {
463 let raw = "<sip:alice@example.com>;index=1";
464 let hi = HistoryInfo::parse(raw).unwrap();
465 assert_eq!(hi.to_string(), raw);
466 }
467
468 #[test]
469 fn display_entry_count_matches_commas() {
470 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
471 let s = hi.to_string();
472 assert_eq!(
473 s.matches(',')
474 .count()
475 + 1,
476 hi.len()
477 );
478 }
479
480 #[test]
481 fn display_roundtrip_real_world() {
482 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
483 let reparsed = HistoryInfo::parse(&hi.to_string()).unwrap();
484 assert_eq!(hi.len(), reparsed.len());
485 for (a, b) in hi
486 .entries()
487 .iter()
488 .zip(reparsed.entries())
489 {
490 assert_eq!(a.index(), b.index());
491 assert_eq!(a.reason_raw(), b.reason_raw());
492 }
493 }
494
495 #[test]
498 fn iter_by_ref() {
499 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
500 let indices: Vec<_> = hi
501 .entries()
502 .iter()
503 .map(|e| e.index())
504 .collect();
505 assert_eq!(indices, vec![Some("1"), Some("2")]);
506 }
507
508 #[test]
509 fn into_entries() {
510 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
511 let entries = hi.into_entries();
512 assert_eq!(entries.len(), 2);
513 }
514
515 #[test]
518 fn parse_reason_full() {
519 let r = parse_reason("SIP;cause=302;text=\"Moved\"");
520 assert_eq!(r.protocol(), "SIP");
521 assert_eq!(r.cause(), Some(302));
522 assert_eq!(r.text(), Some("Moved"));
523 }
524
525 #[test]
526 fn parse_reason_no_text() {
527 let r = parse_reason("Q.850;cause=16");
528 assert_eq!(r.protocol(), "Q.850");
529 assert_eq!(r.cause(), Some(16));
530 assert_eq!(r.text(), None);
531 }
532
533 #[test]
534 fn parse_reason_protocol_only() {
535 let r = parse_reason("SIP");
536 assert_eq!(r.protocol(), "SIP");
537 assert_eq!(r.cause(), None);
538 assert_eq!(r.text(), None);
539 }
540
541 #[test]
542 fn parse_reason_unquoted_text() {
543 let r = parse_reason("SIP;cause=200;text=OK");
544 assert_eq!(r.text(), Some("OK"));
545 }
546
547 #[test]
548 fn parse_reason_quoted_text_hides_cause_lookalike() {
549 let r = parse_reason(r#"SIP;text="because=5";cause=200"#);
550 assert_eq!(r.cause(), Some(200));
551 assert_eq!(r.text(), Some("because=5"));
552 }
553
554 #[test]
555 fn parse_reason_text_unescapes_quoted_pair() {
556 let r = parse_reason(r#"SIP;cause=480;text="say \"hi\"""#);
557 assert_eq!(r.cause(), Some(480));
558 assert_eq!(r.text(), Some(r#"say "hi""#));
559 }
560
561 #[test]
562 fn parse_reason_keys_case_insensitive_and_sws() {
563 let r = parse_reason(r#"SIP ; Cause = 486 ; TEXT = "Busy; here""#);
564 assert_eq!(r.protocol(), "SIP");
565 assert_eq!(r.cause(), Some(486));
566 assert_eq!(r.text(), Some("Busy; here"));
567 }
568
569 #[test]
570 fn parse_reason_key_suffix_not_matched() {
571 let r = parse_reason(r#"SIP;xcause=1;subtext="no";cause=2"#);
572 assert_eq!(r.cause(), Some(2));
573 assert_eq!(r.text(), None);
574 }
575
576 #[test]
577 fn parse_reason_unparseable_cause_is_none() {
578 assert_eq!(parse_reason("SIP;cause=abc").cause(), None);
579 assert_eq!(parse_reason("SIP;cause=70000").cause(), None);
580 }
581
582 #[test]
583 fn reason_plus_is_literal() {
584 let hi = HistoryInfo::parse(
585 "<sip:a@example.com?Reason=SIP%3Bcause%3D200%3Btext%3D%22a%2Bb+c%22>;index=1",
586 )
587 .unwrap();
588 let reason = hi.entries()[0]
589 .reason()
590 .unwrap()
591 .unwrap();
592 assert_eq!(reason.text(), Some("a+b+c"));
593 }
594
595 #[test]
598 fn malformed_display() {
599 let e = HistoryInfoError::Malformed("too many entries".to_string());
600 assert_eq!(
601 e.to_string(),
602 "malformed History-Info value: too many entries"
603 );
604 }
605
606 #[test]
607 fn malformed_no_source() {
608 use std::error::Error;
609 let e = HistoryInfoError::Malformed("framing failure".to_string());
610 assert!(e
611 .source()
612 .is_none());
613 }
614}