sip_header/
target_dialog.rs1use std::fmt;
4
5use crate::replaces::{decode_uri_header_value, parse_dialog_id, write_params, DialogIdError};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum SipTargetDialogError {
11 Empty,
13 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#[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 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 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 pub fn call_id(&self) -> &str {
79 &self.call_id
80 }
81
82 pub fn host(&self) -> Option<&str> {
84 self.call_id
85 .split_once('@')
86 .map(|(_, host)| host)
87 }
88
89 pub fn local_tag(&self) -> &str {
91 &self.local_tag
92 }
93
94 pub fn remote_tag(&self) -> &str {
96 &self.remote_tag
97 }
98
99 pub fn params(&self) -> &[(String, Option<String>)] {
101 &self.params
102 }
103
104 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}