-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(headers): Add Content-Disposition header #680
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A doc test example here would 1) help people understand how to construct or use this header, and 2) properly test the header is usable from outside the crate.