-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(headers): Add Content-Disposition header
fixes #561
- Loading branch information
1 parent
b4a9227
commit 7623ecc
Showing
2 changed files
with
330 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,328 @@ | ||
// # References | ||
// | ||
// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt | ||
// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt | ||
// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc2388.txt | ||
// Browser conformance tests at: http://greenbytes.de/tech/tc2231/ | ||
// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml | ||
|
||
use language_tags::LanguageTag; | ||
use std::fmt; | ||
use std::str::FromStr; | ||
use unicase::UniCase; | ||
use url::percent_encoding; | ||
|
||
use header::{Header, HeaderFormat, parsing}; | ||
use header::shared::Charset; | ||
|
||
/// The implied disposition of the content of the HTTP body | ||
#[derive(Clone, Debug, PartialEq)] | ||
pub enum DispositionType { | ||
/// Inline implies default processing | ||
Inline, | ||
/// Attachment implies that the recipient should prompt the user to save the response locally, | ||
/// rather than process it normally (as per its media type). | ||
Attachment, | ||
/// Extension type. Should be handled by recipients the same way as Attachment | ||
Ext(String) | ||
} | ||
|
||
/// A parameter to the disposition type | ||
#[derive(Clone, Debug, PartialEq)] | ||
pub enum DispositionParam { | ||
/// A Filename consisting of a Charset, an optional LanguageTag, and finally a sequence of | ||
/// bytes representing the filename | ||
Filename(Charset, Option<LanguageTag>, Vec<u8>), | ||
/// Extension type consisting of token and value. Recipients should ignore unrecognized | ||
/// parameters. | ||
Ext(String, String) | ||
} | ||
|
||
/// A `Content-Disposition` header, (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266) | ||
/// | ||
/// The Content-Disposition response header field is used to convey | ||
/// additional information about how to process the response payload, and | ||
/// also can be used to attach additional metadata, such as the filename | ||
/// to use when saving the response payload locally. | ||
/// | ||
/// # ABNF | ||
/// ```plain | ||
/// content-disposition = "Content-Disposition" ":" | ||
/// disposition-type *( ";" disposition-parm ) | ||
/// | ||
/// disposition-type = "inline" | "attachment" | disp-ext-type | ||
/// ; case-insensitive | ||
/// | ||
/// disp-ext-type = token | ||
/// | ||
/// disposition-parm = filename-parm | disp-ext-parm | ||
/// | ||
/// filename-parm = "filename" "=" value | ||
/// | "filename*" "=" ext-value | ||
/// | ||
/// disp-ext-parm = token "=" value | ||
/// | ext-token "=" ext-value | ||
/// | ||
/// ext-token = <the characters in token, followed by "*"> | ||
/// ``` | ||
/// | ||
/// # Example | ||
/// ``` | ||
/// use hyper::header::{Headers, ContentDisposition, DispositionType, DispositionParam, Charset}; | ||
/// | ||
/// let mut headers = Headers::new(); | ||
/// headers.set(ContentDisposition { | ||
/// disposition: DispositionType::Attachment, | ||
/// parameters: vec![DispositionParam::Filename( | ||
/// Charset::Iso_8859_1, // The character set for the bytes of the filename | ||
/// None, // The optional language tag (see `language-tag` crate) | ||
/// b"\xa9 Copyright 1989.txt".to_vec() // the actual bytes of the filename | ||
/// )] | ||
/// }); | ||
/// ``` | ||
#[derive(Clone, Debug, PartialEq)] | ||
pub struct ContentDisposition { | ||
/// The disposition | ||
pub disposition: DispositionType, | ||
/// Disposition parameters | ||
pub parameters: Vec<DispositionParam>, | ||
} | ||
|
||
impl Header for ContentDisposition { | ||
fn header_name() -> &'static str { | ||
"Content-Disposition" | ||
} | ||
|
||
fn parse_header(raw: &[Vec<u8>]) -> ::Result<ContentDisposition> { | ||
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::Header), | ||
}; | ||
|
||
let mut cd = ContentDisposition { | ||
disposition: if UniCase(&*disposition) == UniCase("inline") { | ||
DispositionType::Inline | ||
} else if UniCase(&*disposition) == UniCase("attachment") { | ||
DispositionType::Attachment | ||
} else { | ||
DispositionType::Ext(disposition.to_owned()) | ||
}, | ||
parameters: Vec::new(), | ||
}; | ||
|
||
for section in sections { | ||
let mut parts = section.splitn(2, '='); | ||
|
||
let key = if let Some(key) = parts.next() { | ||
key.trim() | ||
} else { | ||
return Err(::Error::Header); | ||
}; | ||
|
||
let val = if let Some(val) = parts.next() { | ||
val.trim() | ||
} else { | ||
return Err(::Error::Header); | ||
}; | ||
|
||
cd.parameters.push( | ||
if UniCase(&*key) == UniCase("filename") { | ||
DispositionParam::Filename( | ||
Charset::Ext("UTF-8".to_owned()), None, | ||
val.trim_matches('"').as_bytes().to_owned()) | ||
} else if UniCase(&*key) == UniCase("filename*") { | ||
let (charset, opt_language, value) = try!(parse_ext_value(val)); | ||
DispositionParam::Filename(charset, opt_language, value) | ||
} else { | ||
DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned()) | ||
} | ||
); | ||
} | ||
|
||
Ok(cd) | ||
}) | ||
} | ||
} | ||
|
||
impl HeaderFormat for ContentDisposition { | ||
#[inline] | ||
fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
fmt::Display::fmt(&self, f) | ||
} | ||
} | ||
|
||
impl fmt::Display for ContentDisposition { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
match self.disposition { | ||
DispositionType::Inline => try!(write!(f, "inline")), | ||
DispositionType::Attachment => try!(write!(f, "attachment")), | ||
DispositionType::Ext(ref s) => try!(write!(f, "{}", s)), | ||
} | ||
for param in self.parameters.iter() { | ||
match param { | ||
&DispositionParam::Filename(ref charset, ref opt_lang, ref bytes) => { | ||
let mut use_simple_format: bool = false; | ||
if opt_lang.is_none() { | ||
if let Charset::Ext(ref ext) = *charset { | ||
if UniCase(&**ext) == UniCase("utf-8") { | ||
use_simple_format = true; | ||
} | ||
} | ||
} | ||
if use_simple_format { | ||
try!(write!(f, "; filename=\"{}\"", | ||
match String::from_utf8(bytes.clone()) { | ||
Ok(s) => s, | ||
Err(_) => return Err(fmt::Error), | ||
})); | ||
} else { | ||
try!(write!(f, "; filename*={}'", charset)); | ||
if let Some(ref lang) = *opt_lang { | ||
try!(write!(f, "{}", lang)); | ||
}; | ||
try!(write!(f, "'")); | ||
try!(f.write_str( | ||
&*percent_encoding::percent_encode( | ||
bytes, percent_encoding::HTTP_VALUE_ENCODE_SET))) | ||
} | ||
}, | ||
&DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)), | ||
} | ||
} | ||
Ok(()) | ||
} | ||
} | ||
|
||
/// Parsing of `ext-value` | ||
/// https://tools.ietf.org/html/rfc5987#section-3.2 | ||
/// | ||
/// # ABNF | ||
/// ```plain | ||
/// ext-value = charset "'" [ language ] "'" value-chars | ||
/// ; like RFC 2231's <extended-initial-value> | ||
/// ; (see [RFC2231], Section 7) | ||
/// | ||
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset | ||
/// | ||
/// mime-charset = 1*mime-charsetc | ||
/// mime-charsetc = ALPHA / DIGIT | ||
/// / "!" / "#" / "$" / "%" / "&" | ||
/// / "+" / "-" / "^" / "_" / "`" | ||
/// / "{" / "}" / "~" | ||
/// ; as <mime-charset> in Section 2.3 of [RFC2978] | ||
/// ; except that the single quote is not included | ||
/// ; SHOULD be registered in the IANA charset registry | ||
/// | ||
/// language = <Language-Tag, defined in [RFC5646], Section 2.1> | ||
/// | ||
/// value-chars = *( pct-encoded / attr-char ) | ||
/// | ||
/// pct-encoded = "%" HEXDIG HEXDIG | ||
/// ; see [RFC3986], Section 2.1 | ||
/// | ||
/// attr-char = ALPHA / DIGIT | ||
/// / "!" / "#" / "$" / "&" / "+" / "-" / "." | ||
/// / "^" / "_" / "`" / "|" / "~" | ||
/// ; token except ( "*" / "'" / "%" ) | ||
/// ``` | ||
fn parse_ext_value(val: &str) -> ::Result<(Charset, Option<LanguageTag>, Vec<u8>)> { | ||
|
||
// Break into three pieces separated by the single-quote character | ||
let mut parts = val.splitn(3,'\''); | ||
|
||
// Interpret the first piece as a Charset | ||
let charset: Charset = match parts.next() { | ||
None => return Err(::Error::Header), | ||
Some(n) => try!(FromStr::from_str(n)), | ||
}; | ||
|
||
// Interpret the second piece as a language tag | ||
let lang: Option<LanguageTag> = match parts.next() { | ||
None => return Err(::Error::Header), | ||
Some("") => None, | ||
Some(s) => match s.parse() { | ||
Ok(lt) => Some(lt), | ||
Err(_) => return Err(::Error::Header), | ||
} | ||
}; | ||
|
||
// Interpret the third piece as a sequence of value characters | ||
let value: Vec<u8> = match parts.next() { | ||
None => return Err(::Error::Header), | ||
Some(v) => percent_encoding::percent_decode(v.as_bytes()), | ||
}; | ||
|
||
Ok( (charset, lang, value) ) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::{ContentDisposition,DispositionType,DispositionParam}; | ||
use ::header::Header; | ||
use ::header::shared::Charset; | ||
|
||
#[test] | ||
fn test_parse_header() { | ||
assert!(ContentDisposition::parse_header([b"".to_vec()].as_ref()).is_err()); | ||
|
||
let a = [b"form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".to_vec()]; | ||
let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||
let b = ContentDisposition { | ||
disposition: DispositionType::Ext("form-data".to_owned()), | ||
parameters: vec![ | ||
DispositionParam::Ext("dummy".to_owned(), "3".to_owned()), | ||
DispositionParam::Ext("name".to_owned(), "upload".to_owned()), | ||
DispositionParam::Filename( | ||
Charset::Ext("UTF-8".to_owned()), | ||
None, | ||
"sample.png".bytes().collect()) ] | ||
}; | ||
assert_eq!(a, b); | ||
|
||
let a = [b"attachment; filename=\"image.jpg\"".to_vec()]; | ||
let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||
let b = ContentDisposition { | ||
disposition: DispositionType::Attachment, | ||
parameters: vec![ | ||
DispositionParam::Filename( | ||
Charset::Ext("UTF-8".to_owned()), | ||
None, | ||
"image.jpg".bytes().collect()) ] | ||
}; | ||
assert_eq!(a, b); | ||
|
||
let a = [b"attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".to_vec()]; | ||
let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||
let b = ContentDisposition { | ||
disposition: DispositionType::Attachment, | ||
parameters: vec![ | ||
DispositionParam::Filename( | ||
Charset::Ext("UTF-8".to_owned()), | ||
None, | ||
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 a = [b"attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates".to_vec()]; | ||
let as_string = ::std::str::from_utf8(&(a[0])).unwrap(); | ||
let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||
let display_rendered = format!("{}",a); | ||
assert_eq!(as_string, display_rendered); | ||
|
||
let a = [b"attachment; filename*=UTF-8''black%20and%20white.csv".to_vec()]; | ||
let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||
let display_rendered = format!("{}",a); | ||
assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered); | ||
|
||
let a = [b"attachment; filename=colourful.csv".to_vec()]; | ||
let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||
let display_rendered = format!("{}",a); | ||
assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters