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