1use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum SipWarningError {
9 Empty,
11 InvalidFormat(String),
13}
14
15impl fmt::Display for SipWarningError {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 SipWarningError::Empty => write!(f, "empty Warning header"),
19 SipWarningError::InvalidFormat(msg) => write!(f, "invalid Warning format: {}", msg),
20 }
21 }
22}
23
24impl std::error::Error for SipWarningError {}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
36#[non_exhaustive]
37pub struct SipWarningEntry {
38 code: u16,
39 agent: String,
40 text: String,
41}
42
43impl SipWarningEntry {
44 pub fn code(&self) -> u16 {
46 self.code
47 }
48
49 pub fn agent(&self) -> &str {
51 &self.agent
52 }
53
54 pub fn text(&self) -> &str {
56 &self.text
57 }
58
59 fn parse(s: &str) -> Result<Self, SipWarningError> {
60 let s = s.trim();
61 if s.is_empty() {
62 return Err(SipWarningError::InvalidFormat(
63 "empty warning entry".to_string(),
64 ));
65 }
66
67 let space_pos = s
69 .find(' ')
70 .ok_or_else(|| {
71 SipWarningError::InvalidFormat("missing space after warn-code".to_string())
72 })?;
73
74 let code_str = &s[..space_pos];
75 if code_str.len() != 3
76 || !code_str
77 .chars()
78 .all(|c| c.is_ascii_digit())
79 {
80 return Err(SipWarningError::InvalidFormat(format!(
81 "warn-code must be 3 digits, got '{}'",
82 code_str
83 )));
84 }
85
86 let code = code_str
87 .parse::<u16>()
88 .map_err(|_| {
89 SipWarningError::InvalidFormat(format!("invalid warn-code '{}'", code_str))
90 })?;
91
92 let rest = s[space_pos..].trim_start();
93
94 let quote_pos = rest
96 .find('"')
97 .ok_or_else(|| {
98 SipWarningError::InvalidFormat("missing quoted warn-text".to_string())
99 })?;
100
101 if quote_pos == 0 {
102 return Err(SipWarningError::InvalidFormat(
103 "missing warn-agent".to_string(),
104 ));
105 }
106
107 let agent = rest[..quote_pos]
108 .trim_end()
109 .to_string();
110 if agent.is_empty() {
111 return Err(SipWarningError::InvalidFormat(
112 "empty warn-agent".to_string(),
113 ));
114 }
115
116 let text = parse_quoted_string(&rest[quote_pos..])?;
118
119 Ok(SipWarningEntry { code, agent, text })
120 }
121}
122
123impl fmt::Display for SipWarningEntry {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 write!(
126 f,
127 "{:03} {} \"{}\"",
128 self.code,
129 self.agent,
130 crate::escape_quoted_pair(&self.text)
131 )
132 }
133}
134
135fn parse_quoted_string(s: &str) -> Result<String, SipWarningError> {
137 let s = s.trim_start();
138 if !s.starts_with('"') {
139 return Err(SipWarningError::InvalidFormat(
140 "quoted string must start with '\"'".to_string(),
141 ));
142 }
143
144 let content = &s[1..];
146 let mut escaped = false;
147 for (i, c) in content.char_indices() {
148 if escaped {
149 escaped = false;
150 } else if c == '\\' {
151 escaped = true;
152 } else if c == '"' {
153 return Ok(crate::unescape_quoted_pair(&content[..i]));
154 }
155 }
156
157 Err(SipWarningError::InvalidFormat(
158 "unterminated quoted string".to_string(),
159 ))
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
169#[non_exhaustive]
170pub struct SipWarning {
171 entries: Vec<SipWarningEntry>,
172}
173
174impl SipWarning {
175 pub fn parse(raw: &str) -> Result<Self, SipWarningError> {
177 let raw = raw.trim();
178 if raw.is_empty() {
179 return Err(SipWarningError::Empty);
180 }
181 Self::from_entries(crate::split_comma_entries(raw))
182 }
183
184 pub fn from_entries<'a>(
186 entries: impl IntoIterator<Item = &'a str>,
187 ) -> Result<Self, SipWarningError> {
188 let entries = entries
189 .into_iter()
190 .map(SipWarningEntry::parse)
191 .collect::<Result<Vec<_>, _>>()?;
192 if entries.is_empty() {
193 return Err(SipWarningError::Empty);
194 }
195 Ok(SipWarning { entries })
196 }
197
198 pub fn entries(&self) -> &[SipWarningEntry] {
200 &self.entries
201 }
202
203 pub fn into_entries(self) -> Vec<SipWarningEntry> {
205 self.entries
206 }
207
208 pub fn len(&self) -> usize {
210 self.entries
211 .len()
212 }
213
214 pub fn is_empty(&self) -> bool {
216 self.entries
217 .is_empty()
218 }
219}
220
221impl fmt::Display for SipWarning {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 crate::fmt_joined(f, &self.entries, ", ")
224 }
225}
226
227impl_from_str_via_parse!(SipWarning, SipWarningError);
228
229impl IntoIterator for SipWarning {
230 type Item = SipWarningEntry;
231 type IntoIter = std::vec::IntoIter<SipWarningEntry>;
232
233 fn into_iter(self) -> Self::IntoIter {
234 self.entries
235 .into_iter()
236 }
237}
238
239impl<'a> IntoIterator for &'a SipWarning {
240 type Item = &'a SipWarningEntry;
241 type IntoIter = std::slice::Iter<'a, SipWarningEntry>;
242
243 fn into_iter(self) -> Self::IntoIter {
244 self.entries
245 .iter()
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_single_warning() {
255 let input = r#"301 example.com "Incompatible network protocol""#;
256 let warning = SipWarning::parse(input).unwrap();
257 assert_eq!(warning.len(), 1);
258 let entry = &warning.entries()[0];
259 assert_eq!(entry.code(), 301);
260 assert_eq!(entry.agent(), "example.com");
261 assert_eq!(entry.text(), "Incompatible network protocol");
262 }
263
264 #[test]
265 fn test_multiple_warnings() {
266 let input = r#"301 example.com "Incompatible network protocol", 399 198.51.100.1:5060 "Miscellaneous warning""#;
267 let warning = SipWarning::parse(input).unwrap();
268 assert_eq!(warning.len(), 2);
269
270 let entry1 = &warning.entries()[0];
271 assert_eq!(entry1.code(), 301);
272 assert_eq!(entry1.agent(), "example.com");
273 assert_eq!(entry1.text(), "Incompatible network protocol");
274
275 let entry2 = &warning.entries()[1];
276 assert_eq!(entry2.code(), 399);
277 assert_eq!(entry2.agent(), "198.51.100.1:5060");
278 assert_eq!(entry2.text(), "Miscellaneous warning");
279 }
280
281 #[test]
282 fn test_escaped_quotes_in_text() {
283 let input = r#"399 example.org "Warning with \"quoted\" text""#;
284 let warning = SipWarning::parse(input).unwrap();
285 assert_eq!(warning.len(), 1);
286 let entry = &warning.entries()[0];
287 assert_eq!(entry.code(), 399);
288 assert_eq!(entry.agent(), "example.org");
289 assert_eq!(entry.text(), r#"Warning with "quoted" text"#);
290 }
291
292 #[test]
293 fn test_common_warning_codes() {
294 let input1 = r#"301 example.com "Incompatible network protocol""#;
295 let warning1 = SipWarning::parse(input1).unwrap();
296 assert_eq!(warning1.entries()[0].code(), 301);
297
298 let input2 = r#"399 example.net "Miscellaneous warning""#;
299 let warning2 = SipWarning::parse(input2).unwrap();
300 assert_eq!(warning2.entries()[0].code(), 399);
301 }
302
303 #[test]
304 fn test_display_roundtrip() {
305 let input = r#"301 example.com "Incompatible network protocol", 399 198.51.100.1:5060 "Miscellaneous warning""#;
306 let warning = SipWarning::parse(input).unwrap();
307 let output = warning.to_string();
308 let reparsed = SipWarning::parse(&output).unwrap();
309 assert_eq!(warning, reparsed);
310 }
311
312 #[test]
313 fn test_display_roundtrip_with_escaped_quotes() {
314 let input = r#"399 example.org "Warning with \"quoted\" text""#;
315 let warning = SipWarning::parse(input).unwrap();
316 let output = warning.to_string();
317 let reparsed = SipWarning::parse(&output).unwrap();
318 assert_eq!(warning, reparsed);
319 }
320
321 #[test]
322 fn test_empty_input() {
323 let result = SipWarning::parse("");
324 assert!(matches!(result, Err(SipWarningError::Empty)));
325
326 let result = SipWarning::parse(" ");
327 assert!(matches!(result, Err(SipWarningError::Empty)));
328 }
329
330 #[test]
331 fn test_invalid_warn_code() {
332 let result = SipWarning::parse(r#"30 example.com "Short code""#);
333 assert!(matches!(result, Err(SipWarningError::InvalidFormat(_))));
334
335 let result = SipWarning::parse(r#"3001 example.com "Long code""#);
336 assert!(matches!(result, Err(SipWarningError::InvalidFormat(_))));
337
338 let result = SipWarning::parse(r#"abc example.com "Non-numeric""#);
339 assert!(matches!(result, Err(SipWarningError::InvalidFormat(_))));
340 }
341
342 #[test]
343 fn test_missing_warn_agent() {
344 let result = SipWarning::parse(r#"301 "Missing agent""#);
345 assert!(matches!(result, Err(SipWarningError::InvalidFormat(_))));
346 }
347
348 #[test]
349 fn test_missing_warn_text() {
350 let result = SipWarning::parse("301 example.com");
351 assert!(matches!(result, Err(SipWarningError::InvalidFormat(_))));
352 }
353
354 #[test]
355 fn test_unterminated_quoted_string() {
356 let result = SipWarning::parse(r#"301 example.com "Unterminated"#);
357 assert!(matches!(result, Err(SipWarningError::InvalidFormat(_))));
358 }
359
360 #[test]
361 fn test_into_iterator() {
362 let input = r#"301 example.com "First", 399 example.org "Second""#;
363 let warning = SipWarning::parse(input).unwrap();
364
365 let codes: Vec<u16> = warning
366 .into_iter()
367 .map(|e| e.code())
368 .collect();
369 assert_eq!(codes, vec![301, 399]);
370 }
371
372 #[test]
373 fn test_into_iterator_ref() {
374 let input = r#"301 example.com "First", 399 example.org "Second""#;
375 let warning = SipWarning::parse(input).unwrap();
376
377 let codes: Vec<u16> = (&warning)
378 .into_iter()
379 .map(|e| e.code())
380 .collect();
381 assert_eq!(codes, vec![301, 399]);
382
383 assert_eq!(warning.len(), 2);
384 }
385
386 #[test]
387 fn test_is_empty() {
388 let input = r#"301 example.com "Warning""#;
389 let warning = SipWarning::parse(input).unwrap();
390 assert!(!warning.is_empty());
391 }
392
393 #[test]
394 fn test_into_entries() {
395 let input = r#"301 example.com "First", 399 example.org "Second""#;
396 let warning = SipWarning::parse(input).unwrap();
397 let entries = warning.into_entries();
398 assert_eq!(entries.len(), 2);
399 assert_eq!(entries[0].code(), 301);
400 assert_eq!(entries[1].code(), 399);
401 }
402
403 #[test]
404 fn test_comma_in_warn_text() {
405 let input = r#"301 example.com "text, with comma", 399 example.org "fine""#;
406 let warning = SipWarning::parse(input).unwrap();
407 assert_eq!(warning.len(), 2);
408 assert_eq!(warning.entries()[0].text(), "text, with comma");
409 assert_eq!(warning.entries()[1].text(), "fine");
410 }
411
412 #[test]
413 fn test_from_str() {
414 let input = r#"301 example.com "warning""#;
415 let warning: SipWarning = input
416 .parse()
417 .unwrap();
418 assert_eq!(warning.len(), 1);
419 }
420
421 #[test]
422 fn test_ipv6_agent() {
423 let input = r#"301 [2001:db8::1]:5060 "IPv6 warning""#;
424 let warning = SipWarning::parse(input).unwrap();
425 assert_eq!(warning.entries()[0].agent(), "[2001:db8::1]:5060");
426 }
427
428 #[test]
429 fn test_escaped_backslash() {
430 let input = r#"399 example.com "Path: C:\\temp\\file""#;
431 let warning = SipWarning::parse(input).unwrap();
432 assert_eq!(warning.entries()[0].text(), r#"Path: C:\temp\file"#);
433 }
434
435 #[test]
436 fn from_entries_matches_parse() {
437 let a = r#"301 example.com "text, with comma""#;
438 let b = r#"399 example.org "fine""#;
439 let split = SipWarning::from_entries([a, b]).unwrap();
440 let joined = SipWarning::parse(&format!("{a}, {b}")).unwrap();
441 assert_eq!(split, joined);
442 }
443
444 #[test]
445 fn from_entries_bad_entry_is_error() {
446 assert!(matches!(
447 SipWarning::from_entries([r#"399 example.com "ok""#, "nope"]),
448 Err(SipWarningError::InvalidFormat(_))
449 ));
450 }
451
452 #[test]
453 fn from_entries_empty_is_empty_error() {
454 assert!(matches!(
455 SipWarning::from_entries(std::iter::empty::<&str>()),
456 Err(SipWarningError::Empty)
457 ));
458 }
459}