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