Skip to main content

sip_header/
history_info.rs

1//! SIP History-Info header parser (RFC 7044) with embedded RFC 3326 Reason.
2
3use std::fmt;
4use std::str::Utf8Error;
5
6use percent_encoding::percent_decode_str;
7
8use crate::header_addr::{ParseSipHeaderAddrError, SipHeaderAddr};
9
10/// Errors from parsing a History-Info header value (RFC 7044).
11#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum HistoryInfoError {
14    /// The input string was empty or whitespace-only.
15    Empty,
16    /// An entry could not be parsed as a SIP name-addr.
17    InvalidEntry(ParseSipHeaderAddrError),
18    /// The value could not be decoded into entries (transport or framing
19    /// failure before per-entry parsing).
20    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/// Parsed RFC 3326 Reason header value extracted from a History-Info URI.
49///
50/// The Reason header embedded in History-Info URIs as `?Reason=...` follows
51/// the format: `protocol ;cause=code ;text="description"`.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct HistoryInfoReason {
54    protocol: String,
55    cause: Option<u16>,
56    text: Option<String>,
57}
58
59impl HistoryInfoReason {
60    /// The protocol token (e.g. `"SIP"`, `"Q.850"`, `"RouteAction"`).
61    pub fn protocol(&self) -> &str {
62        &self.protocol
63    }
64
65    /// The cause code (e.g. `200`, `302`); `None` when absent or when the
66    /// value is not a `u16`.
67    pub fn cause(&self) -> Option<u16> {
68        self.cause
69    }
70
71    /// The reason text, if present, without its quotes and with
72    /// `quoted-pair` unescaped.
73    pub fn text(&self) -> Option<&str> {
74        self.text
75            .as_deref()
76    }
77}
78
79/// Parse a percent-decoded RFC 3326 `protocol *(SEMI reason-params)`.
80fn 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/// A single entry from a History-Info header (RFC 7044).
114///
115/// Each entry is a SIP name-addr (`<URI>;params`) where the URI may contain
116/// an embedded `?Reason=...` header and the params typically include `index`.
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct HistoryInfoEntry {
119    addr: SipHeaderAddr,
120}
121
122impl HistoryInfoEntry {
123    /// The underlying parsed name-addr with header-level parameters.
124    pub fn addr(&self) -> &SipHeaderAddr {
125        &self.addr
126    }
127
128    /// The URI from this entry.
129    pub fn uri(&self) -> &sip_uri::Uri {
130        self.addr
131            .uri()
132    }
133
134    /// The SIP URI, if this entry uses a `sip:` or `sips:` scheme.
135    pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
136        self.addr
137            .sip_uri()
138    }
139
140    /// The `index` parameter value (e.g. `"1"`, `"1.1"`, `"1.2"`).
141    pub fn index(&self) -> Option<&str> {
142        self.addr
143            .param_raw("index")
144            .flatten()
145    }
146
147    /// Raw percent-encoded Reason value from the URI `?Reason=...` header.
148    ///
149    /// Returns `None` if the URI is not a SIP URI or has no Reason header.
150    pub fn reason_raw(&self) -> Option<&str> {
151        self.addr
152            .sip_uri()?
153            .header("Reason")
154    }
155
156    /// Parse the Reason header embedded in the URI.
157    ///
158    /// The Reason value is percent-decoded as an RFC 3261 `hvalue` and
159    /// parsed into protocol, cause code, and text components per RFC 3326.
160    /// `+` is a literal plus sign, not a space.
161    ///
162    /// Returns `None` if no Reason is present, `Err` if percent-decoding
163    /// produces invalid UTF-8.
164    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/// Parsed History-Info header value (RFC 7044).
181///
182/// Contains one or more routing-chain entries, each with a SIP URI,
183/// optional index, and optional embedded Reason header.
184///
185/// ```
186/// use sip_header::HistoryInfo;
187///
188/// let raw = "<sip:alice@esrp.example.com>;index=1,<sip:sos@psap.example.com>;index=1.1";
189/// let hi = HistoryInfo::parse(raw).unwrap();
190/// assert_eq!(hi.len(), 2);
191/// assert_eq!(hi.entries()[0].index(), Some("1"));
192/// assert_eq!(hi.entries()[1].index(), Some("1.1"));
193/// ```
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct HistoryInfo(Vec<HistoryInfoEntry>);
196
197impl HistoryInfo {
198    /// Parse a standard comma-separated History-Info header value (RFC 7044).
199    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    /// Build from pre-split header entries.
208    ///
209    /// Each entry should be a single `<uri>;params` string. Use this
210    /// when entries have already been split by an external mechanism.
211    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    /// The parsed entries as a slice.
225    pub fn entries(&self) -> &[HistoryInfoEntry] {
226        &self.0
227    }
228
229    /// Consume self and return the entries as a `Vec`.
230    pub fn into_entries(self) -> Vec<HistoryInfoEntry> {
231        self.0
232    }
233
234    /// Number of entries.
235    pub fn len(&self) -> usize {
236        self.0
237            .len()
238    }
239
240    /// Returns `true` if there are no entries.
241    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    // -- Entry count tests --
294
295    #[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    // -- Index accessor tests --
322
323    #[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    // -- URI accessor tests --
345
346    #[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    // -- Reason accessor tests --
393
394    #[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    // -- Display round-trip tests --
460
461    #[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    // -- Iterator tests --
496
497    #[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    // -- parse_reason unit tests --
516
517    #[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    // -- Error variant tests --
596
597    #[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}