refactor(ntex-files): Remove hyperx as dependencies and add required header (#8)

* refactor(ntex-files): Remove hyperx as dependencies and added required header

* bugfix(ntex-files): doc tests

* refactor(ntex-files): Clippy
This commit is contained in:
leone 2023-04-09 14:26:07 +02:00 committed by GitHub
parent 7f7a9faaab
commit be1eb01083
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 2433 additions and 50 deletions

View file

@ -20,16 +20,19 @@ path = "src/lib.rs"
[dependencies]
ntex = "0.6.5"
ntex-http = "0.1.8"
bitflags = "1.3"
bitflags = "2.1"
futures = "0.3"
derive_more = "0.99"
http = "0.2"
hyperx = "1.4.0"
log = "0.4"
mime = "0.3"
mime_guess = "2.0.1"
percent-encoding = "2.1"
v_htmlescape = "0.14.1"
v_htmlescape = "0.15.8"
unicase = "2.6.0"
language-tags = "0.3.2"
httpdate = "1.0.2"
[dev-dependencies]
ntex = { version = "0.6.5", features=["tokio", "openssl", "compress"] }

View file

@ -0,0 +1,153 @@
use std::fmt::{self, Display};
use std::str::FromStr;
use self::Charset::*;
use super::error;
/// A Mime charset.
///
/// The string representation is normalised to upper case.
///
/// See [http://www.iana.org/assignments/character-sets/character-sets.xhtml][url].
///
/// [url]: http://www.iana.org/assignments/character-sets/character-sets.xhtml
#[derive(Clone, Debug, PartialEq)]
#[allow(non_camel_case_types)]
pub enum Charset {
/// US ASCII
Us_Ascii,
/// ISO-8859-1
Iso_8859_1,
/// ISO-8859-2
Iso_8859_2,
/// ISO-8859-3
Iso_8859_3,
/// ISO-8859-4
Iso_8859_4,
/// ISO-8859-5
Iso_8859_5,
/// ISO-8859-6
Iso_8859_6,
/// ISO-8859-7
Iso_8859_7,
/// ISO-8859-8
Iso_8859_8,
/// ISO-8859-9
Iso_8859_9,
/// ISO-8859-10
Iso_8859_10,
/// Shift_JIS
Shift_Jis,
/// EUC-JP
Euc_Jp,
/// ISO-2022-KR
Iso_2022_Kr,
/// EUC-KR
Euc_Kr,
/// ISO-2022-JP
Iso_2022_Jp,
/// ISO-2022-JP-2
Iso_2022_Jp_2,
/// ISO-8859-6-E
Iso_8859_6_E,
/// ISO-8859-6-I
Iso_8859_6_I,
/// ISO-8859-8-E
Iso_8859_8_E,
/// ISO-8859-8-I
Iso_8859_8_I,
/// GB2312
Gb2312,
/// Big5
Big5,
/// KOI8-R
Koi8_R,
/// An arbitrary charset specified as a string
Ext(String),
}
impl Charset {
fn name(&self) -> &str {
match *self {
Us_Ascii => "US-ASCII",
Iso_8859_1 => "ISO-8859-1",
Iso_8859_2 => "ISO-8859-2",
Iso_8859_3 => "ISO-8859-3",
Iso_8859_4 => "ISO-8859-4",
Iso_8859_5 => "ISO-8859-5",
Iso_8859_6 => "ISO-8859-6",
Iso_8859_7 => "ISO-8859-7",
Iso_8859_8 => "ISO-8859-8",
Iso_8859_9 => "ISO-8859-9",
Iso_8859_10 => "ISO-8859-10",
Shift_Jis => "Shift-JIS",
Euc_Jp => "EUC-JP",
Iso_2022_Kr => "ISO-2022-KR",
Euc_Kr => "EUC-KR",
Iso_2022_Jp => "ISO-2022-JP",
Iso_2022_Jp_2 => "ISO-2022-JP-2",
Iso_8859_6_E => "ISO-8859-6-E",
Iso_8859_6_I => "ISO-8859-6-I",
Iso_8859_8_E => "ISO-8859-8-E",
Iso_8859_8_I => "ISO-8859-8-I",
Gb2312 => "GB2312",
Big5 => "5",
Koi8_R => "KOI8-R",
Ext(ref s) => s,
}
}
}
impl Display for Charset {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.name())
}
}
impl FromStr for Charset {
type Err = error::Error;
fn from_str(s: &str) -> error::Result<Charset> {
Ok(match s.to_ascii_uppercase().as_ref() {
"US-ASCII" => Us_Ascii,
"ISO-8859-1" => Iso_8859_1,
"ISO-8859-2" => Iso_8859_2,
"ISO-8859-3" => Iso_8859_3,
"ISO-8859-4" => Iso_8859_4,
"ISO-8859-5" => Iso_8859_5,
"ISO-8859-6" => Iso_8859_6,
"ISO-8859-7" => Iso_8859_7,
"ISO-8859-8" => Iso_8859_8,
"ISO-8859-9" => Iso_8859_9,
"ISO-8859-10" => Iso_8859_10,
"SHIFT-JIS" => Shift_Jis,
"EUC-JP" => Euc_Jp,
"ISO-2022-KR" => Iso_2022_Kr,
"EUC-KR" => Euc_Kr,
"ISO-2022-JP" => Iso_2022_Jp,
"ISO-2022-JP-2" => Iso_2022_Jp_2,
"ISO-8859-6-E" => Iso_8859_6_E,
"ISO-8859-6-I" => Iso_8859_6_I,
"ISO-8859-8-E" => Iso_8859_8_E,
"ISO-8859-8-I" => Iso_8859_8_I,
"GB2312" => Gb2312,
"5" => Big5,
"KOI8-R" => Koi8_R,
s => Ext(s.to_owned()),
})
}
}
#[test]
fn test_parse() {
assert_eq!(Us_Ascii, "us-ascii".parse().unwrap());
assert_eq!(Us_Ascii, "US-Ascii".parse().unwrap());
assert_eq!(Us_Ascii, "US-ASCII".parse().unwrap());
assert_eq!(Shift_Jis, "Shift-JIS".parse().unwrap());
assert_eq!(Ext("ABCD".to_owned()), "abcd".parse().unwrap());
}
#[test]
fn test_display() {
assert_eq!("US-ASCII", format!("{}", Us_Ascii));
assert_eq!("ABCD", format!("{}", Ext("ABCD".to_owned())));
}

View file

