Skip to content
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 1 commit into from Nov 23, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
328 changes: 328 additions & 0 deletions src/header/common/content_disposition.rs
@@ -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 {
Copy link
Member

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.

/// 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);
}
}
2 changes: 2 additions & 0 deletions src/header/common/mod.rs
Expand Up @@ -23,6 +23,7 @@ pub use self::allow::Allow;
pub use self::authorization::{Authorization, Scheme, Basic, Bearer};
pub use self::cache_control::{CacheControl, CacheDirective};
pub use self::connection::{Connection, ConnectionOption};
pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam};
pub use self::content_length::ContentLength;
pub use self::content_encoding::ContentEncoding;
pub use self::content_language::ContentLanguage;
Expand Down Expand Up @@ -371,6 +372,7 @@ mod authorization;
mod cache_control;
mod cookie;
mod connection;
mod content_disposition;
mod content_encoding;
mod content_language;
mod content_length;
Expand Down