use base_message::FieldValueError;
use std::borrow::Cow;
pub(crate) fn is_field_name_byte(byte: u8) -> bool {
matches!(byte, 33..=57 | 59..=126)
}
pub(crate) fn is_field_value_byte(byte: u8) -> bool {
byte == b'\t' || (byte >= 0x20 && byte != 0x7f)
}
pub(crate) fn is_token_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte)
}
pub(crate) fn is_token(text: &str) -> bool {
!text.is_empty() && text.bytes().all(is_token_byte)
}
pub(crate) fn field_text(value: &[u8]) -> Result<&str, FieldValueError> {
let text =
std::str::from_utf8(value).map_err(|_| FieldValueError::new("value is not UTF-8"))?;
if !value.iter().all(|&byte| is_field_value_byte(byte)) {
return Err(FieldValueError::new("value contains a control character"));
}
Ok(text)
}
pub(crate) fn lowercase(text: &str) -> Cow<'_, str> {
if text.bytes().any(|byte| byte.is_ascii_uppercase()) {
Cow::Owned(text.to_ascii_lowercase())
} else {
Cow::Borrowed(text)
}
}
pub(crate) const PARAMETER_COUNT_LIMIT: usize = 64;
pub(crate) type Parameterized<'a> = (Cow<'a, str>, Vec<(Cow<'a, str>, Cow<'a, str>)>);
pub(crate) fn parse_parameterized(text: &str) -> Result<Parameterized<'_>, FieldValueError> {
let mut rest = text.trim_matches([' ', '\t']);
let end = rest
.find(|character: char| !character.is_ascii() || !is_token_byte(character as u8))
.unwrap_or(rest.len());
if end == 0 {
return Err(FieldValueError::new("missing value"));
}
let value = lowercase(&rest[..end]);
rest = &rest[end..];
let mut parameters: Vec<(Cow<'_, str>, Cow<'_, str>)> = Vec::new();
loop {
rest = rest.trim_start_matches([' ', '\t']);
if rest.is_empty() {
return Ok((value, parameters));
}
rest = rest
.strip_prefix(';')
.ok_or(FieldValueError::new("expected a parameter separator"))?
.trim_start_matches([' ', '\t']);
let end = rest
.find(|character: char| !character.is_ascii() || !is_token_byte(character as u8))
.unwrap_or(rest.len());
if end == 0 {
return Err(FieldValueError::new("missing parameter name"));
}
let name = lowercase(&rest[..end]);
rest = rest[end..]
.trim_start_matches([' ', '\t'])
.strip_prefix('=')
.ok_or(FieldValueError::new("expected a parameter value"))?
.trim_start_matches([' ', '\t']);
let (parameter, remaining) = parameter_value(rest)?;
rest = remaining;
if parameters.iter().any(|(existing, _)| *existing == name) {
return Err(FieldValueError::new("duplicate parameter"));
}
if parameters.len() == PARAMETER_COUNT_LIMIT {
return Err(FieldValueError::new("too many parameters"));
}
parameters.push((name, parameter));
}
}
fn parameter_value(text: &str) -> Result<(Cow<'_, str>, &str), FieldValueError> {
let Some(quoted) = text.strip_prefix('"') else {
let end = text
.find(|character: char| !character.is_ascii() || !is_token_byte(character as u8))
.unwrap_or(text.len());
if end == 0 {
return Err(FieldValueError::new("missing parameter value"));
}
return Ok((Cow::Borrowed(&text[..end]), &text[end..]));
};
let mut unescaped: Option<String> = None;
let mut characters = quoted.char_indices();
while let Some((index, character)) = characters.next() {
match character {
'"' => {
let value = unescaped.map_or(Cow::Borrowed("ed[..index]), Cow::Owned);
return Ok((value, "ed[index + 1..]));
}
'\\' if matches!(quoted[index + 1..].chars().next(), Some('"' | '\\')) => {
let (_, escaped) = characters
.next()
.expect("the escaped character was just seen");
unescaped
.get_or_insert_with(|| quoted[..index].to_owned())
.push(escaped);
}
_ => {
if let Some(unescaped) = &mut unescaped {
unescaped.push(character);
}
}
}
}
Err(FieldValueError::new("unterminated quoted string"))
}
pub(crate) fn write_parameter_value(output: &mut String, name: &str, value: &str) {
if name.ends_with('*') {
output.push_str(value);
return;
}
output.push('"');
for character in value.chars() {
if matches!(character, '"' | '\\') {
output.push('\\');
}
output.push(character);
}
output.push('"');
}