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        Self::from_entries(crate::split_comma_entries(raw))
148    }
149
150    /// Build from entries a transport already split; each is one `accept-range`.
151    pub fn from_entries<'a>(
152        entries: impl IntoIterator<Item = &'a str>,
153    ) -> Result<Self, SipAcceptError> {
154        let entries: Vec<_> = entries
155            .into_iter()
156            .map(parse_accept_entry)
157            .collect::<Result<_, _>>()?;
158        if entries.is_empty() {
159            return Err(SipAcceptError::Empty);
160        }
161        Ok(Self(entries))
162    }
163
164    /// The parsed entries as a slice.
165    pub fn entries(&self) -> &[SipAcceptEntry] {
166        &self.0
167    }
168
169    /// Consume self and return entries as a `Vec`.
170    pub fn into_entries(self) -> Vec<SipAcceptEntry> {
171        self.0
172    }
173
174    /// Number of entries.
175    pub fn len(&self) -> usize {
176        self.0
177            .len()
178    }
179
180    /// Returns `true` if there are no entries.
181    pub fn is_empty(&self) -> bool {
182        self.0
183            .is_empty()
184    }
185}
186
187impl fmt::Display for SipAccept {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        crate::fmt_joined(f, &self.0, ", ")
190    }
191}
192
193impl_from_str_via_parse!(SipAccept, SipAcceptError);
194
195impl<'a> IntoIterator for &'a SipAccept {
196    type Item = &'a SipAcceptEntry;
197    type IntoIter = std::slice::Iter<'a, SipAcceptEntry>;
198
199    fn into_iter(self) -> Self::IntoIter {
200        self.0
201            .iter()
202    }
203}
204
205impl IntoIterator for SipAccept {
206    type Item = SipAcceptEntry;
207    type IntoIter = std::vec::IntoIter<SipAcceptEntry>;
208
209    fn into_iter(self) -> Self::IntoIter {
210        self.0
211            .into_iter()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn single_media_type() {
221        let accept = SipAccept::parse("application/sdp").unwrap();
222        assert_eq!(accept.len(), 1);
223        assert_eq!(accept.entries()[0].media_type(), "application");
224        assert_eq!(accept.entries()[0].subtype(), "sdp");
225    }
226
227    #[test]
228    fn multiple_types() {
229        let accept = SipAccept::parse("application/sdp, application/pidf+xml;q=0.5").unwrap();
230        assert_eq!(accept.len(), 2);
231        assert_eq!(accept.entries()[0].media_range(), "application/sdp");
232        assert_eq!(accept.entries()[1].q(), Some("0.5"));
233    }
234
235    #[test]
236    fn wildcard_type() {
237        let accept = SipAccept::parse("*/*").unwrap();
238        assert_eq!(accept.entries()[0].media_type(), "*");
239        assert_eq!(accept.entries()[0].subtype(), "*");
240    }
241
242    #[test]
243    fn wildcard_subtype() {
244        let accept = SipAccept::parse("application/*").unwrap();
245        assert_eq!(accept.entries()[0].media_type(), "application");
246        assert_eq!(accept.entries()[0].subtype(), "*");
247    }
248
249    #[test]
250    fn empty_input() {
251        assert!(matches!(SipAccept::parse(""), Err(SipAcceptError::Empty)));
252    }
253
254    #[test]
255    fn missing_slash() {
256        assert!(matches!(
257            SipAccept::parse("application"),
258            Err(SipAcceptError::InvalidFormat(_))
259        ));
260    }
261
262    #[test]
263    fn from_str() {
264        let accept: SipAccept = "application/sdp"
265            .parse()
266            .unwrap();
267        assert_eq!(accept.len(), 1);
268    }
269
270    #[test]
271    fn display_roundtrip() {
272        let raw = "application/sdp;q=0.8";
273        let accept = SipAccept::parse(raw).unwrap();
274        assert_eq!(accept.to_string(), raw);
275    }
276
277    #[test]
278    fn from_entries_matches_parse() {
279        let a = "application/sdp";
280        let b = "application/pidf+xml;q=0.5";
281        let split = SipAccept::from_entries([a, b]).unwrap();
282        let joined = SipAccept::parse(&format!("{a}, {b}")).unwrap();
283        assert_eq!(split, joined);
284    }
285
286    #[test]
287    fn from_entries_bad_entry_is_error() {
288        assert!(matches!(
289            SipAccept::from_entries(["application/sdp", "noslash"]),
290            Err(SipAcceptError::InvalidFormat(_))
291        ));
292    }
293
294    #[test]
295    fn from_entries_empty_is_empty_error() {
296        assert!(matches!(
297            SipAccept::from_entries(std::iter::empty::<&str>()),
298            Err(SipAcceptError::Empty)
299        ));
300    }
301}