mirror of
https://github.com/actix/actix-extras.git
synced 2024-11-24 16:02:59 +01:00
Merge pull request #284 from axon-q/multipart-content-disposition
multipart: parse and validate Content-Disposition
This commit is contained in:
commit
4fe2f6b763
@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
* Add `.content_disposition()` method to parse Content-Disposition of
|
||||||
|
multipart fields
|
||||||
|
|
||||||
* Re-export `actix::prelude::*` as `actix_web::actix` module.
|
* Re-export `actix::prelude::*` as `actix_web::actix` module.
|
||||||
|
|
||||||
* `HttpRequest::url_for_static()` for a named route with no variables segments
|
* `HttpRequest::url_for_static()` for a named route with no variables segments
|
||||||
|
@ -7,13 +7,12 @@
|
|||||||
// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
|
// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
|
||||||
|
|
||||||
use language_tags::LanguageTag;
|
use language_tags::LanguageTag;
|
||||||
use std::fmt;
|
use header;
|
||||||
use unicase;
|
use header::{Header, IntoHeaderValue, Writer};
|
||||||
|
|
||||||
use header::{Header, Raw, parsing};
|
|
||||||
use header::parsing::{parse_extended_value, http_percent_encode};
|
|
||||||
use header::shared::Charset;
|
use header::shared::Charset;
|
||||||
|
|
||||||
|
use std::fmt::{self, Write};
|
||||||
|
|
||||||
/// The implied disposition of the content of the HTTP body.
|
/// The implied disposition of the content of the HTTP body.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum DispositionType {
|
pub enum DispositionType {
|
||||||
@ -69,17 +68,16 @@ pub enum DispositionParam {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use hyper::header::{Headers, ContentDisposition, DispositionType, DispositionParam, Charset};
|
/// use actix_web::http::header::{ContentDisposition, DispositionType, DispositionParam, Charset};
|
||||||
///
|
///
|
||||||
/// let mut headers = Headers::new();
|
/// let cd = ContentDisposition {
|
||||||
/// headers.set(ContentDisposition {
|
|
||||||
/// disposition: DispositionType::Attachment,
|
/// disposition: DispositionType::Attachment,
|
||||||
/// parameters: vec![DispositionParam::Filename(
|
/// parameters: vec![DispositionParam::Filename(
|
||||||
/// Charset::Iso_8859_1, // The character set for the bytes of the filename
|
/// Charset::Iso_8859_1, // The character set for the bytes of the filename
|
||||||
/// None, // The optional language tag (see `language-tag` crate)
|
/// None, // The optional language tag (see `language-tag` crate)
|
||||||
/// b"\xa9 Copyright 1989.txt".to_vec() // the actual bytes of the filename
|
/// b"\xa9 Copyright 1989.txt".to_vec() // the actual bytes of the filename
|
||||||
/// )]
|
/// )]
|
||||||
/// });
|
/// };
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct ContentDisposition {
|
pub struct ContentDisposition {
|
||||||
@ -88,25 +86,20 @@ pub struct ContentDisposition {
|
|||||||
/// Disposition parameters
|
/// Disposition parameters
|
||||||
pub parameters: Vec<DispositionParam>,
|
pub parameters: Vec<DispositionParam>,
|
||||||
}
|
}
|
||||||
|
impl ContentDisposition {
|
||||||
impl Header for ContentDisposition {
|
/// Parse a raw Content-Disposition header value
|
||||||
fn header_name() -> &'static str {
|
pub fn from_raw(hv: &header::HeaderValue) -> Result<Self, ::error::ParseError> {
|
||||||
static NAME: &'static str = "Content-Disposition";
|
header::from_one_raw_str(Some(hv)).and_then(|s: String| {
|
||||||
NAME
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_header(raw: &Raw) -> ::Result<ContentDisposition> {
|
|
||||||
parsing::from_one_raw_str(raw).and_then(|s: String| {
|
|
||||||
let mut sections = s.split(';');
|
let mut sections = s.split(';');
|
||||||
let disposition = match sections.next() {
|
let disposition = match sections.next() {
|
||||||
Some(s) => s.trim(),
|
Some(s) => s.trim(),
|
||||||
None => return Err(::Error::Header),
|
None => return Err(::error::ParseError::Header),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cd = ContentDisposition {
|
let mut cd = ContentDisposition {
|
||||||
disposition: if unicase::eq_ascii(&*disposition, "inline") {
|
disposition: if disposition.eq_ignore_ascii_case("inline") {
|
||||||
DispositionType::Inline
|
DispositionType::Inline
|
||||||
} else if unicase::eq_ascii(&*disposition, "attachment") {
|
} else if disposition.eq_ignore_ascii_case("attachment") {
|
||||||
DispositionType::Attachment
|
DispositionType::Attachment
|
||||||
} else {
|
} else {
|
||||||
DispositionType::Ext(disposition.to_owned())
|
DispositionType::Ext(disposition.to_owned())
|
||||||
@ -120,22 +113,22 @@ impl Header for ContentDisposition {
|
|||||||
let key = if let Some(key) = parts.next() {
|
let key = if let Some(key) = parts.next() {
|
||||||
key.trim()
|
key.trim()
|
||||||
} else {
|
} else {
|
||||||
return Err(::Error::Header);
|
return Err(::error::ParseError::Header);
|
||||||
};
|
};
|
||||||
|
|
||||||
let val = if let Some(val) = parts.next() {
|
let val = if let Some(val) = parts.next() {
|
||||||
val.trim()
|
val.trim()
|
||||||
} else {
|
} else {
|
||||||
return Err(::Error::Header);
|
return Err(::error::ParseError::Header);
|
||||||
};
|
};
|
||||||
|
|
||||||
cd.parameters.push(
|
cd.parameters.push(
|
||||||
if unicase::eq_ascii(&*key, "filename") {
|
if key.eq_ignore_ascii_case("filename") {
|
||||||
DispositionParam::Filename(
|
DispositionParam::Filename(
|
||||||
Charset::Ext("UTF-8".to_owned()), None,
|
Charset::Ext("UTF-8".to_owned()), None,
|
||||||
val.trim_matches('"').as_bytes().to_owned())
|
val.trim_matches('"').as_bytes().to_owned())
|
||||||
} else if unicase::eq_ascii(&*key, "filename*") {
|
} else if key.eq_ignore_ascii_case("filename*") {
|
||||||
let extended_value = try!(parse_extended_value(val));
|
let extended_value = try!(header::parse_extended_value(val));
|
||||||
DispositionParam::Filename(extended_value.charset, extended_value.language_tag, extended_value.value)
|
DispositionParam::Filename(extended_value.charset, extended_value.language_tag, extended_value.value)
|
||||||
} else {
|
} else {
|
||||||
DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned())
|
DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned())
|
||||||
@ -146,10 +139,29 @@ impl Header for ContentDisposition {
|
|||||||
Ok(cd)
|
Ok(cd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
impl IntoHeaderValue for ContentDisposition {
|
||||||
fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result {
|
type Error = header::InvalidHeaderValueBytes;
|
||||||
f.fmt_line(self)
|
|
||||||
|
fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
|
||||||
|
let mut writer = Writer::new();
|
||||||
|
let _ = write!(&mut writer, "{}", self);
|
||||||
|
header::HeaderValue::from_shared(writer.take())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header for ContentDisposition {
|
||||||
|
fn name() -> header::HeaderName {
|
||||||
|
header::CONTENT_DISPOSITION
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse<T: ::HttpMessage>(msg: &T) -> Result<Self, ::error::ParseError> {
|
||||||
|
if let Some(h) = msg.headers().get(Self::name()) {
|
||||||
|
Self::from_raw(&h)
|
||||||
|
} else {
|
||||||
|
Err(::error::ParseError::Header)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +178,7 @@ impl fmt::Display for ContentDisposition {
|
|||||||
let mut use_simple_format: bool = false;
|
let mut use_simple_format: bool = false;
|
||||||
if opt_lang.is_none() {
|
if opt_lang.is_none() {
|
||||||
if let Charset::Ext(ref ext) = *charset {
|
if let Charset::Ext(ref ext) = *charset {
|
||||||
if unicase::eq_ascii(&**ext, "utf-8") {
|
if ext.eq_ignore_ascii_case("utf-8") {
|
||||||
use_simple_format = true;
|
use_simple_format = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,7 +195,7 @@ impl fmt::Display for ContentDisposition {
|
|||||||
try!(write!(f, "{}", lang));
|
try!(write!(f, "{}", lang));
|
||||||
};
|
};
|
||||||
try!(write!(f, "'"));
|
try!(write!(f, "'"));
|
||||||
try!(http_percent_encode(f, bytes))
|
try!(header::http_percent_encode(f, bytes))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)),
|
DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)),
|
||||||
@ -196,15 +208,14 @@ impl fmt::Display for ContentDisposition {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{ContentDisposition,DispositionType,DispositionParam};
|
use super::{ContentDisposition,DispositionType,DispositionParam};
|
||||||
use ::header::Header;
|
use header::HeaderValue;
|
||||||
use ::header::shared::Charset;
|
use header::shared::Charset;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_header() {
|
fn test_from_raw() {
|
||||||
assert!(ContentDisposition::parse_header(&"".into()).is_err());
|
assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err());
|
||||||
|
|
||||||
let a = "form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".into();
|
let a = HeaderValue::from_static("form-data; dummy=3; name=upload; filename=\"sample.png\"");
|
||||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
|
||||||
let b = ContentDisposition {
|
let b = ContentDisposition {
|
||||||
disposition: DispositionType::Ext("form-data".to_owned()),
|
disposition: DispositionType::Ext("form-data".to_owned()),
|
||||||
parameters: vec![
|
parameters: vec![
|
||||||
@ -217,8 +228,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(a, b);
|
assert_eq!(a, b);
|
||||||
|
|
||||||
let a = "attachment; filename=\"image.jpg\"".into();
|
let a = HeaderValue::from_static("attachment; filename=\"image.jpg\"");
|
||||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
|
||||||
let b = ContentDisposition {
|
let b = ContentDisposition {
|
||||||
disposition: DispositionType::Attachment,
|
disposition: DispositionType::Attachment,
|
||||||
parameters: vec![
|
parameters: vec![
|
||||||
@ -229,8 +240,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(a, b);
|
assert_eq!(a, b);
|
||||||
|
|
||||||
let a = "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".into();
|
let a = HeaderValue::from_static("attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates");
|
||||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
|
||||||
let b = ContentDisposition {
|
let b = ContentDisposition {
|
||||||
disposition: DispositionType::Attachment,
|
disposition: DispositionType::Attachment,
|
||||||
parameters: vec![
|
parameters: vec![
|
||||||
@ -246,18 +257,18 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
|
let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
|
||||||
let a = as_string.into();
|
let a = HeaderValue::from_static(as_string);
|
||||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
|
||||||
let display_rendered = format!("{}",a);
|
let display_rendered = format!("{}",a);
|
||||||
assert_eq!(as_string, display_rendered);
|
assert_eq!(as_string, display_rendered);
|
||||||
|
|
||||||
let a = "attachment; filename*=UTF-8''black%20and%20white.csv".into();
|
let a = HeaderValue::from_static("attachment; filename*=UTF-8''black%20and%20white.csv");
|
||||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
|
||||||
let display_rendered = format!("{}",a);
|
let display_rendered = format!("{}",a);
|
||||||
assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered);
|
assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered);
|
||||||
|
|
||||||
let a = "attachment; filename=colourful.csv".into();
|
let a = HeaderValue::from_static("attachment; filename=colourful.csv");
|
||||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
|
||||||
let display_rendered = format!("{}",a);
|
let display_rendered = format!("{}",a);
|
||||||
assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered);
|
assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered);
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ pub use self::accept_language::AcceptLanguage;
|
|||||||
pub use self::accept::Accept;
|
pub use self::accept::Accept;
|
||||||
pub use self::allow::Allow;
|
pub use self::allow::Allow;
|
||||||
pub use self::cache_control::{CacheControl, CacheDirective};
|
pub use self::cache_control::{CacheControl, CacheDirective};
|
||||||
//pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam};
|
pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam};
|
||||||
pub use self::content_language::ContentLanguage;
|
pub use self::content_language::ContentLanguage;
|
||||||
pub use self::content_range::{ContentRange, ContentRangeSpec};
|
pub use self::content_range::{ContentRange, ContentRangeSpec};
|
||||||
pub use self::content_type::ContentType;
|
pub use self::content_type::ContentType;
|
||||||
@ -334,7 +334,7 @@ mod accept_language;
|
|||||||
mod accept;
|
mod accept;
|
||||||
mod allow;
|
mod allow;
|
||||||
mod cache_control;
|
mod cache_control;
|
||||||
//mod content_disposition;
|
mod content_disposition;
|
||||||
mod content_language;
|
mod content_language;
|
||||||
mod content_range;
|
mod content_range;
|
||||||
mod content_type;
|
mod content_type;
|
||||||
|
@ -8,6 +8,7 @@ use bytes::{Bytes, BytesMut};
|
|||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use modhttp::header::GetAll;
|
use modhttp::header::GetAll;
|
||||||
use modhttp::Error as HttpError;
|
use modhttp::Error as HttpError;
|
||||||
|
use percent_encoding;
|
||||||
|
|
||||||
pub use modhttp::header::*;
|
pub use modhttp::header::*;
|
||||||
|
|
||||||
@ -259,3 +260,197 @@ where
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From hyper v0.11.27 src/header/parsing.rs
|
||||||
|
|
||||||
|
/// An extended header parameter value (i.e., tagged with a character set and optionally,
|
||||||
|
/// a language), as defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct ExtendedValue {
|
||||||
|
/// The character set that is used to encode the `value` to a string.
|
||||||
|
pub charset: Charset,
|
||||||
|
/// The human language details of the `value`, if available.
|
||||||
|
pub language_tag: Option<LanguageTag>,
|
||||||
|
/// The parameter value, as expressed in octets.
|
||||||
|
pub value: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses extended header parameter values (`ext-value`), as defined in
|
||||||
|
/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
|
||||||
|
///
|
||||||
|
/// Extended values are denoted by parameter names that end with `*`.
|
||||||
|
///
|
||||||
|
/// ## ABNF
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// 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 ( "*" / "'" / "%" )
|
||||||
|
/// ```
|
||||||
|
pub fn parse_extended_value(val: &str) -> Result<ExtendedValue, ::error::ParseError> {
|
||||||
|
|
||||||
|
// 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::ParseError::Header),
|
||||||
|
Some(n) => FromStr::from_str(n).map_err(|_| ::error::ParseError::Header)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interpret the second piece as a language tag
|
||||||
|
let lang: Option<LanguageTag> = match parts.next() {
|
||||||
|
None => return Err(::error::ParseError::Header),
|
||||||
|
Some("") => None,
|
||||||
|
Some(s) => match s.parse() {
|
||||||
|
Ok(lt) => Some(lt),
|
||||||
|
Err(_) => return Err(::error::ParseError::Header),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interpret the third piece as a sequence of value characters
|
||||||
|
let value: Vec<u8> = match parts.next() {
|
||||||
|
None => return Err(::error::ParseError::Header),
|
||||||
|
Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ExtendedValue {
|
||||||
|
charset: charset,
|
||||||
|
language_tag: lang,
|
||||||
|
value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl fmt::Display for ExtendedValue {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let encoded_value =
|
||||||
|
percent_encoding::percent_encode(&self.value[..], self::percent_encoding_http::HTTP_VALUE);
|
||||||
|
if let Some(ref lang) = self.language_tag {
|
||||||
|
write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}''{}", self.charset, encoded_value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Percent encode a sequence of bytes with a character set defined in
|
||||||
|
/// [https://tools.ietf.org/html/rfc5987#section-3.2][url]
|
||||||
|
///
|
||||||
|
/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2
|
||||||
|
pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result {
|
||||||
|
let encoded = percent_encoding::percent_encode(bytes, self::percent_encoding_http::HTTP_VALUE);
|
||||||
|
fmt::Display::fmt(&encoded, f)
|
||||||
|
}
|
||||||
|
mod percent_encoding_http {
|
||||||
|
use percent_encoding;
|
||||||
|
|
||||||
|
// internal module because macro is hard-coded to make a public item
|
||||||
|
// but we don't want to public export this item
|
||||||
|
define_encode_set! {
|
||||||
|
// This encode set is used for HTTP header values and is defined at
|
||||||
|
// https://tools.ietf.org/html/rfc5987#section-3.2
|
||||||
|
pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | {
|
||||||
|
' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?',
|
||||||
|
'[', '\\', ']', '{', '}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use header::shared::Charset;
|
||||||
|
use super::{ExtendedValue, parse_extended_value};
|
||||||
|
use language_tags::LanguageTag;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_extended_value_with_encoding_and_language_tag() {
|
||||||
|
let expected_language_tag = "en".parse::<LanguageTag>().unwrap();
|
||||||
|
// RFC 5987, Section 3.2.2
|
||||||
|
// Extended notation, using the Unicode character U+00A3 (POUND SIGN)
|
||||||
|
let result = parse_extended_value("iso-8859-1'en'%A3%20rates");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let extended_value = result.unwrap();
|
||||||
|
assert_eq!(Charset::Iso_8859_1, extended_value.charset);
|
||||||
|
assert!(extended_value.language_tag.is_some());
|
||||||
|
assert_eq!(expected_language_tag, extended_value.language_tag.unwrap());
|
||||||
|
assert_eq!(vec![163, b' ', b'r', b'a', b't', b'e', b's'], extended_value.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_extended_value_with_encoding() {
|
||||||
|
// RFC 5987, Section 3.2.2
|
||||||
|
// Extended notation, using the Unicode characters U+00A3 (POUND SIGN)
|
||||||
|
// and U+20AC (EURO SIGN)
|
||||||
|
let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let extended_value = result.unwrap();
|
||||||
|
assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset);
|
||||||
|
assert!(extended_value.language_tag.is_none());
|
||||||
|
assert_eq!(vec![194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', b't', b'e', b's'], extended_value.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_extended_value_missing_language_tag_and_encoding() {
|
||||||
|
// From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2
|
||||||
|
let result = parse_extended_value("foo%20bar.html");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_extended_value_partially_formatted() {
|
||||||
|
let result = parse_extended_value("UTF-8'missing third part");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_extended_value_partially_formatted_blank() {
|
||||||
|
let result = parse_extended_value("blank second part'");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fmt_extended_value_with_encoding_and_language_tag() {
|
||||||
|
let extended_value = ExtendedValue {
|
||||||
|
charset: Charset::Iso_8859_1,
|
||||||
|
language_tag: Some("en".parse().expect("Could not parse language tag")),
|
||||||
|
value: vec![163, b' ', b'r', b'a', b't', b'e', b's'],
|
||||||
|
};
|
||||||
|
assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fmt_extended_value_with_encoding() {
|
||||||
|
let extended_value = ExtendedValue {
|
||||||
|
charset: Charset::Ext("UTF-8".to_string()),
|
||||||
|
language_tag: None,
|
||||||
|
value: vec![194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
|
||||||
|
b't', b'e', b's'],
|
||||||
|
};
|
||||||
|
assert_eq!("UTF-8''%C2%A3%20and%20%E2%82%AC%20rates",
|
||||||
|
format!("{}", extended_value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -128,6 +128,7 @@ extern crate encoding;
|
|||||||
extern crate flate2;
|
extern crate flate2;
|
||||||
extern crate h2 as http2;
|
extern crate h2 as http2;
|
||||||
extern crate num_cpus;
|
extern crate num_cpus;
|
||||||
|
#[macro_use]
|
||||||
extern crate percent_encoding;
|
extern crate percent_encoding;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
extern crate serde_urlencoded;
|
extern crate serde_urlencoded;
|
||||||
|
@ -7,7 +7,7 @@ use std::{cmp, fmt};
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::task::{current as current_task, Task};
|
use futures::task::{current as current_task, Task};
|
||||||
use futures::{Async, Poll, Stream};
|
use futures::{Async, Poll, Stream};
|
||||||
use http::header::{self, HeaderMap, HeaderName, HeaderValue};
|
use http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue};
|
||||||
use http::HttpTryFrom;
|
use http::HttpTryFrom;
|
||||||
use httparse;
|
use httparse;
|
||||||
use mime;
|
use mime;
|
||||||
@ -408,6 +408,17 @@ where
|
|||||||
pub fn content_type(&self) -> &mime::Mime {
|
pub fn content_type(&self) -> &mime::Mime {
|
||||||
&self.ct
|
&self.ct
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the content disposition of the field, if it exists
|
||||||
|
pub fn content_disposition(&self) -> Option<ContentDisposition> {
|
||||||
|
// RFC 7578: 'Each part MUST contain a Content-Disposition header field
|
||||||
|
// where the disposition type is "form-data".'
|
||||||
|
if let Some(content_disposition) = self.headers.get(::http::header::CONTENT_DISPOSITION) {
|
||||||
|
ContentDisposition::from_raw(content_disposition).ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> Stream for Field<S>
|
impl<S> Stream for Field<S>
|
||||||
@ -723,6 +734,7 @@ mod tests {
|
|||||||
let bytes = Bytes::from(
|
let bytes = Bytes::from(
|
||||||
"testasdadsad\r\n\
|
"testasdadsad\r\n\
|
||||||
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
|
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
|
||||||
|
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
|
||||||
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
|
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
|
||||||
test\r\n\
|
test\r\n\
|
||||||
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
|
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
|
||||||
@ -738,6 +750,12 @@ mod tests {
|
|||||||
match multipart.poll() {
|
match multipart.poll() {
|
||||||
Ok(Async::Ready(Some(item))) => match item {
|
Ok(Async::Ready(Some(item))) => match item {
|
||||||
MultipartItem::Field(mut field) => {
|
MultipartItem::Field(mut field) => {
|
||||||
|
{
|
||||||
|
use http::header::{DispositionType, DispositionParam};
|
||||||
|
let cd = field.content_disposition().unwrap();
|
||||||
|
assert_eq!(cd.disposition, DispositionType::Ext("form-data".into()));
|
||||||
|
assert_eq!(cd.parameters[0], DispositionParam::Ext("name".into(), "file".into()));
|
||||||
|
}
|
||||||
assert_eq!(field.content_type().type_(), mime::TEXT);
|
assert_eq!(field.content_type().type_(), mime::TEXT);
|
||||||
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user