@ -0,0 +1,174 @@
use super::{EntityTag, HttpDate};
use crate::{header, standard_header};
header! {
/// `If-Unmodified-Since` header, defined in
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4)
///
/// The `If-Unmodified-Since` header field makes the request method
/// conditional on the selected representation's last modification date
/// being earlier than or equal to the date provided in the field-value.
/// This field accomplishes the same purpose as If-Match for cases where
/// the user agent does not have an entity-tag for the representation.
///
/// # ABNF
///
/// ```text
/// If-Unmodified-Since = HTTP-date
/// ```
///
/// # Example values
///
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
(IfUnmodifiedSince, "If-Unmodified-Since") => [HttpDate]
}
standard_header!(IfUnmodifiedSince, IF_UNMODIFIED_SINCE);
header! {
/// `If-Modified-Since` header, defined in
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3)
///
/// The `If-Modified-Since` header field makes a GET or HEAD request
/// method conditional on the selected representation's modification date
/// being more recent than the date provided in the field-value.
/// Transfer of the selected representation's data is avoided if that
/// data has not changed.
///
/// # ABNF
///
/// ```text
/// If-Unmodified-Since = HTTP-date
/// ```
///
/// # Example values
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
(IfModifiedSince, "If-Modified-Since") => [HttpDate]
}
standard_header!(IfModifiedSince, IF_MODIFIED_SINCE);
header! {
/// `Last-Modified` header, defined in
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2)
///
/// The `Last-Modified` header field in a response provides a timestamp
/// indicating the date and time at which the origin server believes the
/// selected representation was last modified, as determined at the
/// conclusion of handling the request.
///
/// # ABNF
///
/// ```text
/// Expires = HTTP-date
/// ```
///
/// # Example values
///
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
(LastModified, "Last-Modified") => [HttpDate]
}
standard_header!(LastModified, LAST_MODIFIED);
header! {
/// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3)
///
/// The `ETag` header field in a response provides the current entity-tag
/// for the selected representation, as determined at the conclusion of
/// handling the request. An entity-tag is an opaque validator for
/// differentiating between multiple representations of the same
/// resource, regardless of whether those multiple representations are
/// due to resource state changes over time, content negotiation
/// resulting in multiple representations being valid at the same time,
/// or both. An entity-tag consists of an opaque quoted string, possibly
/// prefixed by a weakness indicator.
///
/// # ABNF
///
/// ```text
/// ETag = entity-tag
/// ```
///
/// # Example values
///
/// * `"xyzzy"`
/// * `W/"xyzzy"`
/// * `""`
///
(ETag, "ETag") => [EntityTag]
}
standard_header!(ETag, ETAG);
header! {
/// `If-None-Match` header, defined in
/// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2)
///
/// The `If-None-Match` header field makes the request method conditional
/// on a recipient cache or origin server either not having any current
/// representation of the target resource, when the field-value is "*",
/// or having a selected representation with an entity-tag that does not
/// match any of those listed in the field-value.
///
/// A recipient MUST use the weak comparison function when comparing
/// entity-tags for If-None-Match (Section 2.3.2), since weak entity-tags
/// can be used for cache validation even if there have been changes to
/// the representation data.
///
/// # ABNF
///
/// ```text
/// If-None-Match = "*" / 1#entity-tag
/// ```
///
/// # Example values
///
/// * `"xyzzy"`
/// * `W/"xyzzy"`
/// * `"xyzzy", "r2d2xxxx", "c3piozzzz"`
/// * `W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"`
/// * `*`
///
(IfNoneMatch, "If-None-Match") => {Any / (EntityTag)+}
}
standard_header!(IfNoneMatch, IF_NONE_MATCH);
header! {
/// `If-Match` header, defined in
/// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1)
///
/// The `If-Match` header field makes the request method conditional on
/// the recipient origin server either having at least one current
/// representation of the target resource, when the field-value is "*",
/// or having a current representation of the target resource that has an
/// entity-tag matching a member of the list of entity-tags provided in
/// the field-value.
///
/// An origin server MUST use the strong comparison function when
/// comparing entity-tags for `If-Match`, since the client
/// intends this precondition to prevent the method from being applied if
/// there have been any changes to the representation data.
///
/// # ABNF
///
/// ```text
/// If-Match = "*" / 1#entity-tag
/// ```
///
/// # Example values
///
/// * `"xyzzy"`
/// * "xyzzy", "r2d2xxxx", "c3piozzzz"
///
(IfMatch, "If-Match") => {Any / (EntityTag)+}
}
standard_header!(IfMatch, IF_MATCH);

View file

@ -0,0 +1,266 @@
// # 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 crate::standard_header;
use super::error;
use super::parsing::{self, http_percent_encode, parse_extended_value};
use super::{Charset, Header, RawLike};
/// 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
/// ```text
/// 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 "*">
/// ```
///
#[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 {
static NAME: &str = "Content-Disposition";
NAME
}
fn parse_header<'a, T>(raw: &'a T) -> error::Result<ContentDisposition>
where
T: RawLike<'a>,
{
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::Error::Header),
};
let mut cd = ContentDisposition {
disposition: if unicase::eq_ascii(disposition, "inline") {
DispositionType::Inline
} else if unicase::eq_ascii(disposition, "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::Error::Header);
};
let val = if let Some(val) = parts.next() {
val.trim()
} else {
return Err(error::Error::Header);
};
cd.parameters.push(if unicase::eq_ascii(key, "filename") {
DispositionParam::Filename(
Charset::Ext("UTF-8".to_owned()),
None,
val.trim_matches('"').as_bytes().to_owned(),
)
} else if unicase::eq_ascii(key, "filename*") {
let extended_value = parse_extended_value(val)?;
DispositionParam::Filename(
extended_value.charset,
extended_value.language_tag,
extended_value.value,
)
} else {
DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned())
});
}
Ok(cd)
})
}
#[inline]
fn fmt_header(&self, f: &mut super::Formatter) -> fmt::Result {
f.fmt_line(self)
}
}
impl fmt::Display for ContentDisposition {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.disposition {
DispositionType::Inline => write!(f, "inline")?,
DispositionType::Attachment => write!(f, "attachment")?,
DispositionType::Ext(ref s) => write!(f, "{}", s)?,
}
for param in &self.parameters {
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::eq_ascii(&**ext, "utf-8") {
use_simple_format = true;
}
}
}
if use_simple_format {
write!(
f,
"; filename=\"{}\"",
match String::from_utf8(bytes.clone()) {
Ok(s) => s,
Err(_) => return Err(fmt::Error),
}
)?;
} else {
write!(f, "; filename*={}'", charset)?;
if let Some(ref lang) = *opt_lang {
write!(f, "{}", lang)?;
};
write!(f, "'")?;
http_percent_encode(f, bytes)?;
}
}
DispositionParam::Ext(ref k, ref v) => write!(f, "; {}=\"{}\"", k, v)?,
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{Charset, ContentDisposition, DispositionParam, DispositionType, Header};
use crate::file_header::Raw;
#[test]
fn test_parse_header() {
let a: Raw = "".into();
assert!(ContentDisposition::parse_header(&a).is_err());
let a: Raw = "form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).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: Raw = "attachment; filename=\"image.jpg\"".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).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: Raw = "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).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 as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
let a: Raw = as_string.into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!(as_string, display_rendered);
let a: Raw = "attachment; filename*=UTF-8''black%20and%20white.csv".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered);
let a: Raw = "attachment; filename=colourful.csv".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered);
}
}
standard_header!(ContentDisposition, CONTENT_DISPOSITION);

View file

