Skip to main content

sip_header/
accept_encoding.rs

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