2019-02-07 22:24:24 +01:00
|
|
|
//! Various http headers
|
|
|
|
// This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header)
|
|
|
|
|
|
|
|
use std::{fmt, str::FromStr};
|
|
|
|
|
|
|
|
use bytes::{Bytes, BytesMut};
|
|
|
|
use http::Error as HttpError;
|
|
|
|
use mime::Mime;
|
2019-08-13 19:48:11 +02:00
|
|
|
use percent_encoding::{AsciiSet, CONTROLS};
|
2019-02-07 22:24:24 +01:00
|
|
|
|
|
|
|
pub use http::header::*;
|
|
|
|
|
|
|
|
use crate::error::ParseError;
|
|
|
|
use crate::httpmessage::HttpMessage;
|
|
|
|
|
|
|
|
mod common;
|
2019-04-07 00:02:02 +02:00
|
|
|
pub(crate) mod map;
|
2019-02-07 22:24:24 +01:00
|
|
|
mod shared;
|
|
|
|
#[doc(hidden)]
|
|
|
|
pub use self::common::*;
|
|
|
|
#[doc(hidden)]
|
|
|
|
pub use self::shared::*;
|
|
|
|
|
2019-04-07 00:02:02 +02:00
|
|
|
#[doc(hidden)]
|
|
|
|
pub use self::map::GetAll;
|
|
|
|
pub use self::map::HeaderMap;
|
|
|
|
|
2019-02-07 22:24:24 +01:00
|
|
|
/// A trait for any object that will represent a header field and value.
|
|
|
|
pub trait Header
|
|
|
|
where
|
|
|
|
Self: IntoHeaderValue,
|
|
|
|
{
|
|
|
|
/// Returns the name of the header field
|
|
|
|
fn name() -> HeaderName;
|
|
|
|
|
|
|
|
/// Parse a header
|
|
|
|
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A trait for any object that can be Converted to a `HeaderValue`
|
|
|
|
pub trait IntoHeaderValue: Sized {
|
|
|
|
/// The type returned in the event of a conversion error.
|
|
|
|
type Error: Into<HttpError>;
|
|
|
|
|
|
|
|
/// Try to convert value to a Header value.
|
|
|
|
fn try_into(self) -> Result<HeaderValue, Self::Error>;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl IntoHeaderValue for HeaderValue {
|
|
|
|
type Error = InvalidHeaderValue;
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
|
|
|
Ok(self)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> IntoHeaderValue for &'a str {
|
|
|
|
type Error = InvalidHeaderValue;
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
|
|
|
self.parse()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> IntoHeaderValue for &'a [u8] {
|
|
|
|
type Error = InvalidHeaderValue;
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
|
|
|
HeaderValue::from_bytes(self)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl IntoHeaderValue for Bytes {
|
|
|
|
type Error = InvalidHeaderValueBytes;
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
|
|
|
HeaderValue::from_shared(self)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl IntoHeaderValue for Vec<u8> {
|
|
|
|
type Error = InvalidHeaderValueBytes;
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
|
|
|
HeaderValue::from_shared(Bytes::from(self))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl IntoHeaderValue for String {
|
|
|
|
type Error = InvalidHeaderValueBytes;
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
|
|
|
HeaderValue::from_shared(Bytes::from(self))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-03 21:44:47 +02:00
|
|
|
impl IntoHeaderValue for usize {
|
|
|
|
type Error = InvalidHeaderValueBytes;
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
|
|
|
let s = format!("{}", self);
|
|
|
|
HeaderValue::from_shared(Bytes::from(s))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl IntoHeaderValue for u64 {
|
|
|
|
type Error = InvalidHeaderValueBytes;
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
|
|
|
let s = format!("{}", self);
|
|
|
|
HeaderValue::from_shared(Bytes::from(s))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-07 22:24:24 +01:00
|
|
|
impl IntoHeaderValue for Mime {
|
|
|
|
type Error = InvalidHeaderValueBytes;
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
|
|
|
HeaderValue::from_shared(Bytes::from(format!("{}", self)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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 {
|
|
|
|
match self {
|
|
|
|
ContentEncoding::Identity | ContentEncoding::Auto => false,
|
|
|
|
_ => true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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)]
|
|
|
|
pub(crate) struct Writer {
|
|
|
|
buf: BytesMut,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Writer {
|
|
|
|
fn new() -> Writer {
|
|
|
|
Writer {
|
|
|
|
buf: BytesMut::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn take(&mut self) -> Bytes {
|
|
|
|
self.buf.take().freeze()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Write for Writer {
|
|
|
|
#[inline]
|
|
|
|
fn write_str(&mut self, s: &str) -> fmt::Result {
|
|
|
|
self.buf.extend_from_slice(s.as_bytes());
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result {
|
|
|
|
fmt::write(self, args)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
#[doc(hidden)]
|
|
|
|
/// Reads a comma-delimited raw header into a Vec.
|
2019-04-07 00:02:02 +02:00
|
|
|
pub fn from_comma_delimited<'a, I: Iterator<Item = &'a HeaderValue> + 'a, T: FromStr>(
|
|
|
|
all: I,
|
2019-02-07 22:24:24 +01:00
|
|
|
) -> 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 {
|
2019-08-13 19:48:11 +02:00
|
|
|
let encoded_value =
|
|
|
|
percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
|
2019-02-07 22:24:24 +01:00
|
|
|
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 {
|
2019-08-13 19:48:11 +02:00
|
|
|
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
|
2019-02-07 22:24:24 +01:00
|
|
|
fmt::Display::fmt(&encoded, f)
|
|
|
|
}
|
|
|
|
|
2019-04-07 00:02:02 +02:00
|
|
|
/// Convert http::HeaderMap to a HeaderMap
|
|
|
|
impl From<http::HeaderMap> for HeaderMap {
|
|
|
|
fn from(map: http::HeaderMap) -> HeaderMap {
|
|
|
|
let mut new_map = HeaderMap::with_capacity(map.capacity());
|
|
|
|
for (h, v) in map.iter() {
|
|
|
|
new_map.append(h.clone(), v.clone());
|
|
|
|
}
|
|
|
|
new_map
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-13 19:48:11 +02:00
|
|
|
// This encode set is used for HTTP header values and is defined at
|
|
|
|
// https://tools.ietf.org/html/rfc5987#section-3.2
|
|
|
|
pub(crate) 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'}');
|
2019-02-07 22:24:24 +01:00
|
|
|
|
|
|
|
#[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)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|