Skip to main content

sip_header/
geolocation.rs

1//! SIP Geolocation header types (RFC 6442).
2
3use std::fmt;
4
5/// A reference extracted from a SIP Geolocation header (RFC 6442).
6///
7/// Each entry is either a `cid:` reference to a MIME body part
8/// (typically containing PIDF-LO XML) or a URL for location dereference.
9///
10/// This crate only parses the header references themselves. Resolving
11/// `cid:` references against the SIP message body (multipart MIME) or
12/// dereferencing HTTP URLs is the caller's responsibility.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum SipGeolocationRef {
15    /// Content-ID reference to a MIME body part (e.g., `cid:uuid`).
16    Cid(String),
17    /// HTTP(S) or other URL for location dereference.
18    Url(String),
19}
20
21impl fmt::Display for SipGeolocationRef {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Self::Cid(id) => write!(f, "<cid:{id}>"),
25            Self::Url(url) => write!(f, "<{url}>"),
26        }
27    }
28}
29
30/// Parsed SIP Geolocation header value (RFC 6442).
31///
32/// Contains one or more `<uri>` references, comma-separated. Each reference
33/// is classified as either a `cid:` body-part reference or a dereference URL.
34///
35/// ```
36/// use sip_header::SipGeolocation;
37///
38/// let raw = "<cid:abc-123>, <https://lis.example.com/held/abc>";
39/// let geo = SipGeolocation::parse(raw);
40/// assert_eq!(geo.len(), 2);
41/// assert!(geo.cid().is_some());
42/// assert!(geo.url().is_some());
43/// ```
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct SipGeolocation(Vec<SipGeolocationRef>);
46
47impl SipGeolocation {
48    /// Parse a raw Geolocation header value into typed references.
49    pub fn parse(raw: &str) -> Self {
50        let refs = raw
51            .split(',')
52            .filter_map(|entry| {
53                let entry = entry.trim();
54                let inner = entry
55                    .strip_prefix('<')?
56                    .strip_suffix('>')?;
57                if inner.is_empty() {
58                    return None;
59                }
60                if let Some(id) = inner.strip_prefix("cid:") {
61                    Some(SipGeolocationRef::Cid(id.to_string()))
62                } else {
63                    Some(SipGeolocationRef::Url(inner.to_string()))
64                }
65            })
66            .collect();
67        Self(refs)
68    }
69
70    /// The parsed references as a slice.
71    pub fn refs(&self) -> &[SipGeolocationRef] {
72        &self.0
73    }
74
75    /// Number of references.
76    pub fn len(&self) -> usize {
77        self.0
78            .len()
79    }
80
81    /// Returns `true` if there are no references.
82    pub fn is_empty(&self) -> bool {
83        self.0
84            .is_empty()
85    }
86
87    /// The first `cid:` reference, if any.
88    pub fn cid(&self) -> Option<&str> {
89        self.0
90            .iter()
91            .find_map(|r| match r {
92                SipGeolocationRef::Cid(id) => Some(id.as_str()),
93                _ => None,
94            })
95    }
96
97    /// The first URL reference, if any.
98    pub fn url(&self) -> Option<&str> {
99        self.0
100            .iter()
101            .find_map(|r| match r {
102                SipGeolocationRef::Url(url) => Some(url.as_str()),
103                _ => None,
104            })
105    }
106
107    /// Iterate over all `cid:` references.
108    pub fn cids(&self) -> impl Iterator<Item = &str> {
109        self.0
110            .iter()
111            .filter_map(|r| match r {
112                SipGeolocationRef::Cid(id) => Some(id.as_str()),
113                _ => None,
114            })
115    }
116
117    /// Iterate over all URL references.
118    pub fn urls(&self) -> impl Iterator<Item = &str> {
119        self.0
120            .iter()
121            .filter_map(|r| match r {
122                SipGeolocationRef::Url(url) => Some(url.as_str()),
123                _ => None,
124            })
125    }
126}
127
128impl fmt::Display for SipGeolocation {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        crate::fmt_joined(f, &self.0, ", ")
131    }
132}
133
134impl<'a> IntoIterator for &'a SipGeolocation {
135    type Item = &'a SipGeolocationRef;
136    type IntoIter = std::slice::Iter<'a, SipGeolocationRef>;
137
138    fn into_iter(self) -> Self::IntoIter {
139        self.0
140            .iter()
141    }
142}
143
144impl IntoIterator for SipGeolocation {
145    type Item = SipGeolocationRef;
146    type IntoIter = std::vec::IntoIter<SipGeolocationRef>;
147
148    fn into_iter(self) -> Self::IntoIter {
149        self.0
150            .into_iter()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn parse_cid_and_url() {
160        let raw = "<cid:32863354-18b4-4069-bd00-7bced5fc6c9b>, <https://lis.example.com/api/v1/held/test>";
161        let geo = SipGeolocation::parse(raw);
162        assert_eq!(geo.len(), 2);
163        assert_eq!(geo.cid(), Some("32863354-18b4-4069-bd00-7bced5fc6c9b"));
164        assert!(geo
165            .url()
166            .unwrap()
167            .contains("lis.example.com"));
168    }
169
170    #[test]
171    fn single_cid() {
172        let geo = SipGeolocation::parse("<cid:abc-123>");
173        assert_eq!(geo.len(), 1);
174        assert_eq!(geo.cid(), Some("abc-123"));
175        assert!(geo
176            .url()
177            .is_none());
178    }
179
180    #[test]
181    fn single_url() {
182        let geo = SipGeolocation::parse("<https://lis.example.com/location>");
183        assert_eq!(geo.len(), 1);
184        assert!(geo
185            .cid()
186            .is_none());
187        assert_eq!(geo.url(), Some("https://lis.example.com/location"));
188    }
189
190    #[test]
191    fn empty_input() {
192        let geo = SipGeolocation::parse("");
193        assert!(geo.is_empty());
194    }
195
196    #[test]
197    fn empty_brackets_skipped() {
198        let geo = SipGeolocation::parse("<>, <cid:test>");
199        assert_eq!(geo.len(), 1);
200        assert_eq!(geo.cid(), Some("test"));
201    }
202
203    #[test]
204    fn display_roundtrip() {
205        let raw = "<cid:abc-123>, <https://lis.example.com/test>";
206        let geo = SipGeolocation::parse(raw);
207        assert_eq!(geo.to_string(), raw);
208    }
209
210    #[test]
211    fn multiple_cids() {
212        let raw = "<cid:first>, <cid:second>, <https://example.com/loc>";
213        let geo = SipGeolocation::parse(raw);
214        let cids: Vec<_> = geo
215            .cids()
216            .collect();
217        assert_eq!(cids, vec!["first", "second"]);
218        let urls: Vec<_> = geo
219            .urls()
220            .collect();
221        assert_eq!(urls, vec!["https://example.com/loc"]);
222    }
223}