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        write_accept_params(f, &self.params)
53    }
54}
55
56/// Errors from parsing an Accept header value.
57#[derive(Debug, Clone, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum SipAcceptError {
60    /// An empty value. Not returned by parsing, which reads an empty value as
61    /// the empty list RFC 3261 §25.1 allows.
62    Empty,
63    /// An entry could not be parsed.
64    InvalidFormat(String),
65}
66
67impl fmt::Display for SipAcceptError {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            Self::Empty => write!(f, "empty Accept header value"),
71            Self::InvalidFormat(raw) => write!(f, "invalid Accept entry ({} bytes)", raw.len()),
72        }
73    }
74}
75
76impl std::error::Error for SipAcceptError {}
77
78fn parse_accept_entry(raw: &str) -> Result<SipAcceptEntry, SipAcceptError> {
79    let raw = raw.trim();
80    if raw.is_empty() {
81        return Err(SipAcceptError::InvalidFormat(raw.to_string()));
82    }
83
84    let (media_part, params_part) = match raw.split_once(';') {
85        Some((m, p)) => (m.trim(), Some(p)),
86        None => (raw, None),
87    };
88
89    let (type_str, subtype_str) = media_part
90        .split_once('/')
91        .ok_or_else(|| SipAcceptError::InvalidFormat(raw.to_string()))?;
92
93    let type_str = type_str.trim();
94    let subtype_str = subtype_str.trim();
95
96    if type_str.is_empty() || subtype_str.is_empty() {
97        return Err(SipAcceptError::InvalidFormat(raw.to_string()));
98    }
99
100    let mut media_range = type_str.to_ascii_lowercase();
101    let slash_pos = media_range.len();
102    media_range.push('/');
103    media_range.push_str(&subtype_str.to_ascii_lowercase());
104
105    Ok(SipAcceptEntry {
106        media_range,
107        slash_pos,
108        params: read_accept_params(params_part.unwrap_or("")),
109    })
110}
111
112/// Read `*(SEMI accept-param)`: keys lowercased, values raw, `""` for a flag.
113pub(crate) fn read_accept_params(s: &str) -> Vec<(String, String)> {
114    crate::parse_params(s)
115        .into_iter()
116        .map(|p| {
117            (
118                p.key
119                    .to_ascii_lowercase(),
120                p.value
121                    .unwrap_or("")
122                    .to_string(),
123            )
124        })
125        .collect()
126}
127
128/// Write `*(SEMI accept-param)`, an empty value as a flag.
129pub(crate) fn write_accept_params(
130    f: &mut fmt::Formatter<'_>,
131    params: &[(String, String)],
132) -> fmt::Result {
133    for (key, value) in params {
134        crate::write_param(
135            f,
136            key,
137            Some(value.as_str()).filter(|v| !v.is_empty()),
138            false,
139        )?;
140    }
141    Ok(())
142}
143
144/// True when every entry is blank: the empty list `[ accept-range *(COMMA
145/// accept-range) ]` allows (RFC 3261 §25.1), shared by the Accept-* headers.
146pub(crate) fn all_blank(entries: &[&str]) -> bool {
147    entries
148        .iter()
149        .all(|e| {
150            e.trim()
151                .is_empty()
152        })
153}
154
155/// Parsed SIP Accept header value.
156#[derive(Debug, Clone, PartialEq, Eq)]
157#[non_exhaustive]
158pub struct SipAccept(Vec<SipAcceptEntry>);
159
160impl SipAccept {
161    /// Parse a comma-separated Accept header value.
162    ///
163    /// An empty or whitespace-only value is the empty list (RFC 3261 §25.1).
164    pub fn parse(raw: &str) -> Result<Self, SipAcceptError> {
165        Self::from_entries(crate::split_comma_entries(raw))
166    }
167
168    /// Build from entries a transport already split; each is one `accept-range`.
169    ///
170    /// No entries, or only blank ones, is the empty list; a blank entry beside
171    /// a real one is an error.
172    pub fn from_entries<'a>(
173        entries: impl IntoIterator<Item = &'a str>,
174    ) -> Result<Self, SipAcceptError> {
175        let entries: Vec<&str> = entries
176            .into_iter()
177            .collect();
178        if all_blank(&entries) {
179            return Ok(Self(Vec::new()));
180        }
181        entries
182            .into_iter()
183            .map(parse_accept_entry)
184            .collect::<Result<_, _>>()
185            .map(Self)
186    }
187
188    /// The parsed entries as a slice.
189    pub fn entries(&self) -> &[SipAcceptEntry] {
190        &self.0
191    }
192
193    /// Consume self and return entries as a `Vec`.
194    pub fn into_entries(self) -> Vec<SipAcceptEntry> {
195        self.0
196    }
197
198    /// Number of entries.
199    pub fn len(&self) -> usize {
200        self.0
201            .len()
202    }
203
204    /// Returns `true` if there are no entries.
205    pub fn is_empty(&self) -> bool {
206        self.0
207            .is_empty()
208    }
209}
210
211impl fmt::Display for SipAccept {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        crate::fmt_joined(f, &self.0, ", ")
214    }
215}
216
217impl_from_str_via_parse!(SipAccept, SipAcceptError);
218
219impl<'a> IntoIterator for &'a SipAccept {
220    type Item = &'a SipAcceptEntry;
221    type IntoIter = std::slice::Iter<'a, SipAcceptEntry>;
222
223    fn into_iter(self) -> Self::IntoIter {
224        self.0
225            .iter()
226    }
227}
228
229impl IntoIterator for SipAccept {
230    type Item = SipAcceptEntry;
231    type IntoIter = std::vec::IntoIter<SipAcceptEntry>;
232
233    fn into_iter(self) -> Self::IntoIter {
234        self.0
235            .into_iter()
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn single_media_type() {
245        let accept = SipAccept::parse("application/sdp").unwrap();
246        assert_eq!(accept.len(), 1);
247        assert_eq!(accept.entries()[0].media_type(), "application");
248        assert_eq!(accept.entries()[0].subtype(), "sdp");
249    }
250
251    #[test]
252    fn multiple_types() {
253        let accept = SipAccept::parse("application/sdp, application/pidf+xml;q=0.5").unwrap();
254        assert_eq!(accept.len(), 2);
255        assert_eq!(accept.entries()[0].media_range(), "application/sdp");
256        assert_eq!(accept.entries()[1].q(), Some("0.5"));
257    }
258
259    #[test]
260    fn wildcard_type() {
261        let accept = SipAccept::parse("*/*").unwrap();
262        assert_eq!(accept.entries()[0].media_type(), "*");
263        assert_eq!(accept.entries()[0].subtype(), "*");
264    }
265
266    #[test]
267    fn wildcard_subtype() {
268        let accept = SipAccept::parse("application/*").unwrap();
269        assert_eq!(accept.entries()[0].media_type(), "application");
270        assert_eq!(accept.entries()[0].subtype(), "*");
271    }
272
273    #[test]
274    fn empty_input_is_empty_list() {
275        assert!(SipAccept::parse("")
276            .unwrap()
277            .is_empty());
278        assert!(SipAccept::parse("  ")
279            .unwrap()
280            .is_empty());
281    }
282
283    #[test]
284    fn flag_param_roundtrip() {
285        let raw = "application/sdp;foo";
286        let accept = SipAccept::parse(raw).unwrap();
287        assert_eq!(accept.entries()[0].param("foo"), Some(""));
288        assert_eq!(accept.to_string(), raw);
289    }
290
291    #[test]
292    fn quoted_param_keeps_semicolon() {
293        let raw = r#"application/sdp;x="a;b";q=0.5"#;
294        let accept = SipAccept::parse(raw).unwrap();
295        assert_eq!(accept.entries()[0].param("x"), Some(r#""a;b""#));
296        assert_eq!(accept.entries()[0].q(), Some("0.5"));
297        assert_eq!(accept.to_string(), raw);
298    }
299
300    #[test]
301    fn error_display_omits_input() {
302        let err = SipAccept::parse("secretvalue").unwrap_err();
303        assert!(!err
304            .to_string()
305            .contains("secretvalue"));
306    }
307
308    #[test]
309    fn missing_slash() {
310        assert!(matches!(
311            SipAccept::parse("application"),
312            Err(SipAcceptError::InvalidFormat(_))
313        ));
314    }
315
316    #[test]
317    fn from_str() {
318        let accept: SipAccept = "application/sdp"
319            .parse()
320            .unwrap();
321        assert_eq!(accept.len(), 1);
322    }
323
324    #[test]
325    fn display_roundtrip() {
326        let raw = "application/sdp;q=0.8";
327        let accept = SipAccept::parse(raw).unwrap();
328        assert_eq!(accept.to_string(), raw);
329    }
330
331    #[test]
332    fn from_entries_matches_parse() {
333        let a = "application/sdp";
334        let b = "application/pidf+xml;q=0.5";
335        let split = SipAccept::from_entries([a, b]).unwrap();
336        let joined = SipAccept::parse(&format!("{a}, {b}")).unwrap();
337        assert_eq!(split, joined);
338    }
339
340    #[test]
341    fn from_entries_bad_entry_is_error() {
342        assert!(matches!(
343            SipAccept::from_entries(["application/sdp", "noslash"]),
344            Err(SipAcceptError::InvalidFormat(_))
345        ));
346    }
347
348    #[test]
349    fn from_entries_empty_is_empty_list() {
350        assert!(SipAccept::from_entries(std::iter::empty::<&str>())
351            .unwrap()
352            .is_empty());
353        assert!(SipAccept::from_entries(["", "  "])
354            .unwrap()
355            .is_empty());
356    }
357}