#![no_std]
#![doc(html_root_url = "https://docs.rs/content-type/0.1.0")]
extern crate alloc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ContentType {
pub type_: String,
pub parameters: Vec<(String, String)>,
}
impl ContentType {
#[must_use]
pub fn new(type_: impl Into<String>) -> Self {
Self {
type_: type_.into(),
parameters: Vec::new(),
}
}
#[must_use]
pub fn with_parameter(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.parameters.push((name.into(), value.into()));
self
}
#[must_use]
pub fn get_parameter(&self, name: &str) -> Option<&str> {
let lname = lowercase(name);
self.parameters
.iter()
.find(|(k, _)| lowercase(k) == lname)
.map(|(_, v)| v.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormatError {
InvalidType(String),
InvalidParameterName(String),
InvalidParameterValue(String),
}
impl fmt::Display for FormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FormatError::InvalidType(t) => write!(f, "invalid type: {t}"),
FormatError::InvalidParameterName(n) => write!(f, "invalid parameter name: {n}"),
FormatError::InvalidParameterValue(v) => write!(f, "invalid parameter value: {v}"),
}
}
}
impl core::error::Error for FormatError {}
#[must_use]
pub fn parse(header: &str) -> ContentType {
parse_with(header, true)
}
#[must_use]
pub fn parse_with(header: &str, parse_parameters: bool) -> ContentType {
let chars: Vec<char> = header.chars().collect();
let len = chars.len();
let mut index = skip_ows(&chars, 0, len);
let value_start = index;
index = skip_value(&chars, index, len);
let value_end = trailing_ows(&chars, value_start, index);
let type_ = lowercase_slice(&chars[value_start..value_end]);
let parameters = if parse_parameters {
parse_parameters_impl(&chars, index, len)
} else {
Vec::new()
};
ContentType { type_, parameters }
}
pub fn format(content_type: &ContentType) -> Result<String, FormatError> {
if !is_type(&content_type.type_) {
return Err(FormatError::InvalidType(content_type.type_.clone()));
}
let mut result = content_type.type_.clone();
for (name, value) in &content_type.parameters {
if !is_token(name) {
return Err(FormatError::InvalidParameterName(name.clone()));
}
result.push_str("; ");
result.push_str(name);
result.push('=');
result.push_str(&qstring(value)?);
}
Ok(result)
}
const SP: char = ' ';
const HTAB: char = '\t';
const SEMI: char = ';';
const EQ: char = '=';
const DQUOTE: char = '"';
const BSLASH: char = '\\';
fn parse_parameters_impl(chars: &[char], mut index: usize, len: usize) -> Vec<(String, String)> {
let mut parameters: Vec<(String, String)> = Vec::new();
'parameter: while index < len {
index = skip_ows(chars, index + 1, len); let key_start = index;
while index < len {
let code = chars[index];
if code == SEMI {
continue 'parameter;
}
if code == EQ {
let key_end = trailing_ows(chars, key_start, index);
let key = lowercase_slice(&chars[key_start..key_end]);
index = skip_ows(chars, index + 1, len);
if index < len && chars[index] == DQUOTE {
index += 1;
let mut value = String::new();
let mut closed = false;
while index < len {
let code = chars[index];
index += 1;
if code == DQUOTE {
index = skip_value(chars, index, len);
closed = true;
break;
}
if code == BSLASH && index < len {
value.push(chars[index]);
index += 1;
continue;
}
value.push(code);
}
if closed {
insert_first(&mut parameters, key, value);
}
continue 'parameter;
}
let value_start = index;
index = skip_value(chars, index, len);
let value_end = trailing_ows(chars, value_start, index);
let value = chars[value_start..value_end].iter().collect();
insert_first(&mut parameters, key, value);
continue 'parameter;
}
index += 1;
}
}
parameters
}
fn insert_first(parameters: &mut Vec<(String, String)>, key: String, value: String) {
if !parameters.iter().any(|(k, _)| *k == key) {
parameters.push((key, value));
}
}
fn skip_value(chars: &[char], mut index: usize, len: usize) -> usize {
while index < len && chars[index] != SEMI {
index += 1;
}
index
}
fn skip_ows(chars: &[char], mut index: usize, len: usize) -> usize {
while index < len && (chars[index] == SP || chars[index] == HTAB) {
index += 1;
}
index
}
fn trailing_ows(chars: &[char], start: usize, mut end: usize) -> usize {
while end > start && (chars[end - 1] == SP || chars[end - 1] == HTAB) {
end -= 1;
}
end
}
fn lowercase(s: &str) -> String {
s.chars().flat_map(char::to_lowercase).collect()
}
fn lowercase_slice(chars: &[char]) -> String {
chars.iter().flat_map(|c| c.to_lowercase()).collect()
}
fn is_token_char(c: char) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
'!' | '#'
| '$'
| '%'
| '&'
| '\''
| '*'
| '+'
| '.'
| '^'
| '_'
| '`'
| '|'
| '~'
| '-'
)
}
fn is_token(s: &str) -> bool {
!s.is_empty() && s.chars().all(is_token_char)
}
fn is_type(s: &str) -> bool {
match s.split_once('/') {
Some((ty, sub)) => is_token(ty) && is_token(sub),
None => false,
}
}
fn is_text_char(c: char) -> bool {
let n = c as u32;
n == 0x09 || (0x20..=0x7e).contains(&n) || (0x80..=0xff).contains(&n)
}
fn qstring(value: &str) -> Result<String, FormatError> {
if is_token(value) {
return Ok(value.to_string());
}
if value.chars().all(is_text_char) {
let mut out = String::with_capacity(value.len() + 2);
out.push(DQUOTE);
for c in value.chars() {
if c == BSLASH || c == DQUOTE {
out.push(BSLASH);
}
out.push(c);
}
out.push(DQUOTE);
return Ok(out);
}
Err(FormatError::InvalidParameterValue(value.to_string()))
}