use unic_langid::LanguageIdentifier;
const MAX_ENTRIES: usize = 16;
#[must_use]
pub fn accept_language(header: &str) -> Vec<LanguageIdentifier> {
let mut scored: Vec<(u16, usize, LanguageIdentifier)> = Vec::new();
for (position, entry) in header.split(',').take(MAX_ENTRIES).enumerate() {
let mut parts = entry.split(';');
let Some(tag) = parts.next().map(str::trim) else {
continue;
};
if tag == "*" {
continue;
}
let Some(locale) = crate::parse_locale(tag) else {
continue;
};
let quality = quality_of(parts);
if quality == 0 {
continue;
}
scored.push((quality, position, locale));
}
scored.sort_by(|left, right| right.0.cmp(&left.0).then(left.1.cmp(&right.1)));
let mut out: Vec<LanguageIdentifier> = Vec::with_capacity(scored.len());
for (_, _, locale) in scored {
if !out.contains(&locale) {
out.push(locale);
}
}
out
}
const DEFAULT_QUALITY: u16 = 1_000;
fn quality_of<'a>(parameters: impl Iterator<Item = &'a str>) -> u16 {
for parameter in parameters {
let Some((name, value)) = parameter.split_once('=') else {
continue;
};
if name.trim().eq_ignore_ascii_case("q") {
return qvalue(value.trim());
}
}
DEFAULT_QUALITY
}
fn qvalue(raw: &str) -> u16 {
let (whole, fraction) = raw.split_once('.').unwrap_or((raw, ""));
if whole != "0" {
return DEFAULT_QUALITY;
}
if fraction.is_empty() {
return 0;
}
if !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
return DEFAULT_QUALITY;
}
let mut thousandths = 0u16;
for index in 0..3 {
let digit = fraction
.as_bytes()
.get(index)
.map_or(0, |byte| u16::from(byte - b'0'));
thousandths = thousandths * 10 + digit;
}
thousandths
}
#[cfg(test)]
mod tests {
use super::*;
fn tags(header: &str) -> Vec<String> {
accept_language(header)
.iter()
.map(ToString::to_string)
.collect()
}
#[test]
fn quality_values_order_the_answer() {
assert_eq!(tags("en;q=0.8, id;q=0.9, de;q=0.1"), ["id", "en", "de"]);
assert_eq!(tags("en;q=0.9, id"), ["id", "en"]);
}
#[test]
fn a_tie_keeps_the_order_the_client_wrote() {
assert_eq!(tags("id, en, de"), ["id", "en", "de"]);
assert_eq!(tags("en;q=0.5, id;q=0.5"), ["en", "id"]);
}
#[test]
fn q_zero_means_not_this_one() {
assert_eq!(tags("en;q=0, id;q=0.4"), ["id"]);
assert!(accept_language("en;q=0").is_empty());
}
#[test]
fn the_wildcard_names_no_locale() {
assert_eq!(tags("*"), Vec::<String>::new());
assert_eq!(tags("*;q=0.1, id"), ["id"]);
}
#[test]
fn garbage_is_dropped_and_nothing_panics() {
for header in [
"",
",,,",
";q=0.5",
"not a language",
"en_US",
"<script>alert(1)</script>",
"en;q=NaN",
"en;q=-1",
"\u{0}\u{1}\u{2}",
] {
let parsed = accept_language(header);
assert!(
parsed.iter().all(|locale| !locale.to_string().is_empty()),
"{header:?} produced {parsed:?}"
);
}
assert!(accept_language("not a language").is_empty());
assert_eq!(tags("en_US"), ["en-US"]);
assert_eq!(tags("en;q=NaN"), ["en"]);
assert_eq!(tags("en;q=-1"), ["en"]);
}
#[test]
fn qvalues_are_read_to_three_digits() {
assert_eq!(qvalue("1"), 1_000);
assert_eq!(qvalue("1.0"), 1_000);
assert_eq!(qvalue("0.8"), 800);
assert_eq!(qvalue("0.85"), 850);
assert_eq!(qvalue("0.856"), 856);
assert_eq!(
qvalue("0.8569"),
856,
"three digits is all RFC 9110 defines"
);
assert_eq!(qvalue("0"), 0);
assert_eq!(qvalue("0.0"), 0);
assert_eq!(qvalue("0.000"), 0);
}
#[test]
fn a_real_browser_header_parses() {
assert_eq!(
tags("id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7"),
["id-ID", "id", "en-US", "en"]
);
}
#[test]
fn a_flood_of_entries_is_bounded() {
let header = (0..500)
.map(|index| format!("en;q=0.{:03}", index % 1000))
.collect::<Vec<_>>()
.join(",");
assert!(
accept_language(&header).len() <= 1,
"duplicates collapse and the scan is bounded"
);
}
#[test]
fn duplicates_collapse_to_the_best_one() {
assert_eq!(tags("en;q=0.2, id, en;q=0.9"), ["id", "en"]);
}
}