@ -0,0 +1,227 @@
use super::error::{Error, Result};
use std::fmt::{self, Display};
use std::str::FromStr;
/// check that each char in the slice is either:
/// 1. `%x21`, or
/// 2. in the range `%x23` to `%x7E`, or
/// 3. above `%x80`
fn check_slice_validity(slice: &str) -> bool {
slice.bytes().all(|c| c == b'\x21' || (b'\x23'..=b'\x7e').contains(&c) | (c >= b'\x80'))
}
/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3)
///
/// An entity tag consists of a string enclosed by two literal double quotes.
/// Preceding the first double quote is an optional weakness indicator,
/// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and `W/"xyzzy"`.
///
/// # ABNF
///
/// ```text
/// entity-tag = [ weak ] opaque-tag
/// weak = %x57.2F ; "W/", case-sensitive
/// opaque-tag = DQUOTE *etagc DQUOTE
/// etagc = %x21 / %x23-7E / obs-text
/// ; VCHAR except double quotes, plus obs-text
/// ```
///
/// # Comparison
/// To check if two entity tags are equivalent in an application always use the `strong_eq` or
/// `weak_eq` methods based on the context of the Tag. Only use `==` to check if two tags are
/// identical.
///
/// The example below shows the results for a set of entity-tag pairs and
/// both the weak and strong comparison function results:
///
/// | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison |
/// |---------|---------|-------------------|-----------------|
/// | `W/"1"` | `W/"1"` | no match | match |
/// | `W/"1"` | `W/"2"` | no match | no match |
/// | `W/"1"` | `"1"` | no match | match |
/// | `"1"` | `"1"` | match | match |
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntityTag {
/// Weakness indicator for the tag
pub weak: bool,
/// The opaque string in between the DQUOTEs
tag: String,
}
impl EntityTag {
/// Constructs a new EntityTag.
/// # Panics
/// If the tag contains invalid characters.
pub fn new(weak: bool, tag: String) -> EntityTag {
assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
EntityTag { weak, tag }
}
/// Constructs a new weak EntityTag.
/// # Panics
/// If the tag contains invalid characters.
pub fn weak(tag: String) -> EntityTag {
EntityTag::new(true, tag)
}
/// Constructs a new strong EntityTag.
/// # Panics
/// If the tag contains invalid characters.
pub fn strong(tag: String) -> EntityTag {
EntityTag::new(false, tag)
}
/// Get the tag.
pub fn tag(&self) -> &str {
self.tag.as_ref()
}
/// Set the tag.
/// # Panics
/// If the tag contains invalid characters.
pub fn set_tag(&mut self, tag: String) {
assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
self.tag = tag
}
/// For strong comparison two entity-tags are equivalent if both are not weak and their
/// opaque-tags match character-by-character.
pub fn strong_eq(&self, other: &EntityTag) -> bool {
!self.weak && !other.weak && self.tag == other.tag
}
/// For weak comparison two entity-tags are equivalent if their
/// opaque-tags match character-by-character, regardless of either or
/// both being tagged as "weak".
pub fn weak_eq(&self, other: &EntityTag) -> bool {
self.tag == other.tag
}
/// The inverse of `EntityTag.strong_eq()`.
pub fn strong_ne(&self, other: &EntityTag) -> bool {
!self.strong_eq(other)
}
/// The inverse of `EntityTag.weak_eq()`.
pub fn weak_ne(&self, other: &EntityTag) -> bool {
!self.weak_eq(other)
}
}
impl Display for EntityTag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.weak {
write!(f, "W/\"{}\"", self.tag)
} else {
write!(f, "\"{}\"", self.tag)
}
}
}
impl FromStr for EntityTag {
type Err = Error;
fn from_str(s: &str) -> Result<EntityTag> {
let length: usize = s.len();
let slice = &s[..];
// Early exits if it doesn't terminate in a DQUOTE.
if !slice.ends_with('"') || slice.len() < 2 {
return Err(Error::Header);
}
// The etag is weak if its first char is not a DQUOTE.
if slice.len() >= 2
&& slice.starts_with('"')
&& check_slice_validity(&slice[1..length - 1])
{
// No need to check if the last char is a DQUOTE,
// we already did that above.
return Ok(EntityTag { weak: false, tag: slice[1..length - 1].to_owned() });
} else if slice.len() >= 4
&& slice.starts_with("W/\"")
&& check_slice_validity(&slice[3..length - 1])
{
return Ok(EntityTag { weak: true, tag: slice[3..length - 1].to_owned() });
}
Err(Error::Header)
}
}
#[cfg(test)]
mod tests {
use super::EntityTag;
#[test]
fn test_etag_parse_success() {
// Expected success
assert_eq!(
"\"foobar\"".parse::<EntityTag>().unwrap(),
EntityTag::strong("foobar".to_owned())
);
assert_eq!("\"\"".parse::<EntityTag>().unwrap(), EntityTag::strong("".to_owned()));
assert_eq!(
"W/\"weaktag\"".parse::<EntityTag>().unwrap(),
EntityTag::weak("weaktag".to_owned())
);
assert_eq!(
"W/\"\x65\x62\"".parse::<EntityTag>().unwrap(),
EntityTag::weak("\x65\x62".to_owned())
);
assert_eq!("W/\"\"".parse::<EntityTag>().unwrap(), EntityTag::weak("".to_owned()));
}
#[test]
fn test_etag_parse_failures() {
// Expected failures
assert!("no-dquotes".parse::<EntityTag>().is_err());
assert!("w/\"the-first-w-is-case-sensitive\"".parse::<EntityTag>().is_err());
assert!("".parse::<EntityTag>().is_err());
assert!("\"unmatched-dquotes1".parse::<EntityTag>().is_err());
assert!("unmatched-dquotes2\"".parse::<EntityTag>().is_err());
assert!("matched-\"dquotes\"".parse::<EntityTag>().is_err());
}
#[test]
fn test_etag_fmt() {
assert_eq!(format!("{}", EntityTag::strong("foobar".to_owned())), "\"foobar\"");
assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\"");
assert_eq!(format!("{}", EntityTag::weak("weak-etag".to_owned())), "W/\"weak-etag\"");
assert_eq!(format!("{}", EntityTag::weak("\u{0065}".to_owned())), "W/\"\x65\"");
assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\"");
}
#[test]
fn test_cmp() {
// | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison |
// |---------|---------|-------------------|-----------------|
// | `W/"1"` | `W/"1"` | no match | match |
// | `W/"1"` | `W/"2"` | no match | no match |
// | `W/"1"` | `"1"` | no match | match |
// | `"1"` | `"1"` | match | match |
let mut etag1 = EntityTag::weak("1".to_owned());
let mut etag2 = EntityTag::weak("1".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
etag1 = EntityTag::weak("1".to_owned());
etag2 = EntityTag::weak("2".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(!etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(etag1.weak_ne(&etag2));
etag1 = EntityTag::weak("1".to_owned());
etag2 = EntityTag::strong("1".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
etag1 = EntityTag::strong("1".to_owned());
etag2 = EntityTag::strong("1".to_owned());
assert!(etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(!etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
}
}

View file

@ -0,0 +1,94 @@
//! Error and Result module.
use std::error::Error as StdError;
use std::fmt;
use std::str::Utf8Error;
use std::string::FromUtf8Error;
use self::Error::{Header, Method, Status, TooLarge, Utf8, Version};
/// Result type often returned from methods that can have hyper `Error`s.
pub type Result<T> = ::std::result::Result<T, Error>;
/// Errors while parsing headers and associated types.
#[derive(Debug)]
pub enum Error {
/// An invalid `Method`, such as `GE,T`.
Method,
/// An invalid `HttpVersion`, such as `HTP/1.1`
Version,
/// An invalid `Header`.
Header,
/// A message head is too large to be reasonable.
TooLarge,
/// An invalid `Status`, such as `1337 ELITE`.
Status,
/// Parsing a field as string failed.
Utf8(Utf8Error),
#[doc(hidden)]
__Nonexhaustive(Void),
}
#[doc(hidden)]
pub struct Void(());
impl fmt::Debug for Void {
fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result {
unreachable!()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Utf8(ref e) => fmt::Display::fmt(e, f),
ref e => f.write_str(e.static_description()),
}
}
}
impl Error {
fn static_description(&self) -> &str {
match *self {
Method => "invalid Method specified",
Version => "invalid HTTP version specified",
Header => "invalid Header provided",
TooLarge => "message head is too large",
Status => "invalid Status provided",
Utf8(_) => "invalid UTF-8 string",
Error::__Nonexhaustive(..) => unreachable!(),
}
}
}
impl StdError for Error {
fn description(&self) -> &str {
self.static_description()
}
fn cause(&self) -> Option<&dyn StdError> {
match *self {
Utf8(ref error) => Some(error),
Error::__Nonexhaustive(..) => unreachable!(),
_ => None,
}
}
}
impl From<Utf8Error> for Error {
fn from(err: Utf8Error) -> Error {
Utf8(err)
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Utf8(err.utf8_error())
}
}
#[doc(hidden)]
trait AssertSendSync: Send + Sync + 'static {}
#[doc(hidden)]
impl AssertSendSync for Error {}

View file

@ -0,0 +1,86 @@
use std::fmt::{self, Display};
use std::str::FromStr;
use std::time::SystemTime;
use super::error::{Error, Result};
use httpdate::HttpDate as InnerDate;
/// A timestamp with HTTP formatting and parsing
// Prior to 1995, there were three different formats commonly used by
// servers to communicate timestamps. For compatibility with old
// implementations, all three are defined here. The preferred format is
// a fixed-length and single-zone subset of the date and time
// specification used by the Internet Message Format [RFC5322].
//
// HTTP-date = IMF-fixdate / obs-date
//
// An example of the preferred format is
//
// Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate
//
// Examples of the two obsolete formats are
//
// Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format
// Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
//
// A recipient that parses a timestamp value in an HTTP header field
// MUST accept all three HTTP-date formats. When a sender generates a
// header field that contains one or more timestamps defined as
// HTTP-date, the sender MUST generate those timestamps in the
// IMF-fixdate format.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HttpDate(InnerDate);
impl FromStr for HttpDate {
type Err = Error;
fn from_str(s: &str) -> Result<HttpDate> {
InnerDate::from_str(s).map(HttpDate).map_err(|_| Error::Header)
}
}
impl Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl From<SystemTime> for HttpDate {
fn from(sys: SystemTime) -> HttpDate {
HttpDate(sys.into())
}
}
impl From<HttpDate> for SystemTime {
fn from(date: HttpDate) -> SystemTime {
date.0.into()
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};
use super::HttpDate;
macro_rules! test_parse {
($function: ident, $date: expr) => {
#[test]
fn $function() {
let nov_07 =
HttpDate((SystemTime::UNIX_EPOCH + Duration::new(784198117, 0)).into());
assert_eq!($date.parse::<HttpDate>().unwrap(), nov_07);
}
};
}
test_parse!(test_imf_fixdate, "Mon, 07 Nov 1994 08:48:37 GMT");
test_parse!(test_rfc_850, "Monday, 07-Nov-94 08:48:37 GMT");
test_parse!(test_asctime, "Mon Nov 7 08:48:37 1994");
#[test]
fn test_no_date() {
assert!("this-is-no-date".parse::<HttpDate>().is_err());
}
}

View file

@ -0,0 +1,252 @@
//! The HTTP request method
use std::convert::{AsRef, TryFrom};
use std::fmt;
use std::str::FromStr;
use self::Method::{Connect, Delete, Extension, Get, Head, Options, Patch, Post, Put, Trace};
/// The Request Method (VERB)
///
/// Currently includes 8 variants representing the 8 methods defined in
/// [RFC 7230](https://tools.ietf.org/html/rfc7231#section-4.1), plus PATCH,
/// and an Extension variant for all extensions.
///
/// It may make sense to grow this to include all variants currently
/// registered with IANA, if they are at all common to use.
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum Method {
/// OPTIONS
Options,
/// GET
Get,
/// POST
Post,
/// PUT
Put,
/// DELETE
Delete,
/// HEAD
Head,
/// TRACE
Trace,
/// CONNECT
Connect,
/// PATCH
Patch,
/// Method extensions. An example would be `let m = Extension("FOO".to_string())`.
Extension(String),
}
impl AsRef<str> for Method {
fn as_ref(&self) -> &str {
match *self {
Options => "OPTIONS",
Get => "GET",
Post => "POST",
Put => "PUT",
Delete => "DELETE",
Head => "HEAD",
Trace => "TRACE",
Connect => "CONNECT",
Patch => "PATCH",
Extension(ref s) => s.as_ref(),
}
}
}
impl Method {
/// Whether a method is considered "safe", meaning the request is
/// essentially read-only.
///
/// See [the spec](https://tools.ietf.org/html/rfc7231#section-4.2.1)
/// for more words.
pub fn safe(&self) -> bool {
matches!(*self, Get | Head | Options | Trace)
}
/// Whether a method is considered "idempotent", meaning the request has
/// the same result if executed multiple times.
///
/// See [the spec](https://tools.ietf.org/html/rfc7231#section-4.2.2) for
/// more words.
pub fn idempotent(&self) -> bool {
if self.safe() {
true
} else {
matches!(*self, Put | Delete)
}
}
}
macro_rules! from_str {
($s:ident, { $($n:pat => { $($text:pat => $var:ident,)* },)* }) => ({
let s = $s;
match s.len() {
$(
$n => match s {
$(
$text => return Ok($var),
)*
_ => {},
},
)*
0 => return Err(super::error::Error::Method),
_ => {},
}
Ok(Extension(s.to_owned()))
})
}
impl FromStr for Method {
type Err = super::error::Error;
fn from_str(s: &str) -> Result<Method, super::error::Error> {
from_str!(s, {
3 => {
"GET" => Get,
"PUT" => Put,
},
4 => {
"HEAD" => Head,
"POST" => Post,
},
5 => {
"PATCH" => Patch,
"TRACE" => Trace,
},
6 => {
"DELETE" => Delete,
},
7 => {
"OPTIONS" => Options,
"CONNECT" => Connect,
},
})
}
}
impl fmt::Display for Method {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(match *self {
Options => "OPTIONS",
Get => "GET",
Post => "POST",
Put => "PUT",
Delete => "DELETE",
Head => "HEAD",
Trace => "TRACE",
Connect => "CONNECT",
Patch => "PATCH",
Extension(ref s) => s.as_ref(),
})
}
}
impl Default for Method {
fn default() -> Method {
Method::Get
}
}
impl From<http::Method> for Method {
fn from(method: http::Method) -> Method {
match method {
http::Method::GET => Method::Get,
http::Method::POST => Method::Post,
http::Method::PUT => Method::Put,
http::Method::DELETE => Method::Delete,
http::Method::HEAD => Method::Head,
http::Method::OPTIONS => Method::Options,
http::Method::CONNECT => Method::Connect,
http::Method::PATCH => Method::Patch,
http::Method::TRACE => Method::Trace,
_ => method.as_ref().parse().expect("attempted to convert invalid method"),
}
}
}
impl From<Method> for http::Method {
fn from(method: Method) -> http::Method {
match method {
Method::Get => http::Method::GET,
Method::Post => http::Method::POST,
Method::Put => http::Method::PUT,
Method::Delete => http::Method::DELETE,
Method::Head => http::Method::HEAD,
Method::Options => http::Method::OPTIONS,
Method::Connect => http::Method::CONNECT,
Method::Patch => http::Method::PATCH,
Method::Trace => http::Method::TRACE,
Method::Extension(s) => {
http::Method::try_from(s.as_str()).expect("attempted to convert invalid method")
}
}
}
}
#[cfg(test)]
mod tests {
use super::Method;
use super::Method::{Extension, Get, Post, Put};
use crate::file_header::error::Error;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::str::FromStr;
#[test]
fn test_safe() {
assert!(Get.safe());
assert!(!Post.safe());
}
#[test]
fn test_idempotent() {
assert!(Get.idempotent());
assert!(Put.idempotent());
assert!(!Post.idempotent());
}
#[test]
fn test_from_str() {
assert_eq!(Get, FromStr::from_str("GET").unwrap());
assert_eq!(Extension("MOVE".to_owned()), FromStr::from_str("MOVE").unwrap());
let x: Result<Method, _> = FromStr::from_str("");
if let Err(Error::Method) = x {
} else {
panic!("An empty method is invalid!")
}
}
#[test]
fn test_fmt() {
assert_eq!("GET".to_owned(), format!("{}", Get));
assert_eq!("MOVE".to_owned(), format!("{}", Extension("MOVE".to_owned())));
}
#[test]
fn test_hashable() {
let mut counter: HashMap<Method, usize> = HashMap::new();
counter.insert(Get, 1);
assert_eq!(Some(&1), counter.get(&Get));
}
#[test]
fn test_as_str() {
assert_eq!(Get.as_ref(), "GET");
assert_eq!(Post.as_ref(), "POST");
assert_eq!(Put.as_ref(), "PUT");
assert_eq!(Extension("MOVE".to_owned()).as_ref(), "MOVE");
}
#[test]
fn test_compat() {
let methods = vec!["GET", "POST", "PUT", "MOVE"];
for method in methods {
let orig_hyper_method = Method::from_str(method).unwrap();
let orig_http_method = http::Method::try_from(method).unwrap();
let conv_hyper_method: Method = orig_http_method.clone().into();
let conv_http_method: http::Method = orig_hyper_method.clone().into();
assert_eq!(orig_hyper_method, conv_hyper_method);
assert_eq!(orig_http_method, conv_http_method);
}
}
}

View file

@ -0,0 +1,521 @@
mod charset;
mod common;
mod content_disposition;
mod entity;
pub(crate) mod error;
mod http_date;
pub(crate) mod method;
pub(crate) mod parsing;
mod raw;
pub use charset::Charset;
pub use common::*;
pub use content_disposition::*;
pub use entity::EntityTag;
use http::HeaderValue;
pub use http_date::HttpDate;
pub use raw::{Raw, RawLike};
use self::sealed::HeaderClone;
/// A trait for any object that will represent a header field and value.
///
/// This trait represents the construction and identification of headers,
/// and contains trait-object unsafe methods.
pub trait Header: 'static + HeaderClone + Send + Sync {
/// Returns the name of the header field this belongs to.
///
/// This will become an associated constant once available.
fn header_name() -> &'static str
where
Self: Sized;
/// Parse a header from a raw stream of bytes.
///
/// It's possible that a request can include a header field more than once,
/// and in that case, the slice will have a length greater than 1. However,
/// it's not necessarily the case that a Header is *allowed* to have more
/// than one field value. If that's the case, you **should** return `None`
/// if `raw.len() > 1`.
fn parse_header<'a, T>(raw: &'a T) -> error::Result<Self>
where
T: RawLike<'a>,
Self: Sized;
/// Format a header to outgoing stream.
///
/// Most headers should be formatted on one line, and so a common pattern
/// would be to implement `std::fmt::Display` for this type as well, and
/// then just call `f.fmt_line(self)`.
///
/// ## Note
///
/// This has the ability to format a header over multiple lines.
///
/// The main example here is `Set-Cookie`, which requires that every
/// cookie being set be specified in a separate line. Almost every other
/// case should only format as 1 single line.
fn fmt_header(&self, f: &mut Formatter) -> std::fmt::Result;
}
mod sealed {
use super::Header;
#[doc(hidden)]
pub trait HeaderClone {
fn clone_box(&self) -> Box<dyn Header + Send + Sync>;
}
impl<T: Header + Clone> HeaderClone for T {
#[inline]
fn clone_box(&self) -> Box<dyn Header + Send + Sync> {
Box::new(self.clone())
}
}
}
/// A formatter used to serialize headers to an output stream.
#[allow(missing_debug_implementations)]
pub struct Formatter<'a, 'b: 'a>(Multi<'a, 'b>);
#[allow(unused)]
enum Multi<'a, 'b: 'a> {
Line(&'a str, &'a mut std::fmt::Formatter<'b>),
Join(bool, &'a mut std::fmt::Formatter<'b>),
Raw(&'a mut raw::Raw),
}
impl<'a, 'b> Formatter<'a, 'b> {
/// Format one 'line' of a header.
///
/// This writes the header name plus the `Display` value as a single line.
///
/// ## Note
///
/// This has the ability to format a header over multiple lines.
///
/// The main example here is `Set-Cookie`, which requires that every
/// cookie being set be specified in a separate line. Almost every other
/// case should only format as 1 single line.
pub fn fmt_line(&mut self, line: &dyn std::fmt::Display) -> std::fmt::Result {
use std::fmt::Write;
match self.0 {
Multi::Line(name, ref mut f) => {
f.write_str(name)?;
f.write_str(": ")?;
write!(NewlineReplacer(*f), "{}", line)?;
f.write_str("\r\n")
}
Multi::Join(ref mut first, ref mut f) => {
if !*first {
f.write_str(", ")?;
} else {
*first = false;
}
write!(NewlineReplacer(*f), "{}", line)
}
Multi::Raw(ref mut raw) => {
let mut s = String::new();
write!(NewlineReplacer(&mut s), "{}", line)?;
raw.push(s);
Ok(())
}
}
}
fn danger_fmt_line_without_newline_replacer<T: std::fmt::Display>(
&mut self,
line: &T,
) -> std::fmt::Result {
use std::fmt::Write;
match self.0 {
Multi::Line(name, ref mut f) => {
f.write_str(name)?;
f.write_str(": ")?;
std::fmt::Display::fmt(line, f)?;
f.write_str("\r\n")
}
Multi::Join(ref mut first, ref mut f) => {
if !*first {
f.write_str(", ")?;
} else {
*first = false;
}
std::fmt::Display::fmt(line, f)
}
Multi::Raw(ref mut raw) => {
let mut s = String::new();
write!(s, "{}", line)?;
raw.push(s);
Ok(())
}
}
}
}
struct NewlineReplacer<'a, F: std::fmt::Write + 'a>(&'a mut F);
impl<'a, F: std::fmt::Write + 'a> std::fmt::Write for NewlineReplacer<'a, F> {
#[inline]
fn write_str(&mut self, s: &str) -> std::fmt::Result {
let mut since = 0;
for (i, &byte) in s.as_bytes().iter().enumerate() {
if byte == b'\r' || byte == b'\n' {
self.0.write_str(&s[since..i])?;
self.0.write_str(" ")?;
since = i + 1;
}
}
if since < s.len() {
self.0.write_str(&s[since..])
} else {
Ok(())
}
}
#[inline]
fn write_fmt(&mut self, args: std::fmt::Arguments) -> std::fmt::Result {
std::fmt::write(self, args)
}
}
/// A trait for the "standard" headers that have an associated `HeaderName`
/// constant in the _http_ crate.
pub trait StandardHeader: Header + Sized {
/// The `HeaderName` from the _http_ crate for this header.
fn http_header_name() -> ::http::header::HeaderName;
}
impl<'a> RawLike<'a> for &'a HeaderValue {
type IntoIter = ::std::iter::Once<&'a [u8]>;
fn len(&'a self) -> usize {
1
}
fn one(&'a self) -> Option<&'a [u8]> {
Some(self.as_bytes())
}
fn iter(&'a self) -> Self::IntoIter {
::std::iter::once(self.as_bytes())
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! standard_header {
($local:ident, $hname:ident) => {
impl $crate::file_header::StandardHeader for $local {
#[inline]
fn http_header_name() -> ::http::header::HeaderName {
::http::header::$hname
}
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __deref__ {
($from:ty => $to:ty) => {
impl ::std::ops::Deref for $from {
type Target = $to;
#[inline]
fn deref(&self) -> &$to {
&self.0
}
}
impl ::std::ops::DerefMut for $from {
#[inline]
fn deref_mut(&mut self) -> &mut $to {
&mut self.0
}
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __tm__ {
($id:ident, $tm:ident{$($tf:item)*}) => {
#[allow(unused_imports)]
#[cfg(test)]
mod $tm{
use std::str;
use $crate::file_header::*;
use mime::*;
use $crate::method::Method;
use super::$id as HeaderField;
$($tf)*
}
}
}
/// Create a custom header type.
#[macro_export]
macro_rules! header {
// $a:meta: Attributes associated with the header item (usually docs)
// $id:ident: Identifier of the header
// $n:expr: Lowercase name of the header
// $nn:expr: Nice name of the header
// List header, zero or more items
($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)*) => {
$(#[$a])*
#[derive(Clone, Debug, PartialEq)]
pub struct $id(pub Vec<$item>);
$crate::__deref__!($id => Vec<$item>);
impl $crate::file_header::Header for $id {
fn header_name() -> &'static str {
static NAME: &'static str = $n;
NAME
}
#[inline]
fn parse_header<'a, T>(raw: &'a T) -> $crate::Result<Self>
where T: $crate::file_header::RawLike<'a>
{
$crate::file_header::parsing::from_comma_delimited(raw).map($id)
}
#[inline]
fn fmt_header(&self, f: &mut $crate::file_header::Formatter) -> ::std::fmt::Result {
f.fmt_line(self)
}
}
impl ::std::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
$crate::file_header::parsing::fmt_comma_delimited(f, &self.0[..])
}
}
};
// List header, one or more items
($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+) => {
$(#[$a])*
#[derive(Clone, Debug, PartialEq)]
pub struct $id(pub Vec<$item>);
$crate::__deref__!($id => Vec<$item>);
impl $crate::file_header::Header for $id {
#[inline]
fn header_name() -> &'static str {
static NAME: &'static str = $n;
NAME
}
#[inline]
fn parse_header<'a, T>(raw: &'a T) -> $crate::Result<Self>
where T: $crate::file_header::RawLike<'a>
{
$crate::file_header::parsing::from_comma_delimited(raw).map($id)
}
#[inline]
fn fmt_header(&self, f: &mut $crate::file_header::Formatter) -> ::std::fmt::Result {
f.fmt_line(self)
}
}
impl ::std::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
$crate::file_header::parsing::fmt_comma_delimited(f, &self.0[..])
}
}
};
// Single value header
($(#[$a:meta])*($id:ident, $n:expr) => [$value:ty]) => {
$(#[$a])*
#[derive(Clone, Debug, PartialEq)]
pub struct $id(pub $value);
$crate::__deref__!($id => $value);
impl $crate::file_header::Header for $id {
#[inline]
fn header_name() -> &'static str {
static NAME: &'static str = $n;
NAME
}
#[inline]
fn parse_header<'a, T>(raw: &'a T) -> $crate::file_header::error::Result<Self>
where T: $crate::file_header::RawLike<'a>
{
$crate::file_header::parsing::from_one_raw_str(raw).map($id)
}
#[inline]
fn fmt_header(&self, f: &mut $crate::file_header::Formatter) -> ::std::fmt::Result {
f.fmt_line(self)
}
}
impl ::std::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::std::fmt::Display::fmt(&self.0, f)
}
}
};
// Single value header (internal)
($(#[$a:meta])*($id:ident, $n:expr) => danger [$value:ty]) => {
$(#[$a])*
#[derive(Clone, Debug, PartialEq)]
pub struct $id(pub $value);
$crate::__deref__!($id => $value);
impl $crate::file_header::Header for $id {
#[inline]
fn header_name() -> &'static str {
static NAME: &'static str = $n;
NAME
}
#[inline]
fn parse_header<'a, T>(raw: &'a T) -> $crate::Result<Self>
where T: $crate::file_header::RawLike<'a>
{
$crate::file_header::parsing::from_one_raw_str(raw).map($id)
}
#[inline]
fn fmt_header(&self, f: &mut $crate::file_header::Formatter) -> ::std::fmt::Result {
f.danger_fmt_line_without_newline_replacer(self)
}
}
impl ::std::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::std::fmt::Display::fmt(&self.0, f)
}
}
};
// Single value cow header
($(#[$a:meta])*($id:ident, $n:expr) => Cow[$value:ty]) => {
$(#[$a])*
#[derive(Clone, Debug, PartialEq)]
pub struct $id(::std::borrow::Cow<'static,$value>);
impl $id {
/// Creates a new $id
pub fn new<I: Into<::std::borrow::Cow<'static,$value>>>(value: I) -> Self {
$id(value.into())
}
}
impl ::std::ops::Deref for $id {
type Target = $value;
#[inline]
fn deref(&self) -> &Self::Target {
&(self.0)
}
}
impl $crate::file_header::Header for $id {
#[inline]
fn header_name() -> &'static str {
static NAME: &'static str = $n;
NAME
}
#[inline]
fn parse_header<'a, T>(raw: &'a T) -> $crate::Result<Self>
where T: $crate::file_header::RawLike<'a>
{
$crate::file_header::parsing::from_one_raw_str::<_, <$value as ::std::borrow::ToOwned>::Owned>(raw).map($id::new)
}
#[inline]
fn fmt_header(&self, f: &mut $crate::file_header::Formatter) -> ::std::fmt::Result {
f.fmt_line(self)
}
}
impl ::std::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::std::fmt::Display::fmt(&self.0, f)
}
}
};
// List header, one or more items with "*" option
($(#[$a:meta])*($id:ident, $n:expr) => {Any / ($item:ty)+}) => {
$(#[$a])*
#[derive(Clone, Debug, PartialEq)]
pub enum $id {
/// Any value is a match
Any,
/// Only the listed items are a match
Items(Vec<$item>),
}
impl $crate::file_header::Header for $id {
#[inline]
fn header_name() -> &'static str {
static NAME: &'static str = $n;
NAME
}
#[inline]
fn parse_header<'a, T>(raw: &'a T) -> $crate::file_header::error::Result<Self>
where T: $crate::file_header::RawLike<'a>
{
// FIXME: Return None if no item is in $id::Only
if let Some(l) = raw.one() {
if l == b"*" {
return Ok($id::Any)
}
}
$crate::file_header::parsing::from_comma_delimited(raw).map($id::Items)
}
#[inline]
fn fmt_header(&self, f: &mut $crate::file_header::Formatter) -> ::std::fmt::Result {
f.fmt_line(self)
}
}
impl ::std::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
match *self {
$id::Any => f.write_str("*"),
$id::Items(ref fields) => $crate::file_header::parsing::fmt_comma_delimited(
f, &fields[..])
}
}
}
};
// optional test module
($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => {
header! {
$(#[$a])*
($id, $n) => ($item)*
}
$crate::__tm__! { $id, $tm { $($tf)* }}
};
($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => {
header! {
$(#[$a])*
($id, $n) => ($item)+
}
$crate::__tm__! { $id, $tm { $($tf)* }}
};
($(#[$a:meta])*($id:ident, $n:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => {
header! {
$(#[$a])*
($id, $n) => [$item]
}
$crate::__tm__! { $id, $tm { $($tf)* }}
};
($(#[$a:meta])*($id:ident, $n:expr) => danger [$item:ty] $tm:ident{$($tf:item)*}) => {
header! {
$(#[$a])*
($id, $n) => danger [$item]
}
$crate::__tm__! { $id, $tm { $($tf)* }}
};
($(#[$a:meta])*($id:ident, $n:expr) => Cow[$item:ty] $tm:ident{$($tf:item)*}) => {
header! {
$(#[$a])*
($id, $n) => Cow[$item]
}
$crate::__tm__! { $id, $tm { $($tf)* }}
};
($(#[$a:meta])*($id:ident, $n:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => {
header! {
$(#[$a])*
($id, $n) => {Any / ($item)+}
}
$crate::__tm__! { $id, $tm { $($tf)* }}
};
}

View file

@ -0,0 +1,271 @@
use language_tags::LanguageTag;
use std::fmt::{self, Display};
use std::str;
use std::str::FromStr;
use super::error;
use super::{Charset, RawLike};
/// Reads a single raw string when parsing a header.
pub fn from_one_raw_str<'a, R, T>(raw: &'a R) -> error::Result<T>
where
R: RawLike<'a>,
T: str::FromStr,
{
if let Some(line) = raw.one() {
if !line.is_empty() {
return from_raw_str(line);
}
}
Err(error::Error::Header)
}
/// Reads a raw string into a value.
pub fn from_raw_str<T: str::FromStr>(raw: &[u8]) -> error::Result<T> {
let s = str::from_utf8(raw)?.trim();
T::from_str(s).or(Err(error::Error::Header))
}
/// Reads a comma-delimited raw header into a Vec.
#[inline]
pub fn from_comma_delimited<'a, R, T>(raw: &'a R) -> error::Result<Vec<T>>
where
R: RawLike<'a>,
T: str::FromStr,
{
let mut result = Vec::new();
for s in raw.iter() {
let s = str::from_utf8(s.as_ref())?;
result.extend(
s.split(',')
.filter_map(|x| match x.trim() {
"" => None,
y => Some(y),
})
.filter_map(|x| x.trim().parse().ok()),
)
}
Ok(result)
}
/// Format an array into a comma-delimited string.
pub fn fmt_comma_delimited<T: Display>(f: &mut fmt::Formatter, parts: &[T]) -> fmt::Result {
let mut iter = parts.iter();
if let Some(part) = iter.next() {
Display::fmt(part, f)?;
}
for part in iter {
f.write_str(", ")?;
Display::fmt(part, f)?;
}
Ok(())
}
/// 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) -> error::Result<ExtendedValue> {
// 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::Error::Header),
Some(n) => FromStr::from_str(n)?,
};
// Interpret the second piece as a language tag
let lang: Option<LanguageTag> = match parts.next() {
None => return Err(error::Error::Header),
Some("") => None,
Some(s) => match s.parse() {
Ok(lt) => Some(lt),
Err(_) => return Err(error::Error::Header),
},
};
// Interpret the third piece as a sequence of value characters
let value: Vec<u8> = match parts.next() {
None => return Err(error::Error::Header),
Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(),
};
Ok(ExtendedValue { charset, language_tag: lang, value })
}
impl 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::{AsciiSet, CONTROLS};
// This encode set is used for HTTP header values and is defined at
// https://tools.ietf.org/html/rfc5987#section-3.2
pub const HTTP_VALUE: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'%')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b',')
.add(b'/')
.add(b':')
.add(b';')
.add(b'<')
.add(b'-')
.add(b'>')
.add(b'?')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'{')
.add(b'}');
}
#[cfg(test)]
mod tests {
use super::{parse_extended_value, Charset, ExtendedValue};
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));
}
}

