1use std::fmt;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub struct SipSecurityMechanism {
11 mechanism: String,
12 params: Vec<(String, Option<String>)>,
13 quoted: Vec<bool>,
14}
15
16impl SipSecurityMechanism {
17 pub fn mechanism(&self) -> &str {
19 &self.mechanism
20 }
21
22 pub fn params(&self) -> &[(String, Option<String>)] {
24 &self.params
25 }
26
27 pub fn param(&self, key: &str) -> Option<Option<&str>> {
29 self.params
30 .iter()
31 .find(|(k, _)| k.eq_ignore_ascii_case(key))
32 .map(|(_, v)| v.as_deref())
33 }
34
35 pub fn q(&self) -> Option<&str> {
37 self.param("q")
38 .flatten()
39 }
40
41 pub fn d_alg(&self) -> Option<&str> {
43 self.param("d-alg")
44 .flatten()
45 }
46
47 pub fn d_qop(&self) -> Option<&str> {
49 self.param("d-qop")
50 .flatten()
51 }
52}
53
54impl fmt::Display for SipSecurityMechanism {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "{}", self.mechanism)?;
57 for ((key, value), quoted) in self
58 .params
59 .iter()
60 .zip(&self.quoted)
61 {
62 crate::write_param(f, key, value.as_deref(), *quoted)?;
63 }
64 Ok(())
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum SipSecurityError {
72 Empty,
74 InvalidFormat(String),
76}
77
78impl fmt::Display for SipSecurityError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Self::Empty => write!(f, "empty security mechanism value"),
82 Self::InvalidFormat(raw) => {
83 write!(f, "invalid security mechanism ({} bytes)", raw.len())
84 }
85 }
86 }
87}
88
89impl std::error::Error for SipSecurityError {}
90
91fn parse_mechanism(raw: &str) -> Result<SipSecurityMechanism, SipSecurityError> {
92 let raw = raw.trim();
93 if raw.is_empty() {
94 return Err(SipSecurityError::InvalidFormat(raw.to_string()));
95 }
96
97 let (mechanism_part, params_part) = match raw.split_once(';') {
98 Some((m, p)) => (m.trim(), Some(p)),
99 None => (raw, None),
100 };
101
102 if mechanism_part.is_empty() {
103 return Err(SipSecurityError::InvalidFormat(raw.to_string()));
104 }
105
106 let mechanism = mechanism_part.to_ascii_lowercase();
107 let (params, quoted) = crate::parse_params(params_part.unwrap_or(""))
108 .into_iter()
109 .map(|p| {
110 let (value, quoted) = p
111 .unquoted()
112 .map_or((None, false), |(v, q)| (Some(v), q));
113 (
114 (
115 p.key
116 .to_ascii_lowercase(),
117 value,
118 ),
119 quoted,
120 )
121 })
122 .unzip();
123
124 Ok(SipSecurityMechanism {
125 mechanism,
126 params,
127 quoted,
128 })
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
133#[non_exhaustive]
134pub struct SipSecurity(Vec<SipSecurityMechanism>);
135
136impl SipSecurity {
137 pub fn parse(raw: &str) -> Result<Self, SipSecurityError> {
139 let raw = raw.trim();
140 if raw.is_empty() {
141 return Err(SipSecurityError::Empty);
142 }
143 Self::from_entries(crate::split_comma_entries(raw))
144 }
145
146 pub fn from_entries<'a>(
148 entries: impl IntoIterator<Item = &'a str>,
149 ) -> Result<Self, SipSecurityError> {
150 let entries: Vec<_> = entries
151 .into_iter()
152 .map(parse_mechanism)
153 .collect::<Result<_, _>>()?;
154 if entries.is_empty() {
155 return Err(SipSecurityError::Empty);
156 }
157 Ok(Self(entries))
158 }
159
160 pub fn entries(&self) -> &[SipSecurityMechanism] {
162 &self.0
163 }
164
165 pub fn into_entries(self) -> Vec<SipSecurityMechanism> {
167 self.0
168 }
169
170 pub fn len(&self) -> usize {
172 self.0
173 .len()
174 }
175
176 pub fn is_empty(&self) -> bool {
178 self.0
179 .is_empty()
180 }
181}
182
183impl fmt::Display for SipSecurity {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 crate::fmt_joined(f, &self.0, ", ")
186 }
187}
188
189impl_from_str_via_parse!(SipSecurity, SipSecurityError);
190
191impl<'a> IntoIterator for &'a SipSecurity {
192 type Item = &'a SipSecurityMechanism;
193 type IntoIter = std::slice::Iter<'a, SipSecurityMechanism>;
194
195 fn into_iter(self) -> Self::IntoIter {
196 self.0
197 .iter()
198 }
199}
200
201impl IntoIterator for SipSecurity {
202 type Item = SipSecurityMechanism;
203 type IntoIter = std::vec::IntoIter<SipSecurityMechanism>;
204
205 fn into_iter(self) -> Self::IntoIter {
206 self.0
207 .into_iter()
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn single_mechanism() {
217 let sec = SipSecurity::parse("digest;d-qop=auth-int;q=0.1").unwrap();
218 assert_eq!(sec.len(), 1);
219 assert_eq!(sec.entries()[0].mechanism(), "digest");
220 assert_eq!(sec.entries()[0].d_qop(), Some("auth-int"));
221 assert_eq!(sec.entries()[0].q(), Some("0.1"));
222 }
223
224 #[test]
225 fn multiple_mechanisms() {
226 let sec = SipSecurity::parse("tls;q=0.2, digest;d-qop=auth;q=0.1").unwrap();
227 assert_eq!(sec.len(), 2);
228 assert_eq!(sec.entries()[0].mechanism(), "tls");
229 assert_eq!(sec.entries()[1].mechanism(), "digest");
230 }
231
232 #[test]
233 fn mechanism_no_params() {
234 let sec = SipSecurity::parse("tls").unwrap();
235 assert_eq!(sec.len(), 1);
236 assert_eq!(sec.entries()[0].mechanism(), "tls");
237 assert!(sec.entries()[0]
238 .params()
239 .is_empty());
240 }
241
242 #[test]
243 fn empty_input() {
244 assert!(matches!(
245 SipSecurity::parse(""),
246 Err(SipSecurityError::Empty)
247 ));
248 }
249
250 #[test]
251 fn display_roundtrip() {
252 let raw = "digest;d-qop=auth;q=0.1";
253 let sec = SipSecurity::parse(raw).unwrap();
254 assert_eq!(sec.to_string(), raw);
255 }
256
257 #[test]
258 fn from_str() {
259 let sec: SipSecurity = "tls;q=0.2"
260 .parse()
261 .unwrap();
262 assert_eq!(sec.len(), 1);
263 }
264
265 #[test]
266 fn d_alg_param() {
267 let sec = SipSecurity::parse("digest;d-alg=MD5;d-qop=auth").unwrap();
268 assert_eq!(sec.entries()[0].d_alg(), Some("MD5"));
269 }
270
271 #[test]
272 fn from_entries_matches_parse() {
273 let split = SipSecurity::from_entries(["tls;q=0.2", "digest;d-qop=auth;q=0.1"]).unwrap();
274 let joined = SipSecurity::parse("tls;q=0.2, digest;d-qop=auth;q=0.1").unwrap();
275 assert_eq!(split, joined);
276 }
277
278 #[test]
279 fn from_entries_bad_entry_is_error() {
280 assert!(matches!(
281 SipSecurity::from_entries(["tls", " "]),
282 Err(SipSecurityError::InvalidFormat(_))
283 ));
284 }
285
286 #[test]
287 fn from_entries_empty_is_empty_error() {
288 assert!(matches!(
289 SipSecurity::from_entries(std::iter::empty::<&str>()),
290 Err(SipSecurityError::Empty)
291 ));
292 }
293}
294
295#[cfg(test)]
296mod param_tests {
297 use super::*;
298
299 #[test]
300 fn d_ver_stays_quoted_on_display() {
301 let input = r#"ipsec-3gpp;d-ver="0000000000000000000000000000abcd";q=0.1"#;
302 let sec = SipSecurity::parse(input).unwrap();
303 assert_eq!(
304 sec.entries()[0].param("d-ver"),
305 Some(Some("0000000000000000000000000000abcd"))
306 );
307 assert_eq!(sec.to_string(), input);
308 }
309
310 #[test]
311 fn quoted_value_keeps_semicolon() {
312 let sec = SipSecurity::parse(r#"digest;x="a;b";q=0.5"#).unwrap();
313 let mech = &sec.entries()[0];
314 assert_eq!(mech.param("x"), Some(Some("a;b")));
315 assert_eq!(mech.q(), Some("0.5"));
316 }
317
318 #[test]
319 fn quoted_pair_unescaped() {
320 let sec = SipSecurity::parse(r#"digest;x="a\"b""#).unwrap();
321 assert_eq!(sec.entries()[0].param("x"), Some(Some(r#"a"b"#)));
322 assert_eq!(sec.to_string(), r#"digest;x="a\"b""#);
323 }
324
325 #[test]
326 fn error_display_omits_input() {
327 let err = SipSecurity::parse(";secret-value").unwrap_err();
328 assert!(!err
329 .to_string()
330 .contains("secret-value"));
331 }
332}