1use std::cmp::Reverse;
9
10use headers::{Error, Header};
11use http::{HeaderName, HeaderValue, header::ACCEPT_LANGUAGE};
12use icu_locale_core::Locale;
13
14#[derive(PartialEq, Eq, Debug)]
15struct AcceptLanguagePart {
16 locale: Option<Locale>,
18
19 quality: u16,
22}
23
24impl PartialOrd for AcceptLanguagePart {
25 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
26 Some(self.cmp(other))
27 }
28}
29
30impl Ord for AcceptLanguagePart {
31 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
32 Reverse(self.quality).cmp(&Reverse(other.quality))
35 }
36}
37
38#[derive(PartialEq, Eq, Debug)]
40pub struct AcceptLanguage {
41 parts: Vec<AcceptLanguagePart>,
42}
43
44impl AcceptLanguage {
45 pub fn iter(&self) -> impl Iterator<Item = &Locale> {
46 self.parts.iter().map_while(|item| item.locale.as_ref())
48 }
49}
50
51const fn trim_bytes(mut bytes: &[u8]) -> &[u8] {
53 while let [first, rest @ ..] = bytes {
55 if first.is_ascii_whitespace() {
56 bytes = rest;
57 } else {
58 break;
59 }
60 }
61
62 while let [rest @ .., last] = bytes {
63 if last.is_ascii_whitespace() {
64 bytes = rest;
65 } else {
66 break;
67 }
68 }
69
70 bytes
71}
72
73impl Header for AcceptLanguage {
74 fn name() -> &'static HeaderName {
75 &ACCEPT_LANGUAGE
76 }
77
78 fn decode<'i, I>(values: &mut I) -> Result<Self, Error>
79 where
80 Self: Sized,
81 I: Iterator<Item = &'i HeaderValue>,
82 {
83 let mut parts = Vec::new();
84 for value in values {
85 for part in value.as_bytes().split(|b| *b == b',') {
86 let mut it = part.split(|b| *b == b';');
87 let locale = it.next().ok_or(Error::invalid())?;
88 let locale = trim_bytes(locale);
89
90 let locale = match locale {
91 b"*" => None,
92 locale => {
93 let locale =
94 Locale::try_from_utf8(locale).map_err(|_e| Error::invalid())?;
95 Some(locale)
96 }
97 };
98
99 let quality = if let Some(quality) = it.next() {
100 let quality = trim_bytes(quality);
101 let quality = quality.strip_prefix(b"q=").ok_or(Error::invalid())?;
102 let quality = std::str::from_utf8(quality).map_err(|_e| Error::invalid())?;
103 let quality = quality.parse::<f64>().map_err(|_e| Error::invalid())?;
104 let quality = quality.clamp(0_f64, 1_f64);
106
107 if it.next().is_some() {
109 return Err(Error::invalid());
110 }
111
112 #[expect(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
113 {
114 f64::round(quality * 1000_f64) as u16
115 }
116 } else {
117 1000
118 };
119
120 parts.push(AcceptLanguagePart { locale, quality });
121 }
122 }
123
124 parts.sort();
125
126 Ok(AcceptLanguage { parts })
127 }
128
129 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
130 let mut value = String::new();
131 let mut first = true;
132 for part in &self.parts {
133 if first {
134 first = false;
135 } else {
136 value.push_str(", ");
137 }
138
139 if let Some(locale) = &part.locale {
140 value.push_str(&locale.to_string());
141 } else {
142 value.push('*');
143 }
144
145 if part.quality != 1000 {
146 value.push_str(";q=");
147 value.push_str(&(f64::from(part.quality) / 1000_f64).to_string());
148 }
149 }
150
151 values.extend(Some(HeaderValue::from_str(&value).unwrap()));
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use headers::HeaderMapExt;
159 use http::{HeaderMap, HeaderValue, header::ACCEPT_LANGUAGE};
160 use icu_locale_core::locale;
161
162 use super::*;
163
164 #[test]
165 fn test_decode() {
166 let headers = HeaderMap::from_iter([(
167 ACCEPT_LANGUAGE,
168 HeaderValue::from_str("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5").unwrap(),
169 )]);
170
171 let accept_language: Option<AcceptLanguage> = headers.typed_get();
172 assert!(accept_language.is_some());
173 let accept_language = accept_language.unwrap();
174
175 assert_eq!(
176 accept_language,
177 AcceptLanguage {
178 parts: vec![
179 AcceptLanguagePart {
180 locale: Some(locale!("fr-CH")),
181 quality: 1000,
182 },
183 AcceptLanguagePart {
184 locale: Some(locale!("fr")),
185 quality: 900,
186 },
187 AcceptLanguagePart {
188 locale: Some(locale!("en")),
189 quality: 800,
190 },
191 AcceptLanguagePart {
192 locale: Some(locale!("de")),
193 quality: 700,
194 },
195 AcceptLanguagePart {
196 locale: None,
197 quality: 500,
198 },
199 ]
200 }
201 );
202 }
203
204 #[test]
205 fn test_decode_order() {
208 let headers = HeaderMap::from_iter([(
209 ACCEPT_LANGUAGE,
210 HeaderValue::from_str("*;q=0.5, fr-CH, en;q=0.8, fr;q=0.9, de;q=0.9").unwrap(),
211 )]);
212
213 let accept_language: Option<AcceptLanguage> = headers.typed_get();
214 assert!(accept_language.is_some());
215 let accept_language = accept_language.unwrap();
216
217 assert_eq!(
218 accept_language,
219 AcceptLanguage {
220 parts: vec![
221 AcceptLanguagePart {
222 locale: Some(locale!("fr-CH")),
223 quality: 1000,
224 },
225 AcceptLanguagePart {
226 locale: Some(locale!("fr")),
227 quality: 900,
228 },
229 AcceptLanguagePart {
230 locale: Some(locale!("de")),
231 quality: 900,
232 },
233 AcceptLanguagePart {
234 locale: Some(locale!("en")),
235 quality: 800,
236 },
237 AcceptLanguagePart {
238 locale: None,
239 quality: 500,
240 },
241 ]
242 }
243 );
244 }
245
246 #[test]
247 fn test_encode() {
248 let accept_language = AcceptLanguage {
249 parts: vec![
250 AcceptLanguagePart {
251 locale: Some(locale!("fr-CH")),
252 quality: 1000,
253 },
254 AcceptLanguagePart {
255 locale: Some(locale!("fr")),
256 quality: 900,
257 },
258 AcceptLanguagePart {
259 locale: Some(locale!("de")),
260 quality: 900,
261 },
262 AcceptLanguagePart {
263 locale: Some(locale!("en")),
264 quality: 800,
265 },
266 AcceptLanguagePart {
267 locale: None,
268 quality: 500,
269 },
270 ],
271 };
272
273 let mut headers = HeaderMap::new();
274 headers.typed_insert(accept_language);
275 let header = headers.get(ACCEPT_LANGUAGE).unwrap();
276 assert_eq!(
277 header.to_str().unwrap(),
278 "fr-CH, fr;q=0.9, de;q=0.9, en;q=0.8, *;q=0.5"
279 );
280 }
281}