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