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