Skip to main content

sip_uri/
name_addr.rs

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/// A SIP name-addr: optional display name with a URI.
11///
12/// Parses the following forms:
13///
14/// - `"Display Name" <sip:user@host>`
15/// - `<sip:user@host>`
16/// - `sip:user@host` (bare URI, no display name)
17///
18/// **Deprecated since 0.2.0, will be removed in 0.3.0.**
19///
20/// The `name-addr` production (RFC 3261 ยง25.1) appears inside SIP header
21/// fields like `From`, `To`, `Contact`, and `Refer-To`, where it is
22/// followed by header-level parameters (`;tag=`, `;expires=`, etc.).
23/// This type rejects those parameters, so it cannot round-trip real SIP
24/// header values.
25///
26/// # Migration
27///
28/// - **Full SIP header parsing**: use
29///   [`SipHeaderAddr`](https://docs.rs/sip-header/latest/sip_header/struct.SipHeaderAddr.html)
30///   from the [`sip-header`](https://crates.io/crates/sip-header)
31///   crate, which handles display names, URIs, and header-level parameters.
32/// - **URI only**: parse directly with [`Uri`](crate::Uri).
33#[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    /// Create a new NameAddr with the given URI and no display name.
47    pub fn new(uri: Uri) -> Self {
48        NameAddr {
49            display_name: None,
50            uri,
51        }
52    }
53
54    /// Set the display name.
55    pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
56        self.display_name = Some(name.into());
57        self
58    }
59
60    /// The display name, if present.
61    pub fn display_name(&self) -> Option<&str> {
62        self.display_name
63            .as_deref()
64    }
65
66    /// The URI.
67    pub fn uri(&self) -> &Uri {
68        &self.uri
69    }
70
71    /// If the URI is a SIP/SIPS URI, return a reference to it.
72    pub fn sip_uri(&self) -> Option<&SipUri> {
73        self.uri
74            .as_sip()
75    }
76
77    /// If the URI is a tel: URI, return a reference to it.
78    pub fn tel_uri(&self) -> Option<&TelUri> {
79        self.uri
80            .as_tel()
81    }
82
83    /// If the URI is a URN, return a reference to it.
84    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        // Case 1: quoted display name followed by <URI>
103        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        // Case 2: <URI> without display name
119        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        // Case 3: unquoted display name followed by <URI>
130        // or bare URI without angle brackets
131        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        // Case 4: bare URI (no angle brackets, no display name)
146        let uri: Uri = s.parse()?;
147        Ok(NameAddr {
148            display_name: None,
149            uri,
150        })
151    }
152}
153
154/// Reject any non-whitespace content after `>`.
155///
156/// Header-level parameters (`;tag=`, `;serviceurn=`, etc.) are part of the
157/// SIP header field grammar, not `name-addr`. Callers must split those off
158/// before parsing.
159fn 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
171/// Extract the URI from between `<` and `>`, returning (uri, rest_after_`>`).
172fn 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
178/// Parse a quoted string and return (unescaped content, rest of input after closing quote).
179fn 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                // +2: skip opening quote + position after closing quote
191                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
230/// Check if a display name needs quoting.
231///
232/// Needs quoting if it contains special chars or whitespace (since
233/// unquoted tokens can't contain spaces per the SIP grammar).
234fn 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
244/// Escape a display name for use within double quotes.
245fn 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}