mirror of https://github.com/fafhrd91/actix-web
clean up header mod
This commit is contained in:
parent
dacf8f7b6b
commit
c34e6dccc7
|
@ -0,0 +1,64 @@
|
||||||
|
/// Represents a supported content encoding.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum ContentEncoding {
|
||||||
|
/// Automatically select encoding based on encoding negotiation.
|
||||||
|
Auto,
|
||||||
|
|
||||||
|
/// A format using the Brotli algorithm.
|
||||||
|
Br,
|
||||||
|
|
||||||
|
/// A format using the zlib structure with deflate algorithm.
|
||||||
|
Deflate,
|
||||||
|
|
||||||
|
/// Gzip algorithm.
|
||||||
|
Gzip,
|
||||||
|
|
||||||
|
/// Indicates the identity function (i.e. no compression, nor modification).
|
||||||
|
Identity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContentEncoding {
|
||||||
|
/// Is the content compressed?
|
||||||
|
#[inline]
|
||||||
|
pub fn is_compression(self) -> bool {
|
||||||
|
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert content encoding to string
|
||||||
|
#[inline]
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ContentEncoding::Br => "br",
|
||||||
|
ContentEncoding::Gzip => "gzip",
|
||||||
|
ContentEncoding::Deflate => "deflate",
|
||||||
|
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default Q-factor (quality) value.
|
||||||
|
#[inline]
|
||||||
|
pub fn quality(self) -> f64 {
|
||||||
|
match self {
|
||||||
|
ContentEncoding::Br => 1.1,
|
||||||
|
ContentEncoding::Gzip => 1.0,
|
||||||
|
ContentEncoding::Deflate => 0.9,
|
||||||
|
ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for ContentEncoding {
|
||||||
|
fn from(val: &str) -> ContentEncoding {
|
||||||
|
let val = val.trim();
|
||||||
|
|
||||||
|
if val.eq_ignore_ascii_case("br") {
|
||||||
|
ContentEncoding::Br
|
||||||
|
} else if val.eq_ignore_ascii_case("gzip") {
|
||||||
|
ContentEncoding::Gzip
|
||||||
|
} else if val.eq_ignore_ascii_case("deflate") {
|
||||||
|
ContentEncoding::Deflate
|
||||||
|
} else {
|
||||||
|
ContentEncoding::Identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -110,7 +110,8 @@ mod test_if_range {
|
||||||
use super::IfRange as HeaderField;
|
use super::IfRange as HeaderField;
|
||||||
use crate::header::*;
|
use crate::header::*;
|
||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
|
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
|
||||||
test_header!(test2, vec![b"\"xyzzy\""]);
|
test_header!(test2, vec![b"\"abc\""]);
|
||||||
test_header!(test3, vec![b"this-is-invalid"], None::<IfRange>);
|
test_header!(test3, vec![b"this-is-invalid"], None::<IfRange>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ pub use self::content_disposition::{
|
||||||
};
|
};
|
||||||
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_encoding::{ContentEncoding};
|
||||||
pub use self::content_type::ContentType;
|
pub use self::content_type::ContentType;
|
||||||
pub use self::date::Date;
|
pub use self::date::Date;
|
||||||
pub use self::etag::ETag;
|
pub use self::etag::ETag;
|
||||||
|
@ -340,6 +341,7 @@ mod allow;
|
||||||
mod cache_control;
|
mod cache_control;
|
||||||
mod content_disposition;
|
mod content_disposition;
|
||||||
mod content_language;
|
mod content_language;
|
||||||
|
mod content_encoding;
|
||||||
mod content_range;
|
mod content_range;
|
||||||
mod content_type;
|
mod content_type;
|
||||||
mod date;
|
mod date;
|
||||||
|
|
|
@ -6,7 +6,7 @@ use http::{
|
||||||
HeaderValue,
|
HeaderValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A trait for transforming things
|
/// Transforms structures into header K/V pairs for inserting into `HeaderMap`s.
|
||||||
pub trait IntoHeaderPair: Sized {
|
pub trait IntoHeaderPair: Sized {
|
||||||
type Error;
|
type Error;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//! Various HTTP headers.
|
//! Various HTTP headers.
|
||||||
|
|
||||||
use std::{fmt, str::FromStr};
|
use std::fmt;
|
||||||
|
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use percent_encoding::{AsciiSet, CONTROLS};
|
use percent_encoding::{AsciiSet, CONTROLS};
|
||||||
|
@ -12,10 +12,12 @@ use crate::httpmessage::HttpMessage;
|
||||||
|
|
||||||
mod into_pair;
|
mod into_pair;
|
||||||
mod into_value;
|
mod into_value;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
pub(crate) mod map;
|
pub(crate) mod map;
|
||||||
mod shared;
|
mod shared;
|
||||||
|
|
||||||
pub use self::common::*;
|
pub use self::common::*;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use self::shared::*;
|
pub use self::shared::*;
|
||||||
|
@ -25,6 +27,7 @@ pub use self::into_value::IntoHeaderValue;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use self::map::GetAll;
|
pub use self::map::GetAll;
|
||||||
pub use self::map::HeaderMap;
|
pub use self::map::HeaderMap;
|
||||||
|
pub use self::utils::*;
|
||||||
|
|
||||||
/// A trait for any object that already represents a valid header field and value.
|
/// A trait for any object that already represents a valid header field and value.
|
||||||
pub trait Header: IntoHeaderValue {
|
pub trait Header: IntoHeaderValue {
|
||||||
|
@ -35,67 +38,6 @@ pub trait Header: IntoHeaderValue {
|
||||||
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
|
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents supported types of content encodings
|
|
||||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
|
||||||
pub enum ContentEncoding {
|
|
||||||
/// Automatically select encoding based on encoding negotiation
|
|
||||||
Auto,
|
|
||||||
/// A format using the Brotli algorithm
|
|
||||||
Br,
|
|
||||||
/// A format using the zlib structure with deflate algorithm
|
|
||||||
Deflate,
|
|
||||||
/// Gzip algorithm
|
|
||||||
Gzip,
|
|
||||||
/// Indicates the identity function (i.e. no compression, nor modification)
|
|
||||||
Identity,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContentEncoding {
|
|
||||||
#[inline]
|
|
||||||
/// Is the content compressed?
|
|
||||||
pub fn is_compression(self) -> bool {
|
|
||||||
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Convert content encoding to string
|
|
||||||
pub fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ContentEncoding::Br => "br",
|
|
||||||
ContentEncoding::Gzip => "gzip",
|
|
||||||
ContentEncoding::Deflate => "deflate",
|
|
||||||
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// default quality value
|
|
||||||
pub fn quality(self) -> f64 {
|
|
||||||
match self {
|
|
||||||
ContentEncoding::Br => 1.1,
|
|
||||||
ContentEncoding::Gzip => 1.0,
|
|
||||||
ContentEncoding::Deflate => 0.9,
|
|
||||||
ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<&'a str> for ContentEncoding {
|
|
||||||
fn from(s: &'a str) -> ContentEncoding {
|
|
||||||
let s = s.trim();
|
|
||||||
|
|
||||||
if s.eq_ignore_ascii_case("br") {
|
|
||||||
ContentEncoding::Br
|
|
||||||
} else if s.eq_ignore_ascii_case("gzip") {
|
|
||||||
ContentEncoding::Gzip
|
|
||||||
} else if s.eq_ignore_ascii_case("deflate") {
|
|
||||||
ContentEncoding::Deflate
|
|
||||||
} else {
|
|
||||||
ContentEncoding::Identity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub(crate) struct Writer {
|
pub(crate) struct Writer {
|
||||||
buf: BytesMut,
|
buf: BytesMut,
|
||||||
|
@ -107,6 +49,7 @@ impl Writer {
|
||||||
buf: BytesMut::new(),
|
buf: BytesMut::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take(&mut self) -> Bytes {
|
fn take(&mut self) -> Bytes {
|
||||||
self.buf.split().freeze()
|
self.buf.split().freeze()
|
||||||
}
|
}
|
||||||
|
@ -125,163 +68,6 @@ impl fmt::Write for Writer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[doc(hidden)]
|
|
||||||
/// Reads a comma-delimited raw header into a Vec.
|
|
||||||
pub fn from_comma_delimited<'a, I: Iterator<Item = &'a HeaderValue> + 'a, T: FromStr>(
|
|
||||||
all: I,
|
|
||||||
) -> Result<Vec<T>, ParseError> {
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for h in all {
|
|
||||||
let s = h.to_str().map_err(|_| ParseError::Header)?;
|
|
||||||
result.extend(
|
|
||||||
s.split(',')
|
|
||||||
.filter_map(|x| match x.trim() {
|
|
||||||
"" => None,
|
|
||||||
y => Some(y),
|
|
||||||
})
|
|
||||||
.filter_map(|x| x.trim().parse().ok()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[doc(hidden)]
|
|
||||||
/// Reads a single string when parsing a header.
|
|
||||||
pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> {
|
|
||||||
if let Some(line) = val {
|
|
||||||
let line = line.to_str().map_err(|_| ParseError::Header)?;
|
|
||||||
if !line.is_empty() {
|
|
||||||
return T::from_str(line).or(Err(ParseError::Header));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(ParseError::Header)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[doc(hidden)]
|
|
||||||
/// Format an array into a comma-delimited string.
|
|
||||||
pub fn fmt_comma_delimited<T>(f: &mut fmt::Formatter<'_>, parts: &[T]) -> fmt::Result
|
|
||||||
where
|
|
||||||
T: fmt::Display,
|
|
||||||
{
|
|
||||||
let mut iter = parts.iter();
|
|
||||||
if let Some(part) = iter.next() {
|
|
||||||
fmt::Display::fmt(part, f)?;
|
|
||||||
}
|
|
||||||
for part in iter {
|
|
||||||
f.write_str(", ")?;
|
|
||||||
fmt::Display::fmt(part, f)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// From hyper v0.11.27 src/header/parsing.rs
|
|
||||||
|
|
||||||
/// The value part of an extended parameter consisting of three parts:
|
|
||||||
/// the REQUIRED character set name (`charset`), the OPTIONAL language information (`language_tag`),
|
|
||||||
/// and a character sequence representing the actual value (`value`), separated by single quote
|
|
||||||
/// characters. It is 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, crate::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(crate::error::ParseError::Header),
|
|
||||||
Some(n) => FromStr::from_str(n).map_err(|_| crate::error::ParseError::Header)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Interpret the second piece as a language tag
|
|
||||||
let language_tag: Option<LanguageTag> = match parts.next() {
|
|
||||||
None => return Err(crate::error::ParseError::Header),
|
|
||||||
Some("") => None,
|
|
||||||
Some(s) => match s.parse() {
|
|
||||||
Ok(lt) => Some(lt),
|
|
||||||
Err(_) => return Err(crate::error::ParseError::Header),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Interpret the third piece as a sequence of value characters
|
|
||||||
let value: Vec<u8> = match parts.next() {
|
|
||||||
None => return Err(crate::error::ParseError::Header),
|
|
||||||
Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ExtendedValue {
|
|
||||||
value,
|
|
||||||
charset,
|
|
||||||
language_tag,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ExtendedValue {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let encoded_value =
|
|
||||||
percent_encoding::percent_encode(&self.value[..], 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>
|
|
||||||
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
|
|
||||||
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
|
|
||||||
fmt::Display::fmt(&encoded, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert http::HeaderMap to a HeaderMap
|
/// Convert http::HeaderMap to a HeaderMap
|
||||||
impl From<http::HeaderMap> for HeaderMap {
|
impl From<http::HeaderMap> for HeaderMap {
|
||||||
fn from(map: http::HeaderMap) -> HeaderMap {
|
fn from(map: http::HeaderMap) -> HeaderMap {
|
||||||
|
@ -316,91 +102,3 @@ pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
|
||||||
.add(b']')
|
.add(b']')
|
||||||
.add(b'{')
|
.add(b'{')
|
||||||
.add(b'}');
|
.add(b'}');
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::shared::Charset;
|
|
||||||
use super::{parse_extended_value, 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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
|
use language_tags::LanguageTag;
|
||||||
|
|
||||||
|
use crate::header::{Charset, HTTP_VALUE};
|
||||||
|
|
||||||
|
// From hyper v0.11.27 src/header/parsing.rs
|
||||||
|
|
||||||
|
/// The value part of an extended parameter consisting of three parts:
|
||||||
|
/// - The REQUIRED character set name (`charset`).
|
||||||
|
/// - The OPTIONAL language information (`language_tag`).
|
||||||
|
/// - A character sequence representing the actual value (`value`), separated by single quotes.
|
||||||
|
///
|
||||||
|
/// It is 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, crate::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(crate::error::ParseError::Header),
|
||||||
|
Some(n) => FromStr::from_str(n).map_err(|_| crate::error::ParseError::Header)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interpret the second piece as a language tag
|
||||||
|
let language_tag: Option<LanguageTag> = match parts.next() {
|
||||||
|
None => return Err(crate::error::ParseError::Header),
|
||||||
|
Some("") => None,
|
||||||
|
Some(s) => match s.parse() {
|
||||||
|
Ok(lt) => Some(lt),
|
||||||
|
Err(_) => return Err(crate::error::ParseError::Header),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interpret the third piece as a sequence of value characters
|
||||||
|
let value: Vec<u8> = match parts.next() {
|
||||||
|
None => return Err(crate::error::ParseError::Header),
|
||||||
|
Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ExtendedValue {
|
||||||
|
value,
|
||||||
|
charset,
|
||||||
|
language_tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtendedValue {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let encoded_value =
|
||||||
|
percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
|
||||||
|
if let Some(ref lang) = self.language_tag {
|
||||||
|
write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}''{}", self.charset, encoded_value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,16 @@
|
||||||
//! Originally taken from `hyper::header::shared`.
|
//! Originally taken from `hyper::header::shared`.
|
||||||
|
|
||||||
pub use self::charset::Charset;
|
|
||||||
pub use self::encoding::Encoding;
|
|
||||||
pub use self::entity::EntityTag;
|
|
||||||
pub use self::httpdate::HttpDate;
|
|
||||||
pub use self::quality_item::{q, qitem, Quality, QualityItem};
|
|
||||||
pub use language_tags::LanguageTag;
|
|
||||||
|
|
||||||
mod charset;
|
mod charset;
|
||||||
mod encoding;
|
mod encoding;
|
||||||
mod entity;
|
mod entity;
|
||||||
|
mod extended;
|
||||||
mod httpdate;
|
mod httpdate;
|
||||||
mod quality_item;
|
mod quality_item;
|
||||||
|
|
||||||
|
pub use self::charset::Charset;
|
||||||
|
pub use self::encoding::Encoding;
|
||||||
|
pub use self::entity::EntityTag;
|
||||||
|
pub use self::extended::{parse_extended_value, ExtendedValue};
|
||||||
|
pub use self::httpdate::HttpDate;
|
||||||
|
pub use self::quality_item::{q, qitem, Quality, QualityItem};
|
||||||
|
pub use language_tags::LanguageTag;
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
|
use http::HeaderValue;
|
||||||
|
|
||||||
|
use crate::{error::ParseError, header::HTTP_VALUE};
|
||||||
|
|
||||||
|
/// Reads a comma-delimited raw header into a Vec.
|
||||||
|
#[inline]
|
||||||
|
pub fn from_comma_delimited<'a, I, T>(all: I) -> Result<Vec<T>, ParseError>
|
||||||
|
where
|
||||||
|
I: Iterator<Item = &'a HeaderValue> + 'a,
|
||||||
|
T: FromStr,
|
||||||
|
{
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for h in all {
|
||||||
|
let s = h.to_str().map_err(|_| ParseError::Header)?;
|
||||||
|
result.extend(
|
||||||
|
s.split(',')
|
||||||
|
.filter_map(|x| match x.trim() {
|
||||||
|
"" => None,
|
||||||
|
y => Some(y),
|
||||||
|
})
|
||||||
|
.filter_map(|x| x.trim().parse().ok()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a single string when parsing a header.
|
||||||
|
#[inline]
|
||||||
|
pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> {
|
||||||
|
if let Some(line) = val {
|
||||||
|
let line = line.to_str().map_err(|_| ParseError::Header)?;
|
||||||
|
if !line.is_empty() {
|
||||||
|
return T::from_str(line).or(Err(ParseError::Header));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ParseError::Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format an array into a comma-delimited string.
|
||||||
|
#[inline]
|
||||||
|
pub fn fmt_comma_delimited<T>(f: &mut fmt::Formatter<'_>, parts: &[T]) -> fmt::Result
|
||||||
|
where
|
||||||
|
T: fmt::Display,
|
||||||
|
{
|
||||||
|
let mut iter = parts.iter();
|
||||||
|
if let Some(part) = iter.next() {
|
||||||
|
fmt::Display::fmt(part, f)?;
|
||||||
|
}
|
||||||
|
for part in iter {
|
||||||
|
f.write_str(", ")?;
|
||||||
|
fmt::Display::fmt(part, f)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Percent encode a sequence of bytes with a character set defined in
|
||||||
|
/// <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, HTTP_VALUE);
|
||||||
|
fmt::Display::fmt(&encoded, f)
|
||||||
|
}
|
|
@ -1,5 +1,9 @@
|
||||||
use std::cell::{Ref, RefMut};
|
//! HTTP requests.
|
||||||
use std::{fmt, net};
|
|
||||||
|
use std::{
|
||||||
|
cell::{Ref, RefMut},
|
||||||
|
fmt, net,
|
||||||
|
};
|
||||||
|
|
||||||
use http::{header, Method, Uri, Version};
|
use http::{header, Method, Uri, Version};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
//! Http response
|
//! HTTP responses.
|
||||||
use std::cell::{Ref, RefMut};
|
|
||||||
use std::convert::TryFrom;
|
use std::{
|
||||||
use std::future::Future;
|
cell::{Ref, RefMut},
|
||||||
use std::pin::Pin;
|
convert::TryFrom,
|
||||||
use std::task::{Context, Poll};
|
fmt,
|
||||||
use std::{fmt, str};
|
future::Future,
|
||||||
|
pin::Pin,
|
||||||
|
str,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
|
|
Loading…
Reference in New Issue