1use std::{borrow::Cow, cmp, fmt, iter, str};
2
3use super::Quality;
4use crate::error::ParseError;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct QualityItem<T> {
38 pub item: T,
40
41 pub quality: Quality,
43}
44
45impl<T> QualityItem<T> {
46 pub fn new(item: T, quality: Quality) -> Self {
50 QualityItem { item, quality }
51 }
52
53 pub fn max(item: T) -> Self {
55 Self::new(item, Quality::MAX)
56 }
57
58 pub fn min(item: T) -> Self {
60 Self::new(item, Quality::MIN)
61 }
62
63 pub fn zero(item: T) -> Self {
65 Self::new(item, Quality::ZERO)
66 }
67}
68
69impl<T: PartialEq> PartialOrd for QualityItem<T> {
70 fn partial_cmp(&self, other: &QualityItem<T>) -> Option<cmp::Ordering> {
71 self.quality.partial_cmp(&other.quality)
72 }
73}
74
75impl<T: fmt::Display> fmt::Display for QualityItem<T> {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 fmt::Display::fmt(&self.item, f)?;
78
79 match self.quality {
80 Quality::MAX => Ok(()),
82
83 Quality::ZERO => f.write_str("; q=0"),
85
86 q => write!(f, "; q={}", q),
88 }
89 }
90}
91
92impl<T: str::FromStr> str::FromStr for QualityItem<T> {
93 type Err = ParseError;
94
95 fn from_str(q_item_str: &str) -> Result<Self, Self::Err> {
96 if !q_item_str.is_ascii() {
97 return Err(ParseError::Header);
98 }
99
100 let mut parameter_start = None;
101 let mut quality_parameter = None;
102 let mut quoted = false;
103 let mut escaped = false;
104
105 for (index, byte) in q_item_str.bytes().chain(iter::once(b';')).enumerate() {
107 if escaped {
108 escaped = false;
110 continue;
111 }
112
113 match byte {
114 b'\\' if quoted => escaped = true,
115
116 b'"' => quoted = !quoted,
117
118 b';' if !quoted => {
119 if let Some(start) = parameter_start {
120 let attribute = q_item_str[start + 1..index].trim();
121
122 if index == q_item_str.len() && attribute.len() < 2 {
124 return Err(ParseError::Header);
125 }
126
127 if attribute.starts_with("q=") || attribute.starts_with("Q=") {
128 quality_parameter = Some((start, index, &attribute[2..]));
130 }
131 }
132
133 parameter_start = Some(index);
134 }
135
136 _ => {}
137 }
138 }
139
140 let mut raw_item = Cow::Borrowed(q_item_str);
141 let mut quality = Quality::MAX;
142
143 if let Some((start, end, value)) = quality_parameter {
144 quality = match value.as_bytes() {
146 [b'0'] => Quality::ZERO,
147 [b'1'] => Quality::MAX,
148
149 [b'0', b'.', digits @ ..]
150 if digits.len() <= 3 && digits.iter().all(u8::is_ascii_digit) =>
151 {
152 let mut value = 0;
153 let mut place = 100;
154
155 for &digit in digits {
156 value += u16::from(digit - b'0') * place;
157 place /= 10;
158 }
159
160 Quality(value)
161 }
162
163 [b'1', b'.', digits @ ..]
164 if digits.len() <= 3 && digits.iter().all(|&digit| digit == b'0') =>
165 {
166 Quality::MAX
167 }
168
169 _ => return Err(ParseError::Header),
170 };
171
172 raw_item = if end == q_item_str.len() {
173 Cow::Borrowed(q_item_str[..start].trim())
174 } else {
175 Cow::Owned([&q_item_str[..start], &q_item_str[end..]].concat())
176 };
177 }
178
179 let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?;
180
181 Ok(QualityItem::new(item, quality))
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn quality_parameter_position() {
191 for input in [
192 "text/plain; q=0.2; charset=utf-8; format=flowed",
193 "text/plain; charset=utf-8; Q=0.2; format=flowed",
194 "text/plain; charset=utf-8; format=flowed; q=0.2",
195 ] {
196 let parsed = input.parse::<QualityItem<mime::Mime>>().unwrap();
197
198 assert_eq!(parsed.quality, Quality(200), "{input}");
199 assert_eq!(
200 parsed.item,
201 "text/plain; charset=utf-8; format=flowed"
202 .parse::<mime::Mime>()
203 .unwrap(),
204 "{input}"
205 );
206 }
207 }
208
209 #[test]
210 fn quoted_quality_is_not_parameter() {
211 for input in [
212 r#"text/plain; note="x; q=0.2""#,
213 r#"text/plain; note="x; q=invalid""#,
214 r#"text/plain; note="x;Q=0""#,
215 r#"text/plain; note="x\";q=0.2""#,
216 r#"text/plain; note="x\\;q=0.2""#,
217 ] {
218 let parsed = input.parse::<QualityItem<String>>().unwrap();
219
220 assert_eq!(parsed.item, input);
221 assert_eq!(parsed.quality, Quality::MAX, "{input}");
222 }
223 }
224
225 #[test]
226 fn quality_parameter_with_quoted_parameters() {
227 for input in [
228 r#"text/plain; q=0.2; note="x; q=0.8"; format=flowed"#,
229 r#"text/plain; note="x; q=0.8"; q=0.2; format=flowed"#,
230 r#"text/plain; note="x; q=0.8"; format=flowed; q=0.2"#,
231 ] {
232 let parsed = input.parse::<QualityItem<mime::Mime>>().unwrap();
233
234 assert_eq!(parsed.quality, Quality(200), "{input}");
235 assert_eq!(parsed.item.get_param("note").unwrap(), "x; q=0.8");
236 assert_eq!(parsed.item.get_param("format").unwrap(), "flowed");
237 assert!(parsed.item.get_param("q").is_none(), "{input}");
238 }
239 }
240
241 #[test]
242 fn invalid_quality_not_last() {
243 for quality in ["2", "invalid", "0.2739999", "NaN", "", "-1"] {
244 for input in [
245 format!("text/plain; q={quality}; charset=utf-8"),
246 format!("text/plain; charset=utf-8; Q={quality}; format=flowed"),
247 ] {
248 assert!(
249 input.parse::<QualityItem<mime::Mime>>().is_err(),
250 "accepted invalid quality in {input}"
251 );
252 }
253 }
254 }
255
256 #[test]
257 fn ordinary_parameters_keep_default_quality() {
258 for input in [
259 "text/plain; charset=utf-8",
260 "text/plain; charset=utf-8; format=flowed",
261 "text/plain; quality=0.2; query=yes",
262 ] {
263 let parsed = input.parse::<QualityItem<mime::Mime>>().unwrap();
264 assert_eq!(parsed.quality, Quality::MAX, "{input}");
265 assert_eq!(parsed.item, input.parse::<mime::Mime>().unwrap());
266 }
267 }
268
269 #[test]
270 fn generic_item_keeps_unrelated_parameters() {
271 for (input, expected) in [
272 ("hello; q=0.2; extension=yes", "hello; extension=yes"),
273 (
274 r#"hello; note="x; q=invalid"; q=0.2; extension=yes"#,
275 r#"hello; note="x; q=invalid"; extension=yes"#,
276 ),
277 (" hello ; q=0.2 ", "hello"),
278 ("hello; q=0.1; q=0.2", "hello; q=0.1"),
279 ] {
280 let parsed = input.parse::<QualityItem<String>>().unwrap();
281 assert_eq!(parsed.item, expected, "{input}");
282 assert_eq!(parsed.quality, Quality(200), "{input}");
283 }
284 }
285
286 #[allow(clippy::enum_variant_names)] #[derive(Debug, Clone, PartialEq, Eq)]
289 pub enum Encoding {
290 Chunked,
291 Brotli,
292 Gzip,
293 Deflate,
294 Compress,
295 Identity,
296 Trailers,
297 EncodingExt(String),
298 }
299
300 impl fmt::Display for Encoding {
301 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302 use Encoding::*;
303 f.write_str(match *self {
304 Chunked => "chunked",
305 Brotli => "br",
306 Gzip => "gzip",
307 Deflate => "deflate",
308 Compress => "compress",
309 Identity => "identity",
310 Trailers => "trailers",
311 EncodingExt(ref s) => s.as_ref(),
312 })
313 }
314 }
315
316 impl str::FromStr for Encoding {
317 type Err = crate::error::ParseError;
318
319 fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
320 use Encoding::*;
321
322 match s {
323 "chunked" => Ok(Chunked),
324 "br" => Ok(Brotli),
325 "deflate" => Ok(Deflate),
326 "gzip" => Ok(Gzip),
327 "compress" => Ok(Compress),
328 "identity" => Ok(Identity),
329 "trailers" => Ok(Trailers),
330 _ => Ok(EncodingExt(s.to_owned())),
331 }
332 }
333 }
334
335 #[test]
336 fn test_quality_item_fmt_q() {
337 assert_eq!(QualityItem::max(Encoding::Chunked).to_string(), "chunked");
338
339 assert_eq!(
340 QualityItem::new(Encoding::Chunked, Quality(1)).to_string(),
341 "chunked; q=0.001",
342 );
343 }
344
345 #[test]
346 fn test_quality_item_fmt_q_05() {
347 let x = QualityItem {
348 item: Encoding::EncodingExt("identity".to_owned()),
349 quality: Quality(500),
350 };
351
352 assert_eq!(x.to_string(), "identity; q=0.5");
353 }
354
355 #[test]
356 fn test_quality_item_fmt_q_0() {
357 let x = QualityItem {
358 item: Encoding::EncodingExt("identity".to_owned()),
359 quality: Quality::ZERO,
360 };
361
362 assert_eq!(x.to_string(), "identity; q=0");
363 }
364
365 #[test]
366 fn test_quality_item_from_str1() {
367 let x = "chunked".parse::<QualityItem<Encoding>>().unwrap();
368
369 assert_eq!(
370 x,
371 QualityItem {
372 item: Encoding::Chunked,
373 quality: Quality(1000),
374 }
375 );
376 }
377
378 #[test]
379 fn test_quality_item_from_str2() {
380 let x = "chunked; q=1".parse::<QualityItem<Encoding>>().unwrap();
381
382 assert_eq!(
383 x,
384 QualityItem {
385 item: Encoding::Chunked,
386 quality: Quality(1000),
387 }
388 );
389 }
390
391 #[test]
392 fn test_quality_item_from_str3() {
393 let x = "gzip; q=0.5".parse::<QualityItem<Encoding>>().unwrap();
394
395 assert_eq!(
396 x,
397 QualityItem {
398 item: Encoding::Gzip,
399 quality: Quality(500),
400 }
401 );
402 }
403
404 #[test]
405 fn test_quality_item_from_str4() {
406 let x = "gzip; q=0.273".parse::<QualityItem<Encoding>>().unwrap();
407
408 assert_eq!(
409 x,
410 QualityItem {
411 item: Encoding::Gzip,
412 quality: Quality(273),
413 }
414 );
415 }
416
417 #[test]
418 fn test_quality_item_from_str5() {
419 "gzip; q=0.2739999"
420 .parse::<QualityItem<Encoding>>()
421 .unwrap_err();
422 }
423
424 #[test]
425 fn test_quality_item_from_str6() {
426 "gzip; q=2".parse::<QualityItem<Encoding>>().unwrap_err();
427 }
428
429 #[test]
430 fn quality_value_rejects_invalid_numeric_syntax() {
431 for value in [
432 "", ".5", ".0001", "+0.5", "-0", "-0.0", "00.5", "00001", "1e-1", "1e-4", "0.0000",
433 "1.0000", "1.001", "0.0.1", "NaN", "inf", " 0.5", "\"0.5\"",
434 ] {
435 let input = format!("gzip; q={value}");
436
437 assert!(
438 input.parse::<QualityItem<Encoding>>().is_err(),
439 "accepted invalid quality in {input:?}"
440 );
441 }
442 }
443
444 #[test]
445 fn quality_value_accepts_valid_numeric_syntax() {
446 for (value, expected) in [
447 ("0", Quality::ZERO),
448 ("0.", Quality::ZERO),
449 ("0.0", Quality::ZERO),
450 ("0.00", Quality::ZERO),
451 ("0.000", Quality::ZERO),
452 ("0.001", Quality::MIN),
453 ("0.01", Quality(10)),
454 ("0.1", Quality(100)),
455 ("0.10", Quality(100)),
456 ("0.100", Quality(100)),
457 ("0.999", Quality(999)),
458 ("1", Quality::MAX),
459 ("1.", Quality::MAX),
460 ("1.0", Quality::MAX),
461 ("1.00", Quality::MAX),
462 ("1.000", Quality::MAX),
463 ] {
464 let input = format!("gzip; q={value}");
465 let parsed = input.parse::<QualityItem<Encoding>>().unwrap();
466
467 assert_eq!(parsed.quality, expected, "{input}");
468 }
469 }
470
471 #[test]
472 fn quality_value_preserves_thousandths() {
473 for value in 0..1000 {
474 let input = format!("gzip; q=0.{value:03}");
475 let parsed = input.parse::<QualityItem<Encoding>>().unwrap();
476
477 assert_eq!(parsed.quality, Quality(value), "{input}");
478 }
479 }
480
481 #[test]
482 fn test_quality_item_ordering() {
483 let x = "gzip; q=0.5".parse::<QualityItem<Encoding>>().unwrap();
484 let y = "gzip; q=0.273".parse::<QualityItem<Encoding>>().unwrap();
485
486 assert!(x > y);
487 }
488
489 #[test]
490 fn test_fuzzing_bugs() {
491 "99999;".parse::<QualityItem<String>>().unwrap_err();
492
493 "\x0d;;;=\u{d6aa}=="
494 .parse::<QualityItem<String>>()
495 .unwrap_err();
496 }
497}