View file

@ -0,0 +1,326 @@
use ntex::util::Bytes;
use std::borrow::Cow;
use std::fmt;
/// Trait for raw bytes parsing access to header values (aka lines) for a single
/// header name.
pub trait RawLike<'a> {
/// The associated type of `Iterator` over values.
type IntoIter: Iterator<Item = &'a [u8]> + 'a;
/// Return the number of values (lines) in the headers.
fn len(&'a self) -> usize;
/// Return the single value (line), if and only if there is exactly
/// one. Otherwise return `None`.
fn one(&'a self) -> Option<&'a [u8]>;
/// Iterate the values (lines) as raw bytes.
fn iter(&'a self) -> Self::IntoIter;
}
/// A raw header value.
#[derive(Clone, Debug)]
pub struct Raw(Lines);
impl Raw {
/// Append a line to this `Raw` header value.
pub fn push<V: Into<Raw>>(&mut self, val: V) {
let raw = val.into();
match raw.0 {
Lines::Empty => (),
Lines::One(one) => self.push_line(one),
Lines::Many(lines) => {
for line in lines {
self.push_line(line);
}
}
}
}
fn push_line(&mut self, line: Bytes) {
let lines = ::std::mem::replace(&mut self.0, Lines::Empty);
match lines {
Lines::Empty => {
self.0 = Lines::One(line);
}
Lines::One(one) => {
self.0 = Lines::Many(vec![one, line]);
}
Lines::Many(mut lines) => {
lines.push(line);
self.0 = Lines::Many(lines);
}
}
}
}
impl<'a> RawLike<'a> for Raw {
type IntoIter = RawLines<'a>;
#[inline]
fn len(&'a self) -> usize {
match self.0 {
Lines::Empty => 0,
Lines::One(..) => 1,
Lines::Many(ref lines) => lines.len(),
}
}
#[inline]
fn one(&'a self) -> Option<&'a [u8]> {
match self.0 {
Lines::One(ref line) => Some(line.as_ref()),
Lines::Many(ref lines) if lines.len() == 1 => Some(lines[0].as_ref()),
_ => None,
}
}
#[inline]
fn iter(&'a self) -> RawLines<'a> {
RawLines { inner: &self.0, pos: 0 }
}
}
#[derive(Clone)]
enum Lines {
Empty,
One(Bytes),
Many(Vec<Bytes>),
}
fn eq_many<A: AsRef<[u8]>, B: AsRef<[u8]>>(a: &[A], b: &[B]) -> bool {
if a.len() != b.len() {
false
} else {
for (a, b) in a.iter().zip(b.iter()) {
if a.as_ref() != b.as_ref() {
return false;
}
}
true
}
}
fn eq<B: AsRef<[u8]>>(raw: &Raw, b: &[B]) -> bool {
match raw.0 {
Lines::Empty => b.is_empty(),
Lines::One(ref line) => eq_many(&[line], b),
Lines::Many(ref lines) => eq_many(lines, b),
}
}
impl PartialEq for Raw {
fn eq(&self, other: &Raw) -> bool {
match other.0 {
Lines::Empty => eq(self, &[] as &[Bytes]),
Lines::One(ref line) => eq(self, &[line]),
Lines::Many(ref lines) => eq(self, lines),
}
}
}
impl Eq for Raw {}
impl PartialEq<[Vec<u8>]> for Raw {
fn eq(&self, bytes: &[Vec<u8>]) -> bool {
eq(self, bytes)
}
}
impl<'a> PartialEq<[&'a [u8]]> for Raw {
fn eq(&self, bytes: &[&[u8]]) -> bool {
eq(self, bytes)
}
}
impl PartialEq<[String]> for Raw {
fn eq(&self, bytes: &[String]) -> bool {
eq(self, bytes)
}
}
impl<'a> PartialEq<[&'a str]> for Raw {
fn eq(&self, bytes: &[&'a str]) -> bool {
eq(self, bytes)
}
}
impl PartialEq<[u8]> for Raw {
fn eq(&self, bytes: &[u8]) -> bool {
match self.0 {
Lines::Empty => bytes.is_empty(),
Lines::One(ref line) => line.as_ref() == bytes,
Lines::Many(..) => false,
}
}
}
impl PartialEq<str> for Raw {
fn eq(&self, s: &str) -> bool {
self == s.as_bytes()
}
}
impl From<Vec<Vec<u8>>> for Raw {
#[inline]
fn from(val: Vec<Vec<u8>>) -> Raw {
Raw(Lines::Many(val.into_iter().map(|vec| maybe_literal(vec.into())).collect()))
}
}
impl From<String> for Raw {
#[inline]
fn from(val: String) -> Raw {
Raw::from(val.into_bytes())
}
}
impl From<Vec<u8>> for Raw {
#[inline]
fn from(val: Vec<u8>) -> Raw {
Raw(Lines::One(maybe_literal(val.into())))
}
}
impl<'a> From<&'a str> for Raw {
fn from(val: &'a str) -> Raw {
Raw::from(val.as_bytes())
}
}
impl<'a> From<&'a [u8]> for Raw {
fn from(val: &'a [u8]) -> Raw {
Raw(Lines::One(maybe_literal(val.into())))
}
}
impl From<Bytes> for Raw {
#[inline]
fn from(val: Bytes) -> Raw {
Raw(Lines::One(val))
}
}
#[cfg(feature = "headers")]
pub fn parsed(val: Bytes) -> Raw {
Raw(Lines::One(From::from(val)))
}
#[cfg(feature = "headers")]
pub fn push(raw: &mut Raw, val: Bytes) {
raw.push_line(val);
}
#[cfg(feature = "headers")]
pub fn new() -> Raw {
Raw(Lines::Empty)
}
impl fmt::Debug for Lines {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Lines::Empty => f.pad("[]"),
Lines::One(ref line) => fmt::Debug::fmt(&[line], f),
Lines::Many(ref lines) => fmt::Debug::fmt(lines, f),
}
}
}
impl ::std::ops::Index<usize> for Raw {
type Output = [u8];
fn index(&self, idx: usize) -> &[u8] {
match self.0 {
Lines::Empty => panic!("index of out of bounds: {}", idx),
Lines::One(ref line) => {
if idx == 0 {
line.as_ref()
} else {
panic!("index out of bounds: {}", idx)
}
}
Lines::Many(ref lines) => lines[idx].as_ref(),
}
}
}
macro_rules! literals {
($($len:expr => $($value:expr),+;)+) => (
fn maybe_literal(s: Cow<[u8]>) -> Bytes {
match s.len() {
$($len => {
$(
if s.as_ref() == $value {
return Bytes::from_static($value);
}
)+
})+
_ => ()
}
Bytes::from(s.into_owned())
}
#[test]
fn test_literal_lens() {
$(
$({
let s = $value;
assert!(s.len() == $len, "{:?} has len of {}, listed as {}", s, s.len(), $len);
})+
)+
}
);
}
literals! {
1 => b"*", b"0";
3 => b"*/*";
4 => b"gzip";
5 => b"close";
7 => b"chunked";
10 => b"keep-alive";
}
impl<'a> IntoIterator for &'a Raw {
type IntoIter = RawLines<'a>;
type Item = &'a [u8];
fn into_iter(self) -> RawLines<'a> {
self.iter()
}
}
pub struct RawLines<'a> {
inner: &'a Lines,
pos: usize,
}
impl<'a> fmt::Debug for RawLines<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("RawLines").field(&self.inner).finish()
}
}
impl<'a> Iterator for RawLines<'a> {
type Item = &'a [u8];
#[inline]
fn next(&mut self) -> Option<&'a [u8]> {
let current_pos = self.pos;
self.pos += 1;
match *self.inner {
Lines::Empty => None,
Lines::One(ref line) => {
if current_pos == 0 {
Some(line.as_ref())
} else {
None
}
}
Lines::Many(ref lines) => lines.get(current_pos).map(|l| l.as_ref()),
}
}
}

View file

@ -9,7 +9,6 @@ use std::{
use futures::future::{ok, ready, Either, FutureExt, LocalBoxFuture, Ready};
use futures::{Future, Stream};
use hyperx::header::DispositionType;
use mime_guess::from_ext;
use ntex::http::error::BlockingError;
use ntex::http::{header, Method, Payload, Uri};
@ -25,6 +24,7 @@ use percent_encoding::{utf8_percent_encode, CONTROLS};
use v_htmlescape::escape as escape_html_entity;
mod error;
mod file_header;
mod named;
mod range;
@ -208,7 +208,7 @@ fn directory_listing(dir: &Directory, req: &HttpRequest) -> Result<WebResponse,
))
}
type MimeOverride = dyn Fn(&mime::Name) -> DispositionType;
type MimeOverride = dyn Fn(&mime::Name) -> file_header::DispositionType;
/// Static files handling
///
@ -245,7 +245,7 @@ impl<Err: ErrorRenderer> Clone for Files<Err> {
redirect_to_slash: self.redirect_to_slash,
default: self.default.clone(),
renderer: self.renderer.clone(),
file_flags: self.file_flags,
file_flags: self.file_flags.clone(),
path: self.path.clone(),
mime_override: self.mime_override.clone(),
guards: self.guards.clone(),
@ -312,7 +312,7 @@ impl<Err: ErrorRenderer> Files<Err> {
/// Specifies mime override callback
pub fn mime_override<F>(mut self, f: F) -> Self
where
F: Fn(&mime::Name) -> DispositionType + 'static,
F: Fn(&mime::Name) -> file_header::DispositionType + 'static,
{
self.mime_override = Some(Rc::new(f));
self
@ -415,7 +415,7 @@ where
default: None,
renderer: self.renderer.clone(),
mime_override: self.mime_override.clone(),
file_flags: self.file_flags,
file_flags: self.file_flags.clone(),
guards: self.guards.clone(),
};
@ -527,7 +527,7 @@ where
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = self.file_flags;
named_file.flags = self.file_flags.clone();
let (req, _) = req.into_parts();
Either::Left(ok(WebResponse::new(named_file.into_response(&req), req)))
}
@ -558,7 +558,7 @@ where
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = self.file_flags;
named_file.flags = self.file_flags.clone();
let (req, _) = req.into_parts();
Either::Left(ok(WebResponse::new(named_file.into_response(&req), req)))
}
@ -637,8 +637,7 @@ mod tests {
#[ntex::test]
async fn test_if_modified_since_without_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap();
let since =
hyperx::header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let since = file_header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default()
.header(http::header::IF_MODIFIED_SINCE, since.to_string())
@ -650,8 +649,7 @@ mod tests {
#[ntex::test]
async fn test_if_modified_since_with_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap();
let since =
hyperx::header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let since = file_header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default()
.header(http::header::IF_NONE_MATCH, "miss_etag")
@ -776,7 +774,9 @@ mod tests {
#[ntex::test]
async fn test_named_file_image_attachment() {
use hyperx::header::{Charset, ContentDisposition, DispositionParam, DispositionType};
use crate::file_header::{
Charset, ContentDisposition, DispositionParam, DispositionType,
};
let cd = ContentDisposition {
disposition: DispositionType::Attachment,
@ -851,8 +851,8 @@ mod tests {
#[ntex::test]
async fn test_mime_override() {
fn all_attachment(_: &mime::Name) -> DispositionType {
DispositionType::Attachment
fn all_attachment(_: &mime::Name) -> file_header::DispositionType {
file_header::DispositionType::Attachment
}
let srv = test::init_service(App::new().service(
@ -1177,7 +1177,7 @@ mod tests {
#[ntex::test]
async fn test_default_handler_file_missing() {
let mut st = Files::new("/", ".")
let st = Files::new("/", ".")
.default_handler(|req: WebRequest<DefaultError>| {
ok(req.into_response(HttpResponse::Ok().body("default content")))
})
@ -1186,7 +1186,7 @@ mod tests {
.unwrap();
let req = TestRequest::with_uri("/missing").to_srv_request();
let resp = test::call_service(&mut st, req).await;
let resp = test::call_service(&st, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await;
assert_eq!(bytes, Bytes::from_static(b"default content"));

View file

@ -13,18 +13,18 @@ use mime_guess::from_path;
use futures::future::{ready, Ready};
use futures::stream::TryStreamExt;
use hyperx::header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType, Header,
};
use ntex::http::body::SizedStream;
use ntex::http::header::ContentEncoding;
use ntex::http::{self, StatusCode};
use ntex::web::{BodyEncoding, ErrorRenderer, HttpRequest, HttpResponse, Responder};
use crate::file_header::{self, Header};
use crate::range::HttpRange;
use crate::ChunkedReadFile;
bitflags! {
#[derive(Clone)]
pub(crate) struct Flags: u8 {
const ETAG = 0b0000_0001;
const LAST_MD = 0b0000_0010;
@ -48,10 +48,20 @@ pub struct NamedFile {
pub(crate) flags: Flags,
pub(crate) status_code: StatusCode,
pub(crate) content_type: mime::Mime,
pub(crate) content_disposition: header::ContentDisposition,
pub(crate) content_disposition: file_header::ContentDisposition,
pub(crate) encoding: Option<ContentEncoding>,
}
impl std::fmt::Debug for Flags {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Flags")
.field("etag", &self.contains(Flags::ETAG))
.field("last_modified", &self.contains(Flags::LAST_MD))
.field("content_disposition", &self.contains(Flags::CONTENT_DISPOSITION))
.finish()
}
}
impl NamedFile {
/// Creates an instance from a previously opened file.
///
@ -92,15 +102,15 @@ impl NamedFile {
let ct = from_path(&path).first_or_octet_stream();
let disposition = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => DispositionType::Attachment,
mime::IMAGE | mime::TEXT | mime::VIDEO => file_header::DispositionType::Inline,
_ => file_header::DispositionType::Attachment,
};
let parameters = vec![DispositionParam::Filename(
Charset::Ext(String::from("UTF-8")),
let parameters = vec![file_header::DispositionParam::Filename(
file_header::Charset::Ext(String::from("UTF-8")),
None,
filename.into_owned().into_bytes(),
)];
let cd = ContentDisposition { disposition, parameters };
let cd = file_header::ContentDisposition { disposition, parameters };
(ct, cd)
};
@ -179,7 +189,7 @@ impl NamedFile {
/// after converting it to UTF-8 using.
/// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy).
#[inline]
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
pub fn set_content_disposition(mut self, cd: file_header::ContentDisposition) -> Self {
self.content_disposition = cd;
self.flags.insert(Flags::CONTENT_DISPOSITION);
self
@ -219,7 +229,7 @@ impl NamedFile {
self
}
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
pub(crate) fn etag(&self) -> Option<file_header::EntityTag> {
// This etag format is similar to Apache's.
self.modified.as_ref().map(|mtime| {
let ino = {
@ -236,7 +246,7 @@ impl NamedFile {
let dur = mtime
.duration_since(UNIX_EPOCH)
.expect("modification time must be after epoch");
header::EntityTag::strong(format!(
file_header::EntityTag::strong(format!(
"{:x}:{:x}:{:x}:{:x}",
ino,
self.md.len(),
@ -246,7 +256,7 @@ impl NamedFile {
})
}
pub(crate) fn last_modified(&self) -> Option<header::HttpDate> {
pub(crate) fn last_modified(&self) -> Option<file_header::HttpDate> {
self.modified.map(|mtime| mtime.into())
}
@ -282,12 +292,12 @@ impl NamedFile {
// check preconditions
let precondition_failed = if !any_match(etag.as_ref(), req) {
true
} else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = {
} else if let (Some(ref m), Some(file_header::IfUnmodifiedSince(ref since))) = {
let mut header = None;
for hdr in req.headers().get_all(http::header::IF_UNMODIFIED_SINCE) {
if let Ok(v) =
header::IfUnmodifiedSince::parse_header(&header::Raw::from(hdr.as_bytes()))
{
if let Ok(v) = file_header::IfUnmodifiedSince::parse_header(
&file_header::Raw::from(hdr.as_bytes()),
) {
header = Some(v);
break;
}
@ -310,12 +320,12 @@ impl NamedFile {
true
} else if req.headers().contains_key(&http::header::IF_NONE_MATCH) {
false
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = {
} else if let (Some(ref m), Some(file_header::IfModifiedSince(ref since))) = {
let mut header = None;
for hdr in req.headers().get_all(http::header::IF_MODIFIED_SINCE) {
if let Ok(v) =
header::IfModifiedSince::parse_header(&header::Raw::from(hdr.as_bytes()))
{
if let Ok(v) = file_header::IfModifiedSince::parse_header(
&file_header::Raw::from(hdr.as_bytes()),
) {
header = Some(v);
break;
}
@ -348,10 +358,10 @@ impl NamedFile {
}
resp.if_some(last_modified, |lm, resp| {
resp.header(http::header::LAST_MODIFIED, header::LastModified(lm).to_string());
resp.header(http::header::LAST_MODIFIED, file_header::LastModified(lm).to_string());
})
.if_some(etag, |etag, resp| {
resp.header(http::header::ETAG, header::ETag(etag).to_string());
resp.header(http::header::ETAG, file_header::ETag(etag).to_string());
});
resp.header(http::header::ACCEPT_RANGES, "bytes");
@ -421,13 +431,13 @@ impl DerefMut for NamedFile {
}
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
fn any_match(etag: Option<&file_header::EntityTag>, req: &HttpRequest) -> bool {
if let Some(val) = req.headers().get(http::header::IF_MATCH) {
let hdr = ::http::HeaderValue::from(val);
if let Ok(val) = header::IfMatch::parse_header(&&hdr) {
if let Ok(val) = file_header::IfMatch::parse_header(&&hdr) {
match val {
header::IfMatch::Any => return true,
header::IfMatch::Items(ref items) => {
file_header::IfMatch::Any => return true,
file_header::IfMatch::Items(ref items) => {
if let Some(some_etag) = etag {
for item in items {
if item.strong_eq(some_etag) {
@ -444,13 +454,13 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
}
/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
fn none_match(etag: Option<&file_header::EntityTag>, req: &HttpRequest) -> bool {
if let Some(val) = req.headers().get(http::header::IF_NONE_MATCH) {
let hdr = ::http::HeaderValue::from(val);
if let Ok(val) = header::IfNoneMatch::parse_header(&&hdr) {
if let Ok(val) = file_header::IfNoneMatch::parse_header(&&hdr) {
return match val {
header::IfNoneMatch::Any => false,
header::IfNoneMatch::Items(ref items) => {
file_header::IfNoneMatch::Any => false,
file_header::IfNoneMatch::Items(ref items) => {
if let Some(some_etag) = etag {
for item in items {
if item.weak_eq(some_etag) {