use super::error;
use super::parsing::{self, ExtendedValue};
use super::{Header, RawLike};
use crate::standard_header;
use regex::Regex;
use std::fmt;
use std::sync::LazyLock;
#[derive(Clone, Debug, PartialEq)]
pub enum DispositionType {
Inline,
Attachment,
FormData,
Ext(String),
}
impl<'a> From<&'a str> for DispositionType {
fn from(origin: &'a str) -> DispositionType {
if unicase::eq_ascii(origin, "inline") {
DispositionType::Inline
} else if unicase::eq_ascii(origin, "attachment") {
DispositionType::Attachment
} else if unicase::eq_ascii(origin, "form-data") {
DispositionType::FormData
} else {
DispositionType::Ext(origin.to_owned())
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DispositionParam {
Name(String),
Filename(String),
FilenameExt(ExtendedValue),
Unknown(String, String),
UnknownExt(String, ExtendedValue),
}
impl DispositionParam {
#[inline]
pub fn is_name(&self) -> bool {
self.as_name().is_some()
}
#[inline]
pub fn is_filename(&self) -> bool {
self.as_filename().is_some()
}
#[inline]
pub fn is_filename_ext(&self) -> bool {
self.as_filename_ext().is_some()
}
#[inline]
pub fn is_unknown<T: AsRef<str>>(&self, name: T) -> bool {
self.as_unknown(name).is_some()
}
#[inline]
pub fn is_unknown_ext<T: AsRef<str>>(&self, name: T) -> bool {
self.as_unknown_ext(name).is_some()
}
#[inline]
pub fn as_name(&self) -> Option<&str> {
match self {
DispositionParam::Name(name) => Some(name.as_str()),
_ => None,
}
}
#[inline]
pub fn as_filename(&self) -> Option<&str> {
match self {
DispositionParam::Filename(filename) => Some(filename.as_str()),
_ => None,
}
}
#[inline]
pub fn as_filename_ext(&self) -> Option<&ExtendedValue> {
match self {
DispositionParam::FilenameExt(value) => Some(value),
_ => None,
}
}
#[inline]
pub fn as_unknown<T: AsRef<str>>(&self, name: T) -> Option<&str> {
match self {
DispositionParam::Unknown(ext_name, value)
if ext_name.eq_ignore_ascii_case(name.as_ref()) =>
{
Some(value.as_str())
}
_ => None,
}
}
#[inline]
pub fn as_unknown_ext<T: AsRef<str>>(&self, name: T) -> Option<&ExtendedValue> {
match self {
DispositionParam::UnknownExt(ext_name, value)
if ext_name.eq_ignore_ascii_case(name.as_ref()) =>
{
Some(value)
}
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ContentDisposition {
pub disposition: DispositionType,
pub parameters: Vec<DispositionParam>,
}
impl ContentDisposition {
pub fn is_inline(&self) -> bool {
matches!(self.disposition, DispositionType::Inline)
}
pub fn is_attachment(&self) -> bool {
matches!(self.disposition, DispositionType::Attachment)
}
pub fn is_form_data(&self) -> bool {
matches!(self.disposition, DispositionType::FormData)
}
pub fn is_ext(&self, disp_type: impl AsRef<str>) -> bool {
matches!(
self.disposition,
DispositionType::Ext(ref t) if t.eq_ignore_ascii_case(disp_type.as_ref())
)
}
pub fn get_name(&self) -> Option<&str> {
self.parameters.iter().find_map(DispositionParam::as_name)
}
pub fn get_filename(&self) -> Option<&str> {
self.parameters.iter().find_map(DispositionParam::as_filename)
}
pub fn get_filename_ext(&self) -> Option<&ExtendedValue> {
self.parameters.iter().find_map(DispositionParam::as_filename_ext)
}
pub fn get_unknown(&self, name: impl AsRef<str>) -> Option<&str> {
let name = name.as_ref();
self.parameters.iter().find_map(|p| p.as_unknown(name))
}
pub fn get_unknown_ext(&self, name: impl AsRef<str>) -> Option<&ExtendedValue> {
let name = name.as_ref();
self.parameters.iter().find_map(|p| p.as_unknown_ext(name))
}
}
impl Header for ContentDisposition {
fn header_name() -> &'static str {
static NAME: &str = "Content-Disposition";
NAME
}
fn parse_header<'a, T>(raw: &'a T) -> error::Result<ContentDisposition>
where
T: RawLike<'a>,
{
parsing::from_one_raw_str(raw).and_then(|s: String| {
let mut sections = s.split(';');
let disposition = match sections.next() {
Some(s) => s.trim(),
None => return Err(error::Error::Header),
};
let mut cd =
ContentDisposition { disposition: disposition.into(), parameters: Vec::new() };
for section in sections {
let mut parts = section.splitn(2, '=');
let key = if let Some(key) = parts.next() {
let key_trimmed = key.trim();
if key_trimmed.is_empty() || key_trimmed == "*" {
return Err(error::Error::Header);
}
key_trimmed
} else {
return Err(error::Error::Header);
};
let val = if let Some(val) = parts.next() {
val.trim()
} else {
return Err(error::Error::Header);
};
if let Some(key) = key.strip_suffix('*') {
let ext_val = parsing::parse_extended_value(val)?;
cd.parameters.push(if unicase::eq_ascii(key, "filename") {
DispositionParam::FilenameExt(ext_val)
} else {
DispositionParam::UnknownExt(key.to_owned(), ext_val)
});
} else {
let val = if val.starts_with('\"') {
let mut escaping = false;
let mut quoted_string = vec![];
for &c in val.as_bytes().iter().skip(1) {
if escaping {
escaping = false;
quoted_string.push(c);
} else if c == 0x5c {
escaping = true;
} else if c == 0x22 {
break;
} else {
quoted_string.push(c);
}
}
String::from_utf8(quoted_string).map_err(|_| error::Error::Header)?
} else {
if val.is_empty() {
return Err(error::Error::Header);
}
val.to_owned()
};
cd.parameters.push(if unicase::eq_ascii(key, "name") {
DispositionParam::Name(val)
} else if unicase::eq_ascii(key, "filename") {
DispositionParam::Filename(val)
} else {
DispositionParam::Unknown(key.to_owned(), val)
});
}
}
Ok(cd)
})
}
#[inline]
fn fmt_header(&self, f: &mut super::Formatter) -> fmt::Result {
f.fmt_line(self)
}
}
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::Ext(s) => write!(f, "{}", s),
}
}
}
impl fmt::Display for DispositionParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap());
match self {
DispositionParam::Name(value) => write!(f, "name={}", value),
DispositionParam::Filename(value) => {
write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref())
}
DispositionParam::Unknown(name, value) => {
write!(f, "{}=\"{}\"", name, &RE.replace_all(value, "\\$0").as_ref())
}
DispositionParam::FilenameExt(ext_value) => {
write!(f, "filename*={}", ext_value)
}
DispositionParam::UnknownExt(name, ext_value) => {
write!(f, "{}*={}", name, ext_value)
}
}
}
}
impl fmt::Display for ContentDisposition {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.disposition)?;
for param in &self.parameters {
write!(f, "; {}", param)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{ContentDisposition, DispositionParam, DispositionType, Header};
use crate::header::parsing::ExtendedValue;
use crate::header::{Charset, Raw};
#[test]
fn test_parse_header() {
let a: Raw = "".into();
assert!(ContentDisposition::parse_header(&a).is_err());
let a: Raw = "form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
DispositionParam::Name("upload".to_owned()),
DispositionParam::Filename("sample.png".to_owned()),
],
};
assert_eq!(a, b);
let a: Raw = "attachment; filename=\"image.jpg\"".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::Filename("image.jpg".to_owned())],
};
assert_eq!(a, b);
let a: Raw = "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::FilenameExt(ExtendedValue {
charset: Charset::Ext(String::from("UTF-8")),
language_tag: None,
value: vec![
0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r',
b'a', b't', b'e', b's',
],
})],
};
assert_eq!(a, b);
}
#[test]
fn test_display() {
let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
let a: Raw = as_string.into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!(as_string, display_rendered);
let a: Raw = "attachment; filename=colourful.csv".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered);
}
}
standard_header!(ContentDisposition, CONTENT_DISPOSITION);