Skip to main content

sip_header/
warning.rs

1//! SIP Warning header parser (RFC 3261 §20.43).
2
3use std::fmt;
4
5/// Error parsing a SIP Warning header.
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum SipWarningError {
9    /// Empty input.
10    Empty,
11    /// Invalid format.
12    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/// A single Warning header entry.
27///
28/// RFC 3261 §20.43:
29/// ```text
30/// warning-value = warn-code SP warn-agent SP warn-text
31/// warn-code = 3DIGIT
32/// warn-agent = hostport / pseudonym
33/// warn-text = quoted-string
34/// ```
35#[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    /// The 3-digit warning code.
45    pub fn code(&self) -> u16 {
46        self.code
47    }
48
49    /// The warn-agent (hostport or pseudonym).
50    pub fn agent(&self) -> &str {
51        &self.agent
52    }
53
54    /// The warn-text (unquoted).
55    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        // Parse warn-code (3DIGIT)
68        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        // Find the quoted warn-text
93        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        // Parse quoted string
115        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
128/// Parse a quoted string starting with `"`, returning the unescaped content.
129fn 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    // Find the closing quote, respecting backslash escapes.
138    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/// SIP Warning header.
156///
157/// RFC 3261 §20.43:
158/// ```text
159/// Warning = "Warning" HCOLON warning-value *(COMMA warning-value)
160/// ```
161#[derive(Debug, Clone, PartialEq, Eq)]
162#[non_exhaustive]
163pub struct SipWarning {
164    entries: Vec<SipWarningEntry>,
165}
166
167impl SipWarning {
168    /// Parse a Warning header value.
169    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    /// Build from entries a transport already split; each is one `warning-value`.
178    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    /// All warning entries.
192    pub fn entries(&self) -> &[SipWarningEntry] {
193        &self.entries
194    }
195
196    /// Consume self and return entries as a `Vec`.
197    pub fn into_entries(self) -> Vec<SipWarningEntry> {
198        self.entries
199    }
200
201    /// Number of warning entries.
202    pub fn len(&self) -> usize {
203        self.entries
204            .len()
205    }
206
207    /// Whether there are no warning entries.
208    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}