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}
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/// Parsed RFC 3326 Reason header value extracted from a History-Info URI.
45///
46/// The Reason header embedded in History-Info URIs as `?Reason=...` follows
47/// the format: `protocol ;cause=code ;text="description"`.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct HistoryInfoReason {
50    protocol: String,
51    cause: Option<u16>,
52    text: Option<String>,
53}
54
55impl HistoryInfoReason {
56    /// The protocol token (e.g. `"SIP"`, `"Q.850"`, `"RouteAction"`).
57    pub fn protocol(&self) -> &str {
58        &self.protocol
59    }
60
61    /// The cause code, if present (e.g. `200`, `302`).
62    pub fn cause(&self) -> Option<u16> {
63        self.cause
64    }
65
66    /// The human-readable reason text, if present.
67    pub fn text(&self) -> Option<&str> {
68        self.text
69            .as_deref()
70    }
71}
72
73/// Parse a percent-decoded RFC 3326 Reason value.
74///
75/// Input format: `protocol;cause=N;text="description"`
76fn 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    // Extract cause (always a simple integer, safe to find by prefix)
85    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    // Extract text (may be quoted, always appears after cause in practice)
98    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/// A single entry from a History-Info header (RFC 7044).
125///
126/// Each entry is a SIP name-addr (`<URI>;params`) where the URI may contain
127/// an embedded `?Reason=...` header and the params typically include `index`.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct HistoryInfoEntry {
130    addr: SipHeaderAddr,
131}
132
133impl HistoryInfoEntry {
134    /// The underlying parsed name-addr with header-level parameters.
135    pub fn addr(&self) -> &SipHeaderAddr {
136        &self.addr
137    }
138
139    /// The URI from this entry.
140    pub fn uri(&self) -> &sip_uri::Uri {
141        self.addr
142            .uri()
143    }
144
145    /// The SIP URI, if this entry uses a `sip:` or `sips:` scheme.
146    pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
147        self.addr
148            .sip_uri()
149    }
150
151    /// The `index` parameter value (e.g. `"1"`, `"1.1"`, `"1.2"`).
152    pub fn index(&self) -> Option<&str> {
153        self.addr
154            .param_raw("index")
155            .flatten()
156    }
157
158    /// Raw percent-encoded Reason value from the URI `?Reason=...` header.
159    ///
160    /// Returns `None` if the URI is not a SIP URI or has no Reason header.
161    pub fn reason_raw(&self) -> Option<&str> {
162        self.addr
163            .sip_uri()?
164            .header("Reason")
165    }
166
167    /// Parse the Reason header embedded in the URI.
168    ///
169    /// The Reason value is percent-decoded (with `+` treated as space,
170    /// matching common SIP URI encoding conventions) and parsed into
171    /// protocol, cause code, and text components per RFC 3326.
172    ///
173    /// Returns `None` if no Reason is present, `Err` if percent-decoding
174    /// produces invalid UTF-8.
175    pub fn reason(&self) -> Option<Result<HistoryInfoReason, Utf8Error>> {
176        let raw = self.reason_raw()?;
177        // SIP stacks commonly use + for space in URI header values
178        // (form-encoding convention). Replace before percent-decoding
179        // so %2B (literal +) is preserved correctly.
180        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/// Parsed History-Info header value (RFC 7044).
196///
197/// Contains one or more routing-chain entries, each with a SIP URI,
198/// optional index, and optional embedded Reason header.
199///
200/// ```
201/// use sip_header::HistoryInfo;
202///
203/// let raw = "<sip:alice@esrp.example.com>;index=1,<sip:sos@psap.example.com>;index=1.1";
204/// let hi = HistoryInfo::parse(raw).unwrap();
205/// assert_eq!(hi.len(), 2);
206/// assert_eq!(hi.entries()[0].index(), Some("1"));
207/// assert_eq!(hi.entries()[1].index(), Some("1.1"));
208/// ```
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct HistoryInfo(Vec<HistoryInfoEntry>);
211
212impl HistoryInfo {
213    /// Parse a standard comma-separated History-Info header value (RFC 7044).
214    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    /// Build from pre-split header entries.
223    ///
224    /// Each entry should be a single `<uri>;params` string. Use this
225    /// when entries have already been split by an external mechanism.
226    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    /// The parsed entries as a slice.
240    pub fn entries(&self) -> &[HistoryInfoEntry] {
241        &self.0
242    }
243
244    /// Consume self and return the entries as a `Vec`.
245    pub fn into_entries(self) -> Vec<HistoryInfoEntry> {
246        self.0
247    }
248
249    /// Number of entries.
250    pub fn len(&self) -> usize {
251        self.0
252            .len()
253    }
254
255    /// Returns `true` if there are no entries.
256    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    // -- Entry count tests --
309
310    #[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    // -- Index accessor tests --
337
338    #[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    // -- URI accessor tests --
360
361    #[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    // -- Reason accessor tests --
408
409    #[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    // -- Display round-trip tests --
475
476    #[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    // -- Iterator tests --
511
512    #[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    // -- parse_reason unit tests --
531
532    #[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}