struct Range<'a> {
kind: &'a str,
sub: &'a str,
q: u16, }
fn parse_q(params: &str) -> u16 {
params
.split(';')
.filter_map(|p| {
p.trim()
.strip_prefix("q=")
.or_else(|| p.trim().strip_prefix("Q="))
})
.filter_map(|v| v.trim().parse::<f32>().ok())
.next()
.map(|q| (q.clamp(0.0, 1.0) * 1000.0).round() as u16)
.unwrap_or(1000)
}
fn ranges(header: &str) -> Vec<Range<'_>> {
header
.split(',')
.filter_map(|item| {
let (mime, params) = item.split_once(';').unwrap_or((item, ""));
let (kind, sub) = mime.trim().split_once('/')?;
Some(Range {
kind,
sub,
q: parse_q(params),
})
})
.collect()
}
pub fn best<'a>(accept: &str, offered: &[&'a str]) -> Option<&'a str> {
if accept.trim().is_empty() {
return offered.first().copied();
}
let ranges = ranges(accept);
let mut winner: Option<(&'a str, u16)> = None;
for &offer in offered {
let Some((kind, sub)) = offer.split_once('/') else {
continue;
};
let q = ranges
.iter()
.filter(|r| {
(r.kind == "*" && r.sub == "*")
|| (r.kind.eq_ignore_ascii_case(kind)
&& (r.sub == "*" || r.sub.eq_ignore_ascii_case(sub)))
})
.max_by_key(|r| usize::from(r.kind != "*") + usize::from(r.sub != "*"))
.map(|r| r.q);
match q {
Some(q) if q > 0 && winner.is_none_or(|(_, best_q)| q > best_q) => {
winner = Some((offer, q));
}
_ => {}
}
}
winner.map(|(offer, _)| offer)
}
pub fn best_language<'a>(accept_language: &str, offered: &[&'a str]) -> Option<&'a str> {
if accept_language.trim().is_empty() {
return offered.first().copied();
}
let ranges: Vec<(String, u16)> = accept_language
.split(',')
.filter_map(|item| {
let (tag, params) = item.split_once(';').unwrap_or((item, ""));
let tag = tag.trim();
(!tag.is_empty()).then(|| (tag.to_ascii_lowercase(), parse_q(params)))
})
.collect();
let mut winner: Option<(&'a str, u16)> = None;
for &offer in offered {
let o = offer.to_ascii_lowercase();
let q = ranges
.iter()
.filter_map(|(tag, q)| {
let rank = if *tag == o {
3
} else if tag
.strip_prefix(o.as_str())
.is_some_and(|r| r.starts_with('-'))
|| o.strip_prefix(tag.as_str())
.is_some_and(|r| r.starts_with('-'))
{
2
} else if tag == "*" {
1
} else {
return None;
};
Some((rank, *q))
})
.max_by_key(|(rank, _)| *rank)
.map(|(_, q)| q);
match q {
Some(q) if q > 0 && winner.is_none_or(|(_, best_q)| q > best_q) => {
winner = Some((offer, q));
}
_ => {}
}
}
winner.map(|(offer, _)| offer)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quality_and_specificity() {
let offers = ["text/html", "application/json"];
assert_eq!(
best("text/html, application/json;q=0.9", &offers),
Some("text/html")
);
assert_eq!(
best("text/html;q=0.1, application/json;q=0.9", &offers),
Some("application/json")
);
assert_eq!(
best("application/json;q=0, */*", &offers),
Some("text/html")
);
assert_eq!(best("text/html;q=0, application/json;q=0", &offers), None);
assert_eq!(best("text/*;q=0.5, */*;q=0.1", &offers), Some("text/html"));
}
#[test]
fn missing_header_takes_the_first_offer() {
assert_eq!(best("", &["a/b", "c/d"]), Some("a/b"));
assert_eq!(best(" ", &[]), None);
assert_eq!(best_language("", &["en", "de"]), Some("en"));
}
#[test]
fn ties_keep_server_preference() {
assert_eq!(
best("*/*", &["application/json", "text/html"]),
Some("application/json")
);
}
#[test]
fn junk_is_ignored() {
assert_eq!(
best("garbage, ;;;, text/plain;q=abc", &["text/plain"]),
Some("text/plain")
);
assert_eq!(best("text/html;q=7", &["text/html"]), Some("text/html"));
}
#[test]
fn languages() {
assert_eq!(best_language("en-US,en;q=0.9", &["de", "en"]), Some("en"));
assert_eq!(
best_language("fr;q=0.2, de;q=0.9", &["fr", "de"]),
Some("de")
);
assert_eq!(best_language("de-CH", &["de"]), Some("de"));
assert_eq!(best_language("de", &["de-AT"]), Some("de-AT"));
assert_eq!(best_language("*;q=0.1", &["pt"]), Some("pt"));
assert_eq!(best_language("ja", &["de", "en"]), None);
assert_eq!(best_language("de;q=0, *;q=0.5", &["de"]), None); assert_eq!(best_language("de;q=0, *;q=0.5", &["de", "fr"]), Some("fr"));
}
}