1use language_tags::LanguageTag;
10use std::fmt;
11use unicase::UniCase;
12use url::percent_encoding;
13
14use crate::header::{Header, HeaderFormat, parsing};
15use crate::header::parsing::{parse_extended_value, HTTP_VALUE};
16use crate::header::shared::Charset;
17
18#[derive(Clone, Debug, PartialEq)]
20pub enum DispositionType {
21 Inline,
23 Attachment,
26 Ext(String)
28}
29
30#[derive(Clone, Debug, PartialEq)]
32pub enum DispositionParam {
33 Filename(Charset, Option<LanguageTag>, Vec<u8>),
36 Ext(String, String)
39}
40
41#[derive(Clone, Debug, PartialEq)]
84pub struct ContentDisposition {
85 pub disposition: DispositionType,
87 pub parameters: Vec<DispositionParam>,
89}
90
91impl Header for ContentDisposition {
92 fn header_name() -> &'static str {
93 "Content-Disposition"
94 }
95
96 fn parse_header(raw: &[Vec<u8>]) -> crate::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(crate::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(crate::Error::Header);
122 };
123
124 let val = if let Some(val) = parts.next() {
125 val.trim()
126 } else {
127 return Err(crate::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 = r#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
149impl HeaderFormat for ContentDisposition {
150 #[inline]
151 fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
152 fmt::Display::fmt(&self, f)
153 }
154}
155
156impl fmt::Display for ContentDisposition {
157 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
158 match self.disposition {
159 DispositionType::Inline => r#try!(write!(f, "inline")),
160 DispositionType::Attachment => r#try!(write!(f, "attachment")),
161 DispositionType::Ext(ref s) => r#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 r#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 r#try!(write!(f, "; filename*={}'", charset));
182 if let Some(ref lang) = *opt_lang {
183 r#try!(write!(f, "{}", lang));
184 };
185 r#try!(write!(f, "'"));
186 r#try!(f.write_str(
187 &percent_encoding::percent_encode(bytes, HTTP_VALUE).to_string()))
188 }
189 },
190 DispositionParam::Ext(ref k, ref v) => r#try!(write!(f, "; {}=\"{}\"", k, v)),
191 }
192 }
193 Ok(())
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::{ContentDisposition,DispositionType,DispositionParam};
200 use crate::header::Header;
201 use crate::header::shared::Charset;
202
203 #[test]
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]
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}