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*/