diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs new file mode 100644 index 0000000000..cefb62757b --- /dev/null +++ b/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, Vec), + /// 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 = +/// ``` +/// +/// # 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, +} + +impl Header for ContentDisposition { + fn header_name() -> &'static str { + "Content-Disposition" + } + + fn parse_header(raw: &[Vec]) -> ::Result { + 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 +/// ; (see [RFC2231], Section 7) +/// +/// charset = "UTF-8" / "ISO-8859-1" / mime-charset +/// +/// mime-charset = 1*mime-charsetc +/// mime-charsetc = ALPHA / DIGIT +/// / "!" / "#" / "$" / "%" / "&" +/// / "+" / "-" / "^" / "_" / "`" +/// / "{" / "}" / "~" +/// ; as in Section 2.3 of [RFC2978] +/// ; except that the single quote is not included +/// ; SHOULD be registered in the IANA charset registry +/// +/// language = +/// +/// 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, Vec)> { + + // 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 = 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 = 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); + } +} diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs index 5a57f71a40..4fa9c6af7d 100644 --- a/src/header/common/mod.rs +++ b/src/header/common/mod.rs @@ -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; @@ -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;