use std::{borrow::Cow, collections::HashMap, fmt::Display};
const MIME_SPECIAL_CHARACTERS: &[u8] = b"()<>@,;:\\\"/[]?=";
fn is_mime_token(value: &str) -> bool {
!value.is_empty()
&& value.bytes().all(|byte| {
byte.is_ascii_graphic() && !MIME_SPECIAL_CHARACTERS.contains(&byte)
})
}
fn parse_parameter(parameter: &str, parameters: &mut HashMap<String, String>) {
let Some((name, value)) = parameter.trim().split_once('=') else {
return;
};
let name = name.trim();
let value = value.trim();
if !is_mime_token(name) {
return;
}
let value = if let Some(quoted) = value.strip_prefix('"') {
let Some(quoted) = quoted.strip_suffix('"') else {
return;
};
let mut unescaped = String::with_capacity(quoted.len());
let mut characters = quoted.chars();
while let Some(character) = characters.next() {
if character == '\\' {
let Some(escaped) = characters.next() else {
return;
};
unescaped.push(escaped);
} else if character == '"' {
return;
} else {
unescaped.push(character);
}
}
unescaped
} else {
value.to_owned()
};
parameters.insert(name.to_owned(), value);
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Meta {
mime: String,
parameters: HashMap<String, String>,
}
impl Display for Meta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.mime)?;
if !self.parameters.is_empty() {
write!(f, "; ")?;
let mut sorted_parameters: Vec<_> = self.parameters.iter().collect();
sorted_parameters.sort_by(|left, right| left.0.cmp(right.0));
for (index, (key, value)) in sorted_parameters.iter().enumerate() {
if index > 0 {
write!(f, "; ")?;
}
write!(f, "{key}=")?;
if is_mime_token(value) {
write!(f, "{value}")?;
} else {
write!(f, "\"")?;
for character in value.chars() {
if matches!(character, '"' | '\\') {
write!(f, "\\")?;
}
write!(f, "{character}")?;
}
write!(f, "\"")?;
}
}
}
Ok(())
}
}
impl Meta {
#[must_use]
pub fn new() -> Self { Self::default() }
#[must_use]
pub fn from_string<'a>(meta: impl Into<std::borrow::Cow<'a, str>>) -> Self {
let meta = meta.into();
let (mime, parameters_source) = meta.split_once(';').unwrap_or((&meta, ""));
let mime = mime.to_owned();
let mut parameters = HashMap::new();
let mut parameter_start = 0;
let mut quoted = false;
let mut escaped = false;
for (index, character) in parameters_source.char_indices() {
if escaped {
escaped = false;
continue;
}
match character {
'\\' if quoted => escaped = true,
'"' => quoted = !quoted,
';' if !quoted => {
parse_parameter(
¶meters_source[parameter_start..index],
&mut parameters,
);
parameter_start = index + 1;
}
_ => {}
}
}
if !quoted {
parse_parameter(¶meters_source[parameter_start..], &mut parameters);
}
Self { mime, parameters }
}
#[allow(clippy::missing_const_for_fn)]
#[must_use]
pub fn mime(&self) -> Cow<'_, str> { Cow::Borrowed(&self.mime) }
pub const fn mime_mut(&mut self) -> &mut String { &mut self.mime }
#[must_use]
pub const fn parameters(&self) -> &HashMap<String, String> {
&self.parameters
}
pub const fn parameters_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.parameters
}
}