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, if present (e.g. `200`, `302`).
66    pub fn cause(&self) -> Option<u16> {
67        self.cause
68    }
69
70    /// The human-readable reason text, if present.
71    pub fn text(&self) -> Option<&str> {
72        self.text
73            .as_deref()
74    }
75}
76
77/// Parse a percent-decoded RFC 3326 Reason value.
78///
79/// Input format: `protocol;cause=N;text="description"`
80fn parse_reason(decoded: &str) -> HistoryInfoReason {
81    let (protocol, rest) = decoded
82        .split_once(';')
83        .unwrap_or((decoded, ""));
84
85    let mut cause = None;
86    let mut text = None;
87
88    // Extract cause (always a simple integer, safe to find by prefix)
89    if let Some(idx) = rest.find("cause=") {
90        let val_start = idx + 6;
91        let val_end = rest[val_start..]
92            .find(';')
93            .map(|i| val_start + i)
94            .unwrap_or(rest.len());
95        cause = rest[val_start..val_end]
96            .trim()
97            .parse::<u16>()
98            .ok();
99    }
100
101    // Extract text (may be quoted, always appears after cause in practice)
102    if let Some(idx) = rest.find("text=") {
103        let val_start = idx + 5;
104        let val = rest[val_start..].trim_start();
105        if let Some(inner) = val.strip_prefix('"') {
106            if let Some(end) = inner.find('"') {
107                text = Some(inner[..end].to_string());
108            } else {
109                text = Some(inner.to_string());
110            }
111        } else {
112            let end = val
113                .find(';')
114                .unwrap_or(val.len());
115            text = Some(val[..end].to_string());
116        }
117    }
118
119    HistoryInfoReason {
120        protocol: protocol
121            .trim()
122            .to_string(),
123        cause,
124        text,
125    }
126}
127
128/// A single entry from a History-Info header (RFC 7044).
129///
130/// Each entry is a SIP name-addr (`<URI>;params`) where the URI may contain
131/// an embedded `?Reason=...` header and the params typically include `index`.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct HistoryInfoEntry {
134    addr: SipHeaderAddr,
135}
136
137impl HistoryInfoEntry {
138    /// The underlying parsed name-addr with header-level parameters.
139    pub fn addr(&self) -> &SipHeaderAddr {
140        &self.addr
141    }
142
143    /// The URI from this entry.
144    pub fn uri(&self) -> &sip_uri::Uri {
145        self.addr
146            .uri()
147    }
148
149    /// The SIP URI, if this entry uses a `sip:` or `sips:` scheme.
150    pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
151        self.addr
152            .sip_uri()
153    }
154
155    /// The `index` parameter value (e.g. `"1"`, `"1.1"`, `"1.2"`).
156    pub fn index(&self) -> Option<&str> {
157        self.addr
158            .param_raw("index")
159            .flatten()
160    }
161
162    /// Raw percent-encoded Reason value from the URI `?Reason=...` header.
163    ///
164    /// Returns `None` if the URI is not a SIP URI or has no Reason header.
165    pub fn reason_raw(&self) -> Option<&str> {
166        self.addr
167            .sip_uri()?
168            .header("Reason")
169    }
170
171    /// Parse the Reason header embedded in the URI.
172    ///
173    /// The Reason value is percent-decoded (with `+` treated as space,
174    /// matching common SIP URI encoding conventions) and parsed into
175    /// protocol, cause code, and text components per RFC 3326.
176    ///
177    /// Returns `None` if no Reason is present, `Err` if percent-decoding
178    /// produces invalid UTF-8.
179    pub fn reason(&self) -> Option<Result<HistoryInfoReason, Utf8Error>> {
180        let raw = self.reason_raw()?;
181        // SIP stacks commonly use + for space in URI header values
182        // (form-encoding convention). Replace before percent-decoding
183        // so %2B (literal +) is preserved correctly.
184        let raw = raw.replace('+', " ");
185        Some(
186            percent_decode_str(&raw)
187                .decode_utf8()
188                .map(|decoded| parse_reason(&decoded)),
189        )
190    }
191}
192
193impl fmt::Display for HistoryInfoEntry {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        write!(f, "{}", self.addr)
196    }
197}
198
199/// Parsed History-Info header value (RFC 7044).
200///
201/// Contains one or more routing-chain entries, each with a SIP URI,
202/// optional index, and optional embedded Reason header.
203///
204/// ```
205/// use sip_header::HistoryInfo;
206///
207/// let raw = "<sip:alice@esrp.example.com>;index=1,<sip:sos@psap.example.com>;index=1.1";
208/// let hi = HistoryInfo::parse(raw).unwrap();
209/// assert_eq!(hi.len(), 2);
210/// assert_eq!(hi.entries()[0].index(), Some("1"));
211/// assert_eq!(hi.entries()[1].index(), Some("1.1"));
212/// ```
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct HistoryInfo(Vec<HistoryInfoEntry>);
215
216impl HistoryInfo {
217    /// Parse a standard comma-separated History-Info header value (RFC 7044).
218    pub fn parse(raw: &str) -> Result<Self, HistoryInfoError> {
219        let raw = raw.trim();
220        if raw.is_empty() {
221            return Err(HistoryInfoError::Empty);
222        }
223        Self::from_entries(crate::split_comma_entries(raw))
224    }
225
226    /// Build from pre-split header entries.
227    ///
228    /// Each entry should be a single `<uri>;params` string. Use this
229    /// when entries have already been split by an external mechanism.
230    pub fn from_entries<'a>(
231        entries: impl IntoIterator<Item = &'a str>,
232    ) -> Result<Self, HistoryInfoError> {
233        let entries: Vec<_> = entries
234            .into_iter()
235            .map(parse_entry)
236            .collect::<Result<_, _>>()?;
237        if entries.is_empty() {
238            return Err(HistoryInfoError::Empty);
239        }
240        Ok(Self(entries))
241    }
242
243    /// The parsed entries as a slice.
244    pub fn entries(&self) -> &[HistoryInfoEntry] {
245        &self.0
246    }
247
248    /// Consume self and return the entries as a `Vec`.
249    pub fn into_entries(self) -> Vec<HistoryInfoEntry> {
250        self.0
251    }
252
253    /// Number of entries.
254    pub fn len(&self) -> usize {
255        self.0
256            .len()
257    }
258
259    /// Returns `true` if there are no entries.
260    pub fn is_empty(&self) -> bool {
261        self.0
262            .is_empty()
263    }
264}
265
266fn parse_entry(raw: &str) -> Result<HistoryInfoEntry, HistoryInfoError> {
267    let addr: SipHeaderAddr = raw
268        .trim()
269        .parse()?;
270    Ok(HistoryInfoEntry { addr })
271}
272
273impl fmt::Display for HistoryInfo {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        crate::fmt_joined(f, &self.0, ",")
276    }
277}
278
279impl<'a> IntoIterator for &'a HistoryInfo {
280    type Item = &'a HistoryInfoEntry;
281    type IntoIter = std::slice::Iter<'a, HistoryInfoEntry>;
282
283    fn into_iter(self) -> Self::IntoIter {
284        self.0
285            .iter()
286    }
287}
288
289impl IntoIterator for HistoryInfo {
290    type Item = HistoryInfoEntry;
291    type IntoIter = std::vec::IntoIter<HistoryInfoEntry>;
292
293    fn into_iter(self) -> Self::IntoIter {
294        self.0
295            .into_iter()
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    const EXAMPLE_1: &str = "\
304<sip:user1@esrp.example.com?Reason=RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22>;index=1,\
305<sip:sos@psap.example.com>;index=2";
306
307    const EXAMPLE_2: &str = "\
308<sip:lsrg.example.com?Reason=SIP%3Bcause%3D200%3Btext%3D%22Legacy+routing%22>;index=1,\
309<sip:user1@esrp2.example.com;lr;transport=udp?Reason=RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22>;index=1.1,\
310<sip:sos@psap.example.com>;index=1.2";
311
312    // -- Entry count tests --
313
314    #[test]
315    fn parse_two_entries() {
316        let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
317        assert_eq!(hi.len(), 2);
318    }
319
320    #[test]
321    fn parse_three_entries() {
322        let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
323        assert_eq!(hi.len(), 3);
324    }
325
326    #[test]
327    fn parse_single_entry() {
328        let hi = HistoryInfo::parse("<sip:alice@example.com>;index=1").unwrap();
329        assert_eq!(hi.len(), 1);
330    }
331
332    #[test]
333    fn empty_input() {
334        assert!(matches!(
335            HistoryInfo::parse(""),
336            Err(HistoryInfoError::Empty)
337        ));
338    }
339
340    // -- Index accessor tests --
341
342    #[test]
343    fn index_simple() {
344        let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
345        assert_eq!(hi.entries()[0].index(), Some("1"));
346        assert_eq!(hi.entries()[1].index(), Some("2"));
347    }
348
349    #[test]
350    fn index_hierarchical() {
351        let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
352        assert_eq!(hi.entries()[0].index(), Some("1"));
353        assert_eq!(hi.entries()[1].index(), Some("1.1"));
354        assert_eq!(hi.entries()[2].index(), Some("1.2"));
355    }
356
357    #[test]
358    fn index_absent() {
359        let hi = HistoryInfo::parse("<sip:alice@example.com>").unwrap();
360        assert_eq!(hi.entries()[0].index(), None);
361    }
362
363    // -- URI accessor tests --
364
365    #[test]
366    fn uri_with_user() {
367        let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
368        let sip = hi.entries()[0]
369            .sip_uri()
370            .unwrap();
371        assert_eq!(sip.user(), Some("user1"));
372        assert_eq!(
373            sip.host()
374                .to_string(),
375            "esrp.example.com"
376        );
377    }
378
379    #[test]
380    fn uri_without_user() {
381        let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
382        let sip = hi.entries()[0]
383            .sip_uri()
384            .unwrap();
385        assert_eq!(sip.user(), None);
386        assert_eq!(
387            sip.host()
388                .to_string(),
389            "lsrg.example.com"
390        );
391    }
392
393    #[test]
394    fn uri_with_params() {
395        let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
396        let sip = hi.entries()[1]
397            .sip_uri()
398            .unwrap();
399        assert_eq!(sip.user(), Some("user1"));
400        assert_eq!(
401            sip.host()
402                .to_string(),
403            "esrp2.example.com"
404        );
405        assert!(sip
406            .param("lr")
407            .is_some());
408        assert_eq!(sip.param("transport"), Some(&Some("udp".to_string())));
409    }
410
411    // -- Reason accessor tests --
412
413    #[test]
414    fn reason_raw_present() {
415        let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
416        assert_eq!(
417            hi.entries()[0].reason_raw(),
418            Some("RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22")
419        );
420    }
421
422    #[test]
423    fn reason_raw_absent() {
424        let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
425        assert_eq!(hi.entries()[1].reason_raw(), None);
426    }
427
428    #[test]
429    fn reason_parsed_route_action() {
430        let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
431        let reason = hi.entries()[0]
432            .reason()
433            .unwrap()
434            .unwrap();
435        assert_eq!(reason.protocol(), "RouteAction");
436        assert_eq!(reason.cause(), Some(200));
437        assert_eq!(reason.text(), Some("Normal Next Hop"));
438    }
439
440    #[test]
441    fn reason_parsed_sip() {
442        let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
443        let reason = hi.entries()[0]
444            .reason()
445            .unwrap()
446            .unwrap();
447        assert_eq!(reason.protocol(), "SIP");
448        assert_eq!(reason.cause(), Some(200));
449        assert_eq!(reason.text(), Some("Legacy routing"));
450    }
451
452    #[test]
453    fn reason_absent_returns_none() {
454        let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
455        assert!(hi.entries()[2]
456            .reason()
457            .is_none());
458    }
459
460    #[test]
461    fn reason_multiple_entries() {
462        let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
463        let r0 = hi.entries()[0]
464            .reason()
465            .unwrap()
466            .unwrap();
467        let r1 = hi.entries()[1]
468            .reason()
469            .unwrap()
470            .unwrap();
471        assert_eq!(r0.protocol(), "SIP");
472        assert_eq!(r1.protocol(), "RouteAction");
473        assert!(hi.entries()[2]
474            .reason()
475            .is_none());
476    }
477
478    // -- Display round-trip tests --
479
480    #[test]
481    fn display_roundtrip_simple() {
482        let raw = "<sip:alice@example.com>;index=1";
483        let hi = HistoryInfo::parse(raw).unwrap();
484        assert_eq!(hi.to_string(), raw);
485    }
486
487    #[test]
488    fn display_entry_count_matches_commas() {
489        let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
490        let s = hi.to_string();
491        assert_eq!(
492            s.matches(',')
493                .count()
494                + 1,
495            hi.len()
496        );
497    }
498
499    #[test]
500    fn display_roundtrip_real_world() {
501        let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
502        let reparsed = HistoryInfo::parse(&hi.to_string()).unwrap();
503        assert_eq!(hi.len(), reparsed.len());
504        for (a, b) in hi
505            .entries()
506            .iter()
507            .zip(reparsed.entries())
508        {
509            assert_eq!(a.index(), b.index());
510            assert_eq!(a.reason_raw(), b.reason_raw());
511        }
512    }
513
514    // -- Iterator tests --
515
516    #[test]
517    fn iter_by_ref() {
518        let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
519        let indices: Vec<_> = hi
520            .entries()
521            .iter()
522            .map(|e| e.index())
523            .collect();
524        assert_eq!(indices, vec![Some("1"), Some("2")]);
525    }
526
527    #[test]
528    fn into_entries() {
529        let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
530        let entries = hi.into_entries();
531        assert_eq!(entries.len(), 2);
532    }
533
534    // -- parse_reason unit tests --
535
536    #[test]
537    fn parse_reason_full() {
538        let r = parse_reason("SIP;cause=302;text=\"Moved\"");
539        assert_eq!(r.protocol(), "SIP");
540        assert_eq!(r.cause(), Some(302));
541        assert_eq!(r.text(), Some("Moved"));
542    }
543
544    #[test]
545    fn parse_reason_no_text() {
546        let r = parse_reason("Q.850;cause=16");
547        assert_eq!(r.protocol(), "Q.850");
548        assert_eq!(r.cause(), Some(16));
549        assert_eq!(r.text(), None);
550    }
551
552    #[test]
553    fn parse_reason_protocol_only() {
554        let r = parse_reason("SIP");
555        assert_eq!(r.protocol(), "SIP");
556        assert_eq!(r.cause(), None);
557        assert_eq!(r.text(), None);
558    }
559
560    #[test]
561    fn parse_reason_unquoted_text() {
562        let r = parse_reason("SIP;cause=200;text=OK");
563        assert_eq!(r.text(), Some("OK"));
564    }
565
566    // -- Error variant tests --
567
568    #[test]
569    fn malformed_display() {
570        let e = HistoryInfoError::Malformed("too many entries".to_string());
571        assert_eq!(
572            e.to_string(),
573            "malformed History-Info value: too many entries"
574        );
575    }
576
577    #[test]
578    fn malformed_no_source() {
579        use std::error::Error;
580        let e = HistoryInfoError::Malformed("framing failure".to_string());
581        assert!(e
582            .source()
583            .is_none());
584    }
585}