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    pub const ONE: Self = Self(1000);
49    /// `q=0`: not acceptable.
50    pub const ZERO: Self = Self(0);
51
52    /// The weight `t` thousandths, if `t` is at most 1000.
53    #[must_use]
54    pub const fn from_thousandths(t: u16) -> Option<Self> {
55        if t <= 1000 { Some(Self(t)) } else { None }
56    }
57
58    /// The weight in thousandths, 0 to 1000.
59    #[must_use]
60    pub const fn thousandths(self) -> u16 {
61        self.0
62    }
63
64    /// Parses `( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] )`.
65    ///
66    /// # Errors
67    ///
68    /// [`ErrorKind::InvalidQValue`] for anything else (`1.5`, `0.1234`, `.5`,
69    /// `-1`, `1.001`, an empty value).
70    pub fn parse(s: &[u8]) -> Result<Self> {
71        let (first, rest) = match s.split_first() {
72            Some((&f, r)) => (f, r),
73            None => return err(ErrorKind::InvalidQValue, 0),
74        };
75        let frac = match rest.split_first() {
76            None => &[][..],
77            Some((b'.', digits)) if digits.len() <= 3 => digits,
78            Some(_) => return err(ErrorKind::InvalidQValue, 1),
79        };
80        if let Some(i) = frac.iter().position(|b| !b.is_ascii_digit()) {
81            return err(ErrorKind::InvalidQValue, 2 + i);
82        }
83        let mut thousandths = 0u16;
84        let mut scale = 100u16;
85        for &d in frac {
86            thousandths += u16::from(d - b'0') * scale;
87            scale /= 10;
88        }
89        match first {
90            b'0' => Ok(Self(thousandths)),
91            b'1' if thousandths == 0 => Ok(Self::ONE),
92            _ => err(ErrorKind::InvalidQValue, 0),
93        }
94    }
95}
96
97/// How `Accept-Language` ranges are matched against offered tags.
98#[non_exhaustive]
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
100pub enum LanguageMatching {
101    /// RFC 4647 section 3.4 lookup: a range matches an offer equal to it or
102    /// reached by removing subtags from its end (`en-US` matches `en`).
103    #[default]
104    Lookup,
105    /// RFC 4647 section 3.3.1 basic filtering: a range matches an offer equal
106    /// to it or starting with it and `-` (`en` matches `en-US`).
107    BasicFilter,
108}
109
110/// What a selection does with a header that does not parse.
111#[non_exhaustive]
112#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
113pub enum MalformedPolicy {
114    /// Treat the header as absent (RFC 9110 lets a server ignore it) and say
115    /// so with [`Outcome::HeaderIgnored`].
116    #[default]
117    TreatAsAbsent,
118    /// Report [`Outcome::Refused`], so the caller can answer 400.
119    Refuse,
120}
121
122/// The result of a selection.
123///
124/// # Examples
125///
126/// ```
127/// use parse_net::{negotiate::{Negotiator, Outcome}, QValue};
128///
129/// let n = Negotiator::new();
130/// assert_eq!(n.media(Some(b"text/html"), &["text/html"]), Outcome::Chosen { index: 0, weight: QValue::ONE });
131/// assert_eq!(n.media(Some(b"image/png"), &["text/html"]), Outcome::NoneAcceptable);
132/// assert_eq!(n.media(Some(b"text/html;q=9"), &["text/html"]).index(), Some(0));
133/// ```
134#[non_exhaustive]
135#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
136pub enum Outcome {
137    /// The offer at `index` is the best, with this weight.
138    Chosen {
139        /// The index into the offers.
140        index: usize,
141        /// Its weight.
142        weight: QValue,
143    },
144    /// No offer is acceptable: answer 406, or send a default.
145    NoneAcceptable,
146    /// The header did not parse and was treated as absent; the offer at
147    /// `index` (the first) is chosen.
148    HeaderIgnored {
149        /// The index into the offers.
150        index: usize,
151    },
152    /// The header did not parse, and the policy is to refuse it.
153    Refused(ParseError),
154}
155
156impl Outcome {
157    /// The chosen offer, if any.
158    #[must_use]
159    pub const fn index(&self) -> Option<usize> {
160        match self {
161            Self::Chosen { index, .. } | Self::HeaderIgnored { index } => Some(*index),
162            _ => None,
163        }
164    }
165}
166
167/// The configurable form of the `best_*` functions.
168///
169/// # Examples
170///
171/// ```
172/// use parse_net::negotiate::{LanguageMatching, MalformedPolicy, Negotiator, Outcome};
173///
174/// let n = Negotiator::new().language_matching(LanguageMatching::BasicFilter);
175/// assert_eq!(n.language(Some(b"en"), &["fr", "en-GB"]).index(), Some(1));
176/// let strict = Negotiator::new().on_malformed(MalformedPolicy::Refuse);
177/// assert!(matches!(strict.media(Some(b"text/html;q=2"), &["text/html"]), Outcome::Refused(_)));
178/// ```
179#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
180pub struct Negotiator {
181    limits: Limits,
182    language: LanguageMatching,
183    malformed: MalformedPolicy,
184}
185
186impl Default for Negotiator {
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192/// How many offers are evaluated per pass over the members.
193const GROUP: usize = 32;
194
195/// The weight and specificity of the best member seen for one offer.
196#[derive(Clone, Copy, Default)]
197struct Best {
198    specificity: u16,
199    weight: u16,
200    seen: bool,
201}
202
203impl Best {
204    /// Takes `weight` when `specificity` beats what is held, or equals it
205    /// for the first time (the earlier member wins a tie).
206    fn offer(&mut self, specificity: u16, weight: QValue) {
207        if !self.seen || specificity > self.specificity {
208            *self = Self {
209                specificity,
210                weight: weight.0,
211                seen: true,
212            };
213        }
214    }
215}
216
217/// The highest weight, ties going to the earlier offer.
218fn pick(weights: impl Iterator<Item = (usize, Option<u16>)>) -> Outcome {
219    let mut best: Option<(usize, u16)> = None;
220    for (index, w) in weights {
221        if let Some(w) = w {
222            if w > 0 && best.is_none_or(|(_, b)| w > b) {
223                best = Some((index, w));
224            }
225        }
226    }
227    match best {
228        Some((index, w)) => Outcome::Chosen {
229            index,
230            weight: QValue(w),
231        },
232        None => Outcome::NoneAcceptable,
233    }
234}
235
236impl Negotiator {
237    /// Default limits, RFC 4647 lookup, malformed headers treated as absent.
238    #[must_use]
239    pub const fn new() -> Self {
240        Self {
241            limits: Limits::DEFAULT,
242            language: LanguageMatching::Lookup,
243            malformed: MalformedPolicy::TreatAsAbsent,
244        }
245    }
246
247    /// Sets the limits the request headers are parsed with.
248    #[must_use]
249    pub const fn limits(self, l: Limits) -> Self {
250        Self { limits: l, ..self }
251    }
252
253    /// Sets how language ranges are matched.
254    #[must_use]
255    pub const fn language_matching(self, m: LanguageMatching) -> Self {
256        Self {
257            language: m,
258            ..self
259        }
260    }
261
262    /// Sets what happens with a header that does not parse.
263    #[must_use]
264    pub const fn on_malformed(self, p: MalformedPolicy) -> Self {
265        Self {
266            malformed: p,
267            ..self
268        }
269    }
270
271    fn malformed(&self, e: ParseError, offers: usize) -> Outcome {
272        match self.malformed {
273            MalformedPolicy::Refuse => Outcome::Refused(e),
274            MalformedPolicy::TreatAsAbsent if offers == 0 => Outcome::NoneAcceptable,
275            MalformedPolicy::TreatAsAbsent => Outcome::HeaderIgnored { index: 0 },
276        }
277    }
278
279    /// Selects the best of `offers` (media types such as `"text/html"`) for
280    /// `Accept`.
281    ///
282    /// An offer's weight is the `q` of the most specific matching range
283    /// (RFC 9110 section 12.5.1); `q=0` means not acceptable; ties go to the
284    /// earlier offer. An absent header makes every offer acceptable, so the
285    /// first wins. An offer that is not a valid media type is never chosen.
286    #[must_use]
287    pub fn media(&self, accept: Option<&[u8]>, offers: &[&str]) -> Outcome {
288        match accept {
289            None => first(offers),
290            Some(v) => self.media_lines([v], offers),
291        }
292    }
293
294    /// As [`media`](Self::media), for several `Accept` field lines read as
295    /// one list.
296    #[must_use]
297    pub fn media_lines<'a, I>(&self, lines: I, offers: &[&str]) -> Outcome
298    where
299        I: IntoIterator<Item = &'a [u8]>,
300        I::IntoIter: Clone,
301    {
302        let lines = lines.into_iter();
303        // Validate every member once, so a malformed header is caught before
304        // any selection.
305        let checked = list::combined(lines.clone(), &self.limits).and_then(|l| {
306            for e in l.iter() {
307                media::parse_range(e, &self.limits).map(|_| ())?;
308            }
309            Ok(l)
310        });
311        let members = match checked {
312            Ok(l) => l,
313            Err(e) => return self.malformed(e, offers.len()),
314        };
315        let mut chosen: Option<(usize, u16)> = None;
316        for (group_index, group) in offers.chunks(GROUP).enumerate() {
317            let mut parsed: [Option<MediaType<'_>>; GROUP] = [None; GROUP];
318            for (slot, offer) in parsed.iter_mut().zip(group) {
319                *slot = media::parse(offer.as_bytes(), &Limits::DEFAULT).ok();
320            }
321            let mut best = [Best::default(); GROUP];
322            for element in members.iter() {
323                let Ok(range) = media::parse_range(element, &self.limits) else {
324                    continue;
325                };
326                for (b, offer) in best.iter_mut().zip(&parsed) {
327                    if let Some(offer) = offer {
328                        if range.matches(offer) {
329                            b.offer(u16::from(range.specificity()), range.weight());
330                        }
331                    }
332                }
333            }
334            let base = group_index * GROUP;
335            if let Outcome::Chosen { index, weight } = pick(
336                best.iter()
337                    .take(group.len())
338                    .enumerate()
339                    .map(|(i, b)| (base + i, b.seen.then_some(b.weight))),
340            ) {
341                if chosen.is_none_or(|(_, w)| weight.0 > w) {
342                    chosen = Some((index, weight.0));
343                }
344            }
345        }
346        match chosen {
347            Some((index, w)) => Outcome::Chosen {
348                index,
349                weight: QValue(w),
350            },
351            None => Outcome::NoneAcceptable,
352        }
353    }
354
355    /// Selects the best of `offers` (language tags such as `"fr-CA"`) for
356    /// `Accept-Language`.
357    ///
358    /// With [`LanguageMatching::Lookup`] (the default), a range matches an
359    /// offer equal to it or reached by removing subtags from the range's end,
360    /// and the exact match has precedence over a shortened one; with
361    /// [`LanguageMatching::BasicFilter`], a range matches an offer equal to
362    /// it or starting with it and `-`, the longest range having precedence.
363    /// `*` matches every offer at the lowest precedence. The highest weight
364    /// wins; between equal weights the closer match (as lookup returns the
365    /// longest truncation), then the earlier offer. Tags compare ASCII
366    /// case-insensitively.
367    #[must_use]
368    pub fn language(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
369        let mode = self.language;
370        self.weighted(value, offers, |range, offer| match mode {
371            LanguageMatching::Lookup => lookup_rank(range, offer),
372            LanguageMatching::BasicFilter => filter_rank(range, offer),
373        })
374        .unwrap_or_else(|| first(offers))
375    }
376
377    /// Selects the best of `offers` (content codings such as `"br"`) for
378    /// `Accept-Encoding` (RFC 9110 section 12.5.3).
379    ///
380    /// `identity` is acceptable unless `identity;q=0`, or `*;q=0` without an
381    /// explicit `identity`; an empty field value means only `identity` is
382    /// acceptable; `x-gzip` equals `gzip` and `x-compress` equals `compress`.
383    #[must_use]
384    pub fn encoding(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
385        let Some(v) = value else {
386            return first(offers);
387        };
388        if text::trim_ows(v).is_empty() {
389            return pick(
390                offers
391                    .iter()
392                    .enumerate()
393                    .map(|(i, o)| (i, o.eq_ignore_ascii_case("identity").then_some(1000))),
394            );
395        }
396        match self.coding_weights(v, offers, true) {
397            Ok(outcome) => outcome,
398            Err(e) => self.malformed(e, offers.len()),
399        }
400    }
401
402    /// Selects the best of `offers` (charsets such as `"utf-8"`) for
403    /// `Accept-Charset`: as [`encoding`](Self::encoding), without the
404    /// `identity` rule.
405    #[must_use]
406    pub fn charset(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
407        let Some(v) = value else {
408            return first(offers);
409        };
410        match self.coding_weights(v, offers, false) {
411            Ok(outcome) => outcome,
412            Err(e) => self.malformed(e, offers.len()),
413        }
414    }
415
416    /// Weights for codings and charsets: an exact match, else `*`, else (for
417    /// `identity` under the encoding rules) acceptable unless refused.
418    fn coding_weights(
419        &self,
420        value: &[u8],
421        offers: &[&str],
422        identity_rule: bool,
423    ) -> Result<Outcome> {
424        let members = parse_weighted(value, &self.limits)?;
425        let mut chosen: Option<(usize, u16)> = None;
426        for (group_index, group) in offers.chunks(GROUP).enumerate() {
427            let mut exact = [None::<u16>; GROUP];
428            let mut star = None::<u16>;
429            for m in members.iter() {
430                if m.value == b"*" {
431                    if star.is_none() {
432                        star = Some(m.weight.0);
433                    }
434                    continue;
435                }
436                let name = canonical(m.value);
437                for (slot, offer) in exact.iter_mut().zip(group) {
438                    if slot.is_none() && name.eq_ignore_ascii_case(canonical(offer.as_bytes())) {
439                        *slot = Some(m.weight.0);
440                    }
441                }
442            }
443            let base = group_index * GROUP;
444            let outcome = pick(group.iter().enumerate().map(|(i, offer)| {
445                let w = exact.get(i).copied().flatten().or(star).or_else(|| {
446                    (identity_rule && offer.eq_ignore_ascii_case("identity")).then_some(1000)
447                });
448                (base + i, w)
449            }));
450            if let Outcome::Chosen { index, weight } = outcome {
451                if chosen.is_none_or(|(_, w)| weight.0 > w) {
452                    chosen = Some((index, weight.0));
453                }
454            }
455        }
456        Ok(match chosen {
457            Some((index, w)) => Outcome::Chosen {
458                index,
459                weight: QValue(w),
460            },
461            None => Outcome::NoneAcceptable,
462        })
463    }
464
465    /// The selection shared by languages: each offer takes the weight of
466    /// the member with the highest rank for it (`rank` returns `None` when
467    /// a member does not match an offer). `None` for an absent header.
468    fn weighted(
469        &self,
470        value: Option<&[u8]>,
471        offers: &[&str],
472        rank: impl Fn(&[u8], &[u8]) -> Option<u16>,
473    ) -> Option<Outcome> {
474        let v = value?;
475        let members = match parse_weighted(v, &self.limits) {
476            Ok(m) => m,
477            Err(e) => return Some(self.malformed(e, offers.len())),
478        };
479        let mut chosen: Option<(usize, u16, u16)> = None;
480        for (group_index, group) in offers.chunks(GROUP).enumerate() {
481            let mut best = [Best::default(); GROUP];
482            for m in members.iter() {
483                for (b, offer) in best.iter_mut().zip(group) {
484                    if let Some(r) = rank(m.value, offer.as_bytes()) {
485                        b.offer(r, m.weight);
486                    }
487                }
488            }
489            // The highest weight wins; between equal weights the closer
490            // match (RFC 4647 lookup returns the longest truncation), then
491            // the earlier offer.
492            let base = group_index * GROUP;
493            for (i, b) in best.iter().take(group.len()).enumerate() {
494                if b.seen
495                    && b.weight > 0
496                    && chosen.is_none_or(|(_, w, r)| (b.weight, b.specificity) > (w, r))
497                {
498                    chosen = Some((base + i, b.weight, b.specificity));
499                }
500            }
501        }
502        Some(match chosen {
503            Some((index, w, _)) => Outcome::Chosen {
504                index,
505                weight: QValue(w),
506            },
507            None => Outcome::NoneAcceptable,
508        })
509    }
510}
511
512/// `x-gzip` is `gzip` and `x-compress` is `compress` (RFC 9110 section
513/// 8.4.1).
514fn canonical(c: &[u8]) -> &[u8] {
515    if c.eq_ignore_ascii_case(b"x-gzip") {
516        b"gzip"
517    } else if c.eq_ignore_ascii_case(b"x-compress") {
518        b"compress"
519    } else {
520        c
521    }
522}
523
524/// Every offer acceptable: the first wins.
525fn first(offers: &[&str]) -> Outcome {
526    if offers.is_empty() {
527        Outcome::NoneAcceptable
528    } else {
529        Outcome::Chosen {
530            index: 0,
531            weight: QValue::ONE,
532        }
533    }
534}
535
536/// RFC 4647 section 3.4: `range` matches `offer` when the offer equals the
537/// range or a truncation of it (removing a subtag from the end, and a
538/// single-character subtag left before it). An exact match ranks highest,
539/// then longer shortened matches; `*` ranks lowest.
540fn lookup_rank(range: &[u8], offer: &[u8]) -> Option<u16> {
541    if range == b"*" {
542        return Some(0);
543    }
544    if range.eq_ignore_ascii_case(offer) {
545        // An exact match has precedence over any shortened one.
546        return Some(u16::MAX);
547    }
548    let mut r = range;
549    loop {
550        if r.eq_ignore_ascii_case(offer) {
551            return Some(
552                u16::try_from(r.len())
553                    .unwrap_or(u16::MAX - 1)
554                    .saturating_add(1)
555                    .min(u16::MAX - 1),
556            );
557        }
558        let cut = r.iter().rposition(|&b| b == b'-')?;
559        r = r.get(..cut)?;
560        // A single-character subtag (an extension or private-use singleton)
561        // goes with the subtag after it.
562        if let Some(dash) = r.iter().rposition(|&b| b == b'-') {
563            if r.len() - dash == 2 {
564                r = r.get(..dash)?;
565            }
566        }
567    }
568}
569
570/// RFC 4647 section 3.3.1: `range` matches `offer` when they are equal or
571/// the offer starts with the range and `-`. Longer ranges rank higher; `*`
572/// ranks lowest.
573fn filter_rank(range: &[u8], offer: &[u8]) -> Option<u16> {
574    if range == b"*" {
575        return Some(0);
576    }
577    let prefix = offer.get(..range.len())?;
578    let boundary = offer.len() == range.len() || offer.get(range.len()) == Some(&b'-');
579    (prefix.eq_ignore_ascii_case(range) && boundary).then(|| {
580        u16::try_from(range.len())
581            .unwrap_or(u16::MAX)
582            .saturating_add(1)
583    })
584}
585
586/// Selects the best media type for `Accept` with the defaults.
587///
588/// `None`: no offer is acceptable (answer 406 or send a default). A header
589/// that does not parse is treated as absent.
590///
591/// # Examples
592///
593/// ```
594/// use parse_net::best_media;
595///
596/// assert_eq!(best_media(Some(b"text/*;q=0.5, text/html;q=0"), &["text/html"]), None);
597/// assert_eq!(best_media(None, &["application/json", "text/html"]), Some(0));
598/// ```
599#[must_use]
600pub fn best_media(accept: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
601    Negotiator::new().media(accept, offers).index()
602}
603
604/// Selects the best language for `Accept-Language` with RFC 4647 lookup.
605///
606/// # Examples
607///
608/// ```
609/// use parse_net::best_language;
610///
611/// assert_eq!(best_language(Some(b"fr-CA, en;q=0.5"), &["en", "fr", "de"]), Some(1));
612/// assert_eq!(best_language(Some(b"de"), &["en"]), None);
613/// ```
614#[must_use]
615pub fn best_language(accept_language: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
616    Negotiator::new().language(accept_language, offers).index()
617}
618
619/// Selects the best content coding for `Accept-Encoding`.
620///
621/// # Examples
622///
623/// ```
624/// use parse_net::best_encoding;
625///
626/// assert_eq!(best_encoding(Some(b"gzip;q=1.0, identity; q=0.5, *;q=0"), &["br", "identity"]), Some(1));
627/// assert_eq!(best_encoding(Some(b""), &["gzip", "identity"]), Some(1));
628/// ```
629#[must_use]
630pub fn best_encoding(accept_encoding: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
631    Negotiator::new().encoding(accept_encoding, offers).index()
632}
633
634/// Selects the best charset for `Accept-Charset`.
635///
636/// # Examples
637///
638/// ```
639/// use parse_net::best_charset;
640///
641/// assert_eq!(best_charset(Some(b"iso-8859-1;q=0.5, utf-8"), &["iso-8859-1", "utf-8"]), Some(1));
642/// ```
643#[must_use]
644pub fn best_charset(accept_charset: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
645    Negotiator::new().charset(accept_charset, offers).index()
646}
647
648/// Parses `Accept` into its media ranges.
649///
650/// # Errors
651///
652/// The list errors and those of [`media::parse_range`].
653///
654/// # Examples
655///
656/// ```
657/// use parse_net::{negotiate::parse_accept, Limits};
658///
659/// let a = parse_accept(b"text/html, */*;q=0.1", &Limits::DEFAULT)?;
660/// assert_eq!(a.iter().map(|r| r.weight().thousandths()).collect::<Vec<_>>(), [1000, 100]);
661/// # Ok::<(), parse_net::ParseError>(())
662/// ```
663pub fn parse_accept<'a>(value: &'a [u8], limits: &Limits) -> Result<AcceptList<'a>> {
664    let l = list::parse(value, limits)?;
665    for e in l.iter() {
666        media::parse_range(e, limits)
667            .map(|_| ())
668            .map_err(|err| shift_into(err, value, e))?;
669    }
670    Ok(AcceptList {
671        list: l,
672        limits: *limits,
673    })
674}
675
676/// Re-bases an error found in `element` onto the offsets of `value`.
677fn shift_into(e: ParseError, value: &[u8], element: &[u8]) -> ParseError {
678    let base = (element.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
679    e.shifted(base)
680}
681
682/// The media ranges of a validated `Accept` value.
683#[derive(Clone, Copy, Debug)]
684pub struct AcceptList<'a> {
685    list: list::List<'a>,
686    limits: Limits,
687}
688
689impl<'a> AcceptList<'a> {
690    /// The ranges, in order.
691    pub fn iter(&self) -> impl Iterator<Item = media::MediaRange<'a>> + '_ {
692        self.list
693            .iter()
694            .filter_map(|e| media::parse_range(e, &self.limits).ok())
695    }
696
697    /// The number of ranges.
698    #[must_use]
699    pub const fn len(&self) -> usize {
700        self.list.len()
701    }
702
703    /// Whether there are no ranges.
704    #[must_use]
705    pub const fn is_empty(&self) -> bool {
706        self.list.is_empty()
707    }
708}
709
710/// One member of a weighted list: a value, its weight, and its parameters.
711#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
712pub struct Weighted<'a> {
713    /// The value (a coding, a charset, or a language range).
714    pub value: &'a [u8],
715    /// Its `q`, 1 when absent.
716    pub weight: QValue,
717    /// The parameters before `q`.
718    pub params: Params<'a>,
719}
720
721/// Parses `token *( OWS ";" OWS parameter )` members with weights, as in
722/// `Accept-Encoding`, `Accept-Charset`, and `Accept-Language`.
723///
724/// # Errors
725///
726/// The list errors; [`ErrorKind::NotToken`] for a member that does not start
727/// with a token; the parameter errors; [`ErrorKind::InvalidQValue`].
728///
729/// # Examples
730///
731/// ```
732/// use parse_net::{negotiate::parse_weighted, Limits};
733///
734/// let w = parse_weighted(b"gzip;q=0.8, br", &Limits::DEFAULT)?;
735/// let all: Vec<_> = w.iter().map(|m| (m.value, m.weight.thousandths())).collect();
736/// assert_eq!(all, [(&b"gzip"[..], 800), (&b"br"[..], 1000)]);
737/// # Ok::<(), parse_net::ParseError>(())
738/// ```
739pub fn parse_weighted<'a>(value: &'a [u8], limits: &Limits) -> Result<WeightedList<'a>> {
740    let l = list::parse(value, limits)?;
741    for e in l.iter() {
742        weighted_member(e, limits)
743            .map(|_| ())
744            .map_err(|err| shift_into(err, value, e))?;
745    }
746    Ok(WeightedList {
747        list: l,
748        limits: *limits,
749    })
750}
751
752fn weighted_member<'a>(element: &'a [u8], limits: &Limits) -> Result<Weighted<'a>> {
753    let (token, n) =
754        text::parse_token(element).map_err(|e| ParseError::new(ErrorKind::NotToken, e.offset()))?;
755    let rest = element.get(n..).unwrap_or(&[]);
756    let (all, used) = param::parse(rest, limits).map_err(|e| e.shifted(n))?;
757    if n + used != element.len() {
758        return err(ErrorKind::NotToken, n + used);
759    }
760    let (params, q) = all.split_at_name(b"q");
761    let weight = match q {
762        None => QValue::ONE,
763        Some(p) => match p.value {
764            Word::Token(t) => QValue::parse(t).map_err(|e| {
765                let at = (t.as_ptr() as usize).saturating_sub(element.as_ptr() as usize);
766                ParseError::new(e.kind(), at)
767            })?,
768            Word::Quoted(_) => return err(ErrorKind::InvalidQValue, n),
769        },
770    };
771    Ok(Weighted {
772        value: token,
773        weight,
774        params,
775    })
776}
777
778/// The members of a validated weighted list.
779#[derive(Clone, Copy, Debug)]
780pub struct WeightedList<'a> {
781    list: list::List<'a>,
782    limits: Limits,
783}
784
785impl<'a> WeightedList<'a> {
786    /// The members, in order.
787    pub fn iter(&self) -> impl Iterator<Item = Weighted<'a>> + '_ {
788        self.list
789            .iter()
790            .filter_map(|e| weighted_member(e, &self.limits).ok())
791    }
792
793    /// The number of members.
794    #[must_use]
795    pub const fn len(&self) -> usize {
796        self.list.len()
797    }
798
799    /// Whether there are no members.
800    #[must_use]
801    pub const fn is_empty(&self) -> bool {
802        self.list.is_empty()
803    }
804}
805
806#[cfg(test)]
807mod tests {
808    use super::*;
809
810    #[test]
811    fn test_qvalue_edge_cases() {
812        let ok = |s: &[u8]| QValue::parse(s).map(QValue::thousandths);
813        assert_eq!(ok(b"0"), Ok(0));
814        assert_eq!(ok(b"0."), Ok(0));
815        assert_eq!(ok(b"0.000"), Ok(0));
816        assert_eq!(ok(b"0.5"), Ok(500));
817        assert_eq!(ok(b"0.05"), Ok(50));
818        assert_eq!(ok(b"0.999"), Ok(999));
819        assert_eq!(ok(b"1"), Ok(1000));
820        assert_eq!(ok(b"1.000"), Ok(1000));
821        for bad in [
822            &b"1.001"[..],
823            b"0.0001",
824            b".5",
825            b"-1",
826            b"1.5",
827            b"",
828            b"2",
829            b"0,5",
830            b"0.5x",
831        ] {
832            assert_eq!(
833                QValue::parse(bad).unwrap_err().kind(),
834                ErrorKind::InvalidQValue
835            );
836        }
837        assert_eq!(QValue::from_thousandths(1001), None);
838        assert_eq!(
839            QValue::from_thousandths(250).map(QValue::thousandths),
840            Some(250)
841        );
842    }
843
844    #[test]
845    fn test_media_negotiation_cases() {
846        let n = Negotiator::new();
847        let i = |h: &[u8], o: &[&str]| n.media(Some(h), o).index();
848        assert_eq!(
849            i(b"*/*;q=0, text/html", &["text/html", "text/plain"]),
850            Some(0)
851        );
852        assert_eq!(
853            i(b"text/*;q=0.5, text/html;q=0", &["text/html", "text/plain"]),
854            Some(1)
855        );
856        assert_eq!(
857            i(
858                b"text/html;level=1, text/html;q=0.1",
859                &["text/html", "text/html;level=1"]
860            ),
861            Some(1)
862        );
863        assert_eq!(
864            i(
865                b"application/json;q=0.5, text/html;q=0.5",
866                &["text/html", "application/json"]
867            ),
868            Some(0)
869        );
870        assert_eq!(i(b"", &["text/html"]), None);
871        assert_eq!(i(b"TEXT/HTML", &["text/html"]), Some(0));
872        assert_eq!(
873            i(b"text/html;charset=UTF-8", &["text/html;charset=utf-8"]),
874            Some(0)
875        );
876        assert_eq!(i(b"text/html;level=1", &["text/html;level=2"]), None);
877        assert_eq!(n.media(None, &[]), Outcome::NoneAcceptable);
878        assert_eq!(i(b"*/*", &["not a media type", "text/html"]), Some(1));
879    }
880
881    #[test]
882    fn test_media_malformed_policies() {
883        let bad = Some(&b"text/html;q=2"[..]);
884        assert_eq!(
885            Negotiator::new().media(bad, &["a/b"]),
886            Outcome::HeaderIgnored { index: 0 }
887        );
888        assert_eq!(Negotiator::new().media(bad, &[]), Outcome::NoneAcceptable);
889        let refused = Negotiator::new()
890            .on_malformed(MalformedPolicy::Refuse)
891            .media(bad, &["a/b"]);
892        assert!(matches!(refused, Outcome::Refused(e) if e.kind() == ErrorKind::InvalidQValue));
893    }
894
895    #[test]
896    fn test_media_more_than_one_group_of_offers() {
897        let names: std::vec::Vec<std::string::String> =
898            (0..70).map(|i| std::format!("type/s{i}")).collect();
899        let offers: std::vec::Vec<&str> = names.iter().map(std::string::String::as_str).collect();
900        assert_eq!(
901            Negotiator::new().media(Some(b"type/s65"), &offers).index(),
902            Some(65)
903        );
904        assert_eq!(
905            Negotiator::new()
906                .media(Some(b"type/s65;q=0.5, type/s3;q=0.4"), &offers)
907                .index(),
908            Some(65)
909        );
910        assert_eq!(
911            Negotiator::new().media(Some(b"type/*"), &offers).index(),
912            Some(0)
913        );
914    }
915
916    #[test]
917    fn test_language_lookup_and_filter() {
918        let n = Negotiator::new();
919        let i = |h: &[u8], o: &[&str]| n.language(Some(h), o).index();
920        assert_eq!(i(b"en-US", &["fr", "en"]), Some(1));
921        assert_eq!(i(b"EN-us", &["en-US"]), Some(0));
922        assert_eq!(i(b"zh-Hant-CN-x-private1", &["zh", "zh-Hant"]), Some(1));
923        assert_eq!(i(b"*", &["de", "fr"]), Some(0));
924        assert_eq!(i(b"en-US, en;q=0", &["en"]), None);
925        assert_eq!(i(b"fr;q=0.5, en;q=0.9", &["fr", "en"]), Some(1));
926        assert_eq!(i(b"en", &["en-US"]), None);
927        let f = Negotiator::new().language_matching(LanguageMatching::BasicFilter);
928        assert_eq!(f.language(Some(b"en"), &["en-US"]).index(), Some(0));
929        assert_eq!(f.language(Some(b"en"), &["eng"]).index(), None);
930        assert_eq!(
931            f.language(Some(b"de-DE;q=0.9, de;q=0.1"), &["de-DE-1996"]),
932            Outcome::Chosen {
933                index: 0,
934                weight: QValue(900)
935            }
936        );
937        assert_eq!(n.language(None, &["en"]).index(), Some(0));
938    }
939
940    #[test]
941    fn test_encoding_identity_rules() {
942        let e = |h: &[u8], o: &[&str]| best_encoding(Some(h), o);
943        assert_eq!(e(b"gzip", &["identity"]), Some(0));
944        assert_eq!(e(b"gzip, identity;q=0", &["identity"]), None);
945        assert_eq!(e(b"*;q=0", &["identity"]), None);
946        assert_eq!(e(b"*;q=0, identity", &["br", "identity"]), Some(1));
947        assert_eq!(e(b"x-gzip", &["br", "gzip"]), Some(1));
948        assert_eq!(e(b"gzip;q=0.5, *;q=0.9", &["gzip", "br"]), Some(1));
949        assert_eq!(e(b"   ", &["gzip", "identity"]), Some(1));
950        assert_eq!(best_encoding(None, &["br"]), Some(0));
951        assert_eq!(e(b"gzip;q=x", &["br", "gzip"]), Some(0));
952    }
953
954    #[test]
955    fn test_charset_rules() {
956        assert_eq!(best_charset(Some(b"utf-8"), &["iso-8859-1"]), None);
957        assert_eq!(best_charset(Some(b"*;q=0.1"), &["iso-8859-1"]), Some(0));
958        assert_eq!(best_charset(Some(b""), &["utf-8"]), None);
959    }
960
961    #[test]
962    fn test_weighted_member_errors() {
963        let l = Limits::DEFAULT;
964        assert_eq!(
965            parse_weighted(b"gzip;q=\"1\"", &l).unwrap_err().kind(),
966            ErrorKind::InvalidQValue
967        );
968        assert_eq!(
969            parse_weighted(b"gzip x", &l).unwrap_err().kind(),
970            ErrorKind::NotToken
971        );
972        assert_eq!(
973            parse_weighted(b"@", &l).unwrap_err().kind(),
974            ErrorKind::NotToken
975        );
976        let e = parse_weighted(b"br, gzip;q=7", &l).unwrap_err();
977        assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidQValue, 11));
978        assert_eq!(
979            parse_weighted(b"a;q=1;q=0", &l).unwrap_err().kind(),
980            ErrorKind::DuplicateParameter
981        );
982        assert_eq!(parse_accept(b"text/html;q=2", &l).unwrap_err().offset(), 12);
983    }
984
985    #[test]
986    fn test_members_times_offers_is_bounded() {
987        let mut v = std::vec::Vec::new();
988        for i in 0..1024 {
989            v.extend_from_slice(std::format!("t{i}/x;q=0.5,").as_bytes());
990        }
991        v.truncate(v.len() - 1);
992        let names: std::vec::Vec<std::string::String> =
993            (0..32).map(|i| std::format!("t{i}/x")).collect();
994        let offers: std::vec::Vec<&str> = names.iter().map(std::string::String::as_str).collect();
995        assert_eq!(best_media(Some(&v), &offers), Some(0));
996    }
997}