sip_header/
target_dialog.rs1use std::fmt;
4
5use crate::replaces::{
6 decode_uri_header_value, parse_dialog_id, validate_call_id, write_params, DialogIdError,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum SipTargetDialogError {
13 Empty,
15 InvalidFormat(String),
17}
18
19impl fmt::Display for SipTargetDialogError {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 match self {
22 Self::Empty => write!(f, "Target-Dialog header is empty"),
23 Self::InvalidFormat(msg) => write!(f, "Invalid Target-Dialog format: {}", msg),
24 }
25 }
26}
27
28impl std::error::Error for SipTargetDialogError {}
29
30impl From<DialogIdError> for SipTargetDialogError {
31 fn from(e: DialogIdError) -> Self {
32 match e {
33 DialogIdError::Empty => Self::Empty,
34 DialogIdError::Invalid(msg) => Self::InvalidFormat(msg),
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
44#[non_exhaustive]
45pub struct SipTargetDialog {
46 call_id: String,
47 local_tag: String,
48 remote_tag: String,
49 params: Vec<(String, Option<String>)>,
50 uri_header_framing: bool,
51}
52
53impl SipTargetDialog {
54 pub fn parse(raw: &str) -> Result<Self, SipTargetDialogError> {
56 let id = parse_dialog_id(raw, "local-tag", "remote-tag", false)?;
57 Ok(Self {
58 call_id: id.call_id,
59 local_tag: id.first_tag,
60 remote_tag: id.second_tag,
61 params: id.params,
62 uri_header_framing: false,
63 })
64 }
65
66 pub fn parse_uri_header(raw: &str) -> Result<Self, SipTargetDialogError> {
73 let decoded = decode_uri_header_value(raw)?;
74 let mut parsed = Self::parse(&decoded)?;
75 parsed.uri_header_framing = true;
76 Ok(parsed)
77 }
78
79 pub fn call_id(&self) -> &str {
81 &self.call_id
82 }
83
84 pub fn with_call_id(
103 mut self,
104 call_id: impl Into<String>,
105 ) -> Result<Self, SipTargetDialogError> {
106 let call_id = call_id.into();
107 validate_call_id(&call_id)?;
108 self.call_id = call_id;
109 Ok(self)
110 }
111
112 pub fn host(&self) -> Option<&str> {
114 self.call_id
115 .split_once('@')
116 .map(|(_, host)| host)
117 }
118
119 pub fn local_tag(&self) -> &str {
121 &self.local_tag
122 }
123
124 pub fn remote_tag(&self) -> &str {
126 &self.remote_tag
127 }
128
129 pub fn params(&self) -> &[(String, Option<String>)] {
131 &self.params
132 }
133
134 pub fn param(&self, key: &str) -> Option<Option<&str>> {
136 let key_lower = key.to_ascii_lowercase();
137 self.params
138 .iter()
139 .find(|(k, _)| k == &key_lower)
140 .map(|(_, v)| v.as_deref())
141 }
142
143 fn wire_form(&self) -> String {
144 let mut s = format!(
145 "{};local-tag={};remote-tag={}",
146 self.call_id, self.local_tag, self.remote_tag
147 );
148 write_params(&mut s, &self.params);
149 s
150 }
151}
152
153impl fmt::Display for SipTargetDialog {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 let wire = self.wire_form();
156 if self.uri_header_framing {
157 f.write_str(&sip_uri::encode_uri_header(&wire))
158 } else {
159 f.write_str(&wire)
160 }
161 }
162}
163
164impl_from_str_via_parse!(SipTargetDialog, SipTargetDialogError);
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn parse_basic() {
172 let t = SipTargetDialog::parse("abc123@203.0.113.5;local-tag=l1;remote-tag=r1").unwrap();
173 assert_eq!(t.call_id(), "abc123@203.0.113.5");
174 assert_eq!(t.host(), Some("203.0.113.5"));
175 assert_eq!(t.local_tag(), "l1");
176 assert_eq!(t.remote_tag(), "r1");
177 }
178
179 #[test]
180 fn missing_local_tag_fails() {
181 assert!(SipTargetDialog::parse("abc@example.com;remote-tag=r1").is_err());
182 }
183
184 #[test]
185 fn missing_remote_tag_fails() {
186 assert!(SipTargetDialog::parse("abc@example.com;local-tag=l1").is_err());
187 }
188
189 #[test]
190 fn empty_fails() {
191 assert!(matches!(
192 SipTargetDialog::parse(""),
193 Err(SipTargetDialogError::Empty)
194 ));
195 }
196
197 #[test]
198 fn generic_params_preserved() {
199 let t =
200 SipTargetDialog::parse("abc@example.com;local-tag=l1;remote-tag=r1;foo=bar").unwrap();
201 assert_eq!(t.param("foo"), Some(Some("bar")));
202 }
203
204 #[test]
205 fn parse_uri_header_encoded() {
206 let t = SipTargetDialog::parse_uri_header(
207 "abc123%40203.0.113.5%3Blocal-tag%3Dl1%3Bremote-tag%3Dr1",
208 )
209 .unwrap();
210 assert_eq!(t.host(), Some("203.0.113.5"));
211 assert_eq!(t.local_tag(), "l1");
212 assert_eq!(t.remote_tag(), "r1");
213 }
214
215 #[test]
216 fn display_roundtrip_wire() {
217 let input = "abc123@203.0.113.5;local-tag=l1;remote-tag=r1;foo=bar";
218 let t = SipTargetDialog::parse(input).unwrap();
219 assert_eq!(t.to_string(), input);
220 assert_eq!(SipTargetDialog::parse(&t.to_string()).unwrap(), t);
221 }
222
223 #[test]
224 fn display_roundtrip_uri_header() {
225 let input = "abc123%40203.0.113.5%3Blocal-tag%3Dl1%3Bremote-tag%3Dr1";
226 let t = SipTargetDialog::parse_uri_header(input).unwrap();
227 assert_eq!(t.to_string(), input);
228 }
229
230 #[test]
231 fn with_call_id_wire_changes_only_call_id() {
232 let input = "abc123@203.0.113.5;local-tag=l1;remote-tag=r1;foo=bar";
233 let t = SipTargetDialog::parse(input)
234 .unwrap()
235 .with_call_id("xyz789@example.com")
236 .unwrap();
237 assert_eq!(
238 t.to_string(),
239 "xyz789@example.com;local-tag=l1;remote-tag=r1;foo=bar"
240 );
241 }
242
243 #[test]
244 fn with_call_id_keeps_uri_header_framing() {
245 let input = "abc123%40203.0.113.5%3Blocal-tag%3Dl1%3Bremote-tag%3Dr1";
246 let t = SipTargetDialog::parse_uri_header(input)
247 .unwrap()
248 .with_call_id("abc123@example.com")
249 .unwrap();
250 assert_eq!(
251 t.to_string(),
252 "abc123%40example.com%3Blocal-tag%3Dl1%3Bremote-tag%3Dr1"
253 );
254 }
255
256 #[test]
257 fn with_call_id_rejects_non_word() {
258 let t = SipTargetDialog::parse("abc@example.com;local-tag=l1;remote-tag=r1").unwrap();
259 for bad in ["", "a;local-tag=l2", "a b", "a@b@c", "@b"] {
260 assert!(
261 t.clone()
262 .with_call_id(bad)
263 .is_err(),
264 "accepted {bad:?}"
265 );
266 }
267 }
268
269 #[test]
270 fn from_str_is_wire_framing() {
271 let t: SipTargetDialog = "abc123@203.0.113.5;local-tag=l1;remote-tag=r1"
272 .parse()
273 .unwrap();
274 assert_eq!(t.local_tag(), "l1");
275 }
276}