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}
19
20impl fmt::Display for HistoryInfoError {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 match self {
23 Self::Empty => write!(f, "empty History-Info header"),
24 Self::InvalidEntry(e) => write!(f, "invalid History-Info entry: {e}"),
25 }
26 }
27}
28
29impl std::error::Error for HistoryInfoError {
30 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
31 match self {
32 Self::InvalidEntry(e) => Some(e),
33 _ => None,
34 }
35 }
36}
37
38impl From<ParseSipHeaderAddrError> for HistoryInfoError {
39 fn from(e: ParseSipHeaderAddrError) -> Self {
40 Self::InvalidEntry(e)
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct HistoryInfoReason {
50 protocol: String,
51 cause: Option<u16>,
52 text: Option<String>,
53}
54
55impl HistoryInfoReason {
56 pub fn protocol(&self) -> &str {
58 &self.protocol
59 }
60
61 pub fn cause(&self) -> Option<u16> {
63 self.cause
64 }
65
66 pub fn text(&self) -> Option<&str> {
68 self.text
69 .as_deref()
70 }
71}
72
73fn parse_reason(decoded: &str) -> HistoryInfoReason {
77 let (protocol, rest) = decoded
78 .split_once(';')
79 .unwrap_or((decoded, ""));
80
81 let mut cause = None;
82 let mut text = None;
83
84 if let Some(idx) = rest.find("cause=") {
86 let val_start = idx + 6;
87 let val_end = rest[val_start..]
88 .find(';')
89 .map(|i| val_start + i)
90 .unwrap_or(rest.len());
91 cause = rest[val_start..val_end]
92 .trim()
93 .parse::<u16>()
94 .ok();
95 }
96
97 if let Some(idx) = rest.find("text=") {
99 let val_start = idx + 5;
100 let val = rest[val_start..].trim_start();
101 if let Some(inner) = val.strip_prefix('"') {
102 if let Some(end) = inner.find('"') {
103 text = Some(inner[..end].to_string());
104 } else {
105 text = Some(inner.to_string());
106 }
107 } else {
108 let end = val
109 .find(';')
110 .unwrap_or(val.len());
111 text = Some(val[..end].to_string());
112 }
113 }
114
115 HistoryInfoReason {
116 protocol: protocol
117 .trim()
118 .to_string(),
119 cause,
120 text,
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct HistoryInfoEntry {
130 addr: SipHeaderAddr,
131}
132
133impl HistoryInfoEntry {
134 pub fn addr(&self) -> &SipHeaderAddr {
136 &self.addr
137 }
138
139 pub fn uri(&self) -> &sip_uri::Uri {
141 self.addr
142 .uri()
143 }
144
145 pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
147 self.addr
148 .sip_uri()
149 }
150
151 pub fn index(&self) -> Option<&str> {
153 self.addr
154 .param_raw("index")
155 .flatten()
156 }
157
158 pub fn reason_raw(&self) -> Option<&str> {
162 self.addr
163 .sip_uri()?
164 .header("Reason")
165 }
166
167 pub fn reason(&self) -> Option<Result<HistoryInfoReason, Utf8Error>> {
176 let raw = self.reason_raw()?;
177 let raw = raw.replace('+', " ");
181 Some(
182 percent_decode_str(&raw)
183 .decode_utf8()
184 .map(|decoded| parse_reason(&decoded)),
185 )
186 }
187}
188
189impl fmt::Display for HistoryInfoEntry {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 write!(f, "{}", self.addr)
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct HistoryInfo(Vec<HistoryInfoEntry>);
211
212impl HistoryInfo {
213 pub fn parse(raw: &str) -> Result<Self, HistoryInfoError> {
215 let raw = raw.trim();
216 if raw.is_empty() {
217 return Err(HistoryInfoError::Empty);
218 }
219 Self::from_entries(crate::split_comma_entries(raw))
220 }
221
222 pub fn from_entries<'a>(
227 entries: impl IntoIterator<Item = &'a str>,
228 ) -> Result<Self, HistoryInfoError> {
229 let entries: Vec<_> = entries
230 .into_iter()
231 .map(parse_entry)
232 .collect::<Result<_, _>>()?;
233 if entries.is_empty() {
234 return Err(HistoryInfoError::Empty);
235 }
236 Ok(Self(entries))
237 }
238
239 pub fn entries(&self) -> &[HistoryInfoEntry] {
241 &self.0
242 }
243
244 pub fn into_entries(self) -> Vec<HistoryInfoEntry> {
246 self.0
247 }
248
249 pub fn len(&self) -> usize {
251 self.0
252 .len()
253 }
254
255 pub fn is_empty(&self) -> bool {
257 self.0
258 .is_empty()
259 }
260}
261
262fn parse_entry(raw: &str) -> Result<HistoryInfoEntry, HistoryInfoError> {
263 let addr: SipHeaderAddr = raw
264 .trim()
265 .parse()?;
266 Ok(HistoryInfoEntry { addr })
267}
268
269impl fmt::Display for HistoryInfo {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 crate::fmt_joined(f, &self.0, ",")
272 }
273}
274
275impl<'a> IntoIterator for &'a HistoryInfo {
276 type Item = &'a HistoryInfoEntry;
277 type IntoIter = std::slice::Iter<'a, HistoryInfoEntry>;
278
279 fn into_iter(self) -> Self::IntoIter {
280 self.0
281 .iter()
282 }
283}
284
285impl IntoIterator for HistoryInfo {
286 type Item = HistoryInfoEntry;
287 type IntoIter = std::vec::IntoIter<HistoryInfoEntry>;
288
289 fn into_iter(self) -> Self::IntoIter {
290 self.0
291 .into_iter()
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 const EXAMPLE_1: &str = "\
300<sip:user1@esrp.example.com?Reason=RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22>;index=1,\
301<sip:sos@psap.example.com>;index=2";
302
303 const EXAMPLE_2: &str = "\
304<sip:lsrg.example.com?Reason=SIP%3Bcause%3D200%3Btext%3D%22Legacy+routing%22>;index=1,\
305<sip:user1@esrp2.example.com;lr;transport=udp?Reason=RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22>;index=1.1,\
306<sip:sos@psap.example.com>;index=1.2";
307
308 #[test]
311 fn parse_two_entries() {
312 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
313 assert_eq!(hi.len(), 2);
314 }
315
316 #[test]
317 fn parse_three_entries() {
318 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
319 assert_eq!(hi.len(), 3);
320 }
321
322 #[test]
323 fn parse_single_entry() {
324 let hi = HistoryInfo::parse("<sip:alice@example.com>;index=1").unwrap();
325 assert_eq!(hi.len(), 1);
326 }
327
328 #[test]
329 fn empty_input() {
330 assert!(matches!(
331 HistoryInfo::parse(""),
332 Err(HistoryInfoError::Empty)
333 ));
334 }
335
336 #[test]
339 fn index_simple() {
340 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
341 assert_eq!(hi.entries()[0].index(), Some("1"));
342 assert_eq!(hi.entries()[1].index(), Some("2"));
343 }
344
345 #[test]
346 fn index_hierarchical() {
347 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
348 assert_eq!(hi.entries()[0].index(), Some("1"));
349 assert_eq!(hi.entries()[1].index(), Some("1.1"));
350 assert_eq!(hi.entries()[2].index(), Some("1.2"));
351 }
352
353 #[test]
354 fn index_absent() {
355 let hi = HistoryInfo::parse("<sip:alice@example.com>").unwrap();
356 assert_eq!(hi.entries()[0].index(), None);
357 }
358
359 #[test]
362 fn uri_with_user() {
363 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
364 let sip = hi.entries()[0]
365 .sip_uri()
366 .unwrap();
367 assert_eq!(sip.user(), Some("user1"));
368 assert_eq!(
369 sip.host()
370 .to_string(),
371 "esrp.example.com"
372 );
373 }
374
375 #[test]
376 fn uri_without_user() {
377 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
378 let sip = hi.entries()[0]
379 .sip_uri()
380 .unwrap();
381 assert_eq!(sip.user(), None);
382 assert_eq!(
383 sip.host()
384 .to_string(),
385 "lsrg.example.com"
386 );
387 }
388
389 #[test]
390 fn uri_with_params() {
391 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
392 let sip = hi.entries()[1]
393 .sip_uri()
394 .unwrap();
395 assert_eq!(sip.user(), Some("user1"));
396 assert_eq!(
397 sip.host()
398 .to_string(),
399 "esrp2.example.com"
400 );
401 assert!(sip
402 .param("lr")
403 .is_some());
404 assert_eq!(sip.param("transport"), Some(&Some("udp".to_string())));
405 }
406
407 #[test]
410 fn reason_raw_present() {
411 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
412 assert_eq!(
413 hi.entries()[0].reason_raw(),
414 Some("RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22")
415 );
416 }
417
418 #[test]
419 fn reason_raw_absent() {
420 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
421 assert_eq!(hi.entries()[1].reason_raw(), None);
422 }
423
424 #[test]
425 fn reason_parsed_route_action() {
426 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
427 let reason = hi.entries()[0]
428 .reason()
429 .unwrap()
430 .unwrap();
431 assert_eq!(reason.protocol(), "RouteAction");
432 assert_eq!(reason.cause(), Some(200));
433 assert_eq!(reason.text(), Some("Normal Next Hop"));
434 }
435
436 #[test]
437 fn reason_parsed_sip() {
438 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
439 let reason = hi.entries()[0]
440 .reason()
441 .unwrap()
442 .unwrap();
443 assert_eq!(reason.protocol(), "SIP");
444 assert_eq!(reason.cause(), Some(200));
445 assert_eq!(reason.text(), Some("Legacy routing"));
446 }
447
448 #[test]
449 fn reason_absent_returns_none() {
450 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
451 assert!(hi.entries()[2]
452 .reason()
453 .is_none());
454 }
455
456 #[test]
457 fn reason_multiple_entries() {
458 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
459 let r0 = hi.entries()[0]
460 .reason()
461 .unwrap()
462 .unwrap();
463 let r1 = hi.entries()[1]
464 .reason()
465 .unwrap()
466 .unwrap();
467 assert_eq!(r0.protocol(), "SIP");
468 assert_eq!(r1.protocol(), "RouteAction");
469 assert!(hi.entries()[2]
470 .reason()
471 .is_none());
472 }
473
474 #[test]
477 fn display_roundtrip_simple() {
478 let raw = "<sip:alice@example.com>;index=1";
479 let hi = HistoryInfo::parse(raw).unwrap();
480 assert_eq!(hi.to_string(), raw);
481 }
482
483 #[test]
484 fn display_entry_count_matches_commas() {
485 let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
486 let s = hi.to_string();
487 assert_eq!(
488 s.matches(',')
489 .count()
490 + 1,
491 hi.len()
492 );
493 }
494
495 #[test]
496 fn display_roundtrip_real_world() {
497 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
498 let reparsed = HistoryInfo::parse(&hi.to_string()).unwrap();
499 assert_eq!(hi.len(), reparsed.len());
500 for (a, b) in hi
501 .entries()
502 .iter()
503 .zip(reparsed.entries())
504 {
505 assert_eq!(a.index(), b.index());
506 assert_eq!(a.reason_raw(), b.reason_raw());
507 }
508 }
509
510 #[test]
513 fn iter_by_ref() {
514 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
515 let indices: Vec<_> = hi
516 .entries()
517 .iter()
518 .map(|e| e.index())
519 .collect();
520 assert_eq!(indices, vec![Some("1"), Some("2")]);
521 }
522
523 #[test]
524 fn into_entries() {
525 let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
526 let entries = hi.into_entries();
527 assert_eq!(entries.len(), 2);
528 }
529
530 #[test]
533 fn parse_reason_full() {
534 let r = parse_reason("SIP;cause=302;text=\"Moved\"");
535 assert_eq!(r.protocol(), "SIP");
536 assert_eq!(r.cause(), Some(302));
537 assert_eq!(r.text(), Some("Moved"));
538 }
539
540 #[test]
541 fn parse_reason_no_text() {
542 let r = parse_reason("Q.850;cause=16");
543 assert_eq!(r.protocol(), "Q.850");
544 assert_eq!(r.cause(), Some(16));
545 assert_eq!(r.text(), None);
546 }
547
548 #[test]
549 fn parse_reason_protocol_only() {
550 let r = parse_reason("SIP");
551 assert_eq!(r.protocol(), "SIP");
552 assert_eq!(r.cause(), None);
553 assert_eq!(r.text(), None);
554 }
555
556 #[test]
557 fn parse_reason_unquoted_text() {
558 let r = parse_reason("SIP;cause=200;text=OK");
559 assert_eq!(r.text(), Some("OK"));
560 }
561}