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