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/rfc7578.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 
9 use lazy_static::lazy_static;
10 use regex::Regex;
11 use std::fmt::{self, Write};
12 
13 use crate::header::{self, ExtendedValue, Header, IntoHeaderValue, Writer};
14 
15 /// Split at the index of the first `needle` if it exists or at the end.
split_once(haystack: &str, needle: char) -> (&str, &str)16 fn split_once(haystack: &str, needle: char) -> (&str, &str) {
17     haystack.find(needle).map_or_else(
18         || (haystack, ""),
19         |sc| {
20             let (first, last) = haystack.split_at(sc);
21             (first, last.split_at(1).1)
22         },
23     )
24 }
25 
26 /// Split at the index of the first `needle` if it exists or at the end, trim the right of the
27 /// first part and the left of the last part.
split_once_and_trim(haystack: &str, needle: char) -> (&str, &str)28 fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) {
29     let (first, last) = split_once(haystack, needle);
30     (first.trim_end(), last.trim_start())
31 }
32 
33 /// The implied disposition of the content of the HTTP body.
34 #[derive(Clone, Debug, PartialEq)]
35 pub enum DispositionType {
36     /// Inline implies default processing
37     Inline,
38     /// Attachment implies that the recipient should prompt the user to save the response locally,
39     /// rather than process it normally (as per its media type).
40     Attachment,
41     /// Used in *multipart/form-data* as defined in
42     /// [RFC7578](https://tools.ietf.org/html/rfc7578) to carry the field name and the file name.
43     FormData,
44     /// Extension type. Should be handled by recipients the same way as Attachment
45     Ext(String),
46 }
47 
48 impl<'a> From<&'a str> for DispositionType {
from(origin: &'a str) -> DispositionType49     fn from(origin: &'a str) -> DispositionType {
50         if origin.eq_ignore_ascii_case("inline") {
51             DispositionType::Inline
52         } else if origin.eq_ignore_ascii_case("attachment") {
53             DispositionType::Attachment
54         } else if origin.eq_ignore_ascii_case("form-data") {
55             DispositionType::FormData
56         } else {
57             DispositionType::Ext(origin.to_owned())
58         }
59     }
60 }
61 
62 /// Parameter in [`ContentDisposition`].
63 ///
64 /// # Examples
65 /// ```
66 /// use actix_http::http::header::DispositionParam;
67 ///
68 /// let param = DispositionParam::Filename(String::from("sample.txt"));
69 /// assert!(param.is_filename());
70 /// assert_eq!(param.as_filename().unwrap(), "sample.txt");
71 /// ```
72 #[derive(Clone, Debug, PartialEq)]
73 #[allow(clippy::large_enum_variant)]
74 pub enum DispositionParam {
75     /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from
76     /// the form.
77     Name(String),
78     /// A plain file name.
79     ///
80     /// It is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any
81     /// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where
82     /// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead
83     /// in case there are Unicode characters in file names.
84     Filename(String),
85     /// An extended file name. It must not exist for `ContentType::Formdata` according to
86     /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2).
87     FilenameExt(ExtendedValue),
88     /// An unrecognized regular parameter as defined in
89     /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *reg-parameter*, in
90     /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should
91     /// ignore unrecognizable parameters.
92     Unknown(String, String),
93     /// An unrecognized extended paramater as defined in
94     /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in
95     /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single
96     /// trailling asterisk is not included. Recipients should ignore unrecognizable parameters.
97     UnknownExt(String, ExtendedValue),
98 }
99 
100 impl DispositionParam {
101     /// Returns `true` if the paramater is [`Name`](DispositionParam::Name).
102     #[inline]
is_name(&self) -> bool103     pub fn is_name(&self) -> bool {
104         self.as_name().is_some()
105     }
106 
107     /// Returns `true` if the paramater is [`Filename`](DispositionParam::Filename).
108     #[inline]
is_filename(&self) -> bool109     pub fn is_filename(&self) -> bool {
110         self.as_filename().is_some()
111     }
112 
113     /// Returns `true` if the paramater is [`FilenameExt`](DispositionParam::FilenameExt).
114     #[inline]
is_filename_ext(&self) -> bool115     pub fn is_filename_ext(&self) -> bool {
116         self.as_filename_ext().is_some()
117     }
118 
119     /// Returns `true` if the paramater is [`Unknown`](DispositionParam::Unknown) and the `name`
120     #[inline]
121     /// matches.
is_unknown<T: AsRef<str>>(&self, name: T) -> bool122     pub fn is_unknown<T: AsRef<str>>(&self, name: T) -> bool {
123         self.as_unknown(name).is_some()
124     }
125 
126     /// Returns `true` if the paramater is [`UnknownExt`](DispositionParam::UnknownExt) and the
127     /// `name` matches.
128     #[inline]
is_unknown_ext<T: AsRef<str>>(&self, name: T) -> bool129     pub fn is_unknown_ext<T: AsRef<str>>(&self, name: T) -> bool {
130         self.as_unknown_ext(name).is_some()
131     }
132 
133     /// Returns the name if applicable.
134     #[inline]
as_name(&self) -> Option<&str>135     pub fn as_name(&self) -> Option<&str> {
136         match self {
137             DispositionParam::Name(ref name) => Some(name.as_str()),
138             _ => None,
139         }
140     }
141 
142     /// Returns the filename if applicable.
143     #[inline]
as_filename(&self) -> Option<&str>144     pub fn as_filename(&self) -> Option<&str> {
145         match self {
146             DispositionParam::Filename(ref filename) => Some(filename.as_str()),
147             _ => None,
148         }
149     }
150 
151     /// Returns the filename* if applicable.
152     #[inline]
as_filename_ext(&self) -> Option<&ExtendedValue>153     pub fn as_filename_ext(&self) -> Option<&ExtendedValue> {
154         match self {
155             DispositionParam::FilenameExt(ref value) => Some(value),
156             _ => None,
157         }
158     }
159 
160     /// Returns the value of the unrecognized regular parameter if it is
161     /// [`Unknown`](DispositionParam::Unknown) and the `name` matches.
162     #[inline]
as_unknown<T: AsRef<str>>(&self, name: T) -> Option<&str>163     pub fn as_unknown<T: AsRef<str>>(&self, name: T) -> Option<&str> {
164         match self {
165             DispositionParam::Unknown(ref ext_name, ref value)
166                 if ext_name.eq_ignore_ascii_case(name.as_ref()) =>
167             {
168                 Some(value.as_str())
169             }
170             _ => None,
171         }
172     }
173 
174     /// Returns the value of the unrecognized extended parameter if it is
175     /// [`Unknown`](DispositionParam::Unknown) and the `name` matches.
176     #[inline]
as_unknown_ext<T: AsRef<str>>(&self, name: T) -> Option<&ExtendedValue>177     pub fn as_unknown_ext<T: AsRef<str>>(&self, name: T) -> Option<&ExtendedValue> {
178         match self {
179             DispositionParam::UnknownExt(ref ext_name, ref value)
180                 if ext_name.eq_ignore_ascii_case(name.as_ref()) =>
181             {
182                 Some(value)
183             }
184             _ => None,
185         }
186     }
187 }
188 
189 /// A *Content-Disposition* header. It is compatible to be used either as
190 /// [a response header for the main body](https://mdn.io/Content-Disposition#As_a_response_header_for_the_main_body)
191 /// as (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266), or as
192 /// [a header for a multipart body](https://mdn.io/Content-Disposition#As_a_header_for_a_multipart_body)
193 /// as (re)defined in [RFC7587](https://tools.ietf.org/html/rfc7578).
194 ///
195 /// In a regular HTTP response, the *Content-Disposition* response header is a header indicating if
196 /// the content is expected to be displayed *inline* in the browser, that is, as a Web page or as
197 /// part of a Web page, or as an attachment, that is downloaded and saved locally, and also can be
198 /// used to attach additional metadata, such as the filename to use when saving the response payload
199 /// locally.
200 ///
201 /// In a *multipart/form-data* body, the HTTP *Content-Disposition* general header is a header that
202 /// can be used on the subpart of a multipart body to give information about the field it applies to.
203 /// The subpart is delimited by the boundary defined in the *Content-Type* header. Used on the body
204 /// itself, *Content-Disposition* has no effect.
205 ///
206 /// # ABNF
207 
208 /// ```text
209 /// content-disposition = "Content-Disposition" ":"
210 ///                       disposition-type *( ";" disposition-parm )
211 ///
212 /// disposition-type    = "inline" | "attachment" | disp-ext-type
213 ///                       ; case-insensitive
214 ///
215 /// disp-ext-type       = token
216 ///
217 /// disposition-parm    = filename-parm | disp-ext-parm
218 ///
219 /// filename-parm       = "filename" "=" value
220 ///                     | "filename*" "=" ext-value
221 ///
222 /// disp-ext-parm       = token "=" value
223 ///                     | ext-token "=" ext-value
224 ///
225 /// ext-token           = <the characters in token, followed by "*">
226 /// ```
227 ///
228 /// # Note
229 ///
230 /// filename is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any
231 /// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where
232 /// filename* with charset UTF-8 may be used instead in case there are Unicode characters in file
233 /// names.
234 /// filename is [acceptable](https://tools.ietf.org/html/rfc7578#section-4.2) to be UTF-8 encoded
235 /// directly in a *Content-Disposition* header for *multipart/form-data*, though.
236 ///
237 /// filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within
238 /// *multipart/form-data*.
239 ///
240 /// # Example
241 ///
242 /// ```
243 /// use actix_http::http::header::{
244 ///     Charset, ContentDisposition, DispositionParam, DispositionType,
245 ///     ExtendedValue,
246 /// };
247 ///
248 /// let cd1 = ContentDisposition {
249 ///     disposition: DispositionType::Attachment,
250 ///     parameters: vec![DispositionParam::FilenameExt(ExtendedValue {
251 ///         charset: Charset::Iso_8859_1, // The character set for the bytes of the filename
252 ///         language_tag: None, // The optional language tag (see `language-tag` crate)
253 ///         value: b"\xa9 Copyright 1989.txt".to_vec(), // the actual bytes of the filename
254 ///     })],
255 /// };
256 /// assert!(cd1.is_attachment());
257 /// assert!(cd1.get_filename_ext().is_some());
258 ///
259 /// let cd2 = ContentDisposition {
260 ///     disposition: DispositionType::FormData,
261 ///     parameters: vec![
262 ///         DispositionParam::Name(String::from("file")),
263 ///         DispositionParam::Filename(String::from("bill.odt")),
264 ///     ],
265 /// };
266 /// assert_eq!(cd2.get_name(), Some("file")); // field name
267 /// assert_eq!(cd2.get_filename(), Some("bill.odt"));
268 ///
269 /// // HTTP response header with Unicode characters in file names
270 /// let cd3 = ContentDisposition {
271 ///     disposition: DispositionType::Attachment,
272 ///     parameters: vec![
273 ///         DispositionParam::FilenameExt(ExtendedValue {
274 ///             charset: Charset::Ext(String::from("UTF-8")),
275 ///             language_tag: None,
276 ///             value: String::from("\u{1f600}.svg").into_bytes(),
277 ///         }),
278 ///         // fallback for better compatibility
279 ///         DispositionParam::Filename(String::from("Grinning-Face-Emoji.svg"))
280 ///     ],
281 /// };
282 /// assert_eq!(cd3.get_filename_ext().map(|ev| ev.value.as_ref()),
283 ///            Some("\u{1f600}.svg".as_bytes()));
284 /// ```
285 ///
286 /// # WARN
287 /// If "filename" parameter is supplied, do not use the file name blindly, check and possibly
288 /// change to match local file system conventions if applicable, and do not use directory path
289 /// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3)
290 /// .
291 #[derive(Clone, Debug, PartialEq)]
292 pub struct ContentDisposition {
293     /// The disposition type
294     pub disposition: DispositionType,
295     /// Disposition parameters
296     pub parameters: Vec<DispositionParam>,
297 }
298 
299 impl ContentDisposition {
300     /// Parse a raw Content-Disposition header value.
from_raw(hv: &header::HeaderValue) -> Result<Self, crate::error::ParseError>301     pub fn from_raw(hv: &header::HeaderValue) -> Result<Self, crate::error::ParseError> {
302         // `header::from_one_raw_str` invokes `hv.to_str` which assumes `hv` contains only visible
303         //  ASCII characters. So `hv.as_bytes` is necessary here.
304         let hv = String::from_utf8(hv.as_bytes().to_vec())
305             .map_err(|_| crate::error::ParseError::Header)?;
306         let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';');
307         if disp_type.is_empty() {
308             return Err(crate::error::ParseError::Header);
309         }
310         let mut cd = ContentDisposition {
311             disposition: disp_type.into(),
312             parameters: Vec::new(),
313         };
314 
315         while !left.is_empty() {
316             let (param_name, new_left) = split_once_and_trim(left, '=');
317             if param_name.is_empty() || param_name == "*" || new_left.is_empty() {
318                 return Err(crate::error::ParseError::Header);
319             }
320             left = new_left;
321             if param_name.ends_with('*') {
322                 // extended parameters
323                 let param_name = &param_name[..param_name.len() - 1]; // trim asterisk
324                 let (ext_value, new_left) = split_once_and_trim(left, ';');
325                 left = new_left;
326                 let ext_value = header::parse_extended_value(ext_value)?;
327 
328                 let param = if param_name.eq_ignore_ascii_case("filename") {
329                     DispositionParam::FilenameExt(ext_value)
330                 } else {
331                     DispositionParam::UnknownExt(param_name.to_owned(), ext_value)
332                 };
333                 cd.parameters.push(param);
334             } else {
335                 // regular parameters
336                 let value = if left.starts_with('\"') {
337                     // quoted-string: defined in RFC6266 -> RFC2616 Section 3.6
338                     let mut escaping = false;
339                     let mut quoted_string = vec![];
340                     let mut end = None;
341                     // search for closing quote
342                     for (i, &c) in left.as_bytes().iter().skip(1).enumerate() {
343                         if escaping {
344                             escaping = false;
345                             quoted_string.push(c);
346                         } else if c == 0x5c {
347                             // backslash
348                             escaping = true;
349                         } else if c == 0x22 {
350                             // double quote
351                             end = Some(i + 1); // cuz skipped 1 for the leading quote
352                             break;
353                         } else {
354                             quoted_string.push(c);
355                         }
356                     }
357                     left = &left[end.ok_or(crate::error::ParseError::Header)? + 1..];
358                     left = split_once(left, ';').1.trim_start();
359                     // In fact, it should not be Err if the above code is correct.
360                     String::from_utf8(quoted_string)
361                         .map_err(|_| crate::error::ParseError::Header)?
362                 } else {
363                     // token: won't contains semicolon according to RFC 2616 Section 2.2
364                     let (token, new_left) = split_once_and_trim(left, ';');
365                     left = new_left;
366                     if token.is_empty() {
367                         // quoted-string can be empty, but token cannot be empty
368                         return Err(crate::error::ParseError::Header);
369                     }
370                     token.to_owned()
371                 };
372 
373                 let param = if param_name.eq_ignore_ascii_case("name") {
374                     DispositionParam::Name(value)
375                 } else if param_name.eq_ignore_ascii_case("filename") {
376                     // See also comments in test_from_raw_uncessary_percent_decode.
377                     DispositionParam::Filename(value)
378                 } else {
379                     DispositionParam::Unknown(param_name.to_owned(), value)
380                 };
381                 cd.parameters.push(param);
382             }
383         }
384 
385         Ok(cd)
386     }
387 
388     /// Returns `true` if it is [`Inline`](DispositionType::Inline).
is_inline(&self) -> bool389     pub fn is_inline(&self) -> bool {
390         match self.disposition {
391             DispositionType::Inline => true,
392             _ => false,
393         }
394     }
395 
396     /// Returns `true` if it is [`Attachment`](DispositionType::Attachment).
is_attachment(&self) -> bool397     pub fn is_attachment(&self) -> bool {
398         match self.disposition {
399             DispositionType::Attachment => true,
400             _ => false,
401         }
402     }
403 
404     /// Returns `true` if it is [`FormData`](DispositionType::FormData).
is_form_data(&self) -> bool405     pub fn is_form_data(&self) -> bool {
406         match self.disposition {
407             DispositionType::FormData => true,
408             _ => false,
409         }
410     }
411 
412     /// Returns `true` if it is [`Ext`](DispositionType::Ext) and the `disp_type` matches.
is_ext<T: AsRef<str>>(&self, disp_type: T) -> bool413     pub fn is_ext<T: AsRef<str>>(&self, disp_type: T) -> bool {
414         match self.disposition {
415             DispositionType::Ext(ref t)
416                 if t.eq_ignore_ascii_case(disp_type.as_ref()) =>
417             {
418                 true
419             }
420             _ => false,
421         }
422     }
423 
424     /// Return the value of *name* if exists.
get_name(&self) -> Option<&str>425     pub fn get_name(&self) -> Option<&str> {
426         self.parameters.iter().filter_map(|p| p.as_name()).nth(0)
427     }
428 
429     /// Return the value of *filename* if exists.
get_filename(&self) -> Option<&str>430     pub fn get_filename(&self) -> Option<&str> {
431         self.parameters
432             .iter()
433             .filter_map(|p| p.as_filename())
434             .nth(0)
435     }
436 
437     /// Return the value of *filename\** if exists.
get_filename_ext(&self) -> Option<&ExtendedValue>438     pub fn get_filename_ext(&self) -> Option<&ExtendedValue> {
439         self.parameters
440             .iter()
441             .filter_map(|p| p.as_filename_ext())
442             .nth(0)
443     }
444 
445     /// Return the value of the parameter which the `name` matches.
get_unknown<T: AsRef<str>>(&self, name: T) -> Option<&str>446     pub fn get_unknown<T: AsRef<str>>(&self, name: T) -> Option<&str> {
447         let name = name.as_ref();
448         self.parameters
449             .iter()
450             .filter_map(|p| p.as_unknown(name))
451             .nth(0)
452     }
453 
454     /// Return the value of the extended parameter which the `name` matches.
get_unknown_ext<T: AsRef<str>>(&self, name: T) -> Option<&ExtendedValue>455     pub fn get_unknown_ext<T: AsRef<str>>(&self, name: T) -> Option<&ExtendedValue> {
456         let name = name.as_ref();
457         self.parameters
458             .iter()
459             .filter_map(|p| p.as_unknown_ext(name))
460             .nth(0)
461     }
462 }
463 
464 impl IntoHeaderValue for ContentDisposition {
465     type Error = header::InvalidHeaderValue;
466 
try_into(self) -> Result<header::HeaderValue, Self::Error>467     fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
468         let mut writer = Writer::new();
469         let _ = write!(&mut writer, "{}", self);
470         header::HeaderValue::from_maybe_shared(writer.take())
471     }
472 }
473 
474 impl Header for ContentDisposition {
name() -> header::HeaderName475     fn name() -> header::HeaderName {
476         header::CONTENT_DISPOSITION
477     }
478 
parse<T: crate::HttpMessage>(msg: &T) -> Result<Self, crate::error::ParseError>479     fn parse<T: crate::HttpMessage>(msg: &T) -> Result<Self, crate::error::ParseError> {
480         if let Some(h) = msg.headers().get(&Self::name()) {
481             Self::from_raw(&h)
482         } else {
483             Err(crate::error::ParseError::Header)
484         }
485     }
486 }
487 
488 impl fmt::Display for DispositionType {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result489     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490         match self {
491             DispositionType::Inline => write!(f, "inline"),
492             DispositionType::Attachment => write!(f, "attachment"),
493             DispositionType::FormData => write!(f, "form-data"),
494             DispositionType::Ext(ref s) => write!(f, "{}", s),
495         }
496     }
497 }
498 
499 impl fmt::Display for DispositionParam {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result500     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501         // All ASCII control characters (0-30, 127) including horizontal tab, double quote, and
502         // backslash should be escaped in quoted-string (i.e. "foobar").
503         // Ref: RFC6266 S4.1 -> RFC2616 S3.6
504         // filename-parm  = "filename" "=" value
505         // value          = token | quoted-string
506         // quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
507         // qdtext         = <any TEXT except <">>
508         // quoted-pair    = "\" CHAR
509         // TEXT           = <any OCTET except CTLs,
510         //                  but including LWS>
511         // LWS            = [CRLF] 1*( SP | HT )
512         // OCTET          = <any 8-bit sequence of data>
513         // CHAR           = <any US-ASCII character (octets 0 - 127)>
514         // CTL            = <any US-ASCII control character
515         //                  (octets 0 - 31) and DEL (127)>
516         //
517         // Ref: RFC7578 S4.2 -> RFC2183 S2 -> RFC2045 S5.1
518         // parameter := attribute "=" value
519         // attribute := token
520         //              ; Matching of attributes
521         //              ; is ALWAYS case-insensitive.
522         // value := token / quoted-string
523         // token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
524         //             or tspecials>
525         // tspecials :=  "(" / ")" / "<" / ">" / "@" /
526         //               "," / ";" / ":" / "\" / <">
527         //               "/" / "[" / "]" / "?" / "="
528         //               ; Must be in quoted-string,
529         //               ; to use within parameter values
530         //
531         //
532         // See also comments in test_from_raw_uncessary_percent_decode.
533         lazy_static! {
534             static ref RE: Regex = Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap();
535         }
536         match self {
537             DispositionParam::Name(ref value) => write!(f, "name={}", value),
538             DispositionParam::Filename(ref value) => {
539                 write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref())
540             }
541             DispositionParam::Unknown(ref name, ref value) => write!(
542                 f,
543                 "{}=\"{}\"",
544                 name,
545                 &RE.replace_all(value, "\\$0").as_ref()
546             ),
547             DispositionParam::FilenameExt(ref ext_value) => {
548                 write!(f, "filename*={}", ext_value)
549             }
550             DispositionParam::UnknownExt(ref name, ref ext_value) => {
551                 write!(f, "{}*={}", name, ext_value)
552             }
553         }
554     }
555 }
556 
557 impl fmt::Display for ContentDisposition {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result558     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559         write!(f, "{}", self.disposition)?;
560         self.parameters
561             .iter()
562             .map(|param| write!(f, "; {}", param))
563             .collect()
564     }
565 }
566 
567 #[cfg(test)]
568 mod tests {
569     use super::{ContentDisposition, DispositionParam, DispositionType};
570     use crate::header::shared::Charset;
571     use crate::header::{ExtendedValue, HeaderValue};
572 
573     #[test]
test_from_raw_basic()574     fn test_from_raw_basic() {
575         assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err());
576 
577         let a = HeaderValue::from_static(
578             "form-data; dummy=3; name=upload; filename=\"sample.png\"",
579         );
580         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
581         let b = ContentDisposition {
582             disposition: DispositionType::FormData,
583             parameters: vec![
584                 DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
585                 DispositionParam::Name("upload".to_owned()),
586                 DispositionParam::Filename("sample.png".to_owned()),
587             ],
588         };
589         assert_eq!(a, b);
590 
591         let a = HeaderValue::from_static("attachment; filename=\"image.jpg\"");
592         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
593         let b = ContentDisposition {
594             disposition: DispositionType::Attachment,
595             parameters: vec![DispositionParam::Filename("image.jpg".to_owned())],
596         };
597         assert_eq!(a, b);
598 
599         let a = HeaderValue::from_static("inline; filename=image.jpg");
600         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
601         let b = ContentDisposition {
602             disposition: DispositionType::Inline,
603             parameters: vec![DispositionParam::Filename("image.jpg".to_owned())],
604         };
605         assert_eq!(a, b);
606 
607         let a = HeaderValue::from_static(
608             "attachment; creation-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"",
609         );
610         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
611         let b = ContentDisposition {
612             disposition: DispositionType::Attachment,
613             parameters: vec![DispositionParam::Unknown(
614                 String::from("creation-date"),
615                 "Wed, 12 Feb 1997 16:29:51 -0500".to_owned(),
616             )],
617         };
618         assert_eq!(a, b);
619     }
620 
621     #[test]
test_from_raw_extended()622     fn test_from_raw_extended() {
623         let a = HeaderValue::from_static(
624             "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates",
625         );
626         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
627         let b = ContentDisposition {
628             disposition: DispositionType::Attachment,
629             parameters: vec![DispositionParam::FilenameExt(ExtendedValue {
630                 charset: Charset::Ext(String::from("UTF-8")),
631                 language_tag: None,
632                 value: vec![
633                     0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20,
634                     b'r', b'a', b't', b'e', b's',
635                 ],
636             })],
637         };
638         assert_eq!(a, b);
639 
640         let a = HeaderValue::from_static(
641             "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates",
642         );
643         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
644         let b = ContentDisposition {
645             disposition: DispositionType::Attachment,
646             parameters: vec![DispositionParam::FilenameExt(ExtendedValue {
647                 charset: Charset::Ext(String::from("UTF-8")),
648                 language_tag: None,
649                 value: vec![
650                     0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20,
651                     b'r', b'a', b't', b'e', b's',
652                 ],
653             })],
654         };
655         assert_eq!(a, b);
656     }
657 
658     #[test]
test_from_raw_extra_whitespace()659     fn test_from_raw_extra_whitespace() {
660         let a = HeaderValue::from_static(
661             "form-data  ; du-mmy= 3  ; name =upload ; filename =  \"sample.png\"  ; ",
662         );
663         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
664         let b = ContentDisposition {
665             disposition: DispositionType::FormData,
666             parameters: vec![
667                 DispositionParam::Unknown("du-mmy".to_owned(), "3".to_owned()),
668                 DispositionParam::Name("upload".to_owned()),
669                 DispositionParam::Filename("sample.png".to_owned()),
670             ],
671         };
672         assert_eq!(a, b);
673     }
674 
675     #[test]
test_from_raw_unordered()676     fn test_from_raw_unordered() {
677         let a = HeaderValue::from_static(
678             "form-data; dummy=3; filename=\"sample.png\" ; name=upload;",
679             // Actually, a trailling semolocon is not compliant. But it is fine to accept.
680         );
681         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
682         let b = ContentDisposition {
683             disposition: DispositionType::FormData,
684             parameters: vec![
685                 DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
686                 DispositionParam::Filename("sample.png".to_owned()),
687                 DispositionParam::Name("upload".to_owned()),
688             ],
689         };
690         assert_eq!(a, b);
691 
692         let a = HeaderValue::from_str(
693             "attachment; filename*=iso-8859-1''foo-%E4.html; filename=\"foo-ä.html\"",
694         )
695         .unwrap();
696         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
697         let b = ContentDisposition {
698             disposition: DispositionType::Attachment,
699             parameters: vec![
700                 DispositionParam::FilenameExt(ExtendedValue {
701                     charset: Charset::Iso_8859_1,
702                     language_tag: None,
703                     value: b"foo-\xe4.html".to_vec(),
704                 }),
705                 DispositionParam::Filename("foo-ä.html".to_owned()),
706             ],
707         };
708         assert_eq!(a, b);
709     }
710 
711     #[test]
test_from_raw_only_disp()712     fn test_from_raw_only_disp() {
713         let a = ContentDisposition::from_raw(&HeaderValue::from_static("attachment"))
714             .unwrap();
715         let b = ContentDisposition {
716             disposition: DispositionType::Attachment,
717             parameters: vec![],
718         };
719         assert_eq!(a, b);
720 
721         let a =
722             ContentDisposition::from_raw(&HeaderValue::from_static("inline ;")).unwrap();
723         let b = ContentDisposition {
724             disposition: DispositionType::Inline,
725             parameters: vec![],
726         };
727         assert_eq!(a, b);
728 
729         let a = ContentDisposition::from_raw(&HeaderValue::from_static(
730             "unknown-disp-param",
731         ))
732         .unwrap();
733         let b = ContentDisposition {
734             disposition: DispositionType::Ext(String::from("unknown-disp-param")),
735             parameters: vec![],
736         };
737         assert_eq!(a, b);
738     }
739 
740     #[test]
from_raw_with_mixed_case()741     fn from_raw_with_mixed_case() {
742         let a = HeaderValue::from_str(
743             "InLInE; fIlenAME*=iso-8859-1''foo-%E4.html; filEName=\"foo-ä.html\"",
744         )
745         .unwrap();
746         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
747         let b = ContentDisposition {
748             disposition: DispositionType::Inline,
749             parameters: vec![
750                 DispositionParam::FilenameExt(ExtendedValue {
751                     charset: Charset::Iso_8859_1,
752                     language_tag: None,
753                     value: b"foo-\xe4.html".to_vec(),
754                 }),
755                 DispositionParam::Filename("foo-ä.html".to_owned()),
756             ],
757         };
758         assert_eq!(a, b);
759     }
760 
761     #[test]
from_raw_with_unicode()762     fn from_raw_with_unicode() {
763         /* RFC7578 Section 4.2:
764         Some commonly deployed systems use multipart/form-data with file names directly encoded
765         including octets outside the US-ASCII range. The encoding used for the file names is
766         typically UTF-8, although HTML forms will use the charset associated with the form.
767 
768         Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above.
769         (And now, only UTF-8 is handled by this implementation.)
770         */
771         let a = HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"")
772             .unwrap();
773         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
774         let b = ContentDisposition {
775             disposition: DispositionType::FormData,
776             parameters: vec![
777                 DispositionParam::Name(String::from("upload")),
778                 DispositionParam::Filename(String::from("文件.webp")),
779             ],
780         };
781         assert_eq!(a, b);
782 
783         let a = HeaderValue::from_str(
784             "form-data; name=upload; filename=\"余固知謇謇之為患兮,忍而不能舍也.pptx\"",
785         )
786         .unwrap();
787         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
788         let b = ContentDisposition {
789             disposition: DispositionType::FormData,
790             parameters: vec![
791                 DispositionParam::Name(String::from("upload")),
792                 DispositionParam::Filename(String::from(
793                     "余固知謇謇之為患兮,忍而不能舍也.pptx",
794                 )),
795             ],
796         };
797         assert_eq!(a, b);
798     }
799 
800     #[test]
test_from_raw_escape()801     fn test_from_raw_escape() {
802         let a = HeaderValue::from_static(
803             "form-data; dummy=3; name=upload; filename=\"s\\amp\\\"le.png\"",
804         );
805         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
806         let b = ContentDisposition {
807             disposition: DispositionType::FormData,
808             parameters: vec![
809                 DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
810                 DispositionParam::Name("upload".to_owned()),
811                 DispositionParam::Filename(
812                     ['s', 'a', 'm', 'p', '\"', 'l', 'e', '.', 'p', 'n', 'g']
813                         .iter()
814                         .collect(),
815                 ),
816             ],
817         };
818         assert_eq!(a, b);
819     }
820 
821     #[test]
test_from_raw_semicolon()822     fn test_from_raw_semicolon() {
823         let a =
824             HeaderValue::from_static("form-data; filename=\"A semicolon here;.pdf\"");
825         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
826         let b = ContentDisposition {
827             disposition: DispositionType::FormData,
828             parameters: vec![DispositionParam::Filename(String::from(
829                 "A semicolon here;.pdf",
830             ))],
831         };
832         assert_eq!(a, b);
833     }
834 
835     #[test]
test_from_raw_uncessary_percent_decode()836     fn test_from_raw_uncessary_percent_decode() {
837         // In fact, RFC7578 (multipart/form-data) Section 2 and 4.2 suggests that filename with
838         // non-ASCII characters MAY be percent-encoded.
839         // On the contrary, RFC6266 or other RFCs related to Content-Disposition response header
840         // do not mention such percent-encoding.
841         // So, it appears to be undecidable whether to percent-decode or not without
842         // knowing the usage scenario (multipart/form-data v.s. HTTP response header) and
843         // inevitable to unnecessarily percent-decode filename with %XX in the former scenario.
844         // Fortunately, it seems that almost all mainstream browsers just send UTF-8 encoded file
845         // names in quoted-string format (tested on Edge, IE11, Chrome and Firefox) without
846         // percent-encoding. So we do not bother to attempt to percent-decode.
847         let a = HeaderValue::from_static(
848             "form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"",
849         );
850         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
851         let b = ContentDisposition {
852             disposition: DispositionType::FormData,
853             parameters: vec![
854                 DispositionParam::Name("photo".to_owned()),
855                 DispositionParam::Filename(String::from("%74%65%73%74%2e%70%6e%67")),
856             ],
857         };
858         assert_eq!(a, b);
859 
860         let a = HeaderValue::from_static(
861             "form-data; name=photo; filename=\"%74%65%73%74.png\"",
862         );
863         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
864         let b = ContentDisposition {
865             disposition: DispositionType::FormData,
866             parameters: vec![
867                 DispositionParam::Name("photo".to_owned()),
868                 DispositionParam::Filename(String::from("%74%65%73%74.png")),
869             ],
870         };
871         assert_eq!(a, b);
872     }
873 
874     #[test]
test_from_raw_param_value_missing()875     fn test_from_raw_param_value_missing() {
876         let a = HeaderValue::from_static("form-data; name=upload ; filename=");
877         assert!(ContentDisposition::from_raw(&a).is_err());
878 
879         let a = HeaderValue::from_static("attachment; dummy=; filename=invoice.pdf");
880         assert!(ContentDisposition::from_raw(&a).is_err());
881 
882         let a = HeaderValue::from_static("inline; filename=  ");
883         assert!(ContentDisposition::from_raw(&a).is_err());
884 
885         let a = HeaderValue::from_static("inline; filename=\"\"");
886         assert!(ContentDisposition::from_raw(&a)
887             .expect("parse cd")
888             .get_filename()
889             .expect("filename")
890             .is_empty());
891     }
892 
893     #[test]
test_from_raw_param_name_missing()894     fn test_from_raw_param_name_missing() {
895         let a = HeaderValue::from_static("inline; =\"test.txt\"");
896         assert!(ContentDisposition::from_raw(&a).is_err());
897 
898         let a = HeaderValue::from_static("inline; =diary.odt");
899         assert!(ContentDisposition::from_raw(&a).is_err());
900 
901         let a = HeaderValue::from_static("inline; =");
902         assert!(ContentDisposition::from_raw(&a).is_err());
903     }
904 
905     #[test]
test_display_extended()906     fn test_display_extended() {
907         let as_string =
908             "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
909         let a = HeaderValue::from_static(as_string);
910         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
911         let display_rendered = format!("{}", a);
912         assert_eq!(as_string, display_rendered);
913 
914         let a = HeaderValue::from_static("attachment; filename=colourful.csv");
915         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
916         let display_rendered = format!("{}", a);
917         assert_eq!(
918             "attachment; filename=\"colourful.csv\"".to_owned(),
919             display_rendered
920         );
921     }
922 
923     #[test]
test_display_quote()924     fn test_display_quote() {
925         let as_string = "form-data; name=upload; filename=\"Quote\\\"here.png\"";
926         as_string
927             .find(['\\', '\"'].iter().collect::<String>().as_str())
928             .unwrap(); // ensure `\"` is there
929         let a = HeaderValue::from_static(as_string);
930         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
931         let display_rendered = format!("{}", a);
932         assert_eq!(as_string, display_rendered);
933     }
934 
935     #[test]
test_display_space_tab()936     fn test_display_space_tab() {
937         let as_string = "form-data; name=upload; filename=\"Space here.png\"";
938         let a = HeaderValue::from_static(as_string);
939         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
940         let display_rendered = format!("{}", a);
941         assert_eq!(as_string, display_rendered);
942 
943         let a: ContentDisposition = ContentDisposition {
944             disposition: DispositionType::Inline,
945             parameters: vec![DispositionParam::Filename(String::from("Tab\there.png"))],
946         };
947         let display_rendered = format!("{}", a);
948         assert_eq!("inline; filename=\"Tab\x09here.png\"", display_rendered);
949     }
950 
951     #[test]
test_display_control_characters()952     fn test_display_control_characters() {
953         /* let a = "attachment; filename=\"carriage\rreturn.png\"";
954         let a = HeaderValue::from_static(a);
955         let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
956         let display_rendered = format!("{}", a);
957         assert_eq!(
958             "attachment; filename=\"carriage\\\rreturn.png\"",
959             display_rendered
960         );*/
961         // No way to create a HeaderValue containing a carriage return.
962 
963         let a: ContentDisposition = ContentDisposition {
964             disposition: DispositionType::Inline,
965             parameters: vec![DispositionParam::Filename(String::from("bell\x07.png"))],
966         };
967         let display_rendered = format!("{}", a);
968         assert_eq!("inline; filename=\"bell\\\x07.png\"", display_rendered);
969     }
970 
971     #[test]
test_param_methods()972     fn test_param_methods() {
973         let param = DispositionParam::Filename(String::from("sample.txt"));
974         assert!(param.is_filename());
975         assert_eq!(param.as_filename().unwrap(), "sample.txt");
976 
977         let param = DispositionParam::Unknown(String::from("foo"), String::from("bar"));
978         assert!(param.is_unknown("foo"));
979         assert_eq!(param.as_unknown("fOo"), Some("bar"));
980     }
981 
982     #[test]
test_disposition_methods()983     fn test_disposition_methods() {
984         let cd = ContentDisposition {
985             disposition: DispositionType::FormData,
986             parameters: vec![
987                 DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
988                 DispositionParam::Name("upload".to_owned()),
989                 DispositionParam::Filename("sample.png".to_owned()),
990             ],
991         };
992         assert_eq!(cd.get_name(), Some("upload"));
993         assert_eq!(cd.get_unknown("dummy"), Some("3"));
994         assert_eq!(cd.get_filename(), Some("sample.png"));
995         assert_eq!(cd.get_unknown_ext("dummy"), None);
996         assert_eq!(cd.get_unknown("duMMy"), Some("3"));
997     }
998 }
999