1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-05-09 03:53:00 +00:00
actix-web/actix-web/src/http/header/accept.rs
2022-02-01 00:30:41 +00:00

294 lines
9.7 KiB
Rust

use std::cmp::Ordering;
use mime::Mime;
use super::{common_header, QualityItem};
use crate::http::header;
common_header! {
/// `Accept` header, defined
/// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2)
///
/// The `Accept` header field can be used by user agents to specify
/// response media types that are acceptable. Accept header fields can
/// be used to indicate that the request is specifically limited to a
/// small set of desired types, as in the case of a request for an
/// in-line image
///
/// # ABNF
/// ```plain
/// Accept = #( media-range [ accept-params ] )
///
/// media-range = ( "*/*"
/// / ( type "/" "*" )
/// / ( type "/" subtype )
/// ) *( OWS ";" OWS parameter )
/// accept-params = weight *( accept-ext )
/// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ]
/// ```
///
/// # Example Values
/// * `audio/*; q=0.2, audio/basic`
/// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c`
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{Accept, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// Accept(vec![
/// QualityItem::max(mime::TEXT_HTML),
/// ])
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{Accept, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// Accept(vec![
/// QualityItem::max(mime::APPLICATION_JSON),
/// ])
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{Accept, QualityItem, q};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// Accept(vec![
/// QualityItem::max(mime::TEXT_HTML),
/// QualityItem::max("application/xhtml+xml".parse().unwrap()),
/// QualityItem::new(mime::TEXT_XML, q(0.9)),
/// QualityItem::max("image/webp".parse().unwrap()),
/// QualityItem::new(mime::STAR_STAR, q(0.8)),
/// ])
/// );
/// ```
(Accept, header::ACCEPT) => (QualityItem<Mime>)*
test_parse_and_format {
// Tests from the RFC
crate::http::header::common_header_test!(
test1,
vec![b"audio/*; q=0.2, audio/basic"],
Some(Accept(vec![
QualityItem::new("audio/*".parse().unwrap(), q(0.2)),
QualityItem::max("audio/basic".parse().unwrap()),
])));
crate::http::header::common_header_test!(
test2,
vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"],
Some(Accept(vec![
QualityItem::new(mime::TEXT_PLAIN, q(0.5)),
QualityItem::max(mime::TEXT_HTML),
QualityItem::new(
"text/x-dvi".parse().unwrap(),
q(0.8)),
QualityItem::max("text/x-c".parse().unwrap()),
])));
// Custom tests
crate::http::header::common_header_test!(
test3,
vec![b"text/plain; charset=utf-8"],
Some(Accept(vec![
QualityItem::max(mime::TEXT_PLAIN_UTF_8),
])));
crate::http::header::common_header_test!(
test4,
vec![b"text/plain; charset=utf-8; q=0.5"],
Some(Accept(vec![
QualityItem::new(mime::TEXT_PLAIN_UTF_8,
q(0.5)),
])));
#[test]
fn test_fuzzing1() {
let req = test::TestRequest::default()
.insert_header((header::ACCEPT, "chunk#;e"))
.finish();
let header = Accept::parse(&req);
assert!(header.is_ok());
}
}
}
impl Accept {
/// Construct `Accept: */*`.
pub fn star() -> Accept {
Accept(vec![QualityItem::max(mime::STAR_STAR)])
}
/// Construct `Accept: application/json`.
pub fn json() -> Accept {
Accept(vec![QualityItem::max(mime::APPLICATION_JSON)])
}
/// Construct `Accept: text/*`.
pub fn text() -> Accept {
Accept(vec![QualityItem::max(mime::TEXT_STAR)])
}
/// Construct `Accept: image/*`.
pub fn image() -> Accept {
Accept(vec![QualityItem::max(mime::IMAGE_STAR)])
}
/// Construct `Accept: text/html`.
pub fn html() -> 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.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn ranked(&self) -> Vec<Mime> {
if self.is_empty() {
return vec![];
}
let mut types = self.0.clone();
// use stable sort so items with equal q-factor and specificity retain listed order
types.sort_by(|a, b| {
// sort by q-factor descending
b.quality.cmp(&a.quality).then_with(|| {
// use specificity rules on mime types with
// same q-factor (eg. text/html > text/* > */*)
// subtypes are not comparable if main type is star, so return
match (a.item.type_(), b.item.type_()) {
(mime::STAR, mime::STAR) => return Ordering::Equal,
// a is sorted after b
(mime::STAR, _) => return Ordering::Greater,
// a is sorted before b
(_, mime::STAR) => return Ordering::Less,
_ => {}
}
// in both these match expressions, the returned ordering appears
// inverted because sort is high-to-low ("descending") precedence
match (a.item.subtype(), b.item.subtype()) {
(mime::STAR, mime::STAR) => Ordering::Equal,
// a is sorted after b
(mime::STAR, _) => Ordering::Greater,
// a is sorted before b
(_, mime::STAR) => Ordering::Less,
_ => Ordering::Equal,
}
})
});
types.into_iter().map(|qitem| qitem.item).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::header::q;
#[test]
fn ranking_precedence() {
let test = Accept(vec![]);
assert!(test.ranked().is_empty());
let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]);
assert_eq!(test.ranked(), vec![mime::APPLICATION_JSON]);
let test = Accept(vec![
QualityItem::max(mime::TEXT_HTML),
"application/xhtml+xml".parse().unwrap(),
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
QualityItem::new(mime::STAR_STAR, q(0.8)),
]);
assert_eq!(
test.ranked(),
vec![
mime::TEXT_HTML,
"application/xhtml+xml".parse().unwrap(),
"application/xml".parse().unwrap(),
mime::STAR_STAR,
]
);
let test = Accept(vec![
QualityItem::max(mime::STAR_STAR),
QualityItem::max(mime::IMAGE_STAR),
QualityItem::max(mime::IMAGE_PNG),
]);
assert_eq!(
test.ranked(),
vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR]
);
}
#[test]
fn preference_selection() {
let test = Accept(vec![
QualityItem::max(mime::TEXT_HTML),
"application/xhtml+xml".parse().unwrap(),
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
QualityItem::new(mime::STAR_STAR, q(0.8)),
]);
assert_eq!(test.preference(), mime::TEXT_HTML);
let test = Accept(vec![
QualityItem::new("video/*".parse().unwrap(), q(0.8)),
QualityItem::max(mime::IMAGE_PNG),
QualityItem::new(mime::STAR_STAR, q(0.5)),
QualityItem::max(mime::IMAGE_SVG),
QualityItem::new(mime::IMAGE_STAR, q(0.8)),
]);
assert_eq!(test.preference(), mime::IMAGE_PNG);
}
}