Skip to main content

sip_header/
target_dialog.rs

1//! RFC 4538 `Target-Dialog` header parser.
2
3use std::fmt;
4
5use crate::replaces::{decode_uri_header_value, parse_dialog_id, write_params, DialogIdError};
6
7/// Error parsing a Target-Dialog header.
8#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum SipTargetDialogError {
11    /// The Target-Dialog header value is empty.
12    Empty,
13    /// The Target-Dialog header value has an invalid format.
14    InvalidFormat(String),
15}
16
17impl fmt::Display for SipTargetDialogError {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            Self::Empty => write!(f, "Target-Dialog header is empty"),
21            Self::InvalidFormat(msg) => write!(f, "Invalid Target-Dialog format: {}", msg),
22        }
23    }
24}
25
26impl std::error::Error for SipTargetDialogError {}
27
28impl From<DialogIdError> for SipTargetDialogError {
29    fn from(e: DialogIdError) -> Self {
30        match e {
31            DialogIdError::Empty => Self::Empty,
32            DialogIdError::Invalid(msg) => Self::InvalidFormat(msg),
33        }
34    }
35}
36
37/// A parsed `Target-Dialog` header value (RFC 4538 ยง7).
38///
39/// Identifies an existing dialog: Call-ID plus the mandatory `local-tag`
40/// and `remote-tag`, both from the perspective of the request recipient.
41#[derive(Debug, Clone, PartialEq, Eq)]
42#[non_exhaustive]
43pub struct SipTargetDialog {
44    call_id: String,
45    local_tag: String,
46    remote_tag: String,
47    params: Vec<(String, Option<String>)>,
48    uri_header_framing: bool,
49}
50
51impl SipTargetDialog {
52    /// Parse a wire-form header value: `callid;local-tag=x;remote-tag=y`.
53    pub fn parse(raw: &str) -> Result<Self, SipTargetDialogError> {
54        let id = parse_dialog_id(raw, "local-tag", "remote-tag", false)?;
55        Ok(Self {
56            call_id: id.call_id,
57            local_tag: id.first_tag,
58            remote_tag: id.second_tag,
59            params: id.params,
60            uri_header_framing: false,
61        })
62    }
63
64    /// Parse the percent-encoded framing found in a URI header,
65    /// e.g. `callid%40host%3Blocal-tag%3Dx%3Bremote-tag%3Dy`.
66    ///
67    /// Accepts the canonicalised value returned by
68    /// [`sip_uri::SipUri::header`]; [`Display`](fmt::Display) re-encodes to
69    /// that same canonical form (uppercase hex).
70    pub fn parse_uri_header(raw: &str) -> Result<Self, SipTargetDialogError> {
71        let decoded = decode_uri_header_value(raw)?;
72        let mut parsed = Self::parse(&decoded)?;
73        parsed.uri_header_framing = true;
74        Ok(parsed)
75    }
76
77    /// The Call-ID of the target dialog.
78    pub fn call_id(&self) -> &str {
79        &self.call_id
80    }
81
82    /// The host part of the Call-ID (after `@`), if present.
83    pub fn host(&self) -> Option<&str> {
84        self.call_id
85            .split_once('@')
86            .map(|(_, host)| host)
87    }
88
89    /// The mandatory `local-tag` value.
90    pub fn local_tag(&self) -> &str {
91        &self.local_tag
92    }
93
94    /// The mandatory `remote-tag` value.
95    pub fn remote_tag(&self) -> &str {
96        &self.remote_tag
97    }
98
99    /// Returns all generic parameters (tags excluded).
100    pub fn params(&self) -> &[(String, Option<String>)] {
101        &self.params
102    }
103
104    /// Returns a specific generic parameter by key (case-insensitive).
105    pub fn param(&self, key: &str) -> Option<Option<&str>> {
106        let key_lower = key.to_ascii_lowercase();
107        self.params
108            .iter()
109            .find(|(k, _)| k == &key_lower)
110            .map(|(_, v)| v.as_deref())
111    }
112
113    fn wire_form(&self) -> String {
114        let mut s = format!(
115            "{};local-tag={};remote-tag={}",
116            self.call_id, self.local_tag, self.remote_tag
117        );
118        write_params(&mut s, &self.params);
119        s
120    }
121}
122
123impl fmt::Display for SipTargetDialog {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        let wire = self.wire_form();
126        if self.uri_header_framing {
127            f.write_str(&sip_uri::encode_uri_header(&wire))
128        } else {
129            f.write_str(&wire)
130        }
131    }
132}
133
134impl_from_str_via_parse!(SipTargetDialog, SipTargetDialogError);
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn parse_basic() {
142        let t = SipTargetDialog::parse("abc123@203.0.113.5;local-tag=l1;remote-tag=r1").unwrap();
143        assert_eq!(t.call_id(), "abc123@203.0.113.5");
144        assert_eq!(t.host(), Some("203.0.113.5"));
145        assert_eq!(t.local_tag(), "l1");
146        assert_eq!(t.remote_tag(), "r1");
147    }
148
149    #[test]
150    fn missing_local_tag_fails() {
151        assert!(SipTargetDialog::parse("abc@example.com;remote-tag=r1").is_err());
152    }
153
154    #[test]
155    fn missing_remote_tag_fails() {
156        assert!(SipTargetDialog::parse("abc@example.com;local-tag=l1").is_err());
157    }
158
159    #[test]
160    fn empty_fails() {
161        assert!(matches!(
162            SipTargetDialog::parse(""),
163            Err(SipTargetDialogError::Empty)
164        ));
165    }
166
167    #[test]
168    fn generic_params_preserved() {
169        let t =
170            SipTargetDialog::parse("abc@example.com;local-tag=l1;remote-tag=r1;foo=bar").unwrap();
171        assert_eq!(t.param("foo"), Some(Some("bar")));
172    }
173
174    #[test]
175    fn parse_uri_header_encoded() {
176        let t = SipTargetDialog::parse_uri_header(
177            "abc123%40203.0.113.5%3Blocal-tag%3Dl1%3Bremote-tag%3Dr1",
178        )
179        .unwrap();
180        assert_eq!(t.host(), Some("203.0.113.5"));
181        assert_eq!(t.local_tag(), "l1");
182        assert_eq!(t.remote_tag(), "r1");
183    }
184
185    #[test]
186    fn display_roundtrip_wire() {
187        let input = "abc123@203.0.113.5;local-tag=l1;remote-tag=r1;foo=bar";
188        let t = SipTargetDialog::parse(input).unwrap();
189        assert_eq!(t.to_string(), input);
190        assert_eq!(SipTargetDialog::parse(&t.to_string()).unwrap(), t);
191    }
192
193    #[test]
194    fn display_roundtrip_uri_header() {
195        let input = "abc123%40203.0.113.5%3Blocal-tag%3Dl1%3Bremote-tag%3Dr1";
196        let t = SipTargetDialog::parse_uri_header(input).unwrap();
197        assert_eq!(t.to_string(), input);
198    }
199
200    #[test]
201    fn from_str_is_wire_framing() {
202        let t: SipTargetDialog = "abc123@203.0.113.5;local-tag=l1;remote-tag=r1"
203            .parse()
204            .unwrap();
205        assert_eq!(t.local_tag(), "l1");
206    }
207}