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; 12 13 use header::{Header, Raw, parsing}; 14 use header::parsing::{parse_extended_value, http_percent_encode}; 15 use header::shared::Charset; 16 17 /// The implied disposition of the content of the HTTP body. 18 #[derive(Clone, Debug, PartialEq)] 19 pub enum DispositionType { 20 /// Inline implies default processing 21 Inline, 22 /// Attachment implies that the recipient should prompt the user to save the response locally, 23 /// rather than process it normally (as per its media type). 24 Attachment, 25 /// Extension type. Should be handled by recipients the same way as Attachment 26 Ext(String) 27 } 28 29 /// A parameter to the disposition type. 30 #[derive(Clone, Debug, PartialEq)] 31 pub enum DispositionParam { 32 /// A Filename consisting of a Charset, an optional LanguageTag, and finally a sequence of 33 /// bytes representing the filename 34 Filename(Charset, Option<LanguageTag>, Vec<u8>), 35 /// Extension type consisting of token and value. Recipients should ignore unrecognized 36 /// parameters. 37 Ext(String, String) 38 } 39 40 /// A `Content-Disposition` header, (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266). 41 /// 42 /// The Content-Disposition response header field is used to convey 43 /// additional information about how to process the response payload, and 44 /// also can be used to attach additional metadata, such as the filename 45 /// to use when saving the response payload locally. 46 /// 47 /// # ABNF 48 49 /// ```text 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 /// ``` 72 /// use hyper::header::{Headers, ContentDisposition, DispositionType, DispositionParam, Charset}; 73 /// 74 /// let mut headers = Headers::new(); 75 /// headers.set(ContentDisposition { 76 /// disposition: DispositionType::Attachment, 77 /// parameters: vec![DispositionParam::Filename( 78 /// Charset::Iso_8859_1, // The character set for the bytes of the filename 79 /// None, // The optional language tag (see `language-tag` crate) 80 /// b"\xa9 Copyright 1989.txt".to_vec() // the actual bytes of the filename 81 /// )] 82 /// }); 83 /// ``` 84 #[derive(Clone, Debug, PartialEq)] 85 pub struct ContentDisposition { 86 /// The disposition 87 pub disposition: DispositionType, 88 /// Disposition parameters 89 pub parameters: Vec<DispositionParam>, 90 } 91 92 impl Header for ContentDisposition { 93 fn header_name() -> &'static str { 94 static NAME: &'static str = "Content-Disposition"; 95 NAME 96 } 97 98 fn parse_header(raw: &Raw) -> ::Result<ContentDisposition> { 99 parsing::from_one_raw_str(raw).and_then(|s: String| { 100 let mut sections = s.split(';'); 101 let disposition = match sections.next() { 102 Some(s) => s.trim(), 103 None => return Err(::Error::Header), 104 }; 105 106 let mut cd = ContentDisposition { 107 disposition: if unicase::eq_ascii(&*disposition, "inline") { 108 DispositionType::Inline 109 } else if unicase::eq_ascii(&*disposition, "attachment") { 110 DispositionType::Attachment 111 } else { 112 DispositionType::Ext(disposition.to_owned()) 113 }, 114 parameters: Vec::new(), 115 }; 116 117 for section in sections { 118 let mut parts = section.splitn(2, '='); 119 120 let key = if let Some(key) = parts.next() { 121 key.trim() 122 } else { 123 return Err(::Error::Header); 124 }; 125 126 let val = if let Some(val) = parts.next() { 127 val.trim() 128 } else { 129 return Err(::Error::Header); 130 }; 131 132 cd.parameters.push( 133 if unicase::eq_ascii(&*key, "filename") { 134 DispositionParam::Filename( 135 Charset::Ext("UTF-8".to_owned()), None, 136 val.trim_matches('"').as_bytes().to_owned()) 137 } else if unicase::eq_ascii(&*key, "filename*") { 138 let extended_value = try!(parse_extended_value(val)); 139 DispositionParam::Filename(extended_value.charset, extended_value.language_tag, extended_value.value) 140 } else { 141 DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned()) 142 } 143 ); 144 } 145 146 Ok(cd) 147 }) 148 } 149 150 #[inline] 151 fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result { 152 f.fmt_line(self) 153 } 154 } 155 156 impl fmt::Display for ContentDisposition { 157 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::eq_ascii(&**ext, "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!(http_percent_encode(f, bytes)) 187 } 188 }, 189 DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)), 190 } 191 } 192 Ok(()) 193 } 194 } 195 196 #[cfg(test)] 197 mod tests { 198 use super::{ContentDisposition,DispositionType,DispositionParam}; 199 use ::header::Header; 200 use ::header::shared::Charset; 201 202 #[test] 203 fn test_parse_header() { 204 assert!(ContentDisposition::parse_header(&"".into()).is_err()); 205 206 let a = "form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".into(); 207 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); 208 let b = ContentDisposition { 209 disposition: DispositionType::Ext("form-data".to_owned()), 210 parameters: vec![ 211 DispositionParam::Ext("dummy".to_owned(), "3".to_owned()), 212 DispositionParam::Ext("name".to_owned(), "upload".to_owned()), 213 DispositionParam::Filename( 214 Charset::Ext("UTF-8".to_owned()), 215 None, 216 "sample.png".bytes().collect()) ] 217 }; 218 assert_eq!(a, b); 219 220 let a = "attachment; filename=\"image.jpg\"".into(); 221 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); 222 let b = ContentDisposition { 223 disposition: DispositionType::Attachment, 224 parameters: vec![ 225 DispositionParam::Filename( 226 Charset::Ext("UTF-8".to_owned()), 227 None, 228 "image.jpg".bytes().collect()) ] 229 }; 230 assert_eq!(a, b); 231 232 let a = "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".into(); 233 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); 234 let b = ContentDisposition { 235 disposition: DispositionType::Attachment, 236 parameters: vec![ 237 DispositionParam::Filename( 238 Charset::Ext("UTF-8".to_owned()), 239 None, 240 vec![0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 241 0xe2, 0x82, 0xac, 0x20, b'r', b'a', b't', b'e', b's']) ] 242 }; 243 assert_eq!(a, b); 244 } 245 246 #[test] 247 fn test_display() { 248 let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates"; 249 let a = as_string.into(); 250 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); 251 let display_rendered = format!("{}",a); 252 assert_eq!(as_string, display_rendered); 253 254 let a = "attachment; filename*=UTF-8''black%20and%20white.csv".into(); 255 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); 256 let display_rendered = format!("{}",a); 257 assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered); 258 259 let a = "attachment; filename=colourful.csv".into(); 260 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); 261 let display_rendered = format!("{}",a); 262 assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered); 263 } 264 } 265