mirror of
https://github.com/fafhrd91/actix-web
synced 2025-09-02 01:31:57 +02:00
Fix AcceptEncoding header (#2501)
This commit is contained in:
@@ -147,6 +147,39 @@ impl Accept {
|
||||
Accept(vec![QualityItem::max(mime::TEXT_HTML)])
|
||||
}
|
||||
|
||||
// TODO: method for getting best content encoding based on q-factors, available from server side
|
||||
// and if none are acceptable return None
|
||||
|
||||
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first mime type is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
|
||||
/// list is empty.
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn preference(&self) -> Mime {
|
||||
use actix_http::header::Quality;
|
||||
|
||||
let mut max_item = None;
|
||||
let mut max_pref = Quality::ZERO;
|
||||
|
||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||
|
||||
for pref in &self.0 {
|
||||
// only change if strictly greater
|
||||
// equal items, even while unsorted, still have higher preference if they appear first
|
||||
if pref.quality > max_pref {
|
||||
max_pref = pref.quality;
|
||||
max_item = Some(pref.item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
max_item.unwrap_or(mime::STAR_STAR)
|
||||
}
|
||||
|
||||
/// Returns a sorted list of mime types from highest to lowest preference, accounting for
|
||||
/// [q-factor weighting] and specificity.
|
||||
///
|
||||
@@ -196,36 +229,6 @@ impl Accept {
|
||||
|
||||
types.into_iter().map(|qitem| qitem.item).collect()
|
||||
}
|
||||
|
||||
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first mime type is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
|
||||
/// list is empty.
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn preference(&self) -> Mime {
|
||||
use actix_http::header::Quality;
|
||||
|
||||
let mut max_item = None;
|
||||
let mut max_pref = Quality::MIN;
|
||||
|
||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||
|
||||
for pref in &self.0 {
|
||||
// only change if strictly greater
|
||||
// equal items, even while unsorted, still have higher preference if they appear first
|
||||
if pref.quality > max_pref {
|
||||
max_pref = pref.quality;
|
||||
max_item = Some(pref.item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
max_item.unwrap_or(mime::STAR_STAR)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -239,7 +242,7 @@ mod tests {
|
||||
assert!(test.ranked().is_empty());
|
||||
|
||||
let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]);
|
||||
assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON));
|
||||
assert_eq!(test.ranked(), vec![mime::APPLICATION_JSON]);
|
||||
|
||||
let test = Accept(vec
|
||||
common_header! {
|
||||
/// `Accept-Charset` header, defined in [RFC 7231 §5.3.3].
|
||||
///
|
||||
/// The `Accept-Charset` header field can be sent by a user agent to
|
||||
/// indicate what charsets are acceptable in textual response content.
|
||||
@@ -52,10 +51,12 @@ crate::http::header::common_header! {
|
||||
/// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))])
|
||||
/// );
|
||||
/// ```
|
||||
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+
|
||||
///
|
||||
/// [RFC 7231 §5.3.3]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3
|
||||
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)*
|
||||
|
||||
test_parse_and_format {
|
||||
// Test case from RFC
|
||||
crate::http::header::common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
|
||||
common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,15 @@
|
||||
use actix_http::header::QualityItem;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{common_header, Encoding};
|
||||
use super::{common_header, ContentEncoding, Encoding, Preference, Quality, QualityItem};
|
||||
use crate::http::header;
|
||||
|
||||
common_header! {
|
||||
/// `Accept-Encoding` header, defined
|
||||
/// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4)
|
||||
///
|
||||
/// The `Accept-Encoding` header field can be used by user agents to
|
||||
/// indicate what response content-codings are
|
||||
/// acceptable in the response. An `identity` token is used as a synonym
|
||||
/// for "no encoding" in order to communicate when no encoding is
|
||||
/// preferred.
|
||||
/// The `Accept-Encoding` header field can be used by user agents to indicate what response
|
||||
/// content-codings are acceptable in the response. An `identity` token is used as a synonym
|
||||
/// for "no encoding" in order to communicate when no encoding is preferred.
|
||||
///
|
||||
/// # ABNF
|
||||
/// ```plain
|
||||
@@ -29,11 +27,11 @@ common_header! {
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::HttpResponse;
|
||||
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem};
|
||||
/// use actix_web::http::header::{AcceptEncoding, Encoding, Preference, QualityItem};
|
||||
///
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// AcceptEncoding(vec![QualityItem::max(Encoding::Chunked)])
|
||||
/// AcceptEncoding(vec![QualityItem::max(Preference::Specific(Encoding::gzip()))])
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
@@ -44,40 +42,388 @@ common_header! {
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// AcceptEncoding(vec![
|
||||
/// QualityItem::max(Encoding::Chunked),
|
||||
/// QualityItem::max(Encoding::Gzip),
|
||||
/// QualityItem::max(Encoding::Deflate),
|
||||
/// "gzip".parse().unwrap(),
|
||||
/// "br".parse().unwrap(),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::HttpResponse;
|
||||
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q};
|
||||
///
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// AcceptEncoding(vec![
|
||||
/// QualityItem::max(Encoding::Chunked),
|
||||
/// QualityItem::new(Encoding::Gzip, q(0.60)),
|
||||
/// QualityItem::min(Encoding::EncodingExt("*".to_owned())),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
(AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Encoding>)*
|
||||
(AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Preference<Encoding>>)*
|
||||
|
||||
test_parse_and_format {
|
||||
// From the RFC
|
||||
common_header_test!(test1, vec![b"compress, gzip"]);
|
||||
common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![])));
|
||||
common_header_test!(test3, vec![b"*"]);
|
||||
common_header_test!(no_headers, vec![b""; 0], Some(AcceptEncoding(vec![])));
|
||||
common_header_test!(empty_header, vec![b""; 1], Some(AcceptEncoding(vec![])));
|
||||
|
||||
common_header_test!(
|
||||
order_of_appearance,
|
||||
vec![b"br, gzip"],
|
||||
Some(AcceptEncoding(vec![
|
||||
QualityItem::max(Preference::Specific(Encoding::brotli())),
|
||||
QualityItem::max(Preference::Specific(Encoding::gzip())),
|
||||
]))
|
||||
);
|
||||
|
||||
common_header_test!(any, vec![b"*"], Some(AcceptEncoding(vec![
|
||||
QualityItem::max(Preference::Any),
|
||||
])));
|
||||
|
||||
// Note: Removed quality 1 from gzip
|
||||
common_header_test!(test4, vec![b"compress;q=0.5, gzip"]);
|
||||
common_header_test!(implicit_quality, vec![b"gzip, identity; q=0.5, *;q=0"]);
|
||||
|
||||
// Note: Removed quality 1 from gzip
|
||||
common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]);
|
||||
common_header_test!(implicit_quality_out_of_order, vec![b"compress;q=0.5, gzip"]);
|
||||
|
||||
common_header_test!(
|
||||
only_gzip_no_identity,
|
||||
vec![b"gzip, *; q=0"],
|
||||
Some(AcceptEncoding(vec![
|
||||
QualityItem::max(Preference::Specific(Encoding::gzip())),
|
||||
QualityItem::zero(Preference::Any),
|
||||
]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: shortcut for EncodingExt(*) = Any
|
||||
impl AcceptEncoding {
|
||||
/// Selects the most acceptable encoding according to client preference and supported types.
|
||||
///
|
||||
/// The "identity" encoding is not assumed and should be included in the `supported` iterator
|
||||
/// if a non-encoded representation can be selected.
|
||||
///
|
||||
/// If `None` is returned, this indicates that none of the supported encodings are acceptable to
|
||||
/// the client. The caller should generate a 406 Not Acceptable response (unencoded) that
|
||||
/// includes the server's supported encodings in the body plus a [`Vary`] header.
|
||||
///
|
||||
/// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
|
||||
pub fn negotiate<'a>(
|
||||
&self,
|
||||
supported: impl Iterator<Item = &'a Encoding>,
|
||||
) -> Option<Encoding> {
|
||||
// 1. If no Accept-Encoding field is in the request, any content-coding is considered
|
||||
// acceptable by the user agent.
|
||||
|
||||
let supported_set = supported.collect::<HashSet<_>>();
|
||||
|
||||
if supported_set.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.0.is_empty() {
|
||||
// though it is not recommended to encode in this case, return identity encoding
|
||||
return Some(Encoding::identity());
|
||||
}
|
||||
|
||||
// 2. If the representation has no content-coding, then it is acceptable by default unless
|
||||
// specifically excluded by the Accept-Encoding field stating either "identity;q=0" or
|
||||
// "*;q=0" without a more specific entry for "identity".
|
||||
|
||||
let acceptable_items = self.ranked_items().collect::<Vec<_>>();
|
||||
|
||||
let identity_acceptable = is_identity_acceptable(&acceptable_items);
|
||||
let identity_supported = supported_set.contains(&Encoding::identity());
|
||||
|
||||
if identity_acceptable && identity_supported && supported_set.len() == 1 {
|
||||
return Some(Encoding::identity());
|
||||
}
|
||||
|
||||
// 3. If the representation's content-coding is one of the content-codings listed in the
|
||||
// Accept-Encoding field, then it is acceptable unless it is accompanied by a qvalue of 0.
|
||||
|
||||
// 4. If multiple content-codings are acceptable, then the acceptable content-coding with
|
||||
// the highest non-zero qvalue is preferred.
|
||||
|
||||
let matched = acceptable_items
|
||||
.into_iter()
|
||||
.filter(|q| q.quality > Quality::ZERO)
|
||||
// search relies on item list being in descending order of quality
|
||||
.find(|q| {
|
||||
let enc = &q.item;
|
||||
matches!(enc, Preference::Specific(enc) if supported_set.contains(enc))
|
||||
})
|
||||
.map(|q| q.item);
|
||||
|
||||
match matched {
|
||||
Some(Preference::Specific(enc)) => Some(enc),
|
||||
|
||||
_ if identity_acceptable => Some(Encoding::identity()),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the most preferable encoding, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first encoding is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// As per the spec, returns [`Preference::Any`] if acceptable list is empty. Though, if this is
|
||||
/// returned, it is recommended to use an un-encoded representation.
|
||||
///
|
||||
/// If `None` is returned, it means that the client has signalled that no representations
|
||||
/// are acceptable. This should never occur for a well behaved user-agent.
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn preference(&self) -> Option<Preference<Encoding>> {
|
||||
// empty header indicates no preference
|
||||
if self.0.is_empty() {
|
||||
return Some(Preference::Any);
|
||||
}
|
||||
|
||||
let mut max_item = None;
|
||||
let mut max_pref = Quality::ZERO;
|
||||
|
||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||
|
||||
for pref in &self.0 {
|
||||
// only change if strictly greater
|
||||
// equal items, even while unsorted, still have higher preference if they appear first
|
||||
if pref.quality > max_pref {
|
||||
max_pref = pref.quality;
|
||||
max_item = Some(pref.item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Return max_item if any items were above 0 quality...
|
||||
max_item.or_else(|| {
|
||||
// ...or else check for "*" or "identity". We can elide quality checks since
|
||||
// entering this block means all items had "q=0".
|
||||
match self.0.iter().find(|pref| {
|
||||
matches!(
|
||||
pref.item,
|
||||
Preference::Any
|
||||
| Preference::Specific(Encoding::Known(ContentEncoding::Identity))
|
||||
)
|
||||
}) {
|
||||
// "identity" or "*" found so no representation is acceptable
|
||||
Some(_) => None,
|
||||
|
||||
// implicit "identity" is acceptable
|
||||
None => Some(Preference::Specific(Encoding::identity())),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a sorted list of encodings from highest to lowest precedence, accounting
|
||||
/// for [q-factor weighting].
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn ranked(&self) -> Vec<Preference<Encoding>> {
|
||||
self.ranked_items().map(|q| q.item).collect()
|
||||
}
|
||||
|
||||
fn ranked_items(&self) -> impl Iterator<Item = QualityItem<Preference<Encoding>>> {
|
||||
if self.0.is_empty() {
|
||||
return vec![].into_iter();
|
||||
}
|
||||
|
||||
let mut types = self.0.clone();
|
||||
|
||||
// use stable sort so items with equal q-factor retain listed order
|
||||
types.sort_by(|a, b| {
|
||||
// sort by q-factor descending
|
||||
b.quality.cmp(&a.quality)
|
||||
});
|
||||
|
||||
types.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if "identity" is an acceptable encoding.
|
||||
///
|
||||
/// Internal algorithm relies on item list being in descending order of quality.
|
||||
fn is_identity_acceptable(items: &'_ [QualityItem<Preference<Encoding>>]) -> bool {
|
||||
if items.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop algorithm depends on items being sorted in descending order of quality. As such, it
|
||||
// is sufficient to return (q > 0) when reaching either an "identity" or "*" item.
|
||||
for q in items {
|
||||
match (q.quality, &q.item) {
|
||||
// occurrence of "identity;q=n"; return true if quality is non-zero
|
||||
(q, Preference::Specific(Encoding::Known(ContentEncoding::Identity))) => {
|
||||
return q > Quality::ZERO
|
||||
}
|
||||
|
||||
// occurrence of "*;q=n"; return true if quality is non-zero
|
||||
(q, Preference::Any) => return q > Quality::ZERO,
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// implicit acceptable identity
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::http::header::*;
|
||||
|
||||
macro_rules! accept_encoding {
|
||||
() => { AcceptEncoding(vec![]) };
|
||||
($($q:expr),+ $(,)?) => { AcceptEncoding(vec![$($q.parse().unwrap()),+]) };
|
||||
}
|
||||
|
||||
/// Parses an encoding string.
|
||||
fn enc(enc: &str) -> Preference<Encoding> {
|
||||
enc.parse().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_identity_acceptable() {
|
||||
macro_rules! accept_encoding_ranked {
|
||||
() => { accept_encoding!().ranked_items().collect::<Vec<_>>() };
|
||||
($($q:expr),+ $(,)?) => { accept_encoding!($($q),+).ranked_items().collect::<Vec<_>>() };
|
||||
}
|
||||
|
||||
let test = accept_encoding_ranked!();
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "br");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "*;q=0.1");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "identity;q=0.1");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "identity;q=0.1", "*;q=0");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0.1");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
|
||||
let test = accept_encoding_ranked!("gzip", "*;q=0");
|
||||
assert!(!is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "identity;q=0");
|
||||
assert!(!is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "identity;q=0", "*;q=0");
|
||||
assert!(!is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0");
|
||||
assert!(!is_identity_acceptable(&test));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoding_negotiation() {
|
||||
// no preference
|
||||
let test = accept_encoding!();
|
||||
assert_eq!(test.negotiate([].iter()), None);
|
||||
|
||||
let test = accept_encoding!();
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::identity()].iter()),
|
||||
Some(Encoding::identity()),
|
||||
);
|
||||
|
||||
let test = accept_encoding!("identity;q=0");
|
||||
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
|
||||
|
||||
let test = accept_encoding!("*;q=0");
|
||||
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
|
||||
|
||||
let test = accept_encoding!();
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::identity()),
|
||||
);
|
||||
|
||||
let test = accept_encoding!("gzip");
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip()),
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
|
||||
Some(Encoding::identity()),
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip()),
|
||||
);
|
||||
|
||||
let test = accept_encoding!("gzip", "identity;q=0");
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip()),
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
|
||||
None
|
||||
);
|
||||
|
||||
let test = accept_encoding!("gzip", "*;q=0");
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip()),
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
|
||||
None
|
||||
);
|
||||
|
||||
let test = accept_encoding!("gzip", "deflate", "br");
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip()),
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
|
||||
Some(Encoding::brotli())
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::deflate(), Encoding::identity()].iter()),
|
||||
Some(Encoding::deflate())
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate(
|
||||
[Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter()
|
||||
),
|
||||
Some(Encoding::gzip())
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::brotli(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip())
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ranking_precedence() {
|
||||
let test = accept_encoding!();
|
||||
assert!(test.ranked().is_empty());
|
||||
|
||||
let test = accept_encoding!("gzip");
|
||||
assert_eq!(test.ranked(), vec![enc("gzip")]);
|
||||
|
||||
let test = accept_encoding!("gzip;q=0.900", "*;q=0.700", "br;q=1.0");
|
||||
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
|
||||
|
||||
let test = accept_encoding!("br", "gzip", "*");
|
||||
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preference_selection() {
|
||||
assert_eq!(accept_encoding!().preference(), Some(Preference::Any));
|
||||
|
||||
assert_eq!(accept_encoding!("identity;q=0").preference(), None);
|
||||
assert_eq!(accept_encoding!("*;q=0").preference(), None);
|
||||
assert_eq!(accept_encoding!("compress;q=0", "*;q=0").preference(), None);
|
||||
assert_eq!(accept_encoding!("identity;q=0", "*;q=0").preference(), None);
|
||||
|
||||
let test = accept_encoding!("*;q=0.5");
|
||||
assert_eq!(test.preference().unwrap(), enc("*"));
|
||||
|
||||
let test = accept_encoding!("br;q=0");
|
||||
assert_eq!(test.preference().unwrap(), enc("identity"));
|
||||
|
||||
let test = accept_encoding!("br;q=0.900", "gzip;q=1.0", "*;q=0.500");
|
||||
assert_eq!(test.preference().unwrap(), enc("gzip"));
|
||||
|
||||
let test = accept_encoding!("br", "gzip", "*");
|
||||
assert_eq!(test.preference().unwrap(), enc("br"));
|
||||
}
|
||||
}
|
||||
|
@@ -37,7 +37,7 @@ common_header! {
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// AcceptLanguage(vec![
|
||||
/// QualityItem::max("en-US".parse().unwrap())
|
||||
/// "en-US".parse().unwrap(),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
@@ -49,9 +49,9 @@ common_header! {
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// AcceptLanguage(vec![
|
||||
/// QualityItem::max("da".parse().unwrap()),
|
||||
/// QualityItem::new("en-GB".parse().unwrap(), q(0.8)),
|
||||
/// QualityItem::new("en".parse().unwrap(), q(0.7)),
|
||||
/// "da".parse().unwrap(),
|
||||
/// "en-GB;q=0.8".parse().unwrap(),
|
||||
/// "en;q=0.7".parse().unwrap(),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
@@ -93,6 +93,33 @@ common_header! {
|
||||
}
|
||||
|
||||
impl AcceptLanguage {
|
||||
/// Extracts the most preferable language, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first language is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// As per the spec, returns [`Preference::Any`] if contained list is empty.
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn preference(&self) -> Preference<LanguageTag> {
|
||||
let mut max_item = None;
|
||||
let mut max_pref = Quality::ZERO;
|
||||
|
||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||
|
||||
for pref in &self.0 {
|
||||
// only change if strictly greater
|
||||
// equal items, even while unsorted, still have higher preference if they appear first
|
||||
if pref.quality > max_pref {
|
||||
max_pref = pref.quality;
|
||||
max_item = Some(pref.item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
max_item.unwrap_or(Preference::Any)
|
||||
}
|
||||
|
||||
/// Returns a sorted list of languages from highest to lowest precedence, accounting
|
||||
/// for [q-factor weighting].
|
||||
///
|
||||
@@ -112,33 +139,6 @@ impl AcceptLanguage {
|
||||
|
||||
types.into_iter().map(|qitem| qitem.item).collect()
|
||||
}
|
||||
|
||||
/// Extracts the most preferable language, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first language is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// As per the spec, returns [`Preference::Any`] if contained list is empty.
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn preference(&self) -> Preference<LanguageTag> {
|
||||
let mut max_item = None;
|
||||
let mut max_pref = Quality::MIN;
|
||||
|
||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||
|
||||
for pref in &self.0 {
|
||||
// only change if strictly greater
|
||||
// equal items, even while unsorted, still have higher preference if they appear first
|
||||
if pref.quality > max_pref {
|
||||
max_pref = pref.quality;
|
||||
max_item = Some(pref.item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
max_item.unwrap_or(Preference::Any)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -152,7 +152,7 @@ mod tests {
|
||||
assert!(test.ranked().is_empty());
|
||||
|
||||
let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]);
|
||||
assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap()));
|
||||
assert_eq!(test.ranked(), vec!["fr-CH".parse().unwrap()]);
|
||||
|
||||
let test = AcceptLanguage(vec.
|
||||
// TODO: think about using private fields and smallvec
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ContentDisposition {
|
||||
/// The disposition type
|
||||
|
@@ -1,69 +1,55 @@
|
||||
use std::{fmt, str};
|
||||
|
||||
pub use self::Encoding::{
|
||||
Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, Zstd,
|
||||
};
|
||||
use actix_http::ContentEncoding;
|
||||
|
||||
/// A value to represent an encoding used in `Transfer-Encoding` or `Accept-Encoding` header.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// A value to represent an encoding used in the `Accept-Encoding` and `Content-Encoding` header.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Encoding {
|
||||
/// The `chunked` encoding.
|
||||
Chunked,
|
||||
/// A supported content encoding. See [`ContentEncoding`] for variants.
|
||||
Known(ContentEncoding),
|
||||
|
||||
/// The `br` encoding.
|
||||
Brotli,
|
||||
/// Some other encoding that is less common, can be any string.
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
/// The `gzip` encoding.
|
||||
Gzip,
|
||||
impl Encoding {
|
||||
pub const fn identity() -> Self {
|
||||
Self::Known(ContentEncoding::Identity)
|
||||
}
|
||||
|
||||
/// The `deflate` encoding.
|
||||
Deflate,
|
||||
pub const fn brotli() -> Self {
|
||||
Self::Known(ContentEncoding::Brotli)
|
||||
}
|
||||
|
||||
/// The `compress` encoding.
|
||||
Compress,
|
||||
pub const fn deflate() -> Self {
|
||||
Self::Known(ContentEncoding::Deflate)
|
||||
}
|
||||
|
||||
/// The `identity` encoding.
|
||||
Identity,
|
||||
pub const fn gzip() -> Self {
|
||||
Self::Known(ContentEncoding::Gzip)
|
||||
}
|
||||
|
||||
/// The `trailers` encoding.
|
||||
Trailers,
|
||||
|
||||
/// The `zstd` encoding.
|
||||
Zstd,
|
||||
|
||||
/// Some other encoding that is less common, can be any String.
|
||||
EncodingExt(String),
|
||||
pub const fn zstd() -> Self {
|
||||
Self::Known(ContentEncoding::Zstd)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Encoding {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match *self {
|
||||
Chunked => "chunked",
|
||||
Brotli => "br",
|
||||
Gzip => "gzip",
|
||||
Deflate => "deflate",
|
||||
Compress => "compress",
|
||||
Identity => "identity",
|
||||
Trailers => "trailers",
|
||||
Zstd => "zstd",
|
||||
EncodingExt(ref s) => s.as_ref(),
|
||||
f.write_str(match self {
|
||||
Encoding::Known(enc) => enc.as_str(),
|
||||
Encoding::Unknown(enc) => enc.as_str(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl str::FromStr for Encoding {
|
||||
type Err = crate::error::ParseError;
|
||||
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
|
||||
match s {
|
||||
"chunked" => Ok(Chunked),
|
||||
"br" => Ok(Brotli),
|
||||
"deflate" => Ok(Deflate),
|
||||
"gzip" => Ok(Gzip),
|
||||
"compress" => Ok(Compress),
|
||||
"identity" => Ok(Identity),
|
||||
"trailers" => Ok(Trailers),
|
||||
"zstd" => Ok(Zstd),
|
||||
_ => Ok(EncodingExt(s.to_owned())),
|
||||
|
||||
fn from_str(enc: &str) -> Result<Self, crate::error::ParseError> {
|
||||
match enc.parse::<ContentEncoding>() {
|
||||
Ok(enc) => Ok(Self::Known(enc)),
|
||||
Err(_) => Ok(Self::Unknown(enc.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user