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()
}