use header;
use header::ExtendedValue;
use header::{Header, IntoHeaderValue, Writer};
use regex::Regex;
use std::fmt::{self, Write};
fn split_once(haystack: &str, needle: char) -> (&str, &str) {
haystack.find(needle).map_or_else(
|| (haystack, ""),
|sc| {
let (first, last) = haystack.split_at(sc);
(first, last.split_at(1).1)
},
)
}
fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) {
let (first, last) = split_once(haystack, needle);
(first.trim_right(), last.trim_left())
}
#[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 origin.eq_ignore_ascii_case("inline") {
DispositionType::Inline
} else if origin.eq_ignore_ascii_case("attachment") {
DispositionType::Attachment
} else if origin.eq_ignore_ascii_case("form-data") {
DispositionType::FormData
} else {
DispositionType::Ext(origin.to_owned())
}
}
}
#[derive(Clone, Debug, PartialEq)]
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(ref name) => Some(name.as_str()),
_ => None,
}
}
#[inline]
pub fn as_filename(&self) -> Option<&str> {
match self {
DispositionParam::Filename(ref filename) => Some(filename.as_str()),
_ => None,
}
}
#[inline]
pub fn as_filename_ext(&self) -> Option<&ExtendedValue> {
match self {
DispositionParam::FilenameExt(ref value) => Some(value),
_ => None,
}
}
#[inline]
pub fn as_unknown<T: AsRef<str>>(&self, name: T) -> Option<&str> {
match self {
DispositionParam::Unknown(ref ext_name, ref 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(ref ext_name, ref 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 from_raw(hv: &header::HeaderValue) -> Result<Self, ::error::ParseError> {
let hv = String::from_utf8(hv.as_bytes().to_vec())
.map_err(|_| ::error::ParseError::Header)?;
let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';');
if disp_type.is_empty() {
return Err(::error::ParseError::Header);
}
let mut cd = ContentDisposition {
disposition: disp_type.into(),
parameters: Vec::new(),
};
while !left.is_empty() {
let (param_name, new_left) = split_once_and_trim(left, '=');
if param_name.is_empty() || param_name == "*" || new_left.is_empty() {
return Err(::error::ParseError::Header);
}
left = new_left;
if param_name.ends_with('*') {
let param_name = ¶m_name[..param_name.len() - 1];
let (ext_value, new_left) = split_once_and_trim(left, ';');
left = new_left;
let ext_value = header::parse_extended_value(ext_value)?;
let param = if param_name.eq_ignore_ascii_case("filename") {
DispositionParam::FilenameExt(ext_value)
} else {
DispositionParam::UnknownExt(param_name.to_owned(), ext_value)
};
cd.parameters.push(param);
} else {
let value = if left.starts_with('\"') {
let mut escaping = false;
let mut quoted_string = vec![];
let mut end = None;
for (i, &c) in left.as_bytes().iter().skip(1).enumerate() {
if escaping {
escaping = false;
quoted_string.push(c);
} else if c == 0x5c {
escaping = true;
} else if c == 0x22 {
end = Some(i + 1);
break;
} else {
quoted_string.push(c);
}
}
left = &left[end.ok_or(::error::ParseError::Header)? + 1..];
left = split_once(left, ';').1.trim_left();
String::from_utf8(quoted_string).map_err(|_| ::error::ParseError::Header)?
} else {
let (token, new_left) = split_once_and_trim(left, ';');
left = new_left;
token.to_owned()
};
if value.is_empty() {
return Err(::error::ParseError::Header);
}
let param = if param_name.eq_ignore_ascii_case("name") {
DispositionParam::Name(value)
} else if param_name.eq_ignore_ascii_case("filename") {
DispositionParam::Filename(value)
} else {
DispositionParam::Unknown(param_name.to_owned(), value)
};
cd.parameters.push(param);
}
}
Ok(cd)
}
pub fn is_inline(&self) -> bool {
match self.disposition {
DispositionType::Inline => true,
_ => false,
}
}
pub fn is_attachment(&self) -> bool {
match self.disposition {
DispositionType::Attachment => true,
_ => false,
}
}
pub fn is_form_data(&self) -> bool {
match self.disposition {
DispositionType::FormData => true,
_ => false,
}
}
pub fn is_ext<T: AsRef<str>>(&self, disp_type: T) -> bool {
match self.disposition {
DispositionType::Ext(ref t)
if t.eq_ignore_ascii_case(disp_type.as_ref()) =>
{
true
}
_ => false,
}
}
pub fn get_name(&self) -> Option<&str> {
self.parameters.iter().filter_map(|p| p.as_name()).nth(0)
}
pub fn get_filename(&self) -> Option<&str> {
self.parameters
.iter()
.filter_map(|p| p.as_filename())
.nth(0)
}
pub fn get_filename_ext(&self) -> Option<&ExtendedValue> {
self.parameters
.iter()
.filter_map(|p| p.as_filename_ext())
.nth(0)
}
pub fn get_unknown<T: AsRef<str>>(&self, name: T) -> Option<&str> {
let name = name.as_ref();
self.parameters
.iter()
.filter_map(|p| p.as_unknown(name))
.nth(0)
}
pub fn get_unknown_ext<T: AsRef<str>>(&self, name: T) -> Option<&ExtendedValue> {
let name = name.as_ref();
self.parameters
.iter()
.filter_map(|p| p.as_unknown_ext(name))
.nth(0)
}
}
impl IntoHeaderValue for ContentDisposition {
type Error = header::InvalidHeaderValueBytes;
fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
header::HeaderValue::from_shared(writer.take())
}
}
impl Header for ContentDisposition {
fn name() -> header::HeaderName {
header::CONTENT_DISPOSITION
}
fn parse<T: ::HttpMessage>(msg: &T) -> Result<Self, ::error::ParseError> {
if let Some(h) = msg.headers().get(Self::name()) {
Self::from_raw(&h)
} else {
Err(::error::ParseError::Header)
}
}
}
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(ref s) => write!(f, "{}", s),
}
}
}
impl fmt::Display for DispositionParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
lazy_static! {
static ref RE: Regex = Regex::new("[\x01-\x08\x10\x1F\x7F\"\\\\]").unwrap();
}
match self {
DispositionParam::Name(ref value) => write!(f, "name={}", value),
DispositionParam::Filename(ref value) => {
write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref())
}
DispositionParam::Unknown(ref name, ref value) => write!(
f,
"{}=\"{}\"",
name,
&RE.replace_all(value, "\\$0").as_ref()
),
DispositionParam::FilenameExt(ref ext_value) => {
write!(f, "filename*={}", ext_value)
}
DispositionParam::UnknownExt(ref name, ref 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)?;
self.parameters
.iter()
.map(|param| write!(f, "; {}", param))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::{ContentDisposition, DispositionParam, DispositionType};
use header::shared::Charset;
use header::{ExtendedValue, HeaderValue};
#[test]
fn test_from_raw_basic() {
assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err());
let a = HeaderValue::from_static(
"form-data; dummy=3; name=upload; filename=\"sample.png\"",
);
let a: ContentDisposition = ContentDisposition::from_raw(&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 = HeaderValue::from_static("attachment; filename=\"image.jpg\"");
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::Filename("image.jpg".to_owned())],
};
assert_eq!(a, b);
let a = HeaderValue::from_static("inline; filename=image.jpg");
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Inline,
parameters: vec![DispositionParam::Filename("image.jpg".to_owned())],
};
assert_eq!(a, b);
let a = HeaderValue::from_static(
"attachment; creation-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::Unknown(
String::from("creation-date"),
"Wed, 12 Feb 1997 16:29:51 -0500".to_owned(),
)],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_extended() {
let a = HeaderValue::from_static(
"attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates",
);
let a: ContentDisposition = ContentDisposition::from_raw(&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);
let a = HeaderValue::from_static(
"attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates",
);
let a: ContentDisposition = ContentDisposition::from_raw(&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_from_raw_extra_whitespace() {
let a = HeaderValue::from_static(
"form-data ; du-mmy= 3 ; name =upload ; filename = \"sample.png\" ; ",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Unknown("du-mmy".to_owned(), "3".to_owned()),
DispositionParam::Name("upload".to_owned()),
DispositionParam::Filename("sample.png".to_owned()),
],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_unordered() {
let a = HeaderValue::from_static(
"form-data; dummy=3; filename=\"sample.png\" ; name=upload;",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
DispositionParam::Filename("sample.png".to_owned()),
DispositionParam::Name("upload".to_owned()),
],
};
assert_eq!(a, b);
let a = HeaderValue::from_str(
"attachment; filename*=iso-8859-1''foo-%E4.html; filename=\"foo-ä.html\"",
).unwrap();
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![
DispositionParam::FilenameExt(ExtendedValue {
charset: Charset::Iso_8859_1,
language_tag: None,
value: b"foo-\xe4.html".to_vec(),
}),
DispositionParam::Filename("foo-ä.html".to_owned()),
],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_only_disp() {
let a = ContentDisposition::from_raw(&HeaderValue::from_static("attachment"))
.unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![],
};
assert_eq!(a, b);
let a =
ContentDisposition::from_raw(&HeaderValue::from_static("inline ;")).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Inline,
parameters: vec![],
};
assert_eq!(a, b);
let a = ContentDisposition::from_raw(&HeaderValue::from_static(
"unknown-disp-param",
)).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Ext(String::from("unknown-disp-param")),
parameters: vec![],
};
assert_eq!(a, b);
}
#[test]
fn from_raw_with_mixed_case() {
let a = HeaderValue::from_str(
"InLInE; fIlenAME*=iso-8859-1''foo-%E4.html; filEName=\"foo-ä.html\"",
).unwrap();
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Inline,
parameters: vec![
DispositionParam::FilenameExt(ExtendedValue {
charset: Charset::Iso_8859_1,
language_tag: None,
value: b"foo-\xe4.html".to_vec(),
}),
DispositionParam::Filename("foo-ä.html".to_owned()),
],
};
assert_eq!(a, b);
}
#[test]
fn from_raw_with_unicode() {
let a =
HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"")
.unwrap();
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Name(String::from("upload")),
DispositionParam::Filename(String::from("文件.webp")),
],
};
assert_eq!(a, b);
let a =
HeaderValue::from_str("form-data; name=upload; filename=\"余固知謇謇之為患兮,忍而不能舍也.pptx\"").unwrap();
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Name(String::from("upload")),
DispositionParam::Filename(String::from(
"余固知謇謇之為患兮,忍而不能舍也.pptx",
)),
],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_escape() {
let a = HeaderValue::from_static(
"form-data; dummy=3; name=upload; filename=\"s\\amp\\\"le.png\"",
);
let a: ContentDisposition = ContentDisposition::from_raw(&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(
['s', 'a', 'm', 'p', '\"', 'l', 'e', '.', 'p', 'n', 'g']
.iter()
.collect(),
),
],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_semicolon() {
let a =
HeaderValue::from_static("form-data; filename=\"A semicolon here;.pdf\"");
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![DispositionParam::Filename(String::from(
"A semicolon here;.pdf",
))],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_uncessary_percent_decode() {
let a = HeaderValue::from_static(
"form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Name("photo".to_owned()),
DispositionParam::Filename(String::from("%74%65%73%74%2e%70%6e%67")),
],
};
assert_eq!(a, b);
let a = HeaderValue::from_static(
"form-data; name=photo; filename=\"%74%65%73%74.png\"",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Name("photo".to_owned()),
DispositionParam::Filename(String::from("%74%65%73%74.png")),
],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_param_value_missing() {
let a = HeaderValue::from_static("form-data; name=upload ; filename=");
assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("attachment; dummy=; filename=invoice.pdf");
assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("inline; filename= ");
assert!(ContentDisposition::from_raw(&a).is_err());
}
#[test]
fn test_from_raw_param_name_missing() {
let a = HeaderValue::from_static("inline; =\"test.txt\"");
assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("inline; =diary.odt");
assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("inline; =");
assert!(ContentDisposition::from_raw(&a).is_err());
}
#[test]
fn test_display_extended() {
let as_string =
"attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
let a = HeaderValue::from_static(as_string);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!(as_string, display_rendered);
let a = HeaderValue::from_static("attachment; filename=colourful.csv");
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!(
"attachment; filename=\"colourful.csv\"".to_owned(),
display_rendered
);
}
#[test]
fn test_display_quote() {
let as_string = "form-data; name=upload; filename=\"Quote\\\"here.png\"";
as_string
.find(['\\', '\"'].iter().collect::<String>().as_str())
.unwrap();
let a = HeaderValue::from_static(as_string);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!(as_string, display_rendered);
}
#[test]
fn test_display_space_tab() {
let as_string = "form-data; name=upload; filename=\"Space here.png\"";
let a = HeaderValue::from_static(as_string);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!(as_string, display_rendered);
let a: ContentDisposition = ContentDisposition {
disposition: DispositionType::Inline,
parameters: vec![DispositionParam::Filename(String::from("Tab\there.png"))],
};
let display_rendered = format!("{}", a);
assert_eq!("inline; filename=\"Tab\x09here.png\"", display_rendered);
}
#[test]
fn test_display_control_characters() {
let a: ContentDisposition = ContentDisposition {
disposition: DispositionType::Inline,
parameters: vec![DispositionParam::Filename(String::from("bell\x07.png"))],
};
let display_rendered = format!("{}", a);
assert_eq!("inline; filename=\"bell\\\x07.png\"", display_rendered);
}
#[test]
fn test_param_methods() {
let param = DispositionParam::Filename(String::from("sample.txt"));
assert!(param.is_filename());
assert_eq!(param.as_filename().unwrap(), "sample.txt");
let param = DispositionParam::Unknown(String::from("foo"), String::from("bar"));
assert!(param.is_unknown("foo"));
assert_eq!(param.as_unknown("fOo"), Some("bar"));
}
#[test]
fn test_disposition_methods() {
let cd = 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!(cd.get_name(), Some("upload"));
assert_eq!(cd.get_unknown("dummy"), Some("3"));
assert_eq!(cd.get_filename(), Some("sample.png"));
assert_eq!(cd.get_unknown_ext("dummy"), None);
assert_eq!(cd.get_unknown("duMMy"), Some("3"));
}
}