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*/