Skip to main content

parse_net/
negotiate.rs

1//! Content negotiation: quality values and the selection of the best offer
2//! for `Accept`, `Accept-Language`, `Accept-Encoding`, and `Accept-Charset`
3//! (RFC 9110 section 12, RFC 4647).
4//!
5//! Every selection walks the request's members against the server's offers
6//! with no allocation: offers are taken 32 at a time, and the members are
7//! read once per group of 32 (work is members times offers, with members
8//! bounded by [`Limits::max_list_members`]).
9//!
10//! # Examples
11//!
12//! ```
13//! use parse_net as pn;
14//!
15//! let chrome = b"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
16//! assert_eq!(pn::best_media(Some(chrome), &["application/json", "text/html"]), Some(1));
17//! assert_eq!(pn::best_language(Some(b"en-US, fr;q=0.8"), &["fr", "en"]), Some(1));
18//! assert_eq!(pn::best_encoding(Some(b"gzip, br;q=0.9"), &["br", "gzip"]), Some(1));
19//! ```
20
21use 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/// A quality value: 0 to 1 in thousandths (RFC 9110 section 12.4.2). No
31/// floating point.
32///
33/// # Examples
34///
35/// ```
36/// use parse_net::QValue;
37///
38/// assert_eq!(QValue::parse(b"0.5")?.thousandths(), 500);
39/// assert_eq!(QValue::parse(b"1.000")?, QValue::ONE);
40/// assert!(QValue::parse(b"1.5").is_err() && QValue::parse(b".5").is_err());
41/// # Ok::<(), parse_net::ParseError>(())
42/// ```
43#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct QValue(u16);
45
46impl QValue {
47    /// `q=1`, the weight of a member without `q`.
48    ///
49    /// # Examples
50    ///
51    /// ```
52    /// use parse_net::QValue;
53    ///
54    /// assert_eq!(QValue::ONE.thousandths(), 1000);
55    /// assert_eq!(QValue::parse(b"1")?, QValue::ONE);
56    /// # Ok::<(), parse_net::ParseError>(())
57    /// ```
58    pub const ONE: Self = Self(1000);
59    /// `q=0`: not acceptable.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use parse_net::QValue;
65    ///
66    /// assert_eq!(QValue::parse(b"0.000")?, QValue::ZERO);
67    /// assert!(QValue::ZERO < QValue::ONE);
68    /// # Ok::<(), parse_net::ParseError>(())
69    /// ```
70    pub const ZERO: Self = Self(0);
71
72    /// The weight `t` thousandths, if `t` is at most 1000.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use parse_net::QValue;
78    ///
79    /// assert_eq!(QValue::from_thousandths(500), Some(QValue::parse(b"0.5")?));
80    /// assert_eq!(QValue::from_thousandths(1001), None);
81    /// # Ok::<(), parse_net::ParseError>(())
82    /// ```
83    #[must_use]
84    pub const fn from_thousandths(t: u16) -> Option<Self> {
85        if t <= 1000 { Some(Self(t)) } else { None }
86    }
87
88    /// The weight in thousandths, 0 to 1000.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// assert_eq!(parse_net::QValue::parse(b"0.05")?.thousandths(), 50);
94    /// # Ok::<(), parse_net::ParseError>(())
95    /// ```
96    #[must_use]
97    pub const fn thousandths(self) -> u16 {
98        self.0
99    }
100
101    /// Parses `( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] )`.
102    ///
103    /// # Errors
104    ///
105    /// [`ErrorKind::InvalidQValue`] for anything else (`1.5`, `0.1234`, `.5`,
106    /// `-1`, `1.001`, an empty value).
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use parse_net::QValue;
112    ///
113    /// assert_eq!(QValue::parse(b"0.125")?.thousandths(), 125);
114    /// assert!(QValue::parse(b"0.1234").is_err());
115    /// # Ok::<(), parse_net::ParseError>(())
116    /// ```
117    pub fn parse(s: &[u8]) -> Result<Self> {
118        let (first, rest) = match s.split_first() {
119            Some((&f, r)) => (f, r),
120            None => return err(ErrorKind::InvalidQValue, 0),
121        };
122        let frac = match rest.split_first() {
123            None => &[][..],
124            Some((b'.', digits)) if digits.len() <= 3 => digits,
125            Some(_) => return err(ErrorKind::InvalidQValue, 1),
126        };
127        if let Some(i) = frac.iter().position(|b| !b.is_ascii_digit()) {
128            return err(ErrorKind::InvalidQValue, 2 + i);
129        }
130        let mut thousandths = 0u16;
131        let mut scale = 100u16;
132        for &d in frac {
133            thousandths += u16::from(d - b'0') * scale;
134            scale /= 10;
135        }
136        match first {
137            b'0' => Ok(Self(thousandths)),
138            b'1' if thousandths == 0 => Ok(Self::ONE),
139            // The first byte that puts the value above 1.
140            b'1' => err(
141                ErrorKind::InvalidQValue,
142                2 + frac.iter().position(|&d| d != b'0').unwrap_or(0),
143            ),
144            _ => err(ErrorKind::InvalidQValue, 0),
145        }
146    }
147}
148
149/// How `Accept-Language` ranges are matched against offered tags.
150///
151/// # Examples
152///
153/// ```
154/// use parse_net::negotiate::{LanguageMatching, Negotiator};
155///
156/// let offers = ["en-US"];
157/// assert_eq!(Negotiator::new().language(Some(b"en"), &offers).index(), None);
158/// let filter = Negotiator::new().language_matching(LanguageMatching::BasicFilter);
159/// assert_eq!(filter.language(Some(b"en"), &offers).index(), Some(0));
160/// ```
161#[non_exhaustive]
162#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
163pub enum LanguageMatching {
164    /// RFC 4647 section 3.4 lookup: a range matches an offer equal to it or
165    /// reached by removing subtags from its end (`en-US` matches `en`).
166    #[default]
167    Lookup,
168    /// RFC 4647 section 3.3.1 basic filtering: a range matches an offer equal
169    /// to it or starting with it and `-` (`en` matches `en-US`).
170    BasicFilter,
171}
172
173/// What a selection does with a header that does not parse.
174///
175/// # Examples
176///
177/// ```
178/// use parse_net::negotiate::{MalformedPolicy, Negotiator, Outcome};
179///
180/// let bad = Some(&b"text/html;q=x"[..]);
181/// assert_eq!(Negotiator::new().media(bad, &["text/html"]), Outcome::HeaderIgnored { index: 0 });
182/// let strict = Negotiator::new().on_malformed(MalformedPolicy::Refuse);
183/// assert!(matches!(strict.media(bad, &["text/html"]), Outcome::Refused(_)));
184/// ```
185#[non_exhaustive]
186#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
187pub enum MalformedPolicy {
188    /// Treat the header as absent (RFC 9110 lets a server ignore it) and say
189    /// so with [`Outcome::HeaderIgnored`].
190    #[default]
191    TreatAsAbsent,
192    /// Report [`Outcome::Refused`], so the caller can answer 400.
193    Refuse,
194}
195
196/// The result of a selection.
197///
198/// # Examples
199///
200/// ```
201/// use parse_net::{negotiate::{Negotiator, Outcome}, QValue};
202///
203/// let n = Negotiator::new();
204/// assert_eq!(n.media(Some(b"text/html"), &["text/html"]), Outcome::Chosen { index: 0, weight: QValue::ONE });
205/// assert_eq!(n.media(Some(b"image/png"), &["text/html"]), Outcome::NoneAcceptable);
206/// assert_eq!(n.media(Some(b"text/html;q=9"), &["text/html"]).index(), Some(0));
207/// ```
208#[non_exhaustive]
209#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
210pub enum Outcome {
211    /// The offer at `index` is the best, with this weight.
212    Chosen {
213        /// The index into the offers.
214        index: usize,
215        /// Its weight.
216        weight: QValue,
217    },
218    /// No offer is acceptable: answer 406, or send a default.
219    NoneAcceptable,
220    /// The header did not parse and was treated as absent; the offer at
221    /// `index` (the first) is chosen.
222    HeaderIgnored {
223        /// The index into the offers.
224        index: usize,
225    },
226    /// The header did not parse, and the policy is to refuse it.
227    Refused(ParseError),
228}
229
230impl Outcome {
231    /// The chosen offer, if any.
232    ///
233    /// # Examples
234    ///
235    /// ```
236    /// use parse_net::negotiate::Negotiator;
237    ///
238    /// let n = Negotiator::new();
239    /// assert_eq!(n.encoding(Some(b"gzip"), &["br", "gzip"]).index(), Some(1));
240    /// assert_eq!(n.encoding(Some(b"gzip"), &["br"]).index(), None);
241    /// ```
242    #[must_use]
243    pub const fn index(&self) -> Option<usize> {
244        match self {
245            Self::Chosen { index, .. } | Self::HeaderIgnored { index } => Some(*index),
246            _ => None,
247        }
248    }
249}
250
251/// The configurable form of the `best_*` functions.
252///
253/// # Examples
254///
255/// ```
256/// use parse_net::negotiate::{LanguageMatching, MalformedPolicy, Negotiator, Outcome};
257///
258/// let n = Negotiator::new().language_matching(LanguageMatching::BasicFilter);
259/// assert_eq!(n.language(Some(b"en"), &["fr", "en-GB"]).index(), Some(1));
260/// let strict = Negotiator::new().on_malformed(MalformedPolicy::Refuse);
261/// assert!(matches!(strict.media(Some(b"text/html;q=2"), &["text/html"]), Outcome::Refused(_)));
262/// ```
263#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
264pub struct Negotiator {
265    limits: Limits,
266    language: LanguageMatching,
267    malformed: MalformedPolicy,
268}
269
270impl Default for Negotiator {
271    fn default() -> Self {
272        Self::new()
273    }
274}
275
276/// How many offers are evaluated per pass over the members.
277const GROUP: usize = 32;
278
279/// The weight and specificity of the best member seen for one offer.
280#[derive(Clone, Copy, Default)]
281struct Best {
282    specificity: u16,
283    weight: u16,
284    seen: bool,
285}
286
287impl Best {
288    /// Takes `weight` when `specificity` beats what is held, or equals it
289    /// for the first time (the earlier member wins a tie).
290    fn offer(&mut self, specificity: u16, weight: QValue) {
291        if !self.seen || specificity > self.specificity {
292            *self = Self {
293                specificity,
294                weight: weight.0,
295                seen: true,
296            };
297        }
298    }
299}
300
301/// The highest weight, ties going to the earlier offer.
302fn pick(weights: impl Iterator<Item = (usize, Option<u16>)>) -> Outcome {
303    let mut best: Option<(usize, u16)> = None;
304    for (index, w) in weights {
305        if let Some(w) = w {
306            if w > 0 && best.is_none_or(|(_, b)| w > b) {
307                best = Some((index, w));
308            }
309        }
310    }
311    match best {
312        Some((index, w)) => Outcome::Chosen {
313            index,
314            weight: QValue(w),
315        },
316        None => Outcome::NoneAcceptable,
317    }
318}
319
320impl Negotiator {
321    /// Default limits, RFC 4647 lookup, malformed headers treated as absent.
322    ///
323    /// # Examples
324    ///
325    /// ```
326    /// use parse_net::negotiate::Negotiator;
327    ///
328    /// let n = Negotiator::new();
329    /// assert_eq!(n, Negotiator::default());
330    /// assert_eq!(n.media(None, &["text/html", "text/plain"]).index(), Some(0));
331    /// ```
332    #[must_use]
333    pub const fn new() -> Self {
334        Self {
335            limits: Limits::DEFAULT,
336            language: LanguageMatching::Lookup,
337            malformed: MalformedPolicy::TreatAsAbsent,
338        }
339    }
340
341    /// Sets the limits the request headers are parsed with.
342    ///
343    /// # Examples
344    ///
345    /// ```
346    /// use parse_net::{negotiate::{Negotiator, Outcome}, Limits};
347    ///
348    /// let n = Negotiator::new().limits(Limits::DEFAULT.max_list_members(1));
349    /// assert_eq!(n.media(Some(b"text/plain, text/html"), &["text/html"]), Outcome::HeaderIgnored { index: 0 });
350    /// ```
351    #[must_use]
352    pub const fn limits(self, l: Limits) -> Self {
353        Self { limits: l, ..self }
354    }
355
356    /// Sets how language ranges are matched.
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// use parse_net::negotiate::{LanguageMatching, Negotiator};
362    ///
363    /// let n = Negotiator::new().language_matching(LanguageMatching::BasicFilter);
364    /// assert_eq!(n.language(Some(b"de"), &["fr", "de-AT"]).index(), Some(1));
365    /// ```
366    #[must_use]
367    pub const fn language_matching(self, m: LanguageMatching) -> Self {
368        Self {
369            language: m,
370            ..self
371        }
372    }
373
374    /// Sets what happens with a header that does not parse.
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// use parse_net::{negotiate::{MalformedPolicy, Negotiator, Outcome}, ErrorKind};
380    ///
381    /// let n = Negotiator::new().on_malformed(MalformedPolicy::Refuse);
382    /// let out = n.encoding(Some(b"gzip;q=2"), &["gzip"]);
383    /// assert!(matches!(out, Outcome::Refused(e) if e.kind() == ErrorKind::InvalidQValue));
384    /// ```
385    #[must_use]
386    pub const fn on_malformed(self, p: MalformedPolicy) -> Self {
387        Self {
388            malformed: p,
389            ..self
390        }
391    }
392
393    /// The outcome for a malformed header; `fallback` is the offer an absent
394    /// header would choose.
395    fn malformed(&self, e: ParseError, fallback: Option<usize>) -> Outcome {
396        match (self.malformed, fallback) {
397            (MalformedPolicy::Refuse, _) => Outcome::Refused(e),
398            (MalformedPolicy::TreatAsAbsent, None) => Outcome::NoneAcceptable,
399            (MalformedPolicy::TreatAsAbsent, Some(index)) => Outcome::HeaderIgnored { index },
400        }
401    }
402
403    /// Selects the best of `offers` (media types such as `"text/html"`) for
404    /// `Accept`.
405    ///
406    /// An offer's weight is the `q` of the most specific matching range
407    /// (RFC 9110 section 12.5.1); `q=0` means not acceptable; ties go to the
408    /// earlier offer. An absent header makes every offer acceptable, so the
409    /// first wins. An offer that is not a valid media type is never chosen.
410    ///
411    /// # Examples
412    ///
413    /// ```
414    /// use parse_net::{negotiate::{Negotiator, Outcome}, QValue};
415    ///
416    /// let n = Negotiator::new();
417    /// let accept = Some(&b"text/*;q=0.5, text/html"[..]);
418    /// assert_eq!(n.media(accept, &["text/plain", "text/html"]), Outcome::Chosen { index: 1, weight: QValue::ONE });
419    /// assert_eq!(n.media(accept, &["image/png"]), Outcome::NoneAcceptable);
420    /// ```
421    #[must_use]
422    pub fn media(&self, accept: Option<&[u8]>, offers: &[&str]) -> Outcome {
423        match (accept, first_media(offers)) {
424            (None, Some(index)) => Outcome::Chosen {
425                index,
426                weight: QValue::ONE,
427            },
428            (None, None) => Outcome::NoneAcceptable,
429            (Some(v), _) => self.media_lines([v], offers),
430        }
431    }
432
433    /// As [`media`](Self::media), for several `Accept` field lines read as
434    /// one list.
435    ///
436    /// # Examples
437    ///
438    /// ```
439    /// use parse_net::negotiate::Negotiator;
440    ///
441    /// let lines: [&[u8]; 2] = [b"text/plain;q=0.5", b"application/json"];
442    /// assert_eq!(Negotiator::new().media_lines(lines, &["text/plain", "application/json"]).index(), Some(1));
443    /// ```
444    #[must_use]
445    pub fn media_lines<'a, I>(&self, lines: I, offers: &[&str]) -> Outcome
446    where
447        I: IntoIterator<Item = &'a [u8]>,
448        I::IntoIter: Clone,
449    {
450        let lines = lines.into_iter();
451        let members = match list::combined(lines.clone(), &self.limits) {
452            Ok(l) => l,
453            Err(e) => return self.malformed(e, first_media(offers)),
454        };
455        let mut chosen: Option<(usize, u16)> = None;
456        for (group_index, group) in offers.chunks(GROUP).enumerate() {
457            let mut parsed: [Option<MediaType<'_>>; GROUP] = [None; GROUP];
458            for (slot, offer) in parsed.iter_mut().zip(group) {
459                *slot = media::parse(offer.as_bytes(), &Limits::DEFAULT).ok();
460            }
461            let parsed = parsed.get(..group.len()).unwrap_or(&[]);
462            let mut best = [Best::default(); GROUP];
463            // The first group's pass also validates every member, so a
464            // malformed header is caught before any offer is chosen, in the
465            // same walk that scores the offers.
466            for element in members.iter() {
467                let range = match media::parse_range(element, &self.limits) {
468                    Ok(r) => r,
469                    Err(e) if group_index == 0 => {
470                        let e = locate(e, element, lines.clone());
471                        return self.malformed(e, first_media(offers));
472                    }
473                    Err(_) => continue,
474                };
475                for (b, offer) in best.iter_mut().zip(parsed) {
476                    if let Some(offer) = offer {
477                        if range.matches(offer) {
478                            b.offer(u16::from(range.specificity()), range.weight());
479                        }
480                    }
481                }
482            }
483            let base = group_index * GROUP;
484            if let Outcome::Chosen { index, weight } = pick(
485                best.iter()
486                    .take(group.len())
487                    .enumerate()
488                    .map(|(i, b)| (base + i, b.seen.then_some(b.weight))),
489            ) {
490                if chosen.is_none_or(|(_, w)| weight.0 > w) {
491                    chosen = Some((index, weight.0));
492                }
493            }
494        }
495        if offers.is_empty() {
496            // No group ran, so validate the members on their own.
497            for element in members.iter() {
498                if let Err(e) = media::parse_range(element, &self.limits) {
499                    return self.malformed(locate(e, element, lines.clone()), None);
500                }
501            }
502        }
503        match chosen {
504            Some((index, w)) => Outcome::Chosen {
505                index,
506                weight: QValue(w),
507            },
508            None => Outcome::NoneAcceptable,
509        }
510    }
511
512    /// Selects the best of `offers` (language tags such as `"fr-CA"`) for
513    /// `Accept-Language`.
514    ///
515    /// With [`LanguageMatching::Lookup`] (the default), a range matches an
516    /// offer equal to it or reached by removing subtags from the range's end,
517    /// and the exact match has precedence over a shortened one; with
518    /// [`LanguageMatching::BasicFilter`], a range matches an offer equal to
519    /// it or starting with it and `-`, the longest range having precedence.
520    /// `*` matches every offer at the lowest precedence. The highest weight
521    /// wins; between equal weights the closer match (as lookup returns the
522    /// longest truncation), then the earlier offer. Tags compare ASCII
523    /// case-insensitively.
524    ///
525    /// # Examples
526    ///
527    /// ```
528    /// use parse_net::{negotiate::{Negotiator, Outcome}, QValue};
529    ///
530    /// let n = Negotiator::new();
531    /// assert_eq!(n.language(Some(b"fr-CA, en;q=0.8"), &["en", "fr"]), Outcome::Chosen { index: 1, weight: QValue::ONE });
532    /// ```
533    #[must_use]
534    pub fn language(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
535        let mode = self.language;
536        self.weighted(value, offers, |range, offer| match mode {
537            LanguageMatching::Lookup => lookup_rank(range, offer),
538            LanguageMatching::BasicFilter => filter_rank(range, offer),
539        })
540        .unwrap_or_else(|| first(offers))
541    }
542
543    /// Selects the best of `offers` (content codings such as `"br"`) for
544    /// `Accept-Encoding` (RFC 9110 section 12.5.3).
545    ///
546    /// `identity` is acceptable unless `identity;q=0`, or `*;q=0` without an
547    /// explicit `identity`; an empty field value means only `identity` is
548    /// acceptable; `x-gzip` equals `gzip` and `x-compress` equals `compress`.
549    ///
550    /// # Examples
551    ///
552    /// ```
553    /// use parse_net::negotiate::{Negotiator, Outcome};
554    ///
555    /// let n = Negotiator::new();
556    /// assert_eq!(n.encoding(Some(b"x-gzip"), &["br", "gzip"]).index(), Some(1));
557    /// assert_eq!(n.encoding(Some(b"*;q=0"), &["gzip", "identity"]), Outcome::NoneAcceptable);
558    /// ```
559    #[must_use]
560    pub fn encoding(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
561        let Some(v) = value else {
562            return first(offers);
563        };
564        if text::trim_ows(v).is_empty() {
565            return pick(
566                offers
567                    .iter()
568                    .enumerate()
569                    .map(|(i, o)| (i, o.eq_ignore_ascii_case("identity").then_some(1000))),
570            );
571        }
572        match self.coding_weights(v, offers, true) {
573            Ok(outcome) => outcome,
574            Err(e) => self.malformed(e, first(offers).index()),
575        }
576    }
577
578    /// Selects the best of `offers` (charsets such as `"utf-8"`) for
579    /// `Accept-Charset`: as [`encoding`](Self::encoding), without the
580    /// `identity` rule.
581    ///
582    /// # Examples
583    ///
584    /// ```
585    /// use parse_net::negotiate::{Negotiator, Outcome};
586    ///
587    /// let n = Negotiator::new();
588    /// assert_eq!(n.charset(Some(b"utf-8, *;q=0.1"), &["iso-8859-1", "utf-8"]).index(), Some(1));
589    /// assert_eq!(n.charset(Some(b"utf-8"), &["iso-8859-1"]), Outcome::NoneAcceptable);
590    /// ```
591    #[must_use]
592    pub fn charset(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
593        let Some(v) = value else {
594            return first(offers);
595        };
596        match self.coding_weights(v, offers, false) {
597            Ok(outcome) => outcome,
598            Err(e) => self.malformed(e, first(offers).index()),
599        }
600    }
601
602    /// Weights for codings and charsets: an exact match, else `*`, else (for
603    /// `identity` under the encoding rules) acceptable unless refused.
604    fn coding_weights(
605        &self,
606        value: &[u8],
607        offers: &[&str],
608        identity_rule: bool,
609    ) -> Result<Outcome> {
610        let members = strict_weighted(value, &self.limits, false)?;
611        let mut chosen: Option<(usize, u16)> = None;
612        for (group_index, group) in offers.chunks(GROUP).enumerate() {
613            let mut exact = [None::<u16>; GROUP];
614            let mut star = None::<u16>;
615            for m in members.iter() {
616                if m.value == b"*" {
617                    if star.is_none() {
618                        star = Some(m.weight.0);
619                    }
620                    continue;
621                }
622                let name = canonical(m.value);
623                for (slot, offer) in exact.iter_mut().zip(group) {
624                    if slot.is_none() && name.eq_ignore_ascii_case(canonical(offer.as_bytes())) {
625                        *slot = Some(m.weight.0);
626                    }
627                }
628            }
629            let base = group_index * GROUP;
630            let outcome = pick(group.iter().enumerate().map(|(i, offer)| {
631                let w = exact.get(i).copied().flatten().or(star).or_else(|| {
632                    (identity_rule && offer.eq_ignore_ascii_case("identity")).then_some(1000)
633                });
634                (base + i, w)
635            }));
636            if let Outcome::Chosen { index, weight } = outcome {
637                if chosen.is_none_or(|(_, w)| weight.0 > w) {
638                    chosen = Some((index, weight.0));
639                }
640            }
641        }
642        Ok(match chosen {
643            Some((index, w)) => Outcome::Chosen {
644                index,
645                weight: QValue(w),
646            },
647            None => Outcome::NoneAcceptable,
648        })
649    }
650
651    /// The selection shared by languages: each offer takes the weight of
652    /// the member with the highest rank for it (`rank` returns `None` when
653    /// a member does not match an offer). `None` for an absent header.
654    fn weighted(
655        &self,
656        value: Option<&[u8]>,
657        offers: &[&str],
658        rank: impl Fn(&[u8], &[u8]) -> Option<u16>,
659    ) -> Option<Outcome> {
660        let v = value?;
661        let members = match strict_weighted(v, &self.limits, true) {
662            Ok(m) => m,
663            Err(e) => return Some(self.malformed(e, first(offers).index())),
664        };
665        let mut chosen: Option<(usize, u16, u16)> = None;
666        for (group_index, group) in offers.chunks(GROUP).enumerate() {
667            let mut best = [Best::default(); GROUP];
668            for m in members.iter() {
669                for (b, offer) in best.iter_mut().zip(group) {
670                    if let Some(r) = rank(m.value, offer.as_bytes()) {
671                        b.offer(r, m.weight);
672                    }
673                }
674            }
675            // The highest weight wins; between equal weights the closer
676            // match (RFC 4647 lookup returns the longest truncation), then
677            // the earlier offer.
678            let base = group_index * GROUP;
679            for (i, b) in best.iter().take(group.len()).enumerate() {
680                if b.seen
681                    && b.weight > 0
682                    && chosen.is_none_or(|(_, w, r)| (b.weight, b.specificity) > (w, r))
683                {
684                    chosen = Some((base + i, b.weight, b.specificity));
685                }
686            }
687        }
688        Some(match chosen {
689            Some((index, w, _)) => Outcome::Chosen {
690                index,
691                weight: QValue(w),
692            },
693            None => Outcome::NoneAcceptable,
694        })
695    }
696}
697
698/// `x-gzip` is `gzip` and `x-compress` is `compress` (RFC 9110 section
699/// 8.4.1).
700fn canonical(c: &[u8]) -> &[u8] {
701    if c.eq_ignore_ascii_case(b"x-gzip") {
702        b"gzip"
703    } else if c.eq_ignore_ascii_case(b"x-compress") {
704        b"compress"
705    } else {
706        c
707    }
708}
709
710/// Every offer acceptable: the first wins.
711fn first(offers: &[&str]) -> Outcome {
712    if offers.is_empty() {
713        Outcome::NoneAcceptable
714    } else {
715        Outcome::Chosen {
716            index: 0,
717            weight: QValue::ONE,
718        }
719    }
720}
721
722/// RFC 4647 section 3.4: `range` matches `offer` when the offer equals the
723/// range or a truncation of it (removing a subtag from the end, and a
724/// single-character subtag left before it). An exact match ranks highest,
725/// then longer shortened matches; `*` ranks lowest.
726/// The first offer that is a valid media type: what an absent `Accept` chooses.
727fn first_media(offers: &[&str]) -> Option<usize> {
728    offers
729        .iter()
730        .position(|o| media::parse(o.as_bytes(), &Limits::DEFAULT).is_ok())
731}
732
733/// Places an error found inside `element` in the field: an offset from the
734/// start of its line, and the line.
735fn locate<'a>(e: ParseError, element: &[u8], lines: impl Iterator<Item = &'a [u8]>) -> ParseError {
736    let at = element.as_ptr() as usize;
737    for (n, line) in lines.enumerate() {
738        let start = line.as_ptr() as usize;
739        if (start..=start + line.len()).contains(&at) {
740            return e.shifted(at - start).on_line(n);
741        }
742    }
743    e
744}
745
746/// Where a `language-range` stops being one (RFC 4647 section 2.1:
747/// `1*8ALPHA *( "-" 1*8alphanum ) / "*"`); `None` when it is one.
748fn language_error(range: &[u8]) -> Option<usize> {
749    if range == b"*" {
750        return None;
751    }
752    let (mut at, mut first) = (0usize, true);
753    for label in range.split(|&b| b == b'-') {
754        let good = |b: &u8| {
755            if first {
756                b.is_ascii_alphabetic()
757            } else {
758                b.is_ascii_alphanumeric()
759            }
760        };
761        if let Some(i) = label.iter().position(|b| !good(b)) {
762            return Some(at + i);
763        }
764        if label.is_empty() || label.len() > 8 {
765            return Some(at + label.len().min(8));
766        }
767        first = false;
768        at += label.len() + 1;
769    }
770    None
771}
772
773/// [`parse_weighted`] with the grammar of the fields themselves: a member is
774/// its value and at most a weight, and with `language` the value is a
775/// `language-range`.
776fn strict_weighted<'a>(
777    value: &'a [u8],
778    limits: &Limits,
779    language: bool,
780) -> Result<WeightedList<'a>> {
781    let members = parse_weighted(value, limits)?;
782    for e in members.list.iter() {
783        let n = text::token_len(e);
784        let rest = e.get(n..).unwrap_or(&[]);
785        if language {
786            if let Some(at) = language_error(e.get(..n).unwrap_or(&[])) {
787                return Err(shift_into(
788                    ParseError::new(ErrorKind::Malformed, at),
789                    value,
790                    e,
791                ));
792            }
793        }
794        let (params, _) = param::parse(rest, limits).map_err(|x| x.shifted(n))?;
795        let only_weight = params
796            .iter()
797            .enumerate()
798            .all(|(i, p)| i == 0 && p.name.eq_ignore_ascii_case(b"q"));
799        if !only_weight {
800            return Err(shift_into(
801                ParseError::new(ErrorKind::TrailingData, n),
802                value,
803                e,
804            ));
805        }
806    }
807    Ok(members)
808}
809
810fn lookup_rank(range: &[u8], offer: &[u8]) -> Option<u16> {
811    if range == b"*" {
812        return Some(0);
813    }
814    if range.eq_ignore_ascii_case(offer) {
815        // An exact match has precedence over any shortened one.
816        return Some(u16::MAX);
817    }
818    let mut r = range;
819    loop {
820        if r.eq_ignore_ascii_case(offer) {
821            return Some(
822                u16::try_from(r.len())
823                    .unwrap_or(u16::MAX - 1)
824                    .saturating_add(1)
825                    .min(u16::MAX - 1),
826            );
827        }
828        let cut = r.iter().rposition(|&b| b == b'-')?;
829        r = r.get(..cut)?;
830        // A single-character subtag (an extension or private-use singleton)
831        // goes with the subtag after it.
832        if let Some(dash) = r.iter().rposition(|&b| b == b'-') {
833            if r.len() - dash == 2 {
834                r = r.get(..dash)?;
835            }
836        }
837    }
838}
839
840/// RFC 4647 section 3.3.1: `range` matches `offer` when they are equal or
841/// the offer starts with the range and `-`. Longer ranges rank higher; `*`
842/// ranks lowest.
843fn filter_rank(range: &[u8], offer: &[u8]) -> Option<u16> {
844    if range == b"*" {
845        return Some(0);
846    }
847    let prefix = offer.get(..range.len())?;
848    let boundary = offer.len() == range.len() || offer.get(range.len()) == Some(&b'-');
849    (prefix.eq_ignore_ascii_case(range) && boundary).then(|| {
850        u16::try_from(range.len())
851            .unwrap_or(u16::MAX)
852            .saturating_add(1)
853    })
854}
855
856/// Selects the best media type for `Accept` with the defaults.
857///
858/// `None`: no offer is acceptable (answer 406 or send a default). A header
859/// that does not parse is treated as absent.
860///
861/// # Examples
862///
863/// ```
864/// use parse_net::best_media;
865///
866/// assert_eq!(best_media(Some(b"text/*;q=0.5, text/html;q=0"), &["text/html"]), None);
867/// assert_eq!(best_media(None, &["application/json", "text/html"]), Some(0));
868/// ```
869#[must_use]
870pub fn best_media(accept: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
871    Negotiator::new().media(accept, offers).index()
872}
873
874/// Selects the best language for `Accept-Language` with RFC 4647 lookup.
875///
876/// # Examples
877///
878/// ```
879/// use parse_net::best_language;
880///
881/// assert_eq!(best_language(Some(b"fr-CA, en;q=0.5"), &["en", "fr", "de"]), Some(1));
882/// assert_eq!(best_language(Some(b"de"), &["en"]), None);
883/// ```
884#[must_use]
885pub fn best_language(accept_language: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
886    Negotiator::new().language(accept_language, offers).index()
887}
888
889/// Selects the best content coding for `Accept-Encoding`.
890///
891/// # Examples
892///
893/// ```
894/// use parse_net::best_encoding;
895///
896/// assert_eq!(best_encoding(Some(b"gzip;q=1.0, identity; q=0.5, *;q=0"), &["br", "identity"]), Some(1));
897/// assert_eq!(best_encoding(Some(b""), &["gzip", "identity"]), Some(1));
898/// ```
899#[must_use]
900pub fn best_encoding(accept_encoding: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
901    Negotiator::new().encoding(accept_encoding, offers).index()
902}
903
904/// Selects the best charset for `Accept-Charset`.
905///
906/// # Examples
907///
908/// ```
909/// use parse_net::best_charset;
910///
911/// assert_eq!(best_charset(Some(b"iso-8859-1;q=0.5, utf-8"), &["iso-8859-1", "utf-8"]), Some(1));
912/// ```
913#[must_use]
914pub fn best_charset(accept_charset: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
915    Negotiator::new().charset(accept_charset, offers).index()
916}
917
918/// Parses `Accept` into its media ranges.
919///
920/// # Errors
921///
922/// The list errors and those of [`media::parse_range`].
923///
924/// # Examples
925///
926/// ```
927/// use parse_net::{negotiate::parse_accept, Limits};
928///
929/// let a = parse_accept(b"text/html, */*;q=0.1", &Limits::DEFAULT)?;
930/// assert_eq!(a.iter().map(|r| r.weight().thousandths()).collect::<Vec<_>>(), [1000, 100]);
931/// # Ok::<(), parse_net::ParseError>(())
932/// ```
933pub fn parse_accept<'a>(value: &'a [u8], limits: &Limits) -> Result<AcceptList<'a>> {
934    let l = list::parse(value, limits)?;
935    for e in l.iter() {
936        media::parse_range(e, limits)
937            .map(|_| ())
938            .map_err(|err| shift_into(err, value, e))?;
939    }
940    Ok(AcceptList {
941        list: l,
942        limits: *limits,
943    })
944}
945
946/// Re-bases an error found in `element` onto the offsets of `value`.
947fn shift_into(e: ParseError, value: &[u8], element: &[u8]) -> ParseError {
948    let base = (element.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
949    e.shifted(base)
950}
951
952/// The media ranges of a validated `Accept` value.
953///
954/// # Examples
955///
956/// ```
957/// use parse_net::{negotiate::parse_accept, Limits};
958///
959/// let a = parse_accept(b"text/html, application/*;q=0.2", &Limits::DEFAULT)?;
960/// assert_eq!(a.len(), 2);
961/// assert!(a.iter().any(|r| r.specificity() == 2));
962/// # Ok::<(), parse_net::ParseError>(())
963/// ```
964#[derive(Clone, Copy, Debug)]
965pub struct AcceptList<'a> {
966    list: list::List<'a>,
967    limits: Limits,
968}
969
970impl<'a> AcceptList<'a> {
971    /// The ranges, in order.
972    ///
973    /// # Examples
974    ///
975    /// ```
976    /// use parse_net::{negotiate::parse_accept, Limits};
977    ///
978    /// let a = parse_accept(b"image/webp, image/*;q=0.8", &Limits::DEFAULT)?;
979    /// let subtypes: Vec<_> = a.iter().map(|r| r.subtype()).collect();
980    /// assert_eq!(subtypes, [&b"webp"[..], &b"*"[..]]);
981    /// # Ok::<(), parse_net::ParseError>(())
982    /// ```
983    pub fn iter(&self) -> impl Iterator<Item = media::MediaRange<'a>> + '_ {
984        self.list
985            .iter()
986            .filter_map(|e| media::parse_range(e, &self.limits).ok())
987    }
988
989    /// The number of ranges.
990    ///
991    /// # Examples
992    ///
993    /// ```
994    /// use parse_net::{negotiate::parse_accept, Limits};
995    ///
996    /// assert_eq!(parse_accept(b"text/html, , text/plain", &Limits::DEFAULT)?.len(), 2);
997    /// # Ok::<(), parse_net::ParseError>(())
998    /// ```
999    #[must_use]
1000    pub const fn len(&self) -> usize {
1001        self.list.len()
1002    }
1003
1004    /// Whether there are no ranges.
1005    ///
1006    /// # Examples
1007    ///
1008    /// ```
1009    /// use parse_net::{negotiate::parse_accept, Limits};
1010    ///
1011    /// assert!(!parse_accept(b"*/*", &Limits::DEFAULT)?.is_empty());
1012    /// # Ok::<(), parse_net::ParseError>(())
1013    /// ```
1014    #[must_use]
1015    pub const fn is_empty(&self) -> bool {
1016        self.list.is_empty()
1017    }
1018}
1019
1020/// One member of a weighted list: a value, its weight, and its parameters.
1021///
1022/// # Examples
1023///
1024/// ```
1025/// use parse_net::{negotiate::parse_weighted, Limits};
1026///
1027/// let w = parse_weighted(b"en-GB;q=0.7", &Limits::DEFAULT)?;
1028/// let m = w.iter().next().expect("one member");
1029/// assert_eq!((m.value, m.weight.thousandths(), m.params.len()), (&b"en-GB"[..], 700, 0));
1030/// # Ok::<(), parse_net::ParseError>(())
1031/// ```
1032#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1033pub struct Weighted<'a> {
1034    /// The value (a coding, a charset, or a language range).
1035    pub value: &'a [u8],
1036    /// Its `q`, 1 when absent.
1037    pub weight: QValue,
1038    /// The parameters before `q`.
1039    pub params: Params<'a>,
1040}
1041
1042/// Parses `token *( OWS ";" OWS parameter )` members with weights, as in
1043/// `Accept-Encoding`, `Accept-Charset`, and `Accept-Language`.
1044///
1045/// # Errors
1046///
1047/// The list errors; [`ErrorKind::NotToken`] for a member that does not start
1048/// with a token; the parameter errors; [`ErrorKind::InvalidQValue`].
1049///
1050/// # Examples
1051///
1052/// ```
1053/// use parse_net::{negotiate::parse_weighted, Limits};
1054///
1055/// let w = parse_weighted(b"gzip;q=0.8, br", &Limits::DEFAULT)?;
1056/// let all: Vec<_> = w.iter().map(|m| (m.value, m.weight.thousandths())).collect();
1057/// assert_eq!(all, [(&b"gzip"[..], 800), (&b"br"[..], 1000)]);
1058/// # Ok::<(), parse_net::ParseError>(())
1059/// ```
1060pub fn parse_weighted<'a>(value: &'a [u8], limits: &Limits) -> Result<WeightedList<'a>> {
1061    let l = list::parse(value, limits)?;
1062    for e in l.iter() {
1063        weighted_member(e, limits)
1064            .map(|_| ())
1065            .map_err(|err| shift_into(err, value, e))?;
1066    }
1067    Ok(WeightedList {
1068        list: l,
1069        limits: *limits,
1070    })
1071}
1072
1073fn weighted_member<'a>(element: &'a [u8], limits: &Limits) -> Result<Weighted<'a>> {
1074    let (token, n) =
1075        text::parse_token(element).map_err(|e| ParseError::new(ErrorKind::NotToken, e.offset()))?;
1076    let rest = element.get(n..).unwrap_or(&[]);
1077    let (all, used) = param::parse(rest, limits).map_err(|e| e.shifted(n))?;
1078    if n + used != element.len() {
1079        let at = n + used + text::skip_ows(element.get(n + used..).unwrap_or(&[]));
1080        return err(ErrorKind::NotToken, at);
1081    }
1082    let (params, q) = all.split_at_name(b"q");
1083    let weight = match q {
1084        None => QValue::ONE,
1085        Some(p) => match p.value {
1086            Word::Token(t) => QValue::parse(t).map_err(|e| {
1087                let at = (t.as_ptr() as usize).saturating_sub(element.as_ptr() as usize);
1088                ParseError::new(e.kind(), at + e.offset())
1089            })?,
1090            Word::Quoted(_) => {
1091                let at = (p.name.as_ptr() as usize).saturating_sub(element.as_ptr() as usize);
1092                return err(ErrorKind::InvalidQValue, at);
1093            }
1094        },
1095    };
1096    Ok(Weighted {
1097        value: token,
1098        weight,
1099        params,
1100    })
1101}
1102
1103/// The members of a validated weighted list.
1104///
1105/// # Examples
1106///
1107/// ```
1108/// use parse_net::{negotiate::parse_weighted, Limits};
1109///
1110/// let w = parse_weighted(b"utf-8, iso-8859-1;q=0.5", &Limits::DEFAULT)?;
1111/// assert_eq!(w.len(), 2);
1112/// assert!(w.iter().all(|m| m.weight.thousandths() >= 500));
1113/// # Ok::<(), parse_net::ParseError>(())
1114/// ```
1115#[derive(Clone, Copy, Debug)]
1116pub struct WeightedList<'a> {
1117    list: list::List<'a>,
1118    limits: Limits,
1119}
1120
1121impl<'a> WeightedList<'a> {
1122    /// The members, in order.
1123    ///
1124    /// # Examples
1125    ///
1126    /// ```
1127    /// use parse_net::{negotiate::parse_weighted, Limits};
1128    ///
1129    /// let w = parse_weighted(b"gzip;q=0.5, br", &Limits::DEFAULT)?;
1130    /// let best = w.iter().max_by_key(|m| m.weight).map(|m| m.value);
1131    /// assert_eq!(best, Some(&b"br"[..]));
1132    /// # Ok::<(), parse_net::ParseError>(())
1133    /// ```
1134    pub fn iter(&self) -> impl Iterator<Item = Weighted<'a>> + '_ {
1135        self.list
1136            .iter()
1137            .filter_map(|e| weighted_member(e, &self.limits).ok())
1138    }
1139
1140    /// The number of members.
1141    ///
1142    /// # Examples
1143    ///
1144    /// ```
1145    /// use parse_net::{negotiate::parse_weighted, Limits};
1146    ///
1147    /// assert_eq!(parse_weighted(b"gzip, deflate, br", &Limits::DEFAULT)?.len(), 3);
1148    /// # Ok::<(), parse_net::ParseError>(())
1149    /// ```
1150    #[must_use]
1151    pub const fn len(&self) -> usize {
1152        self.list.len()
1153    }
1154
1155    /// Whether there are no members.
1156    ///
1157    /// # Examples
1158    ///
1159    /// ```
1160    /// use parse_net::{negotiate::parse_weighted, Limits};
1161    ///
1162    /// assert!(!parse_weighted(b"identity", &Limits::DEFAULT)?.is_empty());
1163    /// # Ok::<(), parse_net::ParseError>(())
1164    /// ```
1165    #[must_use]
1166    pub const fn is_empty(&self) -> bool {
1167        self.list.is_empty()
1168    }
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173    use super::*;
1174
1175    #[test]
1176    fn test_qvalue_edge_cases() {
1177        let ok = |s: &[u8]| QValue::parse(s).map(QValue::thousandths);
1178        assert_eq!(ok(b"0"), Ok(0));
1179        assert_eq!(ok(b"0."), Ok(0));
1180        assert_eq!(ok(b"0.000"), Ok(0));
1181        assert_eq!(ok(b"0.5"), Ok(500));
1182        assert_eq!(ok(b"0.05"), Ok(50));
1183        assert_eq!(ok(b"0.999"), Ok(999));
1184        assert_eq!(ok(b"1"), Ok(1000));
1185        assert_eq!(ok(b"1.000"), Ok(1000));
1186        for bad in [
1187            &b"1.001"[..],
1188            b"0.0001",
1189            b".5",
1190            b"-1",
1191            b"1.5",
1192            b"",
1193            b"2",
1194            b"0,5",
1195            b"0.5x",
1196        ] {
1197            assert_eq!(
1198                QValue::parse(bad).unwrap_err().kind(),
1199                ErrorKind::InvalidQValue
1200            );
1201        }
1202        assert_eq!(QValue::from_thousandths(1001), None);
1203        assert_eq!(
1204            QValue::from_thousandths(250).map(QValue::thousandths),
1205            Some(250)
1206        );
1207    }
1208
1209    #[test]
1210    fn test_media_negotiation_cases() {
1211        let n = Negotiator::new();
1212        let i = |h: &[u8], o: &[&str]| n.media(Some(h), o).index();
1213        assert_eq!(
1214            i(b"*/*;q=0, text/html", &["text/html", "text/plain"]),
1215            Some(0)
1216        );
1217        assert_eq!(
1218            i(b"text/*;q=0.5, text/html;q=0", &["text/html", "text/plain"]),
1219            Some(1)
1220        );
1221        assert_eq!(
1222            i(
1223                b"text/html;level=1, text/html;q=0.1",
1224                &["text/html", "text/html;level=1"]
1225            ),
1226            Some(1)
1227        );
1228        assert_eq!(
1229            i(
1230                b"application/json;q=0.5, text/html;q=0.5",
1231                &["text/html", "application/json"]
1232            ),
1233            Some(0)
1234        );
1235        assert_eq!(i(b"", &["text/html"]), None);
1236        assert_eq!(i(b"TEXT/HTML", &["text/html"]), Some(0));
1237        assert_eq!(
1238            i(b"text/html;charset=UTF-8", &["text/html;charset=utf-8"]),
1239            Some(0)
1240        );
1241        assert_eq!(i(b"text/html;level=1", &["text/html;level=2"]), None);
1242        assert_eq!(n.media(None, &[]), Outcome::NoneAcceptable);
1243        assert_eq!(i(b"*/*", &["not a media type", "text/html"]), Some(1));
1244    }
1245
1246    #[test]
1247    fn test_media_malformed_policies() {
1248        let bad = Some(&b"text/html;q=2"[..]);
1249        assert_eq!(
1250            Negotiator::new().media(bad, &["a/b"]),
1251            Outcome::HeaderIgnored { index: 0 }
1252        );
1253        assert_eq!(Negotiator::new().media(bad, &[]), Outcome::NoneAcceptable);
1254        let refused = Negotiator::new()
1255            .on_malformed(MalformedPolicy::Refuse)
1256            .media(bad, &["a/b"]);
1257        assert!(matches!(refused, Outcome::Refused(e) if e.kind() == ErrorKind::InvalidQValue));
1258    }
1259
1260    #[test]
1261    fn test_media_more_than_one_group_of_offers() {
1262        let names: std::vec::Vec<std::string::String> =
1263            (0..70).map(|i| std::format!("type/s{i}")).collect();
1264        let offers: std::vec::Vec<&str> = names.iter().map(std::string::String::as_str).collect();
1265        assert_eq!(
1266            Negotiator::new().media(Some(b"type/s65"), &offers).index(),
1267            Some(65)
1268        );
1269        assert_eq!(
1270            Negotiator::new()
1271                .media(Some(b"type/s65;q=0.5, type/s3;q=0.4"), &offers)
1272                .index(),
1273            Some(65)
1274        );
1275        assert_eq!(
1276            Negotiator::new().media(Some(b"type/*"), &offers).index(),
1277            Some(0)
1278        );
1279    }
1280
1281    #[test]
1282    fn test_language_lookup_and_filter() {
1283        let n = Negotiator::new();
1284        let i = |h: &[u8], o: &[&str]| n.language(Some(h), o).index();
1285        assert_eq!(i(b"en-US", &["fr", "en"]), Some(1));
1286        assert_eq!(i(b"EN-us", &["en-US"]), Some(0));
1287        assert_eq!(i(b"zh-Hant-CN-x-private1", &["zh", "zh-Hant"]), Some(1));
1288        assert_eq!(i(b"*", &["de", "fr"]), Some(0));
1289        assert_eq!(i(b"en-US, en;q=0", &["en"]), None);
1290        assert_eq!(i(b"fr;q=0.5, en;q=0.9", &["fr", "en"]), Some(1));
1291        assert_eq!(i(b"en", &["en-US"]), None);
1292        let f = Negotiator::new().language_matching(LanguageMatching::BasicFilter);
1293        assert_eq!(f.language(Some(b"en"), &["en-US"]).index(), Some(0));
1294        assert_eq!(f.language(Some(b"en"), &["eng"]).index(), None);
1295        assert_eq!(
1296            f.language(Some(b"de-DE;q=0.9, de;q=0.1"), &["de-DE-1996"]),
1297            Outcome::Chosen {
1298                index: 0,
1299                weight: QValue(900)
1300            }
1301        );
1302        assert_eq!(n.language(None, &["en"]).index(), Some(0));
1303    }
1304
1305    #[test]
1306    fn test_encoding_identity_rules() {
1307        let e = |h: &[u8], o: &[&str]| best_encoding(Some(h), o);
1308        assert_eq!(e(b"gzip", &["identity"]), Some(0));
1309        assert_eq!(e(b"gzip, identity;q=0", &["identity"]), None);
1310        assert_eq!(e(b"*;q=0", &["identity"]), None);
1311        assert_eq!(e(b"*;q=0, identity", &["br", "identity"]), Some(1));
1312        assert_eq!(e(b"x-gzip", &["br", "gzip"]), Some(1));
1313        assert_eq!(e(b"gzip;q=0.5, *;q=0.9", &["gzip", "br"]), Some(1));
1314        assert_eq!(e(b"   ", &["gzip", "identity"]), Some(1));
1315        assert_eq!(best_encoding(None, &["br"]), Some(0));
1316        assert_eq!(e(b"gzip;q=x", &["br", "gzip"]), Some(0));
1317    }
1318
1319    #[test]
1320    fn test_charset_rules() {
1321        assert_eq!(best_charset(Some(b"utf-8"), &["iso-8859-1"]), None);
1322        assert_eq!(best_charset(Some(b"*;q=0.1"), &["iso-8859-1"]), Some(0));
1323        assert_eq!(best_charset(Some(b""), &["utf-8"]), None);
1324    }
1325
1326    #[test]
1327    fn test_weighted_member_errors() {
1328        let l = Limits::DEFAULT;
1329        assert_eq!(
1330            parse_weighted(b"gzip;q=\"1\"", &l).unwrap_err().kind(),
1331            ErrorKind::InvalidQValue
1332        );
1333        assert_eq!(
1334            parse_weighted(b"gzip x", &l).unwrap_err().kind(),
1335            ErrorKind::NotToken
1336        );
1337        assert_eq!(
1338            parse_weighted(b"@", &l).unwrap_err().kind(),
1339            ErrorKind::NotToken
1340        );
1341        let e = parse_weighted(b"br, gzip;q=7", &l).unwrap_err();
1342        assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidQValue, 11));
1343        assert_eq!(
1344            parse_weighted(b"a;q=1;q=0", &l).unwrap_err().kind(),
1345            ErrorKind::DuplicateParameter
1346        );
1347        assert_eq!(parse_accept(b"text/html;q=2", &l).unwrap_err().offset(), 12);
1348    }
1349
1350    #[test]
1351    fn test_members_times_offers_is_bounded() {
1352        let mut v = std::vec::Vec::new();
1353        for i in 0..1024 {
1354            v.extend_from_slice(std::format!("t{i}/x;q=0.5,").as_bytes());
1355        }
1356        v.truncate(v.len() - 1);
1357        let names: std::vec::Vec<std::string::String> =
1358            (0..32).map(|i| std::format!("t{i}/x")).collect();
1359        let offers: std::vec::Vec<&str> = names.iter().map(std::string::String::as_str).collect();
1360        assert_eq!(best_media(Some(&v), &offers), Some(0));
1361    }
1362}