clia_ntex_files/file_header/
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;
11
12use crate::standard_header;
13
14use super::error;
15use super::parsing::{self, http_percent_encode, parse_extended_value};
16use super::{Charset, Header, RawLike};
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
50/// ```text
51/// content-disposition = "Content-Disposition" ":"
52///                       disposition-type *( ";" disposition-parm )
53///
54/// disposition-type    = "inline" | "attachment" | disp-ext-type
55///                       ; case-insensitive
56///
57/// disp-ext-type       = token
58///
59/// disposition-parm    = filename-parm | disp-ext-parm
60///
61/// filename-parm       = "filename" "=" value
62///                     | "filename*" "=" ext-value
63///
64/// disp-ext-parm       = token "=" value
65///                     | ext-token "=" ext-value
66///
67/// ext-token           = <the characters in token, followed by "*">
68/// ```
69///
70#[derive(Clone, Debug, PartialEq)]
71pub struct ContentDisposition {
72    /// The disposition
73    pub disposition: DispositionType,
74    /// Disposition parameters
75    pub parameters: Vec<DispositionParam>,
76}
77
78impl Header for ContentDisposition {
79    fn header_name() -> &'static str {
80        static NAME: &str = "Content-Disposition";
81        NAME
82    }
83
84    fn parse_header<'a, T>(raw: &'a T) -> error::Result<ContentDisposition>
85    where
86        T: RawLike<'a>,
87    {
88        parsing::from_one_raw_str(raw).and_then(|s: String| {
89            let mut sections = s.split(';');
90            let disposition = match sections.next() {
91                Some(s) => s.trim(),
92                None => return Err(error::Error::Header),
93            };
94
95            let mut cd = ContentDisposition {
96                disposition: if unicase::eq_ascii(disposition, "inline") {
97                    DispositionType::Inline
98                } else if unicase::eq_ascii(disposition, "attachment") {
99                    DispositionType::Attachment
100                } else {
101                    DispositionType::Ext(disposition.to_owned())
102                },
103                parameters: Vec::new(),
104            };
105
106            for section in sections {
107                let mut parts = section.splitn(2, '=');
108
109                let key = if let Some(key) = parts.next() {
110                    key.trim()
111                } else {
112                    return Err(error::Error::Header);
113                };
114
115                let val = if let Some(val) = parts.next() {
116                    val.trim()
117                } else {
118                    return Err(error::Error::Header);
119                };
120
121                cd.parameters.push(if unicase::eq_ascii(key, "filename") {
122                    DispositionParam::Filename(
123                        Charset::Ext("UTF-8".to_owned()),
124                        None,
125                        val.trim_matches('"').as_bytes().to_owned(),
126                    )
127                } else if unicase::eq_ascii(key, "filename*") {
128                    let extended_value = parse_extended_value(val)?;
129                    DispositionParam::Filename(
130                        extended_value.charset,
131                        extended_value.language_tag,
132                        extended_value.value,
133                    )
134                } else {
135                    DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned())
136                });
137            }
138
139            Ok(cd)
140        })
141    }
142
143    #[inline]
144    fn fmt_header(&self, f: &mut super::Formatter) -> fmt::Result {
145        f.fmt_line(self)
146    }
147}
148
149impl fmt::Display for ContentDisposition {
150    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151        match self.disposition {
152            DispositionType::Inline => write!(f, "inline")?,
153            DispositionType::Attachment => write!(f, "attachment")?,
154            DispositionType::Ext(ref s) => write!(f, "{}", s)?,
155        }
156        for param in &self.parameters {
157            match *param {
158                DispositionParam::Filename(ref charset, ref opt_lang, ref bytes) => {
159                    let mut use_simple_format: bool = false;
160                    if opt_lang.is_none() {
161                        if let Charset::Ext(ref ext) = *charset {
162                            if unicase::eq_ascii(&**ext, "utf-8") {
163                                use_simple_format = true;
164                            }
165                        }
166                    }
167                    if use_simple_format {
168                        write!(
169                            f,
170                            "; filename=\"{}\"",
171                            match String::from_utf8(bytes.clone()) {
172                                Ok(s) => s,
173                                Err(_) => return Err(fmt::Error),
174                            }
175                        )?;
176                    } else {
177                        write!(f, "; filename*={}'", charset)?;
178                        if let Some(ref lang) = *opt_lang {
179                            write!(f, "{}", lang)?;
180                        };
181                        write!(f, "'")?;
182                        http_percent_encode(f, bytes)?;
183                    }
184                }
185                DispositionParam::Ext(ref k, ref v) => write!(f, "; {}=\"{}\"", k, v)?,
186            }
187        }
188        Ok(())
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::{Charset, ContentDisposition, DispositionParam, DispositionType, Header};
195    use crate::file_header::Raw;
196
197    #[test]
198    fn test_parse_header() {
199        let a: Raw = "".into();
200        assert!(ContentDisposition::parse_header(&a).is_err());
201
202        let a: Raw = "form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".into();
203        let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
204        let b = ContentDisposition {
205            disposition: DispositionType::Ext("form-data".to_owned()),
206            parameters: vec![
207                DispositionParam::Ext("dummy".to_owned(), "3".to_owned()),
208                DispositionParam::Ext("name".to_owned(), "upload".to_owned()),
209                DispositionParam::Filename(
210                    Charset::Ext("UTF-8".to_owned()),
211                    None,
212                    "sample.png".bytes().collect(),
213                ),
214            ],
215        };
216        assert_eq!(a, b);
217
218        let a: Raw = "attachment; filename=\"image.jpg\"".into();
219        let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
220        let b = ContentDisposition {
221            disposition: DispositionType::Attachment,
222            parameters: vec![DispositionParam::Filename(
223                Charset::Ext("UTF-8".to_owned()),
224                None,
225                "image.jpg".bytes().collect(),
226            )],
227        };
228        assert_eq!(a, b);
229
230        let a: Raw = "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".into();
231        let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
232        let b = ContentDisposition {
233            disposition: DispositionType::Attachment,
234            parameters: vec![DispositionParam::Filename(
235                Charset::Ext("UTF-8".to_owned()),
236                None,
237                vec![
238                    0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r',
239                    b'a', b't', b'e', b's',
240                ],
241            )],
242        };
243        assert_eq!(a, b);
244    }
245
246    #[test]
247    fn test_display() {
248        let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
249        let a: Raw = as_string.into();
250        let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
251        let display_rendered = format!("{}", a);
252        assert_eq!(as_string, display_rendered);
253
254        let a: Raw = "attachment; filename*=UTF-8''black%20and%20white.csv".into();
255        let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
256        let display_rendered = format!("{}", a);
257        assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered);
258
259        let a: Raw = "attachment; filename=colourful.csv".into();
260        let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
261        let display_rendered = format!("{}", a);
262        assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered);
263    }
264}
265
266standard_header!(ContentDisposition, CONTENT_DISPOSITION);