use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use crate::validate::{
escape_quotes, is_qdtext_char, is_quoted_pair_char, is_valid_token, trim_ows,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContentDispositionError {
Empty,
InvalidFormat,
InvalidDispositionType,
InvalidParameter,
InvalidExtValue,
DuplicateParameter(String),
TooManyParameters,
}
impl fmt::Display for ContentDispositionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContentDispositionError::Empty => write!(f, "empty content-disposition"),
ContentDispositionError::InvalidFormat => {
write!(f, "invalid content-disposition format")
}
ContentDispositionError::InvalidDispositionType => {
write!(f, "invalid disposition-type")
}
ContentDispositionError::InvalidParameter => write!(f, "invalid parameter"),
ContentDispositionError::InvalidExtValue => write!(f, "invalid ext-value encoding"),
ContentDispositionError::DuplicateParameter(name) => {
write!(f, "duplicate parameter: {}", name)
}
ContentDispositionError::TooManyParameters => {
write!(f, "too many content-disposition parameters")
}
}
}
}
impl core::error::Error for ContentDispositionError {}
const MAX_PARAMS: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DispositionType {
Inline,
Attachment,
FormData,
Unknown(String),
}
impl DispositionType {
fn from_str(s: &str) -> Result<Self, ContentDispositionError> {
let lower = s.to_ascii_lowercase();
match lower.as_str() {
"inline" => Ok(DispositionType::Inline),
"attachment" => Ok(DispositionType::Attachment),
"form-data" => Ok(DispositionType::FormData),
_ => {
if is_valid_token(s) {
Ok(DispositionType::Unknown(lower))
} else {
Err(ContentDispositionError::InvalidDispositionType)
}
}
}
}
}
impl fmt::Display for DispositionType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DispositionType::Inline => write!(f, "inline"),
DispositionType::Attachment => write!(f, "attachment"),
DispositionType::FormData => write!(f, "form-data"),
DispositionType::Unknown(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentDisposition {
disposition_type: DispositionType,
filename: Option<String>,
filename_ext: Option<String>,
name: Option<String>,
parameters: Vec<(String, String)>,
}
impl ContentDisposition {
pub fn parse(input: &str) -> Result<Self, ContentDispositionError> {
let input = trim_ows(input);
if input.is_empty() {
return Err(ContentDispositionError::Empty);
}
let parts = split_params(input);
let type_str = parts
.first()
.ok_or(ContentDispositionError::InvalidFormat)?;
let disposition_type = DispositionType::from_str(trim_ows(type_str))?;
let mut cd = ContentDisposition {
disposition_type,
filename: None,
filename_ext: None,
name: None,
parameters: Vec::new(),
};
let mut seen_params = Vec::new();
for part in parts.iter().skip(1) {
let part = trim_ows(part);
if part.is_empty() {
continue;
}
if let Some(eq_pos) = part.find('=') {
let param_name = trim_ows(&part[..eq_pos]).to_ascii_lowercase();
let param_value = trim_ows(&part[eq_pos + 1..]);
if seen_params.iter().any(|n: &String| n == ¶m_name) {
return Err(ContentDispositionError::DuplicateParameter(param_name));
}
if seen_params.len() >= MAX_PARAMS {
return Err(ContentDispositionError::TooManyParameters);
}
seen_params.push(param_name.clone());
match param_name.as_str() {
"filename" => {
cd.filename = Some(parse_param_value(param_value)?);
}
"filename*" => {
cd.filename_ext = Some(parse_ext_value(param_value)?);
}
"name" => {
cd.name = Some(parse_param_value(param_value)?);
}
_ => {
cd.parameters
.push((param_name, parse_param_value(param_value)?));
}
}
}
}
Ok(cd)
}
pub fn new(disposition_type: DispositionType) -> Self {
ContentDisposition {
disposition_type,
filename: None,
filename_ext: None,
name: None,
parameters: Vec::new(),
}
}
pub fn disposition_type(&self) -> DispositionType {
self.disposition_type.clone()
}
pub fn filename(&self) -> Option<&str> {
self.filename_ext.as_deref().or(self.filename.as_deref())
}
pub fn filename_ascii(&self) -> Option<&str> {
self.filename.as_deref()
}
pub fn filename_ext(&self) -> Option<&str> {
self.filename_ext.as_deref()
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn parameter(&self, name: &str) -> Option<&str> {
let name_lower = name.to_ascii_lowercase();
for (k, v) in &self.parameters {
if k == &name_lower {
return Some(v);
}
}
None
}
pub fn is_inline(&self) -> bool {
self.disposition_type == DispositionType::Inline
}
pub fn is_attachment(&self) -> bool {
self.disposition_type == DispositionType::Attachment
}
pub fn is_form_data(&self) -> bool {
self.disposition_type == DispositionType::FormData
}
pub fn with_filename(mut self, filename: &str) -> Self {
self.filename = Some(filename.to_string());
self
}
pub fn with_filename_ext(mut self, filename: &str) -> Self {
self.filename_ext = Some(filename.to_string());
self
}
pub fn with_name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
}
impl fmt::Display for ContentDisposition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.disposition_type)?;
if let Some(name) = &self.name {
write!(f, "; name=\"{}\"", escape_quotes(name))?;
}
if let Some(filename) = &self.filename {
write!(f, "; filename=\"{}\"", escape_quotes(filename))?;
}
if let Some(filename_ext) = &self.filename_ext {
write!(f, "; filename*=UTF-8''{}", encode_ext_value(filename_ext))?;
}
for (name, value) in &self.parameters {
write!(f, "; {}=\"{}\"", name, escape_quotes(value))?;
}
Ok(())
}
}
fn split_params(input: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
let mut escape_next = false;
for c in input.chars() {
if escape_next {
current.push(c);
escape_next = false;
continue;
}
match c {
'\\' if in_quotes => {
current.push(c);
escape_next = true;
}
'"' => {
current.push(c);
in_quotes = !in_quotes;
}
';' if !in_quotes => {
parts.push(current);
current = String::new();
}
_ => {
current.push(c);
}
}
}
if !current.is_empty() {
parts.push(current);
}
parts
}
fn parse_param_value(value: &str) -> Result<String, ContentDispositionError> {
let value = trim_ows(value);
if value.starts_with('"') {
if value.ends_with('"') && value.len() >= 2 {
parse_quoted_string(&value[1..value.len() - 1])
} else {
Err(ContentDispositionError::InvalidParameter)
}
} else {
if !is_valid_token(value) {
return Err(ContentDispositionError::InvalidParameter);
}
Ok(value.to_string())
}
}
fn parse_quoted_string(s: &str) -> Result<String, ContentDispositionError> {
let mut result = String::with_capacity(s.len());
let mut iter = s.chars();
while let Some(c) = iter.next() {
if c == '\\' {
let next = iter
.next()
.ok_or(ContentDispositionError::InvalidParameter)?;
if !is_quoted_pair_char(next) {
return Err(ContentDispositionError::InvalidParameter);
}
result.push(next);
} else {
if !is_qdtext_char(c) {
return Err(ContentDispositionError::InvalidParameter);
}
result.push(c);
}
}
Ok(result)
}
fn parse_ext_value(value: &str) -> Result<String, ContentDispositionError> {
let value = trim_ows(value);
let first_quote = value
.find('\'')
.ok_or(ContentDispositionError::InvalidExtValue)?;
let charset = &value[..first_quote];
let rest = &value[first_quote + 1..];
let second_quote = rest
.find('\'')
.ok_or(ContentDispositionError::InvalidExtValue)?;
let encoded_value = &rest[second_quote + 1..];
if !charset.eq_ignore_ascii_case("UTF-8") {
return Err(ContentDispositionError::InvalidExtValue);
}
percent_decode(encoded_value)
}
fn percent_decode(s: &str) -> Result<String, ContentDispositionError> {
let mut bytes = Vec::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '%' {
let hex: String = chars.by_ref().take(2).collect();
if hex.len() != 2 {
return Err(ContentDispositionError::InvalidExtValue);
}
let byte = u8::from_str_radix(&hex, 16)
.map_err(|_| ContentDispositionError::InvalidExtValue)?;
bytes.push(byte);
} else {
if !c.is_ascii() || !is_attr_char(c as u8) {
return Err(ContentDispositionError::InvalidExtValue);
}
bytes.push(c as u8);
}
}
String::from_utf8(bytes).map_err(|_| ContentDispositionError::InvalidExtValue)
}
fn encode_ext_value(s: &str) -> String {
let mut result = String::new();
for byte in s.bytes() {
if is_attr_char(byte) {
result.push(byte as char);
} else {
result.push('%');
result.push_str(&alloc::format!("{:02X}", byte));
}
}
result
}
fn is_attr_char(b: u8) -> bool {
matches!(b,
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' |
b'!' | b'#' | b'$' | b'&' | b'+' | b'-' | b'.' |
b'^' | b'_' | b'`' | b'|' | b'~'
)
}