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