Skip to main content

sip_uri/
uri.rs

1use std::fmt;
2use std::str::FromStr;
3
4use crate::error::ParseUriError;
5use crate::sip_uri::SipUri;
6use crate::tel_uri::TelUri;
7use crate::urn_uri::UrnUri;
8
9/// A parsed URI: SIP/SIPS, tel, URN, or an opaque URI with an unrecognized scheme.
10///
11/// The `Other` variant stores the raw URI string for schemes this crate does
12/// not parse (e.g. `http:`, `https:`, `data:`). This allows SIP header values
13/// like `Call-Info` to round-trip without rejecting non-SIP URIs.
14#[derive(Debug, Clone, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum Uri {
17    /// SIP or SIPS URI.
18    Sip(SipUri),
19    /// tel: URI.
20    Tel(TelUri),
21    /// URN (Uniform Resource Name).
22    Urn(UrnUri),
23    /// URI with an unrecognized scheme, stored as-is.
24    Other(String),
25}
26
27impl Uri {
28    /// If this is a SIP/SIPS URI, return a reference to it.
29    pub fn as_sip(&self) -> Option<&SipUri> {
30        match self {
31            Uri::Sip(u) => Some(u),
32            _ => None,
33        }
34    }
35
36    /// If this is a tel: URI, return a reference to it.
37    pub fn as_tel(&self) -> Option<&TelUri> {
38        match self {
39            Uri::Tel(u) => Some(u),
40            _ => None,
41        }
42    }
43
44    /// If this is a URN, return a reference to it.
45    pub fn as_urn(&self) -> Option<&UrnUri> {
46        match self {
47            Uri::Urn(u) => Some(u),
48            _ => None,
49        }
50    }
51
52    /// If this is an unrecognized scheme, return the raw URI string.
53    pub fn as_other(&self) -> Option<&str> {
54        match self {
55            Uri::Other(s) => Some(s),
56            _ => None,
57        }
58    }
59
60    /// Consume and return the inner SIP/SIPS URI, if any.
61    pub fn into_sip(self) -> Option<SipUri> {
62        match self {
63            Uri::Sip(u) => Some(u),
64            _ => None,
65        }
66    }
67
68    /// Consume and return the inner tel: URI, if any.
69    pub fn into_tel(self) -> Option<TelUri> {
70        match self {
71            Uri::Tel(u) => Some(u),
72            _ => None,
73        }
74    }
75
76    /// Consume and return the inner URN, if any.
77    pub fn into_urn(self) -> Option<UrnUri> {
78        match self {
79            Uri::Urn(u) => Some(u),
80            _ => None,
81        }
82    }
83
84    /// Consume and return the raw URI string for an unrecognized scheme.
85    pub fn into_other(self) -> Option<String> {
86        match self {
87            Uri::Other(s) => Some(s),
88            _ => None,
89        }
90    }
91
92    /// The scheme of this URI (lowercase).
93    pub fn scheme(&self) -> &str {
94        match self {
95            Uri::Sip(u) => match u.scheme() {
96                crate::Scheme::Sip => "sip",
97                crate::Scheme::Sips => "sips",
98            },
99            Uri::Tel(_) => "tel",
100            Uri::Urn(_) => "urn",
101            // Uri::Other is only constructed by FromStr, which requires ':'
102            Uri::Other(s) => s
103                .find(':')
104                .map(|i| &s[..i])
105                .unwrap_or(s),
106        }
107    }
108}
109
110impl From<SipUri> for Uri {
111    fn from(u: SipUri) -> Self {
112        Uri::Sip(u)
113    }
114}
115
116impl From<TelUri> for Uri {
117    fn from(u: TelUri) -> Self {
118        Uri::Tel(u)
119    }
120}
121
122impl From<UrnUri> for Uri {
123    fn from(u: UrnUri) -> Self {
124        Uri::Urn(u)
125    }
126}
127
128impl FromStr for Uri {
129    type Err = ParseUriError;
130
131    fn from_str(s: &str) -> Result<Self, Self::Err> {
132        if s == "*" {
133            return Err(ParseUriError(
134                "wildcard '*' is not a URI; handle it at the protocol layer (Contact: * or OPTIONS * SIP/2.0)".into(),
135            ));
136        }
137
138        // Detect scheme by scanning to first `:`
139        let colon = s
140            .find(':')
141            .ok_or_else(|| ParseUriError("missing scheme".into()))?;
142        let scheme = &s[..colon];
143
144        if scheme.eq_ignore_ascii_case("tel") {
145            Ok(Uri::Tel(s.parse::<TelUri>()?))
146        } else if scheme.eq_ignore_ascii_case("sip") || scheme.eq_ignore_ascii_case("sips") {
147            Ok(Uri::Sip(s.parse::<SipUri>()?))
148        } else if scheme.eq_ignore_ascii_case("urn") {
149            Ok(Uri::Urn(s.parse::<UrnUri>()?))
150        } else {
151            Ok(Uri::Other(s.to_string()))
152        }
153    }
154}
155
156impl fmt::Display for Uri {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        match self {
159            Uri::Sip(u) => write!(f, "{u}"),
160            Uri::Tel(u) => write!(f, "{u}"),
161            Uri::Urn(u) => write!(f, "{u}"),
162            Uri::Other(s) => write!(f, "{s}"),
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn dispatch_sip() {
173        let uri: Uri = "sip:alice@example.com"
174            .parse()
175            .unwrap();
176        assert!(uri
177            .as_sip()
178            .is_some());
179        assert!(uri
180            .as_tel()
181            .is_none());
182        assert!(uri
183            .as_urn()
184            .is_none());
185    }
186
187    #[test]
188    fn dispatch_sips() {
189        let uri: Uri = "sips:bob@secure.example.com"
190            .parse()
191            .unwrap();
192        assert!(uri
193            .as_sip()
194            .is_some());
195    }
196
197    #[test]
198    fn dispatch_tel() {
199        let uri: Uri = "tel:+15551234567"
200            .parse()
201            .unwrap();
202        assert!(uri
203            .as_tel()
204            .is_some());
205        assert!(uri
206            .as_sip()
207            .is_none());
208    }
209
210    #[test]
211    fn dispatch_urn() {
212        let uri: Uri = "urn:service:sos"
213            .parse()
214            .unwrap();
215        assert!(uri
216            .as_urn()
217            .is_some());
218        assert!(uri
219            .as_sip()
220            .is_none());
221        assert!(uri
222            .as_tel()
223            .is_none());
224    }
225
226    #[test]
227    fn unknown_scheme_stored_as_other() {
228        let uri: Uri = "http://example.com"
229            .parse()
230            .unwrap();
231        assert_eq!(uri.as_other(), Some("http://example.com"));
232        assert_eq!(uri.scheme(), "http");
233        assert!(uri
234            .as_sip()
235            .is_none());
236        assert!(uri
237            .as_tel()
238            .is_none());
239        assert!(uri
240            .as_urn()
241            .is_none());
242    }
243
244    #[test]
245    fn other_display_roundtrip() {
246        let input = "https://example.com/photo.jpg";
247        let uri: Uri = input
248            .parse()
249            .unwrap();
250        assert_eq!(uri.to_string(), input);
251    }
252
253    #[test]
254    fn missing_scheme_fails() {
255        assert!("no-colon-here"
256            .parse::<Uri>()
257            .is_err());
258    }
259
260    #[test]
261    fn display_roundtrip() {
262        let input = "sip:alice@example.com;transport=tcp";
263        let uri: Uri = input
264            .parse()
265            .unwrap();
266        assert_eq!(uri.to_string(), input);
267    }
268
269    #[test]
270    fn display_roundtrip_urn() {
271        let input = "urn:service:sos";
272        let uri: Uri = input
273            .parse()
274            .unwrap();
275        assert_eq!(uri.to_string(), input);
276    }
277
278    #[test]
279    fn from_sip_uri() {
280        let sip: SipUri = "sip:alice@example.com"
281            .parse()
282            .unwrap();
283        let uri: Uri = sip.into();
284        assert!(uri
285            .as_sip()
286            .is_some());
287    }
288
289    #[test]
290    fn from_urn_uri() {
291        let urn: UrnUri = "urn:service:sos"
292            .parse()
293            .unwrap();
294        let uri: Uri = urn.into();
295        assert!(uri
296            .as_urn()
297            .is_some());
298    }
299}