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