Skip to main content

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