use std::collections::BTreeSet;
#[must_use]
pub(super) fn parse_parameters<const N: usize>(value: &str, names: [&str; N]) -> Option<[Option<String>; N]> {
if value.bytes().any(is_forbidden_header_byte) {
return None;
}
let segments = segments(value)?;
let mut result = std::array::from_fn(|_| None);
let mut seen = BTreeSet::new();
for segment in segments.into_iter().skip(1) {
let (name, raw) = segment.split_once('=')?;
let name = trim_ows(name);
if name.is_empty() || !name.bytes().all(is_token_byte) {
return None;
}
let canonical_name = name.to_ascii_lowercase();
if !seen.insert(canonical_name) {
return None;
}
let decoded = decode(trim_ows(raw))?;
let Some(index) = names.iter().position(|wanted| name.eq_ignore_ascii_case(wanted)) else {
continue;
};
result[index] = Some(decoded);
}
Some(result)
}
#[must_use]
pub(super) fn leading_token(value: &str) -> Option<&str> {
if value.bytes().any(is_forbidden_header_byte) {
return None;
}
let segments = segments(value)?;
let leading = *segments.first()?;
(!leading.is_empty() && leading.bytes().all(is_token_byte)).then_some(leading)
}
#[must_use]
pub(super) fn leading_value(value: &str) -> Option<&str> {
if value.bytes().any(is_forbidden_header_byte) {
return None;
}
segments(value)?.first().copied()
}
#[must_use]
fn segments(value: &str) -> Option<Vec<&str>> {
let mut result = Vec::new();
let mut start = 0;
let mut quoted = false;
let mut escaped = false;
for (index, character) in value.char_indices() {
if escaped {
escaped = false;
} else if quoted && character == '\\' {
escaped = true;
} else if character == '"' {
quoted = !quoted;
} else if character == ';' && !quoted {
result.push(trim_ows(&value[start..index]));
start = index + 1;
}
}
if quoted || escaped {
return None;
}
result.push(trim_ows(&value[start..]));
Some(result)
}
#[must_use]
fn decode(value: &str) -> Option<String> {
if !value.starts_with('"') {
return (!value.is_empty() && value.bytes().all(is_token_byte)).then(|| value.to_string());
}
let mut result = String::new();
let mut chars = value[1..].char_indices();
while let Some(character) = chars.next() {
let (index, character) = character;
if character == '"' {
return trim_ows(&value[index + 2..]).is_empty().then_some(result);
}
if character == '\\' {
let (_, escaped) = chars.next()?;
if !is_quoted_pair_character(escaped) {
return None;
}
result.push(escaped);
} else if is_qdtext_character(character) {
result.push(character);
} else {
return None;
}
}
None
}
#[must_use]
#[inline(always)]
fn trim_ows(value: &str) -> &str {
value.trim_matches([' ', '\t'])
}
#[must_use]
#[inline]
const fn is_forbidden_header_byte(byte: u8) -> bool {
matches!(byte, 0x00..=0x08 | 0x0a..=0x1f | 0x7f)
}
#[must_use]
#[inline]
pub(super) const fn is_token_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' | b'^' | b'_' | b'`' | b'|' | b'~'
)
}
#[must_use]
#[inline]
const fn is_qdtext_character(character: char) -> bool {
matches!(character, '\t' | ' ' | '!' | '#'..='[' | ']'..='~') || !character.is_ascii()
}
#[must_use]
#[inline]
const fn is_quoted_pair_character(character: char) -> bool {
matches!(character, '\t' | ' '..='~') || !character.is_ascii()
}