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