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 // Validate every member once, so a malformed header is caught before
441 // any selection.
442 let checked = list::combined(lines.clone(), &self.limits).and_then(|l| {
443 for e in l.iter() {
444 media::parse_range(e, &self.limits).map(|_| ())?;
445 }
446 Ok(l)
447 });
448 let members = match checked {
449 Ok(l) => l,
450 Err(e) => return self.malformed(e, offers.len()),
451 };
452 let mut chosen: Option<(usize, u16)> = None;
453 for (group_index, group) in offers.chunks(GROUP).enumerate() {
454 let mut parsed: [Option<MediaType<'_>>; GROUP] = [None; GROUP];
455 for (slot, offer) in parsed.iter_mut().zip(group) {
456 *slot = media::parse(offer.as_bytes(), &Limits::DEFAULT).ok();
457 }
458 let mut best = [Best::default(); GROUP];
459 for element in members.iter() {
460 let Ok(range) = media::parse_range(element, &self.limits) else {
461 continue;
462 };
463 for (b, offer) in best.iter_mut().zip(&parsed) {
464 if let Some(offer) = offer {
465 if range.matches(offer) {
466 b.offer(u16::from(range.specificity()), range.weight());
467 }
468 }
469 }
470 }
471 let base = group_index * GROUP;
472 if let Outcome::Chosen { index, weight } = pick(
473 best.iter()
474 .take(group.len())
475 .enumerate()
476 .map(|(i, b)| (base + i, b.seen.then_some(b.weight))),
477 ) {
478 if chosen.is_none_or(|(_, w)| weight.0 > w) {
479 chosen = Some((index, weight.0));
480 }
481 }
482 }
483 match chosen {
484 Some((index, w)) => Outcome::Chosen {
485 index,
486 weight: QValue(w),
487 },
488 None => Outcome::NoneAcceptable,
489 }
490 }
491
492 /// Selects the best of `offers` (language tags such as `"fr-CA"`) for
493 /// `Accept-Language`.
494 ///
495 /// With [`LanguageMatching::Lookup`] (the default), a range matches an
496 /// offer equal to it or reached by removing subtags from the range's end,
497 /// and the exact match has precedence over a shortened one; with
498 /// [`LanguageMatching::BasicFilter`], a range matches an offer equal to
499 /// it or starting with it and `-`, the longest range having precedence.
500 /// `*` matches every offer at the lowest precedence. The highest weight
501 /// wins; between equal weights the closer match (as lookup returns the
502 /// longest truncation), then the earlier offer. Tags compare ASCII
503 /// case-insensitively.
504 ///
505 /// # Examples
506 ///
507 /// ```
508 /// use parse_net::{negotiate::{Negotiator, Outcome}, QValue};
509 ///
510 /// let n = Negotiator::new();
511 /// assert_eq!(n.language(Some(b"fr-CA, en;q=0.8"), &["en", "fr"]), Outcome::Chosen { index: 1, weight: QValue::ONE });
512 /// ```
513 #[must_use]
514 pub fn language(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
515 let mode = self.language;
516 self.weighted(value, offers, |range, offer| match mode {
517 LanguageMatching::Lookup => lookup_rank(range, offer),
518 LanguageMatching::BasicFilter => filter_rank(range, offer),
519 })
520 .unwrap_or_else(|| first(offers))
521 }
522
523 /// Selects the best of `offers` (content codings such as `"br"`) for
524 /// `Accept-Encoding` (RFC 9110 section 12.5.3).
525 ///
526 /// `identity` is acceptable unless `identity;q=0`, or `*;q=0` without an
527 /// explicit `identity`; an empty field value means only `identity` is
528 /// acceptable; `x-gzip` equals `gzip` and `x-compress` equals `compress`.
529 ///
530 /// # Examples
531 ///
532 /// ```
533 /// use parse_net::negotiate::{Negotiator, Outcome};
534 ///
535 /// let n = Negotiator::new();
536 /// assert_eq!(n.encoding(Some(b"x-gzip"), &["br", "gzip"]).index(), Some(1));
537 /// assert_eq!(n.encoding(Some(b"*;q=0"), &["gzip", "identity"]), Outcome::NoneAcceptable);
538 /// ```
539 #[must_use]
540 pub fn encoding(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
541 let Some(v) = value else {
542 return first(offers);
543 };
544 if text::trim_ows(v).is_empty() {
545 return pick(
546 offers
547 .iter()
548 .enumerate()
549 .map(|(i, o)| (i, o.eq_ignore_ascii_case("identity").then_some(1000))),
550 );
551 }
552 match self.coding_weights(v, offers, true) {
553 Ok(outcome) => outcome,
554 Err(e) => self.malformed(e, offers.len()),
555 }
556 }
557
558 /// Selects the best of `offers` (charsets such as `"utf-8"`) for
559 /// `Accept-Charset`: as [`encoding`](Self::encoding), without the
560 /// `identity` rule.
561 ///
562 /// # Examples
563 ///
564 /// ```
565 /// use parse_net::negotiate::{Negotiator, Outcome};
566 ///
567 /// let n = Negotiator::new();
568 /// assert_eq!(n.charset(Some(b"utf-8, *;q=0.1"), &["iso-8859-1", "utf-8"]).index(), Some(1));
569 /// assert_eq!(n.charset(Some(b"utf-8"), &["iso-8859-1"]), Outcome::NoneAcceptable);
570 /// ```
571 #[must_use]
572 pub fn charset(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
573 let Some(v) = value else {
574 return first(offers);
575 };
576 match self.coding_weights(v, offers, false) {
577 Ok(outcome) => outcome,
578 Err(e) => self.malformed(e, offers.len()),
579 }
580 }
581
582 /// Weights for codings and charsets: an exact match, else `*`, else (for
583 /// `identity` under the encoding rules) acceptable unless refused.
584 fn coding_weights(
585 &self,
586 value: &[u8],
587 offers: &[&str],
588 identity_rule: bool,
589 ) -> Result<Outcome> {
590 let members = parse_weighted(value, &self.limits)?;
591 let mut chosen: Option<(usize, u16)> = None;
592 for (group_index, group) in offers.chunks(GROUP).enumerate() {
593 let mut exact = [None::<u16>; GROUP];
594 let mut star = None::<u16>;
595 for m in members.iter() {
596 if m.value == b"*" {
597 if star.is_none() {
598 star = Some(m.weight.0);
599 }
600 continue;
601 }
602 let name = canonical(m.value);
603 for (slot, offer) in exact.iter_mut().zip(group) {
604 if slot.is_none() && name.eq_ignore_ascii_case(canonical(offer.as_bytes())) {
605 *slot = Some(m.weight.0);
606 }
607 }
608 }
609 let base = group_index * GROUP;
610 let outcome = pick(group.iter().enumerate().map(|(i, offer)| {
611 let w = exact.get(i).copied().flatten().or(star).or_else(|| {
612 (identity_rule && offer.eq_ignore_ascii_case("identity")).then_some(1000)
613 });
614 (base + i, w)
615 }));
616 if let Outcome::Chosen { index, weight } = outcome {
617 if chosen.is_none_or(|(_, w)| weight.0 > w) {
618 chosen = Some((index, weight.0));
619 }
620 }
621 }
622 Ok(match chosen {
623 Some((index, w)) => Outcome::Chosen {
624 index,
625 weight: QValue(w),
626 },
627 None => Outcome::NoneAcceptable,
628 })
629 }
630
631 /// The selection shared by languages: each offer takes the weight of
632 /// the member with the highest rank for it (`rank` returns `None` when
633 /// a member does not match an offer). `None` for an absent header.
634 fn weighted(
635 &self,
636 value: Option<&[u8]>,
637 offers: &[&str],
638 rank: impl Fn(&[u8], &[u8]) -> Option<u16>,
639 ) -> Option<Outcome> {
640 let v = value?;
641 let members = match parse_weighted(v, &self.limits) {
642 Ok(m) => m,
643 Err(e) => return Some(self.malformed(e, offers.len())),
644 };
645 let mut chosen: Option<(usize, u16, u16)> = None;
646 for (group_index, group) in offers.chunks(GROUP).enumerate() {
647 let mut best = [Best::default(); GROUP];
648 for m in members.iter() {
649 for (b, offer) in best.iter_mut().zip(group) {
650 if let Some(r) = rank(m.value, offer.as_bytes()) {
651 b.offer(r, m.weight);
652 }
653 }
654 }
655 // The highest weight wins; between equal weights the closer
656 // match (RFC 4647 lookup returns the longest truncation), then
657 // the earlier offer.
658 let base = group_index * GROUP;
659 for (i, b) in best.iter().take(group.len()).enumerate() {
660 if b.seen
661 && b.weight > 0
662 && chosen.is_none_or(|(_, w, r)| (b.weight, b.specificity) > (w, r))
663 {
664 chosen = Some((base + i, b.weight, b.specificity));
665 }
666 }
667 }
668 Some(match chosen {
669 Some((index, w, _)) => Outcome::Chosen {
670 index,
671 weight: QValue(w),
672 },
673 None => Outcome::NoneAcceptable,
674 })
675 }
676}
677
678/// `x-gzip` is `gzip` and `x-compress` is `compress` (RFC 9110 section
679/// 8.4.1).
680fn canonical(c: &[u8]) -> &[u8] {
681 if c.eq_ignore_ascii_case(b"x-gzip") {
682 b"gzip"
683 } else if c.eq_ignore_ascii_case(b"x-compress") {
684 b"compress"
685 } else {
686 c
687 }
688}
689
690/// Every offer acceptable: the first wins.
691fn first(offers: &[&str]) -> Outcome {
692 if offers.is_empty() {
693 Outcome::NoneAcceptable
694 } else {
695 Outcome::Chosen {
696 index: 0,
697 weight: QValue::ONE,
698 }
699 }
700}
701
702/// RFC 4647 section 3.4: `range` matches `offer` when the offer equals the
703/// range or a truncation of it (removing a subtag from the end, and a
704/// single-character subtag left before it). An exact match ranks highest,
705/// then longer shortened matches; `*` ranks lowest.
706fn lookup_rank(range: &[u8], offer: &[u8]) -> Option<u16> {
707 if range == b"*" {
708 return Some(0);
709 }
710 if range.eq_ignore_ascii_case(offer) {
711 // An exact match has precedence over any shortened one.
712 return Some(u16::MAX);
713 }
714 let mut r = range;
715 loop {
716 if r.eq_ignore_ascii_case(offer) {
717 return Some(
718 u16::try_from(r.len())
719 .unwrap_or(u16::MAX - 1)
720 .saturating_add(1)
721 .min(u16::MAX - 1),
722 );
723 }
724 let cut = r.iter().rposition(|&b| b == b'-')?;
725 r = r.get(..cut)?;
726 // A single-character subtag (an extension or private-use singleton)
727 // goes with the subtag after it.
728 if let Some(dash) = r.iter().rposition(|&b| b == b'-') {
729 if r.len() - dash == 2 {
730 r = r.get(..dash)?;
731 }
732 }
733 }
734}
735
736/// RFC 4647 section 3.3.1: `range` matches `offer` when they are equal or
737/// the offer starts with the range and `-`. Longer ranges rank higher; `*`
738/// ranks lowest.
739fn filter_rank(range: &[u8], offer: &[u8]) -> Option<u16> {
740 if range == b"*" {
741 return Some(0);
742 }
743 let prefix = offer.get(..range.len())?;
744 let boundary = offer.len() == range.len() || offer.get(range.len()) == Some(&b'-');
745 (prefix.eq_ignore_ascii_case(range) && boundary).then(|| {
746 u16::try_from(range.len())
747 .unwrap_or(u16::MAX)
748 .saturating_add(1)
749 })
750}
751
752/// Selects the best media type for `Accept` with the defaults.
753///
754/// `None`: no offer is acceptable (answer 406 or send a default). A header
755/// that does not parse is treated as absent.
756///
757/// # Examples
758///
759/// ```
760/// use parse_net::best_media;
761///
762/// assert_eq!(best_media(Some(b"text/*;q=0.5, text/html;q=0"), &["text/html"]), None);
763/// assert_eq!(best_media(None, &["application/json", "text/html"]), Some(0));
764/// ```
765#[must_use]
766pub fn best_media(accept: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
767 Negotiator::new().media(accept, offers).index()
768}
769
770/// Selects the best language for `Accept-Language` with RFC 4647 lookup.
771///
772/// # Examples
773///
774/// ```
775/// use parse_net::best_language;
776///
777/// assert_eq!(best_language(Some(b"fr-CA, en;q=0.5"), &["en", "fr", "de"]), Some(1));
778/// assert_eq!(best_language(Some(b"de"), &["en"]), None);
779/// ```
780#[must_use]
781pub fn best_language(accept_language: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
782 Negotiator::new().language(accept_language, offers).index()
783}
784
785/// Selects the best content coding for `Accept-Encoding`.
786///
787/// # Examples
788///
789/// ```
790/// use parse_net::best_encoding;
791///
792/// assert_eq!(best_encoding(Some(b"gzip;q=1.0, identity; q=0.5, *;q=0"), &["br", "identity"]), Some(1));
793/// assert_eq!(best_encoding(Some(b""), &["gzip", "identity"]), Some(1));
794/// ```
795#[must_use]
796pub fn best_encoding(accept_encoding: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
797 Negotiator::new().encoding(accept_encoding, offers).index()
798}
799
800/// Selects the best charset for `Accept-Charset`.
801///
802/// # Examples
803///
804/// ```
805/// use parse_net::best_charset;
806///
807/// assert_eq!(best_charset(Some(b"iso-8859-1;q=0.5, utf-8"), &["iso-8859-1", "utf-8"]), Some(1));
808/// ```
809#[must_use]
810pub fn best_charset(accept_charset: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
811 Negotiator::new().charset(accept_charset, offers).index()
812}
813
814/// Parses `Accept` into its media ranges.
815///
816/// # Errors
817///
818/// The list errors and those of [`media::parse_range`].
819///
820/// # Examples
821///
822/// ```
823/// use parse_net::{negotiate::parse_accept, Limits};
824///
825/// let a = parse_accept(b"text/html, */*;q=0.1", &Limits::DEFAULT)?;
826/// assert_eq!(a.iter().map(|r| r.weight().thousandths()).collect::<Vec<_>>(), [1000, 100]);
827/// # Ok::<(), parse_net::ParseError>(())
828/// ```
829pub fn parse_accept<'a>(value: &'a [u8], limits: &Limits) -> Result<AcceptList<'a>> {
830 let l = list::parse(value, limits)?;
831 for e in l.iter() {
832 media::parse_range(e, limits)
833 .map(|_| ())
834 .map_err(|err| shift_into(err, value, e))?;
835 }
836 Ok(AcceptList {
837 list: l,
838 limits: *limits,
839 })
840}
841
842/// Re-bases an error found in `element` onto the offsets of `value`.
843fn shift_into(e: ParseError, value: &[u8], element: &[u8]) -> ParseError {
844 let base = (element.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
845 e.shifted(base)
846}
847
848/// The media ranges of a validated `Accept` value.
849///
850/// # Examples
851///
852/// ```
853/// use parse_net::{negotiate::parse_accept, Limits};
854///
855/// let a = parse_accept(b"text/html, application/*;q=0.2", &Limits::DEFAULT)?;
856/// assert_eq!(a.len(), 2);
857/// assert!(a.iter().any(|r| r.specificity() == 2));
858/// # Ok::<(), parse_net::ParseError>(())
859/// ```
860#[derive(Clone, Copy, Debug)]
861pub struct AcceptList<'a> {
862 list: list::List<'a>,
863 limits: Limits,
864}
865
866impl<'a> AcceptList<'a> {
867 /// The ranges, in order.
868 ///
869 /// # Examples
870 ///
871 /// ```
872 /// use parse_net::{negotiate::parse_accept, Limits};
873 ///
874 /// let a = parse_accept(b"image/webp, image/*;q=0.8", &Limits::DEFAULT)?;
875 /// let subtypes: Vec<_> = a.iter().map(|r| r.subtype()).collect();
876 /// assert_eq!(subtypes, [&b"webp"[..], &b"*"[..]]);
877 /// # Ok::<(), parse_net::ParseError>(())
878 /// ```
879 pub fn iter(&self) -> impl Iterator<Item = media::MediaRange<'a>> + '_ {
880 self.list
881 .iter()
882 .filter_map(|e| media::parse_range(e, &self.limits).ok())
883 }
884
885 /// The number of ranges.
886 ///
887 /// # Examples
888 ///
889 /// ```
890 /// use parse_net::{negotiate::parse_accept, Limits};
891 ///
892 /// assert_eq!(parse_accept(b"text/html, , text/plain", &Limits::DEFAULT)?.len(), 2);
893 /// # Ok::<(), parse_net::ParseError>(())
894 /// ```
895 #[must_use]
896 pub const fn len(&self) -> usize {
897 self.list.len()
898 }
899
900 /// Whether there are no ranges.
901 ///
902 /// # Examples
903 ///
904 /// ```
905 /// use parse_net::{negotiate::parse_accept, Limits};
906 ///
907 /// assert!(!parse_accept(b"*/*", &Limits::DEFAULT)?.is_empty());
908 /// # Ok::<(), parse_net::ParseError>(())
909 /// ```
910 #[must_use]
911 pub const fn is_empty(&self) -> bool {
912 self.list.is_empty()
913 }
914}
915
916/// One member of a weighted list: a value, its weight, and its parameters.
917///
918/// # Examples
919///
920/// ```
921/// use parse_net::{negotiate::parse_weighted, Limits};
922///
923/// let w = parse_weighted(b"en-GB;q=0.7", &Limits::DEFAULT)?;
924/// let m = w.iter().next().expect("one member");
925/// assert_eq!((m.value, m.weight.thousandths(), m.params.len()), (&b"en-GB"[..], 700, 0));
926/// # Ok::<(), parse_net::ParseError>(())
927/// ```
928#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
929pub struct Weighted<'a> {
930 /// The value (a coding, a charset, or a language range).
931 pub value: &'a [u8],
932 /// Its `q`, 1 when absent.
933 pub weight: QValue,
934 /// The parameters before `q`.
935 pub params: Params<'a>,
936}
937
938/// Parses `token *( OWS ";" OWS parameter )` members with weights, as in
939/// `Accept-Encoding`, `Accept-Charset`, and `Accept-Language`.
940///
941/// # Errors
942///
943/// The list errors; [`ErrorKind::NotToken`] for a member that does not start
944/// with a token; the parameter errors; [`ErrorKind::InvalidQValue`].
945///
946/// # Examples
947///
948/// ```
949/// use parse_net::{negotiate::parse_weighted, Limits};
950///
951/// let w = parse_weighted(b"gzip;q=0.8, br", &Limits::DEFAULT)?;
952/// let all: Vec<_> = w.iter().map(|m| (m.value, m.weight.thousandths())).collect();
953/// assert_eq!(all, [(&b"gzip"[..], 800), (&b"br"[..], 1000)]);
954/// # Ok::<(), parse_net::ParseError>(())
955/// ```
956pub fn parse_weighted<'a>(value: &'a [u8], limits: &Limits) -> Result<WeightedList<'a>> {
957 let l = list::parse(value, limits)?;
958 for e in l.iter() {
959 weighted_member(e, limits)
960 .map(|_| ())
961 .map_err(|err| shift_into(err, value, e))?;
962 }
963 Ok(WeightedList {
964 list: l,
965 limits: *limits,
966 })
967}
968
969fn weighted_member<'a>(element: &'a [u8], limits: &Limits) -> Result<Weighted<'a>> {
970 let (token, n) =
971 text::parse_token(element).map_err(|e| ParseError::new(ErrorKind::NotToken, e.offset()))?;
972 let rest = element.get(n..).unwrap_or(&[]);
973 let (all, used) = param::parse(rest, limits).map_err(|e| e.shifted(n))?;
974 if n + used != element.len() {
975 return err(ErrorKind::NotToken, n + used);
976 }
977 let (params, q) = all.split_at_name(b"q");
978 let weight = match q {
979 None => QValue::ONE,
980 Some(p) => match p.value {
981 Word::Token(t) => QValue::parse(t).map_err(|e| {
982 let at = (t.as_ptr() as usize).saturating_sub(element.as_ptr() as usize);
983 ParseError::new(e.kind(), at)
984 })?,
985 Word::Quoted(_) => return err(ErrorKind::InvalidQValue, n),
986 },
987 };
988 Ok(Weighted {
989 value: token,
990 weight,
991 params,
992 })
993}
994
995/// The members of a validated weighted list.
996///
997/// # Examples
998///
999/// ```
1000/// use parse_net::{negotiate::parse_weighted, Limits};
1001///
1002/// let w = parse_weighted(b"utf-8, iso-8859-1;q=0.5", &Limits::DEFAULT)?;
1003/// assert_eq!(w.len(), 2);
1004/// assert!(w.iter().all(|m| m.weight.thousandths() >= 500));
1005/// # Ok::<(), parse_net::ParseError>(())
1006/// ```
1007#[derive(Clone, Copy, Debug)]
1008pub struct WeightedList<'a> {
1009 list: list::List<'a>,
1010 limits: Limits,
1011}
1012
1013impl<'a> WeightedList<'a> {
1014 /// The members, in order.
1015 ///
1016 /// # Examples
1017 ///
1018 /// ```
1019 /// use parse_net::{negotiate::parse_weighted, Limits};
1020 ///
1021 /// let w = parse_weighted(b"gzip;q=0.5, br", &Limits::DEFAULT)?;
1022 /// let best = w.iter().max_by_key(|m| m.weight).map(|m| m.value);
1023 /// assert_eq!(best, Some(&b"br"[..]));
1024 /// # Ok::<(), parse_net::ParseError>(())
1025 /// ```
1026 pub fn iter(&self) -> impl Iterator<Item = Weighted<'a>> + '_ {
1027 self.list
1028 .iter()
1029 .filter_map(|e| weighted_member(e, &self.limits).ok())
1030 }
1031
1032 /// The number of members.
1033 ///
1034 /// # Examples
1035 ///
1036 /// ```
1037 /// use parse_net::{negotiate::parse_weighted, Limits};
1038 ///
1039 /// assert_eq!(parse_weighted(b"gzip, deflate, br", &Limits::DEFAULT)?.len(), 3);
1040 /// # Ok::<(), parse_net::ParseError>(())
1041 /// ```
1042 #[must_use]
1043 pub const fn len(&self) -> usize {
1044 self.list.len()
1045 }
1046
1047 /// Whether there are no members.
1048 ///
1049 /// # Examples
1050 ///
1051 /// ```
1052 /// use parse_net::{negotiate::parse_weighted, Limits};
1053 ///
1054 /// assert!(!parse_weighted(b"identity", &Limits::DEFAULT)?.is_empty());
1055 /// # Ok::<(), parse_net::ParseError>(())
1056 /// ```
1057 #[must_use]
1058 pub const fn is_empty(&self) -> bool {
1059 self.list.is_empty()
1060 }
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065 use super::*;
1066
1067 #[test]
1068 fn test_qvalue_edge_cases() {
1069 let ok = |s: &[u8]| QValue::parse(s).map(QValue::thousandths);
1070 assert_eq!(ok(b"0"), Ok(0));
1071 assert_eq!(ok(b"0."), Ok(0));
1072 assert_eq!(ok(b"0.000"), Ok(0));
1073 assert_eq!(ok(b"0.5"), Ok(500));
1074 assert_eq!(ok(b"0.05"), Ok(50));
1075 assert_eq!(ok(b"0.999"), Ok(999));
1076 assert_eq!(ok(b"1"), Ok(1000));
1077 assert_eq!(ok(b"1.000"), Ok(1000));
1078 for bad in [
1079 &b"1.001"[..],
1080 b"0.0001",
1081 b".5",
1082 b"-1",
1083 b"1.5",
1084 b"",
1085 b"2",
1086 b"0,5",
1087 b"0.5x",
1088 ] {
1089 assert_eq!(
1090 QValue::parse(bad).unwrap_err().kind(),
1091 ErrorKind::InvalidQValue
1092 );
1093 }
1094 assert_eq!(QValue::from_thousandths(1001), None);
1095 assert_eq!(
1096 QValue::from_thousandths(250).map(QValue::thousandths),
1097 Some(250)
1098 );
1099 }
1100
1101 #[test]
1102 fn test_media_negotiation_cases() {
1103 let n = Negotiator::new();
1104 let i = |h: &[u8], o: &[&str]| n.media(Some(h), o).index();
1105 assert_eq!(
1106 i(b"*/*;q=0, text/html", &["text/html", "text/plain"]),
1107 Some(0)
1108 );
1109 assert_eq!(
1110 i(b"text/*;q=0.5, text/html;q=0", &["text/html", "text/plain"]),
1111 Some(1)
1112 );
1113 assert_eq!(
1114 i(
1115 b"text/html;level=1, text/html;q=0.1",
1116 &["text/html", "text/html;level=1"]
1117 ),
1118 Some(1)
1119 );
1120 assert_eq!(
1121 i(
1122 b"application/json;q=0.5, text/html;q=0.5",
1123 &["text/html", "application/json"]
1124 ),
1125 Some(0)
1126 );
1127 assert_eq!(i(b"", &["text/html"]), None);
1128 assert_eq!(i(b"TEXT/HTML", &["text/html"]), Some(0));
1129 assert_eq!(
1130 i(b"text/html;charset=UTF-8", &["text/html;charset=utf-8"]),
1131 Some(0)
1132 );
1133 assert_eq!(i(b"text/html;level=1", &["text/html;level=2"]), None);
1134 assert_eq!(n.media(None, &[]), Outcome::NoneAcceptable);
1135 assert_eq!(i(b"*/*", &["not a media type", "text/html"]), Some(1));
1136 }
1137
1138 #[test]
1139 fn test_media_malformed_policies() {
1140 let bad = Some(&b"text/html;q=2"[..]);
1141 assert_eq!(
1142 Negotiator::new().media(bad, &["a/b"]),
1143 Outcome::HeaderIgnored { index: 0 }
1144 );
1145 assert_eq!(Negotiator::new().media(bad, &[]), Outcome::NoneAcceptable);
1146 let refused = Negotiator::new()
1147 .on_malformed(MalformedPolicy::Refuse)
1148 .media(bad, &["a/b"]);
1149 assert!(matches!(refused, Outcome::Refused(e) if e.kind() == ErrorKind::InvalidQValue));
1150 }
1151
1152 #[test]
1153 fn test_media_more_than_one_group_of_offers() {
1154 let names: std::vec::Vec<std::string::String> =
1155 (0..70).map(|i| std::format!("type/s{i}")).collect();
1156 let offers: std::vec::Vec<&str> = names.iter().map(std::string::String::as_str).collect();
1157 assert_eq!(
1158 Negotiator::new().media(Some(b"type/s65"), &offers).index(),
1159 Some(65)
1160 );
1161 assert_eq!(
1162 Negotiator::new()
1163 .media(Some(b"type/s65;q=0.5, type/s3;q=0.4"), &offers)
1164 .index(),
1165 Some(65)
1166 );
1167 assert_eq!(
1168 Negotiator::new().media(Some(b"type/*"), &offers).index(),
1169 Some(0)
1170 );
1171 }
1172
1173 #[test]
1174 fn test_language_lookup_and_filter() {
1175 let n = Negotiator::new();
1176 let i = |h: &[u8], o: &[&str]| n.language(Some(h), o).index();
1177 assert_eq!(i(b"en-US", &["fr", "en"]), Some(1));
1178 assert_eq!(i(b"EN-us", &["en-US"]), Some(0));
1179 assert_eq!(i(b"zh-Hant-CN-x-private1", &["zh", "zh-Hant"]), Some(1));
1180 assert_eq!(i(b"*", &["de", "fr"]), Some(0));
1181 assert_eq!(i(b"en-US, en;q=0", &["en"]), None);
1182 assert_eq!(i(b"fr;q=0.5, en;q=0.9", &["fr", "en"]), Some(1));
1183 assert_eq!(i(b"en", &["en-US"]), None);
1184 let f = Negotiator::new().language_matching(LanguageMatching::BasicFilter);
1185 assert_eq!(f.language(Some(b"en"), &["en-US"]).index(), Some(0));
1186 assert_eq!(f.language(Some(b"en"), &["eng"]).index(), None);
1187 assert_eq!(
1188 f.language(Some(b"de-DE;q=0.9, de;q=0.1"), &["de-DE-1996"]),
1189 Outcome::Chosen {
1190 index: 0,
1191 weight: QValue(900)
1192 }
1193 );
1194 assert_eq!(n.language(None, &["en"]).index(), Some(0));
1195 }
1196
1197 #[test]
1198 fn test_encoding_identity_rules() {
1199 let e = |h: &[u8], o: &[&str]| best_encoding(Some(h), o);
1200 assert_eq!(e(b"gzip", &["identity"]), Some(0));
1201 assert_eq!(e(b"gzip, identity;q=0", &["identity"]), None);
1202 assert_eq!(e(b"*;q=0", &["identity"]), None);
1203 assert_eq!(e(b"*;q=0, identity", &["br", "identity"]), Some(1));
1204 assert_eq!(e(b"x-gzip", &["br", "gzip"]), Some(1));
1205 assert_eq!(e(b"gzip;q=0.5, *;q=0.9", &["gzip", "br"]), Some(1));
1206 assert_eq!(e(b" ", &["gzip", "identity"]), Some(1));
1207 assert_eq!(best_encoding(None, &["br"]), Some(0));
1208 assert_eq!(e(b"gzip;q=x", &["br", "gzip"]), Some(0));
1209 }
1210
1211 #[test]
1212 fn test_charset_rules() {
1213 assert_eq!(best_charset(Some(b"utf-8"), &["iso-8859-1"]), None);
1214 assert_eq!(best_charset(Some(b"*;q=0.1"), &["iso-8859-1"]), Some(0));
1215 assert_eq!(best_charset(Some(b""), &["utf-8"]), None);
1216 }
1217
1218 #[test]
1219 fn test_weighted_member_errors() {
1220 let l = Limits::DEFAULT;
1221 assert_eq!(
1222 parse_weighted(b"gzip;q=\"1\"", &l).unwrap_err().kind(),
1223 ErrorKind::InvalidQValue
1224 );
1225 assert_eq!(
1226 parse_weighted(b"gzip x", &l).unwrap_err().kind(),
1227 ErrorKind::NotToken
1228 );
1229 assert_eq!(
1230 parse_weighted(b"@", &l).unwrap_err().kind(),
1231 ErrorKind::NotToken
1232 );
1233 let e = parse_weighted(b"br, gzip;q=7", &l).unwrap_err();
1234 assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidQValue, 11));
1235 assert_eq!(
1236 parse_weighted(b"a;q=1;q=0", &l).unwrap_err().kind(),
1237 ErrorKind::DuplicateParameter
1238 );
1239 assert_eq!(parse_accept(b"text/html;q=2", &l).unwrap_err().offset(), 12);
1240 }
1241
1242 #[test]
1243 fn test_members_times_offers_is_bounded() {
1244 let mut v = std::vec::Vec::new();
1245 for i in 0..1024 {
1246 v.extend_from_slice(std::format!("t{i}/x;q=0.5,").as_bytes());
1247 }
1248 v.truncate(v.len() - 1);
1249 let names: std::vec::Vec<std::string::String> =
1250 (0..32).map(|i| std::format!("t{i}/x")).collect();
1251 let offers: std::vec::Vec<&str> = names.iter().map(std::string::String::as_str).collect();
1252 assert_eq!(best_media(Some(&v), &offers), Some(0));
1253 }
1254}