1use std::fmt;
2use std::str::FromStr;
3
4use crate::error::ParseNameAddrError;
5use crate::sip_uri::SipUri;
6use crate::tel_uri::TelUri;
7use crate::uri::Uri;
8use crate::urn_uri::UrnUri;
9
10#[deprecated(
34 since = "0.2.0",
35 note = "name-addr is header-level grammar; use sip_header::SipHeaderAddr or parse the URI with sip_uri::Uri directly"
36)]
37#[derive(Debug, Clone, PartialEq, Eq)]
38#[non_exhaustive]
39pub struct NameAddr {
40 display_name: Option<String>,
41 uri: Uri,
42}
43
44#[allow(deprecated)]
45impl NameAddr {
46 pub fn new(uri: Uri) -> Self {
48 NameAddr {
49 display_name: None,
50 uri,
51 }
52 }
53
54 pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
56 self.display_name = Some(name.into());
57 self
58 }
59
60 pub fn display_name(&self) -> Option<&str> {
62 self.display_name
63 .as_deref()
64 }
65
66 pub fn uri(&self) -> &Uri {
68 &self.uri
69 }
70
71 pub fn sip_uri(&self) -> Option<&SipUri> {
73 self.uri
74 .as_sip()
75 }
76
77 pub fn tel_uri(&self) -> Option<&TelUri> {
79 self.uri
80 .as_tel()
81 }
82
83 pub fn urn_uri(&self) -> Option<&UrnUri> {
85 self.uri
86 .as_urn()
87 }
88}
89
90#[allow(deprecated)]
91impl FromStr for NameAddr {
92 type Err = ParseNameAddrError;
93
94 fn from_str(input: &str) -> Result<Self, Self::Err> {
95 let err = |msg: &str| ParseNameAddrError(msg.to_string());
96 let s = input.trim();
97
98 if s.is_empty() {
99 return Err(err("empty input"));
100 }
101
102 if s.starts_with('"') {
104 let (display_name, rest) = parse_quoted_string(s).map_err(|e| err(&e))?;
105 let rest = rest.trim_start();
106 let (uri_str, trailing) = extract_angle_uri(rest)
107 .ok_or_else(|| err("expected '<URI>' after quoted display name"))?;
108 reject_trailing(trailing)?;
109 let uri: Uri = uri_str.parse()?;
110 let display_name = if display_name.is_empty() {
111 None
112 } else {
113 Some(display_name)
114 };
115 return Ok(NameAddr { display_name, uri });
116 }
117
118 if s.starts_with('<') {
120 let (uri_str, trailing) = extract_angle_uri(s).ok_or_else(|| err("unclosed '<'"))?;
121 reject_trailing(trailing)?;
122 let uri: Uri = uri_str.parse()?;
123 return Ok(NameAddr {
124 display_name: None,
125 uri,
126 });
127 }
128
129 if let Some(angle_start) = s.find('<') {
132 let display_name = s[..angle_start].trim();
133 let display_name = if display_name.is_empty() {
134 None
135 } else {
136 Some(display_name.to_string())
137 };
138 let (uri_str, trailing) =
139 extract_angle_uri(&s[angle_start..]).ok_or_else(|| err("unclosed '<'"))?;
140 reject_trailing(trailing)?;
141 let uri: Uri = uri_str.parse()?;
142 return Ok(NameAddr { display_name, uri });
143 }
144
145 let uri: Uri = s.parse()?;
147 Ok(NameAddr {
148 display_name: None,
149 uri,
150 })
151 }
152}
153
154fn reject_trailing(s: &str) -> Result<(), ParseNameAddrError> {
160 let trimmed = s.trim();
161 if trimmed.is_empty() {
162 Ok(())
163 } else {
164 Err(ParseNameAddrError(format!(
165 "trailing content after '>': \"{trimmed}\" \
166 (header-level parameters belong in SIP header parsing, not name-addr)"
167 )))
168 }
169}
170
171fn extract_angle_uri(s: &str) -> Option<(&str, &str)> {
173 let s = s.strip_prefix('<')?;
174 let end = s.find('>')?;
175 Some((&s[..end], &s[end + 1..]))
176}
177
178fn parse_quoted_string(s: &str) -> Result<(String, &str), String> {
180 if !s.starts_with('"') {
181 return Err("expected opening quote".into());
182 }
183
184 let mut result = String::new();
185 let mut chars = s[1..].char_indices();
186
187 while let Some((i, c)) = chars.next() {
188 match c {
189 '"' => {
190 return Ok((result, &s[i + 2..]));
192 }
193 '\\' => {
194 let (_, escaped) = chars
195 .next()
196 .ok_or("unterminated escape in quoted string")?;
197 result.push(escaped);
198 }
199 _ => {
200 result.push(c);
201 }
202 }
203 }
204
205 Err("unterminated quoted string".into())
206}
207
208#[allow(deprecated)]
209impl fmt::Display for NameAddr {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self
212 .display_name
213 .as_deref()
214 {
215 Some(name) if !name.is_empty() => {
216 if needs_quoting(name) {
217 write!(f, "\"{}\" ", escape_display_name(name))?;
218 } else {
219 write!(f, "{name} ")?;
220 }
221 write!(f, "<{}>", self.uri)
222 }
223 _ => {
224 write!(f, "<{}>", self.uri)
225 }
226 }
227 }
228}
229
230fn needs_quoting(name: &str) -> bool {
235 name.bytes()
236 .any(|b| {
237 matches!(
238 b,
239 b'"' | b'\\' | b'<' | b'>' | b',' | b';' | b':' | b'@' | b' ' | b'\t'
240 )
241 })
242}
243
244fn escape_display_name(name: &str) -> String {
246 let mut out = String::with_capacity(name.len());
247 for c in name.chars() {
248 if matches!(c, '"' | '\\') {
249 out.push('\\');
250 }
251 out.push(c);
252 }
253 out
254}
255
256#[cfg(test)]
257#[allow(deprecated)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn parse_quoted_display_name() {
263 let na: NameAddr =
264 r#""EXAMPLE CO" <sip:+15551234567;cpc=emergency;oli=0@198.51.100.1;user=phone>"#
265 .parse()
266 .unwrap();
267 assert_eq!(na.display_name(), Some("EXAMPLE CO"));
268 let sip = na
269 .sip_uri()
270 .unwrap();
271 assert_eq!(sip.user(), Some("+15551234567"));
272 assert_eq!(
273 sip.user_params(),
274 &[
275 ("cpc".into(), Some("emergency".into())),
276 ("oli".into(), Some("0".into())),
277 ]
278 );
279 assert_eq!(sip.param("user"), Some(&Some("phone".into())));
280 }
281
282 #[test]
283 fn parse_angle_brackets_no_name() {
284 let na: NameAddr = "<sip:1305@pbx.example.com;user=phone>"
285 .parse()
286 .unwrap();
287 assert_eq!(na.display_name(), None);
288 assert!(na
289 .sip_uri()
290 .is_some());
291 }
292
293 #[test]
294 fn reject_trailing_params_after_angle_bracket() {
295 let cases = [
296 "<sip:user@example.com>;tag=abc123",
297 "<sip:user@example.com>;expires=3600;foo=bar",
298 "<sip:user@example.com> trailing",
299 ];
300 for input in cases {
301 assert!(
302 input
303 .parse::<NameAddr>()
304 .is_err(),
305 "should reject trailing content: {input}",
306 );
307 }
308 }
309
310 #[test]
311 fn reject_trailing_params_after_quoted_name() {
312 assert!(r#""Alice" <sip:alice@example.com>;expires=3600"#
313 .parse::<NameAddr>()
314 .is_err());
315 }
316
317 #[test]
318 fn reject_trailing_params_after_unquoted_name() {
319 assert!("Alice <sip:alice@example.com>;tag=xyz"
320 .parse::<NameAddr>()
321 .is_err());
322 }
323
324 #[test]
325 fn parse_bare_uri() {
326 let na: NameAddr = "sip:alice@example.com"
327 .parse()
328 .unwrap();
329 assert_eq!(na.display_name(), None);
330 assert!(na
331 .sip_uri()
332 .is_some());
333 }
334
335 #[test]
336 fn parse_tel_in_angle_brackets() {
337 let na: NameAddr = "<tel:+15551234567;cpc=emergency>"
338 .parse()
339 .unwrap();
340 assert_eq!(na.display_name(), None);
341 let tel = na
342 .tel_uri()
343 .unwrap();
344 assert_eq!(tel.number(), "+15551234567");
345 }
346
347 #[test]
348 fn parse_unquoted_display_name() {
349 let na: NameAddr = "Alice <sip:alice@example.com>"
350 .parse()
351 .unwrap();
352 assert_eq!(na.display_name(), Some("Alice"));
353 }
354
355 #[test]
356 fn parse_escaped_quotes_in_display_name() {
357 let na: NameAddr = r#""Say \"Hello\"" <sip:u@h>"#
358 .parse()
359 .unwrap();
360 assert_eq!(na.display_name(), Some(r#"Say "Hello""#));
361 }
362
363 #[test]
364 fn display_roundtrip_with_name() {
365 let na: NameAddr = r#""EXAMPLE CO" <sip:+15551234567@198.51.100.1;user=phone>"#
366 .parse()
367 .unwrap();
368 assert_eq!(
369 na.to_string(),
370 r#""EXAMPLE CO" <sip:+15551234567@198.51.100.1;user=phone>"#
371 );
372 }
373
374 #[test]
375 fn display_no_name() {
376 let na: NameAddr = "<sip:alice@example.com>"
377 .parse()
378 .unwrap();
379 assert_eq!(na.to_string(), "<sip:alice@example.com>");
380 }
381
382 #[test]
383 fn builder() {
384 let uri: Uri = "sip:alice@example.com"
385 .parse()
386 .unwrap();
387 let na = NameAddr::new(uri).with_display_name("Alice");
388 assert_eq!(na.to_string(), "Alice <sip:alice@example.com>");
389 }
390
391 #[test]
392 fn empty_input_fails() {
393 assert!(""
394 .parse::<NameAddr>()
395 .is_err());
396 }
397}