Skip to main content

sip_header/
accept_encoding.rs

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