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}