headers_ext/common/content_disposition.rs
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/// A `Content-Disposition` header, (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266).
10///
11/// The Content-Disposition response header field is used to convey
12/// additional information about how to process the response payload, and
13/// also can be used to attach additional metadata, such as the filename
14/// to use when saving the response payload locally.
15///
16/// # ABNF
17
18/// ```text
19/// content-disposition = "Content-Disposition" ":"
20/// disposition-type *( ";" disposition-parm )
21///
22/// disposition-type = "inline" | "attachment" | disp-ext-type
23/// ; case-insensitive
24///
25/// disp-ext-type = token
26///
27/// disposition-parm = filename-parm | disp-ext-parm
28///
29/// filename-parm = "filename" "=" value
30/// | "filename*" "=" ext-value
31///
32/// disp-ext-parm = token "=" value
33/// | ext-token "=" ext-value
34///
35/// ext-token = <the characters in token, followed by "*">
36/// ```
37///
38/// # Example
39///
40/// ```
41/// # extern crate headers_ext as headers;
42/// use headers::ContentDisposition;
43///
44/// let cd = ContentDisposition::inline();
45/// ```
46#[derive(Clone, Debug)]
47pub struct ContentDisposition(::HeaderValue);
48
49impl ContentDisposition {
50 /// Construct a `Content-Disposition: inline` header.
51 pub fn inline() -> ContentDisposition {
52 ContentDisposition(::HeaderValue::from_static("inline"))
53 }
54
55 /*
56 pub fn attachment(filename: &str) -> ContentDisposition {
57 let full = Bytes::from(format!("attachment; filename={}", filename));
58 match ::HeaderValue::from_shared(full) {
59 Ok(val) => ContentDisposition(val),
60 Err(_) => {
61 unimplemented!("filename that isn't ASCII");
62 }
63 }
64 }
65 */
66
67 /// Check if the disposition-type is `inline`.
68 pub fn is_inline(&self) -> bool {
69 self.get_type() == "inline"
70 }
71
72 /// Check if the disposition-type is `attachment`.
73 pub fn is_attachment(&self) -> bool {
74 self.get_type() == "attachment"
75 }
76
77 /// Check if the disposition-type is `form-data`.
78 pub fn is_form_data(&self) -> bool {
79 self.get_type() == "form-data"
80 }
81
82 fn get_type(&self) -> &str {
83 self
84 .0
85 .to_str()
86 .unwrap_or("")
87 .split(';')
88 .next()
89 .expect("split always has at least 1 item")
90 }
91}
92
93impl ::Header for ContentDisposition {
94 const NAME: &'static ::HeaderName = &::http::header::CONTENT_DISPOSITION;
95
96 fn decode<'i, I: Iterator<Item = &'i ::HeaderValue>>(values: &mut I) -> Result<Self, ::Error> {
97 //TODO: parse harder
98 values
99 .next()
100 .cloned()
101 .map(ContentDisposition)
102 .ok_or_else(::Error::invalid)
103 }
104
105 fn encode<E: Extend<::HeaderValue>>(&self, values: &mut E) {
106 values.extend(::std::iter::once(self.0.clone()));
107 }
108}
109/*
110use language_tags::LanguageTag;
111use std::fmt;
112use unicase;
113
114use {Header, Raw, parsing};
115use parsing::{parse_extended_value, http_percent_encode};
116use shared::Charset;
117
118/// The implied disposition of the content of the HTTP body.
119#[derive(Clone, Debug, PartialEq)]
120pub enum DispositionType {
121 /// Inline implies default processing
122 Inline,
123 /// Attachment implies that the recipient should prompt the user to save the response locally,
124 /// rather than process it normally (as per its media type).
125 Attachment,
126 /// Extension type. Should be handled by recipients the same way as Attachment
127 Ext(String)
128}
129
130/// A parameter to the disposition type.
131#[derive(Clone, Debug, PartialEq)]
132pub enum DispositionParam {
133 /// A Filename consisting of a Charset, an optional LanguageTag, and finally a sequence of
134 /// bytes representing the filename
135 Filename(Charset, Option<LanguageTag>, Vec<u8>),
136 /// Extension type consisting of token and value. Recipients should ignore unrecognized
137 /// parameters.
138 Ext(String, String)
139}
140
141#[derive(Clone, Debug, PartialEq)]
142pub struct ContentDisposition {
143 /// The disposition
144 pub disposition: DispositionType,
145 /// Disposition parameters
146 pub parameters: Vec<DispositionParam>,
147}
148
149impl Header for ContentDisposition {
150 fn header_name() -> &'static str {
151 static NAME: &'static str = "Content-Disposition";
152 NAME
153 }
154
155 fn parse_header(raw: &Raw) -> ::Result<ContentDisposition> {
156 parsing::from_one_raw_str(raw).and_then(|s: String| {
157 let mut sections = s.split(';');
158 let disposition = match sections.next() {
159 Some(s) => s.trim(),
160 None => return Err(::Error::Header),
161 };
162
163 let mut cd = ContentDisposition {
164 disposition: if unicase::eq_ascii(&*disposition, "inline") {
165 DispositionType::Inline
166 } else if unicase::eq_ascii(&*disposition, "attachment") {
167 DispositionType::Attachment
168 } else {
169 DispositionType::Ext(disposition.to_owned())
170 },
171 parameters: Vec::new(),
172 };
173
174 for section in sections {
175 let mut parts = section.splitn(2, '=');
176
177 let key = if let Some(key) = parts.next() {
178 key.trim()
179 } else {
180 return Err(::Error::Header);
181 };
182
183 let val = if let Some(val) = parts.next() {
184 val.trim()
185 } else {
186 return Err(::Error::Header);
187 };
188
189 cd.parameters.push(
190 if unicase::eq_ascii(&*key, "filename") {
191 DispositionParam::Filename(
192 Charset::Ext("UTF-8".to_owned()), None,
193 val.trim_matches('"').as_bytes().to_owned())
194 } else if unicase::eq_ascii(&*key, "filename*") {
195 let extended_value = try!(parse_extended_value(val));
196 DispositionParam::Filename(extended_value.charset, extended_value.language_tag, extended_value.value)
197 } else {
198 DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned())
199 }
200 );
201 }
202
203 Ok(cd)
204 })
205 }
206
207 #[inline]
208 fn fmt_header(&self, f: &mut ::Formatter) -> fmt::Result {
209 f.fmt_line(self)
210 }
211}
212
213impl fmt::Display for ContentDisposition {
214 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
215 match self.disposition {
216 DispositionType::Inline => try!(write!(f, "inline")),
217 DispositionType::Attachment => try!(write!(f, "attachment")),
218 DispositionType::Ext(ref s) => try!(write!(f, "{}", s)),
219 }
220 for param in &self.parameters {
221 match *param {
222 DispositionParam::Filename(ref charset, ref opt_lang, ref bytes) => {
223 let mut use_simple_format: bool = false;
224 if opt_lang.is_none() {
225 if let Charset::Ext(ref ext) = *charset {
226 if unicase::eq_ascii(&**ext, "utf-8") {
227 use_simple_format = true;
228 }
229 }
230 }
231 if use_simple_format {
232 try!(write!(f, "; filename=\"{}\"",
233 match String::from_utf8(bytes.clone()) {
234 Ok(s) => s,
235 Err(_) => return Err(fmt::Error),
236 }));
237 } else {
238 try!(write!(f, "; filename*={}'", charset));
239 if let Some(ref lang) = *opt_lang {
240 try!(write!(f, "{}", lang));
241 };
242 try!(write!(f, "'"));
243 try!(http_percent_encode(f, bytes))
244 }
245 },
246 DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)),
247 }
248 }
249 Ok(())
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::{ContentDisposition,DispositionType,DispositionParam};
256 use ::Header;
257 use ::shared::Charset;
258
259 #[test]
260 fn test_parse_header() {
261 assert!(ContentDisposition::parse_header(&"".into()).is_err());
262
263 let a = "form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".into();
264 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
265 let b = ContentDisposition {
266 disposition: DispositionType::Ext("form-data".to_owned()),
267 parameters: vec![
268 DispositionParam::Ext("dummy".to_owned(), "3".to_owned()),
269 DispositionParam::Ext("name".to_owned(), "upload".to_owned()),
270 DispositionParam::Filename(
271 Charset::Ext("UTF-8".to_owned()),
272 None,
273 "sample.png".bytes().collect()) ]
274 };
275 assert_eq!(a, b);
276
277 let a = "attachment; filename=\"image.jpg\"".into();
278 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
279 let b = ContentDisposition {
280 disposition: DispositionType::Attachment,
281 parameters: vec![
282 DispositionParam::Filename(
283 Charset::Ext("UTF-8".to_owned()),
284 None,
285 "image.jpg".bytes().collect()) ]
286 };
287 assert_eq!(a, b);
288
289 let a = "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".into();
290 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
291 let b = ContentDisposition {
292 disposition: DispositionType::Attachment,
293 parameters: vec![
294 DispositionParam::Filename(
295 Charset::Ext("UTF-8".to_owned()),
296 None,
297 vec![0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20,
298 0xe2, 0x82, 0xac, 0x20, b'r', b'a', b't', b'e', b's']) ]
299 };
300 assert_eq!(a, b);
301 }
302
303 #[test]
304 fn test_display() {
305 let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
306 let a = as_string.into();
307 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
308 let display_rendered = format!("{}",a);
309 assert_eq!(as_string, display_rendered);
310
311 let a = "attachment; filename*=UTF-8''black%20and%20white.csv".into();
312 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
313 let display_rendered = format!("{}",a);
314 assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered);
315
316 let a = "attachment; filename=colourful.csv".into();
317 let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
318 let display_rendered = format!("{}",a);
319 assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered);
320 }
321}
322*/