Skip to main content

sip_header/
accept_language.rs

1//! SIP Accept-Language header parser (RFC 3261 §20.3).
2
3use std::fmt;
4
5use crate::accept::{all_blank, read_accept_params, write_accept_params};
6
7/// A single Accept-Language entry: `language-range *(SEMI accept-param)`.
8#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub struct SipAcceptLanguageEntry {
11    language: String,
12    params: Vec<(String, String)>,
13}
14
15impl SipAcceptLanguageEntry {
16    /// The language tag (e.g. `"en"`, `"en-US"`, `"*"`).
17    pub fn language(&self) -> &str {
18        &self.language
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 SipAcceptLanguageEntry {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(f, "{}", self.language)?;
43        write_accept_params(f, &self.params)
44    }
45}
46
47/// Errors from parsing an Accept-Language header value.
48#[derive(Debug, Clone, PartialEq, Eq)]
49#[non_exhaustive]
50pub enum SipAcceptLanguageError {
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 SipAcceptLanguageError {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::Empty => write!(f, "empty Accept-Language header value"),
62            Self::InvalidFormat(raw) => {
63                write!(f, "invalid Accept-Language entry ({} bytes)", raw.len())
64            }
65        }
66    }
67}
68
69impl std::error::Error for SipAcceptLanguageError {}
70
71fn parse_entry(raw: &str) -> Result<SipAcceptLanguageEntry, SipAcceptLanguageError> {
72    let raw = raw.trim();
73    if raw.is_empty() {
74        return Err(SipAcceptLanguageError::InvalidFormat(raw.to_string()));
75    }
76
77    let (lang_part, params_part) = match raw.split_once(';') {
78        Some((l, p)) => (l.trim(), Some(p)),
79        None => (raw, None),
80    };
81
82    if lang_part.is_empty() {
83        return Err(SipAcceptLanguageError::InvalidFormat(raw.to_string()));
84    }
85
86    Ok(SipAcceptLanguageEntry {
87        language: lang_part.to_ascii_lowercase(),
88        params: read_accept_params(params_part.unwrap_or("")),
89    })
90}
91
92/// Parsed SIP Accept-Language header value.
93#[derive(Debug, Clone, PartialEq, Eq)]
94#[non_exhaustive]
95pub struct SipAcceptLanguage(Vec<SipAcceptLanguageEntry>);
96
97impl SipAcceptLanguage {
98    /// Parse a comma-separated Accept-Language 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, SipAcceptLanguageError> {
102        Self::from_entries(crate::split_comma_entries(raw))
103    }
104
105    /// Build from entries a transport already split; each is one `language`.
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, SipAcceptLanguageError> {
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) -> &[SipAcceptLanguageEntry] {
127        &self.0
128    }
129
130    /// Consume self and return entries as a `Vec`.
131    pub fn into_entries(self) -> Vec<SipAcceptLanguageEntry> {
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 SipAcceptLanguage {
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!(SipAcceptLanguage, SipAcceptLanguageError);
155
156impl<'a> IntoIterator for &'a SipAcceptLanguage {
157    type Item = &'a SipAcceptLanguageEntry;
158    type IntoIter = std::slice::Iter<'a, SipAcceptLanguageEntry>;
159
160    fn into_iter(self) -> Self::IntoIter {
161        self.0
162            .iter()
163    }
164}
165
166impl IntoIterator for SipAcceptLanguage {
167    type Item = SipAcceptLanguageEntry;
168    type IntoIter = std::vec::IntoIter<SipAcceptLanguageEntry>;
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_language() {
182        let al = SipAcceptLanguage::parse("en").unwrap();
183        assert_eq!(al.len(), 1);
184        assert_eq!(al.entries()[0].language(), "en");
185    }
186
187    #[test]
188    fn multiple_languages_with_q() {
189        let al = SipAcceptLanguage::parse("en;q=0.9, fr;q=0.8, *;q=0.1").unwrap();
190        assert_eq!(al.len(), 3);
191        assert_eq!(al.entries()[0].language(), "en");
192        assert_eq!(al.entries()[1].q(), Some("0.8"));
193        assert_eq!(al.entries()[2].language(), "*");
194    }
195
196    #[test]
197    fn language_subtag() {
198        let al = SipAcceptLanguage::parse("en-US").unwrap();
199        assert_eq!(al.entries()[0].language(), "en-us");
200    }
201
202    #[test]
203    fn empty_input_is_empty_list() {
204        assert!(SipAcceptLanguage::parse("")
205            .unwrap()
206            .is_empty());
207        assert!(SipAcceptLanguage::parse("  ")
208            .unwrap()
209            .is_empty());
210    }
211
212    #[test]
213    fn flag_param_roundtrip() {
214        let raw = "en;foo";
215        let al = SipAcceptLanguage::parse(raw).unwrap();
216        assert_eq!(al.entries()[0].param("foo"), Some(""));
217        assert_eq!(al.to_string(), raw);
218    }
219
220    #[test]
221    fn quoted_param_keeps_semicolon() {
222        let raw = r#"en;x="a;b";q=0.5"#;
223        let al = SipAcceptLanguage::parse(raw).unwrap();
224        assert_eq!(al.entries()[0].param("x"), Some(r#""a;b""#));
225        assert_eq!(al.entries()[0].q(), Some("0.5"));
226        assert_eq!(al.to_string(), raw);
227    }
228
229    #[test]
230    fn error_display_omits_input() {
231        let err = SipAcceptLanguage::parse(";secretvalue").unwrap_err();
232        assert!(!err
233            .to_string()
234            .contains("secretvalue"));
235    }
236
237    #[test]
238    fn from_str() {
239        let al: SipAcceptLanguage = "en"
240            .parse()
241            .unwrap();
242        assert_eq!(al.len(), 1);
243    }
244
245    #[test]
246    fn display_roundtrip() {
247        let raw = "en;q=0.9";
248        let al = SipAcceptLanguage::parse(raw).unwrap();
249        assert_eq!(al.to_string(), raw);
250    }
251
252    #[test]
253    fn from_entries_matches_parse() {
254        let split = SipAcceptLanguage::from_entries(["en;q=0.9", "fr-CA"]).unwrap();
255        let joined = SipAcceptLanguage::parse("en;q=0.9, fr-CA").unwrap();
256        assert_eq!(split, joined);
257    }
258
259    #[test]
260    fn from_entries_bad_entry_is_error() {
261        assert!(matches!(
262            SipAcceptLanguage::from_entries(["en", "   "]),
263            Err(SipAcceptLanguageError::InvalidFormat(_))
264        ));
265    }
266
267    #[test]
268    fn from_entries_empty_is_empty_list() {
269        assert!(SipAcceptLanguage::from_entries(std::iter::empty::<&str>())
270            .unwrap()
271            .is_empty());
272        assert!(SipAcceptLanguage::from_entries(["", "  "])
273            .unwrap()
274            .is_empty());
275    }
276}