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