328
									
								
								src/header/common/content_disposition.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								src/header/common/content_disposition.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,328 @@ | |||||||
|  | // # References | ||||||
|  | // | ||||||
|  | // "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt | ||||||
|  | // "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt | ||||||
|  | // "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc2388.txt | ||||||
|  | // Browser conformance tests at: http://greenbytes.de/tech/tc2231/ | ||||||
|  | // IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml | ||||||
|  |  | ||||||
|  | use language_tags::LanguageTag; | ||||||
|  | use std::fmt; | ||||||
|  | use std::str::FromStr; | ||||||
|  | use unicase::UniCase; | ||||||
|  | use url::percent_encoding; | ||||||
|  |  | ||||||
|  | use header::{Header, HeaderFormat, parsing}; | ||||||
|  | use header::shared::Charset; | ||||||
|  |  | ||||||
|  | /// The implied disposition of the content of the HTTP body | ||||||
|  | #[derive(Clone, Debug, PartialEq)] | ||||||
|  | pub enum DispositionType { | ||||||
|  |     /// Inline implies default processing | ||||||
|  |     Inline, | ||||||
|  |     /// Attachment implies that the recipient should prompt the user to save the response locally, | ||||||
|  |     /// rather than process it normally (as per its media type). | ||||||
|  |     Attachment, | ||||||
|  |     /// Extension type.  Should be handled by recipients the same way as Attachment | ||||||
|  |     Ext(String) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// A parameter to the disposition type | ||||||
|  | #[derive(Clone, Debug, PartialEq)] | ||||||
|  | pub enum DispositionParam { | ||||||
|  |     /// A Filename consisting of a Charset, an optional LanguageTag, and finally a sequence of | ||||||
|  |     /// bytes representing the filename | ||||||
|  |     Filename(Charset, Option<LanguageTag>, Vec<u8>), | ||||||
|  |     /// Extension type consisting of token and value.  Recipients should ignore unrecognized | ||||||
|  |     /// parameters. | ||||||
|  |     Ext(String, String) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// A `Content-Disposition` header, (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266) | ||||||
|  | /// | ||||||
|  | /// The Content-Disposition response header field is used to convey | ||||||
|  | /// additional information about how to process the response payload, and | ||||||
|  | /// also can be used to attach additional metadata, such as the filename | ||||||
|  | /// to use when saving the response payload locally. | ||||||
|  | /// | ||||||
|  | /// # ABNF | ||||||
|  | /// ```plain | ||||||
|  | /// content-disposition = "Content-Disposition" ":" | ||||||
|  | ///                       disposition-type *( ";" disposition-parm ) | ||||||
|  | /// | ||||||
|  | /// disposition-type    = "inline" | "attachment" | disp-ext-type | ||||||
|  | ///                       ; case-insensitive | ||||||
|  | /// | ||||||
|  | /// disp-ext-type       = token | ||||||
|  | /// | ||||||
|  | /// disposition-parm    = filename-parm | disp-ext-parm | ||||||
|  | /// | ||||||
|  | /// filename-parm       = "filename" "=" value | ||||||
|  | ///                     | "filename*" "=" ext-value | ||||||
|  | /// | ||||||
|  | /// disp-ext-parm       = token "=" value | ||||||
|  | ///                     | ext-token "=" ext-value | ||||||
|  | /// | ||||||
|  | /// ext-token           = <the characters in token, followed by "*"> | ||||||
|  | /// ``` | ||||||
|  | /// | ||||||
|  | /// # Example | ||||||
|  | /// ``` | ||||||
|  | /// use hyper::header::{Headers, ContentDisposition, DispositionType, DispositionParam, Charset}; | ||||||
|  | /// | ||||||
|  | /// let mut headers = Headers::new(); | ||||||
|  | /// headers.set(ContentDisposition { | ||||||
|  | ///     disposition: DispositionType::Attachment, | ||||||
|  | ///     parameters: vec![DispositionParam::Filename( | ||||||
|  | ///       Charset::Iso_8859_1, // The character set for the bytes of the filename | ||||||
|  | ///       None, // The optional language tag (see `language-tag` crate) | ||||||
|  | ///       b"\xa9 Copyright 1989.txt".to_vec() // the actual bytes of the filename | ||||||
|  | ///     )] | ||||||
|  | /// }); | ||||||
|  | /// ``` | ||||||
|  | #[derive(Clone, Debug, PartialEq)] | ||||||
|  | pub struct ContentDisposition { | ||||||
|  |     /// The disposition | ||||||
|  |     pub disposition: DispositionType, | ||||||
|  |     /// Disposition parameters | ||||||
|  |     pub parameters: Vec<DispositionParam>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Header for ContentDisposition { | ||||||
|  |     fn header_name() -> &'static str { | ||||||
|  |         "Content-Disposition" | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn parse_header(raw: &[Vec<u8>]) -> ::Result<ContentDisposition> { | ||||||
|  |         parsing::from_one_raw_str(raw).and_then(|s: String| { | ||||||
|  |             let mut sections = s.split(';'); | ||||||
|  |             let disposition = match sections.next() { | ||||||
|  |                 Some(s) => s.trim(), | ||||||
|  |                 None => return Err(::Error::Header), | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             let mut cd = ContentDisposition { | ||||||
|  |                 disposition: if UniCase(&*disposition) == UniCase("inline") { | ||||||
|  |                     DispositionType::Inline | ||||||
|  |                 } else if UniCase(&*disposition) == UniCase("attachment") { | ||||||
|  |                     DispositionType::Attachment | ||||||
|  |                 } else { | ||||||
|  |                     DispositionType::Ext(disposition.to_owned()) | ||||||
|  |                 }, | ||||||
|  |                 parameters: Vec::new(), | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             for section in sections { | ||||||
|  |                 let mut parts = section.splitn(2, '='); | ||||||
|  |  | ||||||
|  |                 let key = if let Some(key) = parts.next() { | ||||||
|  |                     key.trim() | ||||||
|  |                 } else { | ||||||
|  |                     return Err(::Error::Header); | ||||||
|  |                 }; | ||||||
|  |  | ||||||
|  |                 let val = if let Some(val) = parts.next() { | ||||||
|  |                     val.trim() | ||||||
|  |                 } else { | ||||||
|  |                     return Err(::Error::Header); | ||||||
|  |                 }; | ||||||
|  |  | ||||||
|  |                 cd.parameters.push( | ||||||
|  |                     if UniCase(&*key) == UniCase("filename") { | ||||||
|  |                         DispositionParam::Filename( | ||||||
|  |                             Charset::Ext("UTF-8".to_owned()), None, | ||||||
|  |                             val.trim_matches('"').as_bytes().to_owned()) | ||||||
|  |                     } else if UniCase(&*key) == UniCase("filename*") { | ||||||
|  |                         let (charset, opt_language, value) = try!(parse_ext_value(val)); | ||||||
|  |                         DispositionParam::Filename(charset, opt_language, value) | ||||||
|  |                     } else { | ||||||
|  |                         DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned()) | ||||||
|  |                     } | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             Ok(cd) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl HeaderFormat for ContentDisposition { | ||||||
|  |     #[inline] | ||||||
|  |     fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||||||
|  |         fmt::Display::fmt(&self, f) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl fmt::Display for ContentDisposition { | ||||||
|  |     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||||||
|  |         match self.disposition { | ||||||
|  |             DispositionType::Inline => try!(write!(f, "inline")), | ||||||
|  |             DispositionType::Attachment => try!(write!(f, "attachment")), | ||||||
|  |             DispositionType::Ext(ref s) => try!(write!(f, "{}", s)), | ||||||
|  |         } | ||||||
|  |         for param in self.parameters.iter() { | ||||||
|  |             match param { | ||||||
|  |                 &DispositionParam::Filename(ref charset, ref opt_lang, ref bytes) => { | ||||||
|  |                     let mut use_simple_format: bool = false; | ||||||
|  |                     if opt_lang.is_none() { | ||||||
|  |                         if let Charset::Ext(ref ext) = *charset { | ||||||
|  |                             if UniCase(&**ext) == UniCase("utf-8") { | ||||||
|  |                                 use_simple_format = true; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     if use_simple_format { | ||||||
|  |                         try!(write!(f, "; filename=\"{}\"", | ||||||
|  |                                     match String::from_utf8(bytes.clone()) { | ||||||
|  |                                         Ok(s) => s, | ||||||
|  |                                         Err(_) => return Err(fmt::Error), | ||||||
|  |                                     })); | ||||||
|  |                     } else { | ||||||
|  |                         try!(write!(f, "; filename*={}'", charset)); | ||||||
|  |                         if let Some(ref lang) = *opt_lang { | ||||||
|  |                             try!(write!(f, "{}", lang)); | ||||||
|  |                         }; | ||||||
|  |                         try!(write!(f, "'")); | ||||||
|  |                         try!(f.write_str( | ||||||
|  |                             &*percent_encoding::percent_encode( | ||||||
|  |                                 bytes, percent_encoding::HTTP_VALUE_ENCODE_SET))) | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 &DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)), | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Parsing of `ext-value` | ||||||
|  | /// https://tools.ietf.org/html/rfc5987#section-3.2 | ||||||
|  | /// | ||||||
|  | /// # ABNF | ||||||
|  | /// ```plain | ||||||
|  | /// ext-value     = charset  "'" [ language ] "'" value-chars | ||||||
|  | ///               ; like RFC 2231's <extended-initial-value> | ||||||
|  | ///               ; (see [RFC2231], Section 7) | ||||||
|  | /// | ||||||
|  | /// charset       = "UTF-8" / "ISO-8859-1" / mime-charset | ||||||
|  | /// | ||||||
|  | /// mime-charset  = 1*mime-charsetc | ||||||
|  | /// mime-charsetc = ALPHA / DIGIT | ||||||
|  | ///               / "!" / "#" / "$" / "%" / "&" | ||||||
|  | ///               / "+" / "-" / "^" / "_" / "`" | ||||||
|  | ///               / "{" / "}" / "~" | ||||||
|  | ///               ; as <mime-charset> in Section 2.3 of [RFC2978] | ||||||
|  | ///               ; except that the single quote is not included | ||||||
|  | ///               ; SHOULD be registered in the IANA charset registry | ||||||
|  | /// | ||||||
|  | /// language      = <Language-Tag, defined in [RFC5646], Section 2.1> | ||||||
|  | /// | ||||||
|  | /// value-chars   = *( pct-encoded / attr-char ) | ||||||
|  | /// | ||||||
|  | /// pct-encoded   = "%" HEXDIG HEXDIG | ||||||
|  | ///               ; see [RFC3986], Section 2.1 | ||||||
|  | /// | ||||||
|  | /// attr-char     = ALPHA / DIGIT | ||||||
|  | ///               / "!" / "#" / "$" / "&" / "+" / "-" / "." | ||||||
|  | ///               / "^" / "_" / "`" / "|" / "~" | ||||||
|  | ///               ; token except ( "*" / "'" / "%" ) | ||||||
|  | /// ``` | ||||||
|  | fn parse_ext_value(val: &str) -> ::Result<(Charset, Option<LanguageTag>, Vec<u8>)> { | ||||||
|  |  | ||||||
|  |     // Break into three pieces separated by the single-quote character | ||||||
|  |     let mut parts = val.splitn(3,'\''); | ||||||
|  |  | ||||||
|  |     // Interpret the first piece as a Charset | ||||||
|  |     let charset: Charset = match parts.next() { | ||||||
|  |         None => return Err(::Error::Header), | ||||||
|  |         Some(n) => try!(FromStr::from_str(n)), | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Interpret the second piece as a language tag | ||||||
|  |     let lang: Option<LanguageTag> = match parts.next() { | ||||||
|  |         None => return Err(::Error::Header), | ||||||
|  |         Some("") => None, | ||||||
|  |         Some(s) => match s.parse() { | ||||||
|  |             Ok(lt) => Some(lt), | ||||||
|  |             Err(_) => return Err(::Error::Header), | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Interpret the third piece as a sequence of value characters | ||||||
|  |     let value: Vec<u8> = match parts.next() { | ||||||
|  |         None => return Err(::Error::Header), | ||||||
|  |         Some(v) => percent_encoding::percent_decode(v.as_bytes()), | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     Ok( (charset, lang, value) ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::{ContentDisposition,DispositionType,DispositionParam}; | ||||||
|  |     use ::header::Header; | ||||||
|  |     use ::header::shared::Charset; | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_header() { | ||||||
|  |         assert!(ContentDisposition::parse_header([b"".to_vec()].as_ref()).is_err()); | ||||||
|  |  | ||||||
|  |         let a = [b"form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".to_vec()]; | ||||||
|  |         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||||||
|  |         let b = ContentDisposition { | ||||||
|  |             disposition: DispositionType::Ext("form-data".to_owned()), | ||||||
|  |             parameters: vec![ | ||||||
|  |                 DispositionParam::Ext("dummy".to_owned(), "3".to_owned()), | ||||||
|  |                 DispositionParam::Ext("name".to_owned(), "upload".to_owned()), | ||||||
|  |                 DispositionParam::Filename( | ||||||
|  |                     Charset::Ext("UTF-8".to_owned()), | ||||||
|  |                     None, | ||||||
|  |                     "sample.png".bytes().collect()) ] | ||||||
|  |         }; | ||||||
|  |         assert_eq!(a, b); | ||||||
|  |  | ||||||
|  |         let a = [b"attachment; filename=\"image.jpg\"".to_vec()]; | ||||||
|  |         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||||||
|  |         let b = ContentDisposition { | ||||||
|  |             disposition: DispositionType::Attachment, | ||||||
|  |             parameters: vec![ | ||||||
|  |                 DispositionParam::Filename( | ||||||
|  |                     Charset::Ext("UTF-8".to_owned()), | ||||||
|  |                     None, | ||||||
|  |                     "image.jpg".bytes().collect()) ] | ||||||
|  |         }; | ||||||
|  |         assert_eq!(a, b); | ||||||
|  |  | ||||||
|  |         let a = [b"attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".to_vec()]; | ||||||
|  |         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||||||
|  |         let b = ContentDisposition { | ||||||
|  |             disposition: DispositionType::Attachment, | ||||||
|  |             parameters: vec![ | ||||||
|  |                 DispositionParam::Filename( | ||||||
|  |                     Charset::Ext("UTF-8".to_owned()), | ||||||
|  |                     None, | ||||||
|  |                     vec![0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, | ||||||
|  |                          0xe2, 0x82, 0xac, 0x20, b'r', b'a', b't', b'e', b's']) ] | ||||||
|  |         }; | ||||||
|  |         assert_eq!(a, b); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn test_display() { | ||||||
|  |         let a = [b"attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates".to_vec()]; | ||||||
|  |         let as_string = ::std::str::from_utf8(&(a[0])).unwrap(); | ||||||
|  |         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||||||
|  |         let display_rendered = format!("{}",a); | ||||||
|  |         assert_eq!(as_string, display_rendered); | ||||||
|  |  | ||||||
|  |         let a = [b"attachment; filename*=UTF-8''black%20and%20white.csv".to_vec()]; | ||||||
|  |         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||||||
|  |         let display_rendered = format!("{}",a); | ||||||
|  |         assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered); | ||||||
|  |  | ||||||
|  |         let a = [b"attachment; filename=colourful.csv".to_vec()]; | ||||||
|  |         let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap(); | ||||||
|  |         let display_rendered = format!("{}",a); | ||||||
|  |         assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -23,6 +23,7 @@ pub use self::allow::Allow; | |||||||
| pub use self::authorization::{Authorization, Scheme, Basic, Bearer}; | pub use self::authorization::{Authorization, Scheme, Basic, Bearer}; | ||||||
| pub use self::cache_control::{CacheControl, CacheDirective}; | pub use self::cache_control::{CacheControl, CacheDirective}; | ||||||
| pub use self::connection::{Connection, ConnectionOption}; | pub use self::connection::{Connection, ConnectionOption}; | ||||||
|  | pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam}; | ||||||
| pub use self::content_length::ContentLength; | pub use self::content_length::ContentLength; | ||||||
| pub use self::content_encoding::ContentEncoding; | pub use self::content_encoding::ContentEncoding; | ||||||
| pub use self::content_language::ContentLanguage; | pub use self::content_language::ContentLanguage; | ||||||
| @@ -371,6 +372,7 @@ mod authorization; | |||||||
| mod cache_control; | mod cache_control; | ||||||
| mod cookie; | mod cookie; | ||||||
| mod connection; | mod connection; | ||||||
|  | mod content_disposition; | ||||||
| mod content_encoding; | mod content_encoding; | ||||||
| mod content_language; | mod content_language; | ||||||
| mod content_length; | mod content_length; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user