1 // # References
2 //
3 // "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt
4 // "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt
5 // "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc2388.txt
6 // Browser conformance tests at: http://greenbytes.de/tech/tc2231/
7 // IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
8 
9 use language_tags::LanguageTag;
10 use std::fmt;
11 use unicase::UniCase;
12 use url::percent_encoding;
13 
14 use header::{Header, HeaderFormat, parsing};
15 use header::parsing::{parse_extended_value, HTTP_VALUE};
16 use header::shared::Charset;
17 
18 /// The implied disposition of the content of the HTTP body
19 #[derive(Clone, Debug, PartialEq)]
20 pub enum DispositionType {
21     /// Inline implies default processing
22     Inline,
23     /// Attachment implies that the recipient should prompt the user to save the response locally,
24     /// rather than process it normally (as per its media type).
25     Attachment,
26     /// Extension type.  Should be handled by recipients the same way as Attachment
27     Ext(String)
28 }
29 
30 /// A parameter to the disposition type
31 #[derive(Clone, Debug, PartialEq)]
32 pub enum DispositionParam {
33     /// A Filename consisting of a Charset, an optional LanguageTag, and finally a sequence of
34     /// bytes representing the filename
35     Filename(Charset, Option<LanguageTag>, Vec<u8>),
36     /// Extension type consisting of token and value.  Recipients should ignore unrecognized
37     /// parameters.
38     Ext(String, String)
39 }
40 
41 /// A `Content-Disposition` header, (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266)
42 ///
43 /// The Content-Disposition response header field is used to convey
44 /// additional information about how to process the response payload, and
45 /// also can be used to attach additional metadata, such as the filename
46 /// to use when saving the response payload locally.
47 ///
48 /// # ABNF
49 /// ```plain
50 /// content-disposition = "Content-Disposition" ":"
51 ///                       disposition-type *( ";" disposition-parm )
52 ///
53 /// disposition-type    = "inline" | "attachment" | disp-ext-type
54 ///                       ; case-insensitive
55 ///
56 /// disp-ext-type       = token
57 ///
58 /// disposition-parm    = filename-parm | disp-ext-parm
59 ///
60 /// filename-parm       = "filename" "=" value
61 ///                     | "filename*" "=" ext-value
62 ///
63 /// disp-ext-parm       = token "=" value
64 ///                     | ext-token "=" ext-value
65 ///
66 /// ext-token           = <the characters in token, followed by "*">
67 /// ```
68 ///
69 /// # Example
70 /// ```
71 /// use hyper::header::{Headers, ContentDisposition, DispositionType, DispositionParam, Charset};
72 ///
73 /// let mut headers = Headers::new();
74 /// headers.set(ContentDisposition {
75 ///     disposition: DispositionType::Attachment,
76 ///     parameters: vec![DispositionParam::Filename(
77 ///       Charset::Iso_8859_1, // The character set for the bytes of the filename
78 ///       None, // The optional language tag (see `language-tag` crate)
79 ///       b"\xa9 Copyright 1989.txt".to_vec() // the actual bytes of the filename
80 ///     )]
81 /// });
82 /// ```
83 #[derive(Clone, Debug, PartialEq)]
84 pub struct ContentDisposition {
85     /// The disposition
86     pub disposition: DispositionType,
87     /// Disposition parameters
88     pub parameters: Vec<DispositionParam>,
89 }
90 
91 impl Header for ContentDisposition {
header_name() -> &'static str92     fn header_name() -> &'static str {
93         "Content-Disposition"
94     }
95 
parse_header(raw: &[Vec<u8>]) -> ::Result<ContentDisposition>96     fn parse_header(raw: &[Vec<u8>]) -> ::Result<ContentDisposition> {
97         parsing::from_one_raw_str(raw).and_then(|s: String| {
98             let mut sections = s.split(';');
99             let disposition = match sections.next() {
100                 Some(s) => s.trim(),
101                 None => return Err(::Error::Header),
102             };
103 
104             let mut cd = ContentDisposition {
105                 disposition: if UniCase(&*disposition) == UniCase("inline") {
106                     DispositionType::Inline
107                 } else if UniCase(&*disposition) == UniCase("attachment") {
108                     DispositionType::Attachment
109                 } else {
110                     DispositionType::Ext(disposition.to_owned())
111                 },
112                 parameters: Vec::new(),
113             };
114 
115             for section in sections {
116                 let mut parts = section.splitn(2, '=');
117 
118                 let key = if let Some(key) = parts.next() {
119                     key.trim()
120                 } else {
121                     return Err(::Error::Header);
122                 };
123 
124                 let val = if let Some(val) = parts.next() {
125                     val.trim()
126                 } else {
127                     return Err(::Error::Header);
128                 };
129 
130                 cd.parameters.push(
131                     if UniCase(&*key) == UniCase("filename") {
132                         DispositionParam::Filename(
133                             Charset::Ext("UTF-8".to_owned()), None,
134                             val.trim_matches('"').as_bytes().to_owned())
135                     } else if UniCase(&*key) == UniCase("filename*") {
136                         let extended_value = try!(parse_extended_value(val));
137                         DispositionParam::Filename(extended_value.charset, extended_value.language_tag, extended_value.value)
138                     } else {
139                         DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned())
140                     }
141                 );
142             }
143 
144             Ok(cd)
145         })
146     }
147 }
148 
149 impl HeaderFormat for ContentDisposition {
150     #[inline]
fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result151     fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
152         fmt::Display::fmt(&self, f)
153     }
154 }
155 
156 impl fmt::Display for ContentDisposition {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result157     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
158         match self.disposition {
159             DispositionType::Inline => try!(write!(f, "inline")),
160             DispositionType::Attachment => try!(write!(f, "attachment")),
161             DispositionType::Ext(ref s) => try!(write!(f, "{}", s)),
162         }
163         for param in &self.parameters {
164             match *param {
165                 DispositionParam::Filename(ref charset, ref opt_lang, ref bytes) => {
166                     let mut use_simple_format: bool = false;
167                     if opt_lang.is_none() {
168                         if let Charset::Ext(ref ext) = *charset {
169                             if UniCase(&**ext) == UniCase("utf-8") {
170                                 use_simple_format = true;
171                             }
172                         }
173                     }
174                     if use_simple_format {
175                         try!(write!(f, "; filename=\"{}\"",
176                                     match String::from_utf8(bytes.clone()) {
177                                         Ok(s) => s,
178                                         Err(_) => return Err(fmt::Error),
179                                     }));
180                     } else {
181                         try!(write!(f, "; filename*={}'", charset));
182                         if let Some(ref lang) = *opt_lang {
183                             try!(write!(f, "{}", lang));
184                         };
185                         try!(write!(f, "'"));
186                         try!(f.write_str(
187                             &percent_encoding::percent_encode(bytes, HTTP_VALUE).to_string()))
188                     }
189                 },
190                 DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)),
191             }
192         }
193         Ok(())
194     }
195 }
196 
197 #[cfg(test)]
198 mod tests {
199     use super::{ContentDisposition,DispositionType,DispositionParam};
200     use ::header::Header;
201     use ::header::shared::Charset;
202 
203     #[test]
test_parse_header()204     fn test_parse_header() {
205         assert!(ContentDisposition::parse_header([b"".to_vec()].as_ref()).is_err());
206 
207         let a = [b"form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".to_vec()];
208         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap();
209         let b = ContentDisposition {
210             disposition: DispositionType::Ext("form-data".to_owned()),
211             parameters: vec![
212                 DispositionParam::Ext("dummy".to_owned(), "3".to_owned()),
213                 DispositionParam::Ext("name".to_owned(), "upload".to_owned()),
214                 DispositionParam::Filename(
215                     Charset::Ext("UTF-8".to_owned()),
216                     None,
217                     "sample.png".bytes().collect()) ]
218         };
219         assert_eq!(a, b);
220 
221         let a = [b"attachment; filename=\"image.jpg\"".to_vec()];
222         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap();
223         let b = ContentDisposition {
224             disposition: DispositionType::Attachment,
225             parameters: vec![
226                 DispositionParam::Filename(
227                     Charset::Ext("UTF-8".to_owned()),
228                     None,
229                     "image.jpg".bytes().collect()) ]
230         };
231         assert_eq!(a, b);
232 
233         let a = [b"attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".to_vec()];
234         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap();
235         let b = ContentDisposition {
236             disposition: DispositionType::Attachment,
237             parameters: vec![
238                 DispositionParam::Filename(
239                     Charset::Ext("UTF-8".to_owned()),
240                     None,
241                     vec![0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20,
242                          0xe2, 0x82, 0xac, 0x20, b'r', b'a', b't', b'e', b's']) ]
243         };
244         assert_eq!(a, b);
245     }
246 
247     #[test]
test_display()248     fn test_display() {
249         let a = [b"attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates".to_vec()];
250         let as_string = ::std::str::from_utf8(&(a[0])).unwrap();
251         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap();
252         let display_rendered = format!("{}",a);
253         assert_eq!(as_string, display_rendered);
254 
255         let a = [b"attachment; filename*=UTF-8''black%20and%20white.csv".to_vec()];
256         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap();
257         let display_rendered = format!("{}",a);
258         assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered);
259 
260         let a = [b"attachment; filename=colourful.csv".to_vec()];
261         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap();
262         let display_rendered = format!("{}",a);
263         assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered);
264     }
265 }
266