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