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