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 '{}'",
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        // Find the quoted warn-text
95        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        // Parse quoted string
117        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
135/// Parse a quoted string starting with `"`, returning the unescaped content.
136fn 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    // Find the closing quote, respecting backslash escapes.
145    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/// SIP Warning header.
163///
164/// RFC 3261 §20.43:
165/// ```text
166/// Warning = "Warning" HCOLON warning-value *(COMMA warning-value)
167/// ```
168#[derive(Debug, Clone, PartialEq, Eq)]
169#[non_exhaustive]
170pub struct SipWarning {
171    entries: Vec<SipWarningEntry>,
172}
173
174impl SipWarning {
175    /// Parse a Warning header value.
176    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    /// Build from entries a transport already split; each is one `warning-value`.
185    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    /// All warning entries.
199    pub fn entries(&self) -> &[SipWarningEntry] {
200        &self.entries
201    }
202
203    /// Consume self and return entries as a `Vec`.
204    pub fn into_entries(self) -> Vec<SipWarningEntry> {
205        self.entries
206    }
207
208    /// Number of warning entries.
209    pub fn len(&self) -> usize {
210        self.entries
211            .len()
212    }
213
214    /// Whether there are no warning entries.
215    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}