1use crate::{
22 error::{ErrorKind, ParseError, Result, err},
23 limits::Limits,
24 list,
25 media::{self, MediaType},
26 param::{self, Params},
27 text::{self, Word},
28};
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct QValue(u16);
45
46impl QValue {
47 pub const ONE: Self = Self(1000);
49 pub const ZERO: Self = Self(0);
51
52 #[must_use]
54 pub const fn from_thousandths(t: u16) -> Option<Self> {
55 if t <= 1000 { Some(Self(t)) } else { None }
56 }
57
58 #[must_use]
60 pub const fn thousandths(self) -> u16 {
61 self.0
62 }
63
64 pub fn parse(s: &[u8]) -> Result<Self> {
71 let (first, rest) = match s.split_first() {
72 Some((&f, r)) => (f, r),
73 None => return err(ErrorKind::InvalidQValue, 0),
74 };
75 let frac = match rest.split_first() {
76 None => &[][..],
77 Some((b'.', digits)) if digits.len() <= 3 => digits,
78 Some(_) => return err(ErrorKind::InvalidQValue, 1),
79 };
80 if let Some(i) = frac.iter().position(|b| !b.is_ascii_digit()) {
81 return err(ErrorKind::InvalidQValue, 2 + i);
82 }
83 let mut thousandths = 0u16;
84 let mut scale = 100u16;
85 for &d in frac {
86 thousandths += u16::from(d - b'0') * scale;
87 scale /= 10;
88 }
89 match first {
90 b'0' => Ok(Self(thousandths)),
91 b'1' if thousandths == 0 => Ok(Self::ONE),
92 _ => err(ErrorKind::InvalidQValue, 0),
93 }
94 }
95}
96
97#[non_exhaustive]
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
100pub enum LanguageMatching {
101 #[default]
104 Lookup,
105 BasicFilter,
108}
109
110#[non_exhaustive]
112#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
113pub enum MalformedPolicy {
114 #[default]
117 TreatAsAbsent,
118 Refuse,
120}
121
122#[non_exhaustive]
135#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
136pub enum Outcome {
137 Chosen {
139 index: usize,
141 weight: QValue,
143 },
144 NoneAcceptable,
146 HeaderIgnored {
149 index: usize,
151 },
152 Refused(ParseError),
154}
155
156impl Outcome {
157 #[must_use]
159 pub const fn index(&self) -> Option<usize> {
160 match self {
161 Self::Chosen { index, .. } | Self::HeaderIgnored { index } => Some(*index),
162 _ => None,
163 }
164 }
165}
166
167#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
180pub struct Negotiator {
181 limits: Limits,
182 language: LanguageMatching,
183 malformed: MalformedPolicy,
184}
185
186impl Default for Negotiator {
187 fn default() -> Self {
188 Self::new()
189 }
190}
191
192const GROUP: usize = 32;
194
195#[derive(Clone, Copy, Default)]
197struct Best {
198 specificity: u16,
199 weight: u16,
200 seen: bool,
201}
202
203impl Best {
204 fn offer(&mut self, specificity: u16, weight: QValue) {
207 if !self.seen || specificity > self.specificity {
208 *self = Self {
209 specificity,
210 weight: weight.0,
211 seen: true,
212 };
213 }
214 }
215}
216
217fn pick(weights: impl Iterator<Item = (usize, Option<u16>)>) -> Outcome {
219 let mut best: Option<(usize, u16)> = None;
220 for (index, w) in weights {
221 if let Some(w) = w {
222 if w > 0 && best.is_none_or(|(_, b)| w > b) {
223 best = Some((index, w));
224 }
225 }
226 }
227 match best {
228 Some((index, w)) => Outcome::Chosen {
229 index,
230 weight: QValue(w),
231 },
232 None => Outcome::NoneAcceptable,
233 }
234}
235
236impl Negotiator {
237 #[must_use]
239 pub const fn new() -> Self {
240 Self {
241 limits: Limits::DEFAULT,
242 language: LanguageMatching::Lookup,
243 malformed: MalformedPolicy::TreatAsAbsent,
244 }
245 }
246
247 #[must_use]
249 pub const fn limits(self, l: Limits) -> Self {
250 Self { limits: l, ..self }
251 }
252
253 #[must_use]
255 pub const fn language_matching(self, m: LanguageMatching) -> Self {
256 Self {
257 language: m,
258 ..self
259 }
260 }
261
262 #[must_use]
264 pub const fn on_malformed(self, p: MalformedPolicy) -> Self {
265 Self {
266 malformed: p,
267 ..self
268 }
269 }
270
271 fn malformed(&self, e: ParseError, offers: usize) -> Outcome {
272 match self.malformed {
273 MalformedPolicy::Refuse => Outcome::Refused(e),
274 MalformedPolicy::TreatAsAbsent if offers == 0 => Outcome::NoneAcceptable,
275 MalformedPolicy::TreatAsAbsent => Outcome::HeaderIgnored { index: 0 },
276 }
277 }
278
279 #[must_use]
287 pub fn media(&self, accept: Option<&[u8]>, offers: &[&str]) -> Outcome {
288 match accept {
289 None => first(offers),
290 Some(v) => self.media_lines([v], offers),
291 }
292 }
293
294 #[must_use]
297 pub fn media_lines<'a, I>(&self, lines: I, offers: &[&str]) -> Outcome
298 where
299 I: IntoIterator<Item = &'a [u8]>,
300 I::IntoIter: Clone,
301 {
302 let lines = lines.into_iter();
303 let checked = list::combined(lines.clone(), &self.limits).and_then(|l| {
306 for e in l.iter() {
307 media::parse_range(e, &self.limits).map(|_| ())?;
308 }
309 Ok(l)
310 });
311 let members = match checked {
312 Ok(l) => l,
313 Err(e) => return self.malformed(e, offers.len()),
314 };
315 let mut chosen: Option<(usize, u16)> = None;
316 for (group_index, group) in offers.chunks(GROUP).enumerate() {
317 let mut parsed: [Option<MediaType<'_>>; GROUP] = [None; GROUP];
318 for (slot, offer) in parsed.iter_mut().zip(group) {
319 *slot = media::parse(offer.as_bytes(), &Limits::DEFAULT).ok();
320 }
321 let mut best = [Best::default(); GROUP];
322 for element in members.iter() {
323 let Ok(range) = media::parse_range(element, &self.limits) else {
324 continue;
325 };
326 for (b, offer) in best.iter_mut().zip(&parsed) {
327 if let Some(offer) = offer {
328 if range.matches(offer) {
329 b.offer(u16::from(range.specificity()), range.weight());
330 }
331 }
332 }
333 }
334 let base = group_index * GROUP;
335 if let Outcome::Chosen { index, weight } = pick(
336 best.iter()
337 .take(group.len())
338 .enumerate()
339 .map(|(i, b)| (base + i, b.seen.then_some(b.weight))),
340 ) {
341 if chosen.is_none_or(|(_, w)| weight.0 > w) {
342 chosen = Some((index, weight.0));
343 }
344 }
345 }
346 match chosen {
347 Some((index, w)) => Outcome::Chosen {
348 index,
349 weight: QValue(w),
350 },
351 None => Outcome::NoneAcceptable,
352 }
353 }
354
355 #[must_use]
368 pub fn language(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
369 let mode = self.language;
370 self.weighted(value, offers, |range, offer| match mode {
371 LanguageMatching::Lookup => lookup_rank(range, offer),
372 LanguageMatching::BasicFilter => filter_rank(range, offer),
373 })
374 .unwrap_or_else(|| first(offers))
375 }
376
377 #[must_use]
384 pub fn encoding(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
385 let Some(v) = value else {
386 return first(offers);
387 };
388 if text::trim_ows(v).is_empty() {
389 return pick(
390 offers
391 .iter()
392 .enumerate()
393 .map(|(i, o)| (i, o.eq_ignore_ascii_case("identity").then_some(1000))),
394 );
395 }
396 match self.coding_weights(v, offers, true) {
397 Ok(outcome) => outcome,
398 Err(e) => self.malformed(e, offers.len()),
399 }
400 }
401
402 #[must_use]
406 pub fn charset(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
407 let Some(v) = value else {
408 return first(offers);
409 };
410 match self.coding_weights(v, offers, false) {
411 Ok(outcome) => outcome,
412 Err(e) => self.malformed(e, offers.len()),
413 }
414 }
415
416 fn coding_weights(
419 &self,
420 value: &[u8],
421 offers: &[&str],
422 identity_rule: bool,
423 ) -> Result<Outcome> {
424 let members = parse_weighted(value, &self.limits)?;
425 let mut chosen: Option<(usize, u16)> = None;
426 for (group_index, group) in offers.chunks(GROUP).enumerate() {
427 let mut exact = [None::<u16>; GROUP];
428 let mut star = None::<u16>;
429 for m in members.iter() {
430 if m.value == b"*" {
431 if star.is_none() {
432 star = Some(m.weight.0);
433 }
434 continue;
435 }
436 let name = canonical(m.value);
437 for (slot, offer) in exact.iter_mut().zip(group) {
438 if slot.is_none() && name.eq_ignore_ascii_case(canonical(offer.as_bytes())) {
439 *slot = Some(m.weight.0);
440 }
441 }
442 }
443 let base = group_index * GROUP;
444 let outcome = pick(group.iter().enumerate().map(|(i, offer)| {
445 let w = exact.get(i).copied().flatten().or(star).or_else(|| {
446 (identity_rule && offer.eq_ignore_ascii_case("identity")).then_some(1000)
447 });
448 (base + i, w)
449 }));
450 if let Outcome::Chosen { index, weight } = outcome {
451 if chosen.is_none_or(|(_, w)| weight.0 > w) {
452 chosen = Some((index, weight.0));
453 }
454 }
455 }
456 Ok(match chosen {
457 Some((index, w)) => Outcome::Chosen {
458 index,
459 weight: QValue(w),
460 },
461 None => Outcome::NoneAcceptable,
462 })
463 }
464
465 fn weighted(
469 &self,
470 value: Option<&[u8]>,
471 offers: &[&str],
472 rank: impl Fn(&[u8], &[u8]) -> Option<u16>,
473 ) -> Option<Outcome> {
474 let v = value?;
475 let members = match parse_weighted(v, &self.limits) {
476 Ok(m) => m,
477 Err(e) => return Some(self.malformed(e, offers.len())),
478 };
479 let mut chosen: Option<(usize, u16, u16)> = None;
480 for (group_index, group) in offers.chunks(GROUP).enumerate() {
481 let mut best = [Best::default(); GROUP];
482 for m in members.iter() {
483 for (b, offer) in best.iter_mut().zip(group) {
484 if let Some(r) = rank(m.value, offer.as_bytes()) {
485 b.offer(r, m.weight);
486 }
487 }
488 }
489 let base = group_index * GROUP;
493 for (i, b) in best.iter().take(group.len()).enumerate() {
494 if b.seen
495 && b.weight > 0
496 && chosen.is_none_or(|(_, w, r)| (b.weight, b.specificity) > (w, r))
497 {
498 chosen = Some((base + i, b.weight, b.specificity));
499 }
500 }
501 }
502 Some(match chosen {
503 Some((index, w, _)) => Outcome::Chosen {
504 index,
505 weight: QValue(w),
506 },
507 None => Outcome::NoneAcceptable,
508 })
509 }
510}
511
512fn canonical(c: &[u8]) -> &[u8] {
515 if c.eq_ignore_ascii_case(b"x-gzip") {
516 b"gzip"
517 } else if c.eq_ignore_ascii_case(b"x-compress") {
518 b"compress"
519 } else {
520 c
521 }
522}
523
524fn first(offers: &[&str]) -> Outcome {
526 if offers.is_empty() {
527 Outcome::NoneAcceptable
528 } else {
529 Outcome::Chosen {
530 index: 0,
531 weight: QValue::ONE,
532 }
533 }
534}
535
536fn lookup_rank(range: &[u8], offer: &[u8]) -> Option<u16> {
541 if range == b"*" {
542 return Some(0);
543 }
544 if range.eq_ignore_ascii_case(offer) {
545 return Some(u16::MAX);
547 }
548 let mut r = range;
549 loop {
550 if r.eq_ignore_ascii_case(offer) {
551 return Some(
552 u16::try_from(r.len())
553 .unwrap_or(u16::MAX - 1)
554 .saturating_add(1)
555 .min(u16::MAX - 1),
556 );
557 }
558 let cut = r.iter().rposition(|&b| b == b'-')?;
559 r = r.get(..cut)?;
560 if let Some(dash) = r.iter().rposition(|&b| b == b'-') {
563 if r.len() - dash == 2 {
564 r = r.get(..dash)?;
565 }
566 }
567 }
568}
569
570fn filter_rank(range: &[u8], offer: &[u8]) -> Option<u16> {
574 if range == b"*" {
575 return Some(0);
576 }
577 let prefix = offer.get(..range.len())?;
578 let boundary = offer.len() == range.len() || offer.get(range.len()) == Some(&b'-');
579 (prefix.eq_ignore_ascii_case(range) && boundary).then(|| {
580 u16::try_from(range.len())
581 .unwrap_or(u16::MAX)
582 .saturating_add(1)
583 })
584}
585
586#[must_use]
600pub fn best_media(accept: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
601 Negotiator::new().media(accept, offers).index()
602}
603
604#[must_use]
615pub fn best_language(accept_language: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
616 Negotiator::new().language(accept_language, offers).index()
617}
618
619#[must_use]
630pub fn best_encoding(accept_encoding: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
631 Negotiator::new().encoding(accept_encoding, offers).index()
632}
633
634#[must_use]
644pub fn best_charset(accept_charset: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
645 Negotiator::new().charset(accept_charset, offers).index()
646}
647
648pub fn parse_accept<'a>(value: &'a [u8], limits: &Limits) -> Result<AcceptList<'a>> {
664 let l = list::parse(value, limits)?;
665 for e in l.iter() {
666 media::parse_range(e, limits)
667 .map(|_| ())
668 .map_err(|err| shift_into(err, value, e))?;
669 }
670 Ok(AcceptList {
671 list: l,
672 limits: *limits,
673 })
674}
675
676fn shift_into(e: ParseError, value: &[u8], element: &[u8]) -> ParseError {
678 let base = (element.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
679 e.shifted(base)
680}
681
682#[derive(Clone, Copy, Debug)]
684pub struct AcceptList<'a> {
685 list: list::List<'a>,
686 limits: Limits,
687}
688
689impl<'a> AcceptList<'a> {
690 pub fn iter(&self) -> impl Iterator<Item = media::MediaRange<'a>> + '_ {
692 self.list
693 .iter()
694 .filter_map(|e| media::parse_range(e, &self.limits).ok())
695 }
696
697 #[must_use]
699 pub const fn len(&self) -> usize {
700 self.list.len()
701 }
702
703 #[must_use]
705 pub const fn is_empty(&self) -> bool {
706 self.list.is_empty()
707 }
708}
709
710#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
712pub struct Weighted<'a> {
713 pub value: &'a [u8],
715 pub weight: QValue,
717 pub params: Params<'a>,
719}
720
721pub fn parse_weighted<'a>(value: &'a [u8], limits: &Limits) -> Result<WeightedList<'a>> {
740 let l = list::parse(value, limits)?;
741 for e in l.iter() {
742 weighted_member(e, limits)
743 .map(|_| ())
744 .map_err(|err| shift_into(err, value, e))?;
745 }
746 Ok(WeightedList {
747 list: l,
748 limits: *limits,
749 })
750}
751
752fn weighted_member<'a>(element: &'a [u8], limits: &Limits) -> Result<Weighted<'a>> {
753 let (token, n) =
754 text::parse_token(element).map_err(|e| ParseError::new(ErrorKind::NotToken, e.offset()))?;
755 let rest = element.get(n..).unwrap_or(&[]);
756 let (all, used) = param::parse(rest, limits).map_err(|e| e.shifted(n))?;
757 if n + used != element.len() {
758 return err(ErrorKind::NotToken, n + used);
759 }
760 let (params, q) = all.split_at_name(b"q");
761 let weight = match q {
762 None => QValue::ONE,
763 Some(p) => match p.value {
764 Word::Token(t) => QValue::parse(t).map_err(|e| {
765 let at = (t.as_ptr() as usize).saturating_sub(element.as_ptr() as usize);
766 ParseError::new(e.kind(), at)
767 })?,
768 Word::Quoted(_) => return err(ErrorKind::InvalidQValue, n),
769 },
770 };
771 Ok(Weighted {
772 value: token,
773 weight,
774 params,
775 })
776}
777
778#[derive(Clone, Copy, Debug)]
780pub struct WeightedList<'a> {
781 list: list::List<'a>,
782 limits: Limits,
783}
784
785impl<'a> WeightedList<'a> {
786 pub fn iter(&self) -> impl Iterator<Item = Weighted<'a>> + '_ {
788 self.list
789 .iter()
790 .filter_map(|e| weighted_member(e, &self.limits).ok())
791 }
792
793 #[must_use]
795 pub const fn len(&self) -> usize {
796 self.list.len()
797 }
798
799 #[must_use]
801 pub const fn is_empty(&self) -> bool {
802 self.list.is_empty()
803 }
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809
810 #[test]
811 fn test_qvalue_edge_cases() {
812 let ok = |s: &[u8]| QValue::parse(s).map(QValue::thousandths);
813 assert_eq!(ok(b"0"), Ok(0));
814 assert_eq!(ok(b"0."), Ok(0));
815 assert_eq!(ok(b"0.000"), Ok(0));
816 assert_eq!(ok(b"0.5"), Ok(500));
817 assert_eq!(ok(b"0.05"), Ok(50));
818 assert_eq!(ok(b"0.999"), Ok(999));
819 assert_eq!(ok(b"1"), Ok(1000));
820 assert_eq!(ok(b"1.000"), Ok(1000));
821 for bad in [
822 &b"1.001"[..],
823 b"0.0001",
824 b".5",
825 b"-1",
826 b"1.5",
827 b"",
828 b"2",
829 b"0,5",
830 b"0.5x",
831 ] {
832 assert_eq!(
833 QValue::parse(bad).unwrap_err().kind(),
834 ErrorKind::InvalidQValue
835 );
836 }
837 assert_eq!(QValue::from_thousandths(1001), None);
838 assert_eq!(
839 QValue::from_thousandths(250).map(QValue::thousandths),
840 Some(250)
841 );
842 }
843
844 #[test]
845 fn test_media_negotiation_cases() {
846 let n = Negotiator::new();
847 let i = |h: &[u8], o: &[&str]| n.media(Some(h), o).index();
848 assert_eq!(
849 i(b"*/*;q=0, text/html", &["text/html", "text/plain"]),
850 Some(0)
851 );
852 assert_eq!(
853 i(b"text/*;q=0.5, text/html;q=0", &["text/html", "text/plain"]),
854 Some(1)
855 );
856 assert_eq!(
857 i(
858 b"text/html;level=1, text/html;q=0.1",
859 &["text/html", "text/html;level=1"]
860 ),
861 Some(1)
862 );
863 assert_eq!(
864 i(
865 b"application/json;q=0.5, text/html;q=0.5",
866 &["text/html", "application/json"]
867 ),
868 Some(0)
869 );
870 assert_eq!(i(b"", &["text/html"]), None);
871 assert_eq!(i(b"TEXT/HTML", &["text/html"]), Some(0));
872 assert_eq!(
873 i(b"text/html;charset=UTF-8", &["text/html;charset=utf-8"]),
874 Some(0)
875 );
876 assert_eq!(i(b"text/html;level=1", &["text/html;level=2"]), None);
877 assert_eq!(n.media(None, &[]), Outcome::NoneAcceptable);
878 assert_eq!(i(b"*/*", &["not a media type", "text/html"]), Some(1));
879 }
880
881 #[test]
882 fn test_media_malformed_policies() {
883 let bad = Some(&b"text/html;q=2"[..]);
884 assert_eq!(
885 Negotiator::new().media(bad, &["a/b"]),
886 Outcome::HeaderIgnored { index: 0 }
887 );
888 assert_eq!(Negotiator::new().media(bad, &[]), Outcome::NoneAcceptable);
889 let refused = Negotiator::new()
890 .on_malformed(MalformedPolicy::Refuse)
891 .media(bad, &["a/b"]);
892 assert!(matches!(refused, Outcome::Refused(e) if e.kind() == ErrorKind::InvalidQValue));
893 }
894
895 #[test]
896 fn test_media_more_than_one_group_of_offers() {
897 let names: std::vec::Vec<std::string::String> =
898 (0..70).map(|i| std::format!("type/s{i}")).collect();
899 let offers: std::vec::Vec<&str> = names.iter().map(std::string::String::as_str).collect();
900 assert_eq!(
901 Negotiator::new().media(Some(b"type/s65"), &offers).index(),
902 Some(65)
903 );
904 assert_eq!(
905 Negotiator::new()
906 .media(Some(b"type/s65;q=0.5, type/s3;q=0.4"), &offers)
907 .index(),
908 Some(65)
909 );
910 assert_eq!(
911 Negotiator::new().media(Some(b"type/*"), &offers).index(),
912 Some(0)
913 );
914 }
915
916 #[test]
917 fn test_language_lookup_and_filter() {
918 let n = Negotiator::new();
919 let i = |h: &[u8], o: &[&str]| n.language(Some(h), o).index();
920 assert_eq!(i(b"en-US", &["fr", "en"]), Some(1));
921 assert_eq!(i(b"EN-us", &["en-US"]), Some(0));
922 assert_eq!(i(b"zh-Hant-CN-x-private1", &["zh", "zh-Hant"]), Some(1));
923 assert_eq!(i(b"*", &["de", "fr"]), Some(0));
924 assert_eq!(i(b"en-US, en;q=0", &["en"]), None);
925 assert_eq!(i(b"fr;q=0.5, en;q=0.9", &["fr", "en"]), Some(1));
926 assert_eq!(i(b"en", &["en-US"]), None);
927 let f = Negotiator::new().language_matching(LanguageMatching::BasicFilter);
928 assert_eq!(f.language(Some(b"en"), &["en-US"]).index(), Some(0));
929 assert_eq!(f.language(Some(b"en"), &["eng"]).index(), None);
930 assert_eq!(
931 f.language(Some(b"de-DE;q=0.9, de;q=0.1"), &["de-DE-1996"]),
932 Outcome::Chosen {
933 index: 0,
934 weight: QValue(900)
935 }
936 );
937 assert_eq!(n.language(None, &["en"]).index(), Some(0));
938 }
939
940 #[test]
941 fn test_encoding_identity_rules() {
942 let e = |h: &[u8], o: &[&str]| best_encoding(Some(h), o);
943 assert_eq!(e(b"gzip", &["identity"]), Some(0));
944 assert_eq!(e(b"gzip, identity;q=0", &["identity"]), None);
945 assert_eq!(e(b"*;q=0", &["identity"]), None);
946 assert_eq!(e(b"*;q=0, identity", &["br", "identity"]), Some(1));
947 assert_eq!(e(b"x-gzip", &["br", "gzip"]), Some(1));
948 assert_eq!(e(b"gzip;q=0.5, *;q=0.9", &["gzip", "br"]), Some(1));
949 assert_eq!(e(b" ", &["gzip", "identity"]), Some(1));
950 assert_eq!(best_encoding(None, &["br"]), Some(0));
951 assert_eq!(e(b"gzip;q=x", &["br", "gzip"]), Some(0));
952 }
953
954 #[test]
955 fn test_charset_rules() {
956 assert_eq!(best_charset(Some(b"utf-8"), &["iso-8859-1"]), None);
957 assert_eq!(best_charset(Some(b"*;q=0.1"), &["iso-8859-1"]), Some(0));
958 assert_eq!(best_charset(Some(b""), &["utf-8"]), None);
959 }
960
961 #[test]
962 fn test_weighted_member_errors() {
963 let l = Limits::DEFAULT;
964 assert_eq!(
965 parse_weighted(b"gzip;q=\"1\"", &l).unwrap_err().kind(),
966 ErrorKind::InvalidQValue
967 );
968 assert_eq!(
969 parse_weighted(b"gzip x", &l).unwrap_err().kind(),
970 ErrorKind::NotToken
971 );
972 assert_eq!(
973 parse_weighted(b"@", &l).unwrap_err().kind(),
974 ErrorKind::NotToken
975 );
976 let e = parse_weighted(b"br, gzip;q=7", &l).unwrap_err();
977 assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidQValue, 11));
978 assert_eq!(
979 parse_weighted(b"a;q=1;q=0", &l).unwrap_err().kind(),
980 ErrorKind::DuplicateParameter
981 );
982 assert_eq!(parse_accept(b"text/html;q=2", &l).unwrap_err().offset(), 12);
983 }
984
985 #[test]
986 fn test_members_times_offers_is_bounded() {
987 let mut v = std::vec::Vec::new();
988 for i in 0..1024 {
989 v.extend_from_slice(std::format!("t{i}/x;q=0.5,").as_bytes());
990 }
991 v.truncate(v.len() - 1);
992 let names: std::vec::Vec<std::string::String> =
993 (0..32).map(|i| std::format!("t{i}/x")).collect();
994 let offers: std::vec::Vec<&str> = names.iter().map(std::string::String::as_str).collect();
995 assert_eq!(best_media(Some(&v), &offers), Some(0));
996 }
997}