use crate::{
error::{ErrorKind, ParseError, Result, err},
limits::Limits,
list,
media::{self, MediaType},
param::{self, Params},
text::{self, Word},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct QValue(u16);
impl QValue {
pub const ONE: Self = Self(1000);
pub const ZERO: Self = Self(0);
#[must_use]
pub const fn from_thousandths(t: u16) -> Option<Self> {
if t <= 1000 { Some(Self(t)) } else { None }
}
#[must_use]
pub const fn thousandths(self) -> u16 {
self.0
}
pub fn parse(s: &[u8]) -> Result<Self> {
let (first, rest) = match s.split_first() {
Some((&f, r)) => (f, r),
None => return err(ErrorKind::InvalidQValue, 0),
};
let frac = match rest.split_first() {
None => &[][..],
Some((b'.', digits)) if digits.len() <= 3 => digits,
Some(_) => return err(ErrorKind::InvalidQValue, 1),
};
if let Some(i) = frac.iter().position(|b| !b.is_ascii_digit()) {
return err(ErrorKind::InvalidQValue, 2 + i);
}
let mut thousandths = 0u16;
let mut scale = 100u16;
for &d in frac {
thousandths += u16::from(d - b'0') * scale;
scale /= 10;
}
match first {
b'0' => Ok(Self(thousandths)),
b'1' if thousandths == 0 => Ok(Self::ONE),
_ => err(ErrorKind::InvalidQValue, 0),
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum LanguageMatching {
#[default]
Lookup,
BasicFilter,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum MalformedPolicy {
#[default]
TreatAsAbsent,
Refuse,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Outcome {
Chosen {
index: usize,
weight: QValue,
},
NoneAcceptable,
HeaderIgnored {
index: usize,
},
Refused(ParseError),
}
impl Outcome {
#[must_use]
pub const fn index(&self) -> Option<usize> {
match self {
Self::Chosen { index, .. } | Self::HeaderIgnored { index } => Some(*index),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Negotiator {
limits: Limits,
language: LanguageMatching,
malformed: MalformedPolicy,
}
impl Default for Negotiator {
fn default() -> Self {
Self::new()
}
}
const GROUP: usize = 32;
#[derive(Clone, Copy, Default)]
struct Best {
specificity: u16,
weight: u16,
seen: bool,
}
impl Best {
fn offer(&mut self, specificity: u16, weight: QValue) {
if !self.seen || specificity > self.specificity {
*self = Self {
specificity,
weight: weight.0,
seen: true,
};
}
}
}
fn pick(weights: impl Iterator<Item = (usize, Option<u16>)>) -> Outcome {
let mut best: Option<(usize, u16)> = None;
for (index, w) in weights {
if let Some(w) = w {
if w > 0 && best.is_none_or(|(_, b)| w > b) {
best = Some((index, w));
}
}
}
match best {
Some((index, w)) => Outcome::Chosen {
index,
weight: QValue(w),
},
None => Outcome::NoneAcceptable,
}
}
impl Negotiator {
#[must_use]
pub const fn new() -> Self {
Self {
limits: Limits::DEFAULT,
language: LanguageMatching::Lookup,
malformed: MalformedPolicy::TreatAsAbsent,
}
}
#[must_use]
pub const fn limits(self, l: Limits) -> Self {
Self { limits: l, ..self }
}
#[must_use]
pub const fn language_matching(self, m: LanguageMatching) -> Self {
Self {
language: m,
..self
}
}
#[must_use]
pub const fn on_malformed(self, p: MalformedPolicy) -> Self {
Self {
malformed: p,
..self
}
}
fn malformed(&self, e: ParseError, offers: usize) -> Outcome {
match self.malformed {
MalformedPolicy::Refuse => Outcome::Refused(e),
MalformedPolicy::TreatAsAbsent if offers == 0 => Outcome::NoneAcceptable,
MalformedPolicy::TreatAsAbsent => Outcome::HeaderIgnored { index: 0 },
}
}
#[must_use]
pub fn media(&self, accept: Option<&[u8]>, offers: &[&str]) -> Outcome {
match accept {
None => first(offers),
Some(v) => self.media_lines([v], offers),
}
}
#[must_use]
pub fn media_lines<'a, I>(&self, lines: I, offers: &[&str]) -> Outcome
where
I: IntoIterator<Item = &'a [u8]>,
I::IntoIter: Clone,
{
let lines = lines.into_iter();
let members = match list::combined(lines, &self.limits) {
Ok(l) => l,
Err(e) => return self.malformed(e, offers.len()),
};
let mut chosen: Option<(usize, u16)> = None;
for (group_index, group) in offers.chunks(GROUP).enumerate() {
let mut parsed: [Option<MediaType<'_>>; GROUP] = [None; GROUP];
for (slot, offer) in parsed.iter_mut().zip(group) {
*slot = media::parse(offer.as_bytes(), &Limits::DEFAULT).ok();
}
let parsed = parsed.get(..group.len()).unwrap_or(&[]);
let mut best = [Best::default(); GROUP];
for element in members.iter() {
let range = match media::parse_range(element, &self.limits) {
Ok(r) => r,
Err(e) if group_index == 0 => return self.malformed(e, offers.len()),
Err(_) => continue,
};
for (b, offer) in best.iter_mut().zip(parsed) {
if let Some(offer) = offer {
if range.matches(offer) {
b.offer(u16::from(range.specificity()), range.weight());
}
}
}
}
let base = group_index * GROUP;
if let Outcome::Chosen { index, weight } = pick(
best.iter()
.take(group.len())
.enumerate()
.map(|(i, b)| (base + i, b.seen.then_some(b.weight))),
) {
if chosen.is_none_or(|(_, w)| weight.0 > w) {
chosen = Some((index, weight.0));
}
}
}
if offers.is_empty() {
for element in members.iter() {
if let Err(e) = media::parse_range(element, &self.limits) {
return self.malformed(e, 0);
}
}
}
match chosen {
Some((index, w)) => Outcome::Chosen {
index,
weight: QValue(w),
},
None => Outcome::NoneAcceptable,
}
}
#[must_use]
pub fn language(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
let mode = self.language;
self.weighted(value, offers, |range, offer| match mode {
LanguageMatching::Lookup => lookup_rank(range, offer),
LanguageMatching::BasicFilter => filter_rank(range, offer),
})
.unwrap_or_else(|| first(offers))
}
#[must_use]
pub fn encoding(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
let Some(v) = value else {
return first(offers);
};
if text::trim_ows(v).is_empty() {
return pick(
offers
.iter()
.enumerate()
.map(|(i, o)| (i, o.eq_ignore_ascii_case("identity").then_some(1000))),
);
}
match self.coding_weights(v, offers, true) {
Ok(outcome) => outcome,
Err(e) => self.malformed(e, offers.len()),
}
}
#[must_use]
pub fn charset(&self, value: Option<&[u8]>, offers: &[&str]) -> Outcome {
let Some(v) = value else {
return first(offers);
};
match self.coding_weights(v, offers, false) {
Ok(outcome) => outcome,
Err(e) => self.malformed(e, offers.len()),
}
}
fn coding_weights(
&self,
value: &[u8],
offers: &[&str],
identity_rule: bool,
) -> Result<Outcome> {
let members = parse_weighted(value, &self.limits)?;
let mut chosen: Option<(usize, u16)> = None;
for (group_index, group) in offers.chunks(GROUP).enumerate() {
let mut exact = [None::<u16>; GROUP];
let mut star = None::<u16>;
for m in members.iter() {
if m.value == b"*" {
if star.is_none() {
star = Some(m.weight.0);
}
continue;
}
let name = canonical(m.value);
for (slot, offer) in exact.iter_mut().zip(group) {
if slot.is_none() && name.eq_ignore_ascii_case(canonical(offer.as_bytes())) {
*slot = Some(m.weight.0);
}
}
}
let base = group_index * GROUP;
let outcome = pick(group.iter().enumerate().map(|(i, offer)| {
let w = exact.get(i).copied().flatten().or(star).or_else(|| {
(identity_rule && offer.eq_ignore_ascii_case("identity")).then_some(1000)
});
(base + i, w)
}));
if let Outcome::Chosen { index, weight } = outcome {
if chosen.is_none_or(|(_, w)| weight.0 > w) {
chosen = Some((index, weight.0));
}
}
}
Ok(match chosen {
Some((index, w)) => Outcome::Chosen {
index,
weight: QValue(w),
},
None => Outcome::NoneAcceptable,
})
}
fn weighted(
&self,
value: Option<&[u8]>,
offers: &[&str],
rank: impl Fn(&[u8], &[u8]) -> Option<u16>,
) -> Option<Outcome> {
let v = value?;
let members = match parse_weighted(v, &self.limits) {
Ok(m) => m,
Err(e) => return Some(self.malformed(e, offers.len())),
};
let mut chosen: Option<(usize, u16, u16)> = None;
for (group_index, group) in offers.chunks(GROUP).enumerate() {
let mut best = [Best::default(); GROUP];
for m in members.iter() {
for (b, offer) in best.iter_mut().zip(group) {
if let Some(r) = rank(m.value, offer.as_bytes()) {
b.offer(r, m.weight);
}
}
}
let base = group_index * GROUP;
for (i, b) in best.iter().take(group.len()).enumerate() {
if b.seen
&& b.weight > 0
&& chosen.is_none_or(|(_, w, r)| (b.weight, b.specificity) > (w, r))
{
chosen = Some((base + i, b.weight, b.specificity));
}
}
}
Some(match chosen {
Some((index, w, _)) => Outcome::Chosen {
index,
weight: QValue(w),
},
None => Outcome::NoneAcceptable,
})
}
}
fn canonical(c: &[u8]) -> &[u8] {
if c.eq_ignore_ascii_case(b"x-gzip") {
b"gzip"
} else if c.eq_ignore_ascii_case(b"x-compress") {
b"compress"
} else {
c
}
}
fn first(offers: &[&str]) -> Outcome {
if offers.is_empty() {
Outcome::NoneAcceptable
} else {
Outcome::Chosen {
index: 0,
weight: QValue::ONE,
}
}
}
fn lookup_rank(range: &[u8], offer: &[u8]) -> Option<u16> {
if range == b"*" {
return Some(0);
}
if range.eq_ignore_ascii_case(offer) {
return Some(u16::MAX);
}
let mut r = range;
loop {
if r.eq_ignore_ascii_case(offer) {
return Some(
u16::try_from(r.len())
.unwrap_or(u16::MAX - 1)
.saturating_add(1)
.min(u16::MAX - 1),
);
}
let cut = r.iter().rposition(|&b| b == b'-')?;
r = r.get(..cut)?;
if let Some(dash) = r.iter().rposition(|&b| b == b'-') {
if r.len() - dash == 2 {
r = r.get(..dash)?;
}
}
}
}
fn filter_rank(range: &[u8], offer: &[u8]) -> Option<u16> {
if range == b"*" {
return Some(0);
}
let prefix = offer.get(..range.len())?;
let boundary = offer.len() == range.len() || offer.get(range.len()) == Some(&b'-');
(prefix.eq_ignore_ascii_case(range) && boundary).then(|| {
u16::try_from(range.len())
.unwrap_or(u16::MAX)
.saturating_add(1)
})
}
#[must_use]
pub fn best_media(accept: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
Negotiator::new().media(accept, offers).index()
}
#[must_use]
pub fn best_language(accept_language: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
Negotiator::new().language(accept_language, offers).index()
}
#[must_use]
pub fn best_encoding(accept_encoding: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
Negotiator::new().encoding(accept_encoding, offers).index()
}
#[must_use]
pub fn best_charset(accept_charset: Option<&[u8]>, offers: &[&str]) -> Option<usize> {
Negotiator::new().charset(accept_charset, offers).index()
}
pub fn parse_accept<'a>(value: &'a [u8], limits: &Limits) -> Result<AcceptList<'a>> {
let l = list::parse(value, limits)?;
for e in l.iter() {
media::parse_range(e, limits)
.map(|_| ())
.map_err(|err| shift_into(err, value, e))?;
}
Ok(AcceptList {
list: l,
limits: *limits,
})
}
fn shift_into(e: ParseError, value: &[u8], element: &[u8]) -> ParseError {
let base = (element.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
e.shifted(base)
}
#[derive(Clone, Copy, Debug)]
pub struct AcceptList<'a> {
list: list::List<'a>,
limits: Limits,
}
impl<'a> AcceptList<'a> {
pub fn iter(&self) -> impl Iterator<Item = media::MediaRange<'a>> + '_ {
self.list
.iter()
.filter_map(|e| media::parse_range(e, &self.limits).ok())
}
#[must_use]
pub const fn len(&self) -> usize {
self.list.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.list.is_empty()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Weighted<'a> {
pub value: &'a [u8],
pub weight: QValue,
pub params: Params<'a>,
}
pub fn parse_weighted<'a>(value: &'a [u8], limits: &Limits) -> Result<WeightedList<'a>> {
let l = list::parse(value, limits)?;
for e in l.iter() {
weighted_member(e, limits)
.map(|_| ())
.map_err(|err| shift_into(err, value, e))?;
}
Ok(WeightedList {
list: l,
limits: *limits,
})
}
fn weighted_member<'a>(element: &'a [u8], limits: &Limits) -> Result<Weighted<'a>> {
let (token, n) =
text::parse_token(element).map_err(|e| ParseError::new(ErrorKind::NotToken, e.offset()))?;
let rest = element.get(n..).unwrap_or(&[]);
let (all, used) = param::parse(rest, limits).map_err(|e| e.shifted(n))?;
if n + used != element.len() {
return err(ErrorKind::NotToken, n + used);
}
let (params, q) = all.split_at_name(b"q");
let weight = match q {
None => QValue::ONE,
Some(p) => match p.value {
Word::Token(t) => QValue::parse(t).map_err(|e| {
let at = (t.as_ptr() as usize).saturating_sub(element.as_ptr() as usize);
ParseError::new(e.kind(), at)
})?,
Word::Quoted(_) => return err(ErrorKind::InvalidQValue, n),
},
};
Ok(Weighted {
value: token,
weight,
params,
})
}
#[derive(Clone, Copy, Debug)]
pub struct WeightedList<'a> {
list: list::List<'a>,
limits: Limits,
}
impl<'a> WeightedList<'a> {
pub fn iter(&self) -> impl Iterator<Item = Weighted<'a>> + '_ {
self.list
.iter()
.filter_map(|e| weighted_member(e, &self.limits).ok())
}
#[must_use]
pub const fn len(&self) -> usize {
self.list.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.list.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_qvalue_edge_cases() {
let ok = |s: &[u8]| QValue::parse(s).map(QValue::thousandths);
assert_eq!(ok(b"0"), Ok(0));
assert_eq!(ok(b"0."), Ok(0));
assert_eq!(ok(b"0.000"), Ok(0));
assert_eq!(ok(b"0.5"), Ok(500));
assert_eq!(ok(b"0.05"), Ok(50));
assert_eq!(ok(b"0.999"), Ok(999));
assert_eq!(ok(b"1"), Ok(1000));
assert_eq!(ok(b"1.000"), Ok(1000));
for bad in [
&b"1.001"[..],
b"0.0001",
b".5",
b"-1",
b"1.5",
b"",
b"2",
b"0,5",
b"0.5x",
] {
assert_eq!(
QValue::parse(bad).unwrap_err().kind(),
ErrorKind::InvalidQValue
);
}
assert_eq!(QValue::from_thousandths(1001), None);
assert_eq!(
QValue::from_thousandths(250).map(QValue::thousandths),
Some(250)
);
}
#[test]
fn test_media_negotiation_cases() {
let n = Negotiator::new();
let i = |h: &[u8], o: &[&str]| n.media(Some(h), o).index();
assert_eq!(
i(b"*/*;q=0, text/html", &["text/html", "text/plain"]),
Some(0)
);
assert_eq!(
i(b"text/*;q=0.5, text/html;q=0", &["text/html", "text/plain"]),
Some(1)
);
assert_eq!(
i(
b"text/html;level=1, text/html;q=0.1",
&["text/html", "text/html;level=1"]
),
Some(1)
);
assert_eq!(
i(
b"application/json;q=0.5, text/html;q=0.5",
&["text/html", "application/json"]
),
Some(0)
);
assert_eq!(i(b"", &["text/html"]), None);
assert_eq!(i(b"TEXT/HTML", &["text/html"]), Some(0));
assert_eq!(
i(b"text/html;charset=UTF-8", &["text/html;charset=utf-8"]),
Some(0)
);
assert_eq!(i(b"text/html;level=1", &["text/html;level=2"]), None);
assert_eq!(n.media(None, &[]), Outcome::NoneAcceptable);
assert_eq!(i(b"*/*", &["not a media type", "text/html"]), Some(1));
}
#[test]
fn test_media_malformed_policies() {
let bad = Some(&b"text/html;q=2"[..]);
assert_eq!(
Negotiator::new().media(bad, &["a/b"]),
Outcome::HeaderIgnored { index: 0 }
);
assert_eq!(Negotiator::new().media(bad, &[]), Outcome::NoneAcceptable);
let refused = Negotiator::new()
.on_malformed(MalformedPolicy::Refuse)
.media(bad, &["a/b"]);
assert!(matches!(refused, Outcome::Refused(e) if e.kind() == ErrorKind::InvalidQValue));
}
#[test]
fn test_media_more_than_one_group_of_offers() {
let names: std::vec::Vec<std::string::String> =
(0..70).map(|i| std::format!("type/s{i}")).collect();
let offers: std::vec::Vec<&str> = names.iter().map(std::string::String::as_str).collect();
assert_eq!(
Negotiator::new().media(Some(b"type/s65"), &offers).index(),
Some(65)
);
assert_eq!(
Negotiator::new()
.media(Some(b"type/s65;q=0.5, type/s3;q=0.4"), &offers)
.index(),
Some(65)
);
assert_eq!(
Negotiator::new().media(Some(b"type/*"), &offers).index(),
Some(0)
);
}
#[test]
fn test_language_lookup_and_filter() {
let n = Negotiator::new();
let i = |h: &[u8], o: &[&str]| n.language(Some(h), o).index();
assert_eq!(i(b"en-US", &["fr", "en"]), Some(1));
assert_eq!(i(b"EN-us", &["en-US"]), Some(0));
assert_eq!(i(b"zh-Hant-CN-x-private1", &["zh", "zh-Hant"]), Some(1));
assert_eq!(i(b"*", &["de", "fr"]), Some(0));
assert_eq!(i(b"en-US, en;q=0", &["en"]), None);
assert_eq!(i(b"fr;q=0.5, en;q=0.9", &["fr", "en"]), Some(1));
assert_eq!(i(b"en", &["en-US"]), None);
let f = Negotiator::new().language_matching(LanguageMatching::BasicFilter);
assert_eq!(f.language(Some(b"en"), &["en-US"]).index(), Some(0));
assert_eq!(f.language(Some(b"en"), &["eng"]).index(), None);
assert_eq!(
f.language(Some(b"de-DE;q=0.9, de;q=0.1"), &["de-DE-1996"]),
Outcome::Chosen {
index: 0,
weight: QValue(900)
}
);
assert_eq!(n.language(None, &["en"]).index(), Some(0));
}
#[test]
fn test_encoding_identity_rules() {
let e = |h: &[u8], o: &[&str]| best_encoding(Some(h), o);
assert_eq!(e(b"gzip", &["identity"]), Some(0));
assert_eq!(e(b"gzip, identity;q=0", &["identity"]), None);
assert_eq!(e(b"*;q=0", &["identity"]), None);
assert_eq!(e(b"*;q=0, identity", &["br", "identity"]), Some(1));
assert_eq!(e(b"x-gzip", &["br", "gzip"]), Some(1));
assert_eq!(e(b"gzip;q=0.5, *;q=0.9", &["gzip", "br"]), Some(1));
assert_eq!(e(b" ", &["gzip", "identity"]), Some(1));
assert_eq!(best_encoding(None, &["br"]), Some(0));
assert_eq!(e(b"gzip;q=x", &["br", "gzip"]), Some(0));
}
#[test]
fn test_charset_rules() {
assert_eq!(best_charset(Some(b"utf-8"), &["iso-8859-1"]), None);
assert_eq!(best_charset(Some(b"*;q=0.1"), &["iso-8859-1"]), Some(0));
assert_eq!(best_charset(Some(b""), &["utf-8"]), None);
}
#[test]
fn test_weighted_member_errors() {
let l = Limits::DEFAULT;
assert_eq!(
parse_weighted(b"gzip;q=\"1\"", &l).unwrap_err().kind(),
ErrorKind::InvalidQValue
);
assert_eq!(
parse_weighted(b"gzip x", &l).unwrap_err().kind(),
ErrorKind::NotToken
);
assert_eq!(
parse_weighted(b"@", &l).unwrap_err().kind(),
ErrorKind::NotToken
);
let e = parse_weighted(b"br, gzip;q=7", &l).unwrap_err();
assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidQValue, 11));
assert_eq!(
parse_weighted(b"a;q=1;q=0", &l).unwrap_err().kind(),
ErrorKind::DuplicateParameter
);
assert_eq!(parse_accept(b"text/html;q=2", &l).unwrap_err().offset(), 12);
}
#[test]
fn test_members_times_offers_is_bounded() {
let mut v = std::vec::Vec::new();
for i in 0..1024 {
v.extend_from_slice(std::format!("t{i}/x;q=0.5,").as_bytes());
}
v.truncate(v.len() - 1);
let names: std::vec::Vec<std::string::String> =
(0..32).map(|i| std::format!("t{i}/x")).collect();
let offers: std::vec::Vec<&str> = names.iter().map(std::string::String::as_str).collect();
assert_eq!(best_media(Some(&v), &offers), Some(0));
}
}