Skip to main content

sip_header/
accept.rs

1//! SIP Accept header parser (RFC 3261 ยง20.1).
2
3use std::fmt;
4
5/// A single Accept entry: `type/subtype *(SEMI accept-param)`.
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub struct SipAcceptEntry {
9    media_range: String,
10    slash_pos: usize,
11    params: Vec<(String, String)>,
12}
13
14impl SipAcceptEntry {
15    /// The media type (e.g. `"application"`).
16    pub fn media_type(&self) -> &str {
17        &self.media_range[..self.slash_pos]
18    }
19
20    /// The media subtype (e.g. `"sdp"`).
21    pub fn subtype(&self) -> &str {
22        &self.media_range[self.slash_pos + 1..]
23    }
24
25    /// The full media range as `type/subtype`.
26    pub fn media_range(&self) -> &str {
27        &self.media_range
28    }
29
30    /// All parameters as `(key, value)` pairs.
31    pub fn params(&self) -> &[(String, String)] {
32        &self.params
33    }
34
35    /// Look up a parameter by key (case-insensitive).
36    pub fn param(&self, key: &str) -> Option<&str> {
37        self.params
38            .iter()
39            .find(|(k, _)| k.eq_ignore_ascii_case(key))
40            .map(|(_, v)| v.as_str())
41    }
42
43    /// The `q` quality value, if present.
44    pub fn q(&self) -> Option<&str> {
45        self.param("q")
46    }
47}
48
49impl fmt::Display for SipAcceptEntry {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(f, "{}", self.media_range)?;
52        for (key, value) in &self.params {
53            write!(f, ";{key}={value}")?;
54        }
55        Ok(())
56    }
57}
58
59/// Errors from parsing an Accept header value.
60#[derive(Debug, Clone, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum SipAcceptError {
63    /// The input string was empty or whitespace-only.
64    Empty,
65    /// An entry could not be parsed.
66    InvalidFormat(String),
67}
68
69impl fmt::Display for SipAcceptError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            Self::Empty => write!(f, "empty Accept header value"),
73            Self::InvalidFormat(raw) => write!(f, "invalid Accept entry: {raw}"),
74        }
75    }
76}
77
78impl std::error::Error for SipAcceptError {}
79
80fn parse_accept_entry(raw: &str) -> Result<SipAcceptEntry, SipAcceptError> {
81    let raw = raw.trim();
82    if raw.is_empty() {
83        return Err(SipAcceptError::InvalidFormat(raw.to_string()));
84    }
85
86    let (media_part, params_part) = match raw.split_once(';') {
87        Some((m, p)) => (m.trim(), Some(p)),
88        None => (raw, None),
89    };
90
91    let (type_str, subtype_str) = media_part
92        .split_once('/')
93        .ok_or_else(|| SipAcceptError::InvalidFormat(raw.to_string()))?;
94
95    let type_str = type_str.trim();
96    let subtype_str = subtype_str.trim();
97
98    if type_str.is_empty() || subtype_str.is_empty() {
99        return Err(SipAcceptError::InvalidFormat(raw.to_string()));
100    }
101
102    let mut media_range = type_str.to_ascii_lowercase();
103    let slash_pos = media_range.len();
104    media_range.push('/');
105    media_range.push_str(&subtype_str.to_ascii_lowercase());
106
107    let mut params = Vec::new();
108    if let Some(params_str) = params_part {
109        for segment in params_str.split(';') {
110            let segment = segment.trim();
111            if segment.is_empty() {
112                continue;
113            }
114            if let Some((key, value)) = segment.split_once('=') {
115                params.push((
116                    key.trim()
117                        .to_ascii_lowercase(),
118                    value
119                        .trim()
120                        .to_string(),
121                ));
122            } else {
123                params.push((segment.to_ascii_lowercase(), String::new()));
124            }
125        }
126    }
127
128    Ok(SipAcceptEntry {
129        media_range,
130        slash_pos,
131        params,
132    })
133}
134
135/// Parsed SIP Accept header value.
136#[derive(Debug, Clone, PartialEq, Eq)]
137#[non_exhaustive]
138pub struct SipAccept(Vec<SipAcceptEntry>);
139
140impl SipAccept {
141    /// Parse a comma-separated Accept header value.
142    pub fn parse(raw: &str) -> Result<Self, SipAcceptError> {
143        let raw = raw.trim();
144        if raw.is_empty() {
145            return Err(SipAcceptError::Empty);
146        }
147        let entries: Vec<_> = crate::split_comma_entries(raw)
148            .into_iter()
149            .map(parse_accept_entry)
150            .collect::<Result<_, _>>()?;
151        if entries.is_empty() {
152            return Err(SipAcceptError::Empty);
153        }
154        Ok(Self(entries))
155    }
156
157    /// The parsed entries as a slice.
158    pub fn entries(&self) -> &[SipAcceptEntry] {
159        &self.0
160    }
161
162    /// Consume self and return entries as a `Vec`.
163    pub fn into_entries(self) -> Vec<SipAcceptEntry> {
164        self.0
165    }
166
167    /// Number of entries.
168    pub fn len(&self) -> usize {
169        self.0
170            .len()
171    }
172
173    /// Returns `true` if there are no entries.
174    pub fn is_empty(&self) -> bool {
175        self.0
176            .is_empty()
177    }
178}
179
180impl fmt::Display for SipAccept {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        crate::fmt_joined(f, &self.0, ", ")
183    }
184}
185
186impl_from_str_via_parse!(SipAccept, SipAcceptError);
187
188impl<'a> IntoIterator for &'a SipAccept {
189    type Item = &'a SipAcceptEntry;
190    type IntoIter = std::slice::Iter<'a, SipAcceptEntry>;
191
192    fn into_iter(self) -> Self::IntoIter {
193        self.0
194            .iter()
195    }
196}
197
198impl IntoIterator for SipAccept {
199    type Item = SipAcceptEntry;
200    type IntoIter = std::vec::IntoIter<SipAcceptEntry>;
201
202    fn into_iter(self) -> Self::IntoIter {
203        self.0
204            .into_iter()
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn single_media_type() {
214        let accept = SipAccept::parse("application/sdp").unwrap();
215        assert_eq!(accept.len(), 1);
216        assert_eq!(accept.entries()[0].media_type(), "application");
217        assert_eq!(accept.entries()[0].subtype(), "sdp");
218    }
219
220    #[test]
221    fn multiple_types() {
222        let accept = SipAccept::parse("application/sdp, application/pidf+xml;q=0.5").unwrap();
223        assert_eq!(accept.len(), 2);
224        assert_eq!(accept.entries()[0].media_range(), "application/sdp");
225        assert_eq!(accept.entries()[1].q(), Some("0.5"));
226    }
227
228    #[test]
229    fn wildcard_type() {
230        let accept = SipAccept::parse("*/*").unwrap();
231        assert_eq!(accept.entries()[0].media_type(), "*");
232        assert_eq!(accept.entries()[0].subtype(), "*");
233    }
234
235    #[test]
236    fn wildcard_subtype() {
237        let accept = SipAccept::parse("application/*").unwrap();
238        assert_eq!(accept.entries()[0].media_type(), "application");
239        assert_eq!(accept.entries()[0].subtype(), "*");
240    }
241
242    #[test]
243    fn empty_input() {
244        assert!(matches!(SipAccept::parse(""), Err(SipAcceptError::Empty)));
245    }
246
247    #[test]
248    fn missing_slash() {
249        assert!(matches!(
250            SipAccept::parse("application"),
251            Err(SipAcceptError::InvalidFormat(_))
252        ));
253    }
254
255    #[test]
256    fn from_str() {
257        let accept: SipAccept = "application/sdp"
258            .parse()
259            .unwrap();
260        assert_eq!(accept.len(), 1);
261    }
262
263    #[test]
264    fn display_roundtrip() {
265        let raw = "application/sdp;q=0.8";
266        let accept = SipAccept::parse(raw).unwrap();
267        assert_eq!(accept.to_string(), raw);
268    }
269}