From 075d871e6392d5970908d95e1e043cf16aac1e3b Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 2 Dec 2021 13:59:25 +0000 Subject: [PATCH] wrap `LanguageTags` type in new `AnyOrSome` type to support wildcards (#2480) --- CHANGES.md | 11 + actix-http/src/header/mod.rs | 2 +- actix-http/src/header/shared/quality_item.rs | 15 +- actix-http/src/header/utils.rs | 31 ++- src/http/header/accept.rs | 51 +++-- src/http/header/accept_language.rs | 210 ++++++++++++++++--- src/http/header/macros.rs | 138 +++++++----- src/http/header/mod.rs | 77 ++++--- src/http/header/preference.rs | 70 +++++++ 9 files changed, 469 insertions(+), 136 deletions(-) create mode 100644 src/http/header/preference.rs diff --git a/CHANGES.md b/CHANGES.md index 00bf85f27..36a56b828 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,17 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +* Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] + +### Changed +* Rename `Accept::{mime_precedence => ranked}`. [#2480] +* Rename `Accept::{mime_preference => preference}`. [#2480] + +### Fixed +* Accept wildcard `*` items in `AcceptLanguage`. [#2480] + +[#2480]: https://github.com/actix/actix-web/pull/2480 ## 4.0.0-beta.13 - 2021-11-30 diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 721f4ea79..308cb0123 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -55,7 +55,7 @@ pub trait Header: IntoHeaderValue { fn name() -> HeaderName; /// Parse a header - fn parse(msg: &T) -> Result; + fn parse(msg: &M) -> Result; } /// Convert `http::HeaderMap` to our `HeaderMap`. diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index ad1f6877d..b9cca9112 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -1,8 +1,7 @@ use std::{ cmp, convert::{TryFrom, TryInto}, - fmt, - str::{self, FromStr}, + fmt, str, }; use derive_more::{Display, Error}; @@ -83,16 +82,17 @@ impl TryFrom for Quality { /// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1). #[derive(Clone, PartialEq, Debug)] pub struct QualityItem { - /// The actual contents of the field. + /// The wrapped contents of the field. pub item: T, + /// The quality (client or server preference) for the value. pub quality: Quality, } impl QualityItem { - /// Creates a new `QualityItem` from an item and a quality. - /// The item can be of any type. - /// The quality should be a value in the range [0, 1]. + /// Constructs a new `QualityItem` from an item and a quality value. + /// + /// The item can be of any type. The quality should be a value in the range [0, 1]. pub fn new(item: T, quality: Quality) -> QualityItem { QualityItem { item, quality } } @@ -116,7 +116,7 @@ impl fmt::Display for QualityItem { } } -impl FromStr for QualityItem { +impl str::FromStr for QualityItem { type Err = ParseError; fn from_str(qitem_str: &str) -> Result { @@ -128,6 +128,7 @@ impl FromStr for QualityItem { let mut raw_item = qitem_str; let mut quality = 1f32; + // TODO: MSRV(1.52): use rsplit_once let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect(); if parts.len() == 2 { diff --git a/actix-http/src/header/utils.rs b/actix-http/src/header/utils.rs index 94c5b6dcb..2168202b9 100644 --- a/actix-http/src/header/utils.rs +++ b/actix-http/src/header/utils.rs @@ -12,7 +12,8 @@ where I: Iterator + 'a, T: FromStr, { - let mut result = Vec::new(); + let size_guess = all.size_hint().1.unwrap_or(2); + let mut result = Vec::with_capacity(size_guess); for h in all { let s = h.to_str().map_err(|_| ParseError::Header)?; @@ -26,6 +27,7 @@ where .filter_map(|x| x.trim().parse().ok()), ) } + Ok(result) } @@ -34,10 +36,12 @@ where pub fn from_one_raw_str(val: Option<&HeaderValue>) -> Result { 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) } @@ -48,13 +52,16 @@ 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(()) } @@ -65,3 +72,25 @@ pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Res let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE); fmt::Display::fmt(&encoded, f) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn comma_delimited_parsing() { + let headers = vec![]; + let res: Vec = from_comma_delimited(headers.iter()).unwrap(); + assert_eq!(res, vec![0; 0]); + + let headers = vec![ + HeaderValue::from_static(""), + HeaderValue::from_static(","), + HeaderValue::from_static(" "), + HeaderValue::from_static("1 ,"), + HeaderValue::from_static(""), + ]; + let res: Vec = from_comma_delimited(headers.iter()).unwrap(); + assert_eq!(res, vec![1]); + } +} diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index 3c4d5f599..bc794c02c 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -77,7 +77,7 @@ crate::http::header::common_header! { /// ]) /// ); /// ``` - (Accept, header::ACCEPT) => (QualityItem)+ + (Accept, header::ACCEPT) => (QualityItem)* test_parse_and_format { // Tests from the RFC @@ -88,6 +88,7 @@ crate::http::header::common_header! { QualityItem::new("audio/*".parse().unwrap(), q(200)), qitem("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"], @@ -99,6 +100,7 @@ crate::http::header::common_header! { q(800)), qitem("text/x-c".parse().unwrap()), ]))); + // Custom tests crate::http::header::common_header_test!( test3, @@ -154,7 +156,11 @@ impl Accept { /// [q-factor weighting] and specificity. /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 - pub fn mime_precedence(&self) -> Vec { + pub fn ranked(&self) -> Vec { + 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 @@ -201,12 +207,29 @@ impl Accept { /// If no q-factors are provided, the first mime type is chosen. Note that items without /// q-factors are given the maximum preference value. /// - /// Returns `None` if contained list is empty. + /// 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 mime_preference(&self) -> Option { - let types = self.mime_precedence(); - types.first().cloned() + pub fn preference(&self) -> Mime { + use actix_http::header::q; + + let mut max_item = None; + let mut max_pref = q(0); + + // 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) } } @@ -216,12 +239,12 @@ mod tests { use crate::http::header::q; #[test] - fn test_mime_precedence() { + fn ranking_precedence() { let test = Accept(vec![]); - assert!(test.mime_precedence().is_empty()); + assert!(test.ranked().is_empty()); let test = Accept(vec![qitem(mime::APPLICATION_JSON)]); - assert_eq!(test.mime_precedence(), vec!(mime::APPLICATION_JSON)); + assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON)); let test = Accept(vec![ qitem(mime::TEXT_HTML), @@ -230,7 +253,7 @@ mod tests { QualityItem::new(mime::STAR_STAR, q(0.8)), ]); assert_eq!( - test.mime_precedence(), + test.ranked(), vec![ mime::TEXT_HTML, "application/xhtml+xml".parse().unwrap(), @@ -245,20 +268,20 @@ mod tests { qitem(mime::IMAGE_PNG), ]); assert_eq!( - test.mime_precedence(), + test.ranked(), vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR] ); } #[test] - fn test_mime_preference() { + fn preference_selection() { let test = Accept(vec![ qitem(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.mime_preference(), Some(mime::TEXT_HTML)); + assert_eq!(test.preference(), mime::TEXT_HTML); let test = Accept(vec![ QualityItem::new("video/*".parse().unwrap(), q(0.8)), @@ -267,6 +290,6 @@ mod tests { qitem(mime::IMAGE_SVG), QualityItem::new(mime::IMAGE_STAR, q(0.8)), ]); - assert_eq!(test.mime_preference(), Some(mime::IMAGE_PNG)); + assert_eq!(test.preference(), mime::IMAGE_PNG); } } diff --git a/src/http/header/accept_language.rs b/src/http/header/accept_language.rs index a7b60e366..e96d1d13e 100644 --- a/src/http/header/accept_language.rs +++ b/src/http/header/accept_language.rs @@ -1,66 +1,224 @@ use language_tags::LanguageTag; -use super::{QualityItem, ACCEPT_LANGUAGE}; +use super::{common_header, Preference, QualityItem}; +use crate::http::header; -crate::http::header::common_header! { +common_header! { /// `Accept-Language` header, defined /// in [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) /// - /// The `Accept-Language` header field can be used by user agents to - /// indicate the set of natural languages that are preferred in the - /// response. + /// The `Accept-Language` header field can be used by user agents to indicate the set of natural + /// languages that are preferred in the response. + /// + /// The `Accept-Language` header is defined in + /// [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) using language + /// ranges defined in [RFC 4647 §2.1](https://datatracker.ietf.org/doc/html/rfc4647#section-2.1). /// /// # ABNF - /// /// ```text /// Accept-Language = 1#( language-range [ weight ] ) - /// language-range = + /// language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*" + /// alphanum = ALPHA / DIGIT + /// weight = OWS ";" OWS "q=" qvalue + /// qvalue = ( "0" [ "." 0*3DIGIT ] ) + /// / ( "1" [ "." 0*3("0") ] ) /// ``` /// - /// # Example values - /// * `da, en-gb;q=0.8, en;q=0.7` - /// * `en-us;q=1.0, en;q=0.5, fr` + /// # Example Values + /// - `da, en-gb;q=0.8, en;q=0.7` + /// - `en-us;q=1.0, en;q=0.5, fr` + /// - `fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5` /// /// # Examples - /// /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, LanguageTag, qitem}; + /// use actix_web::http::header::{AcceptLanguage, qitem}; /// /// let mut builder = HttpResponse::Ok(); - /// let langtag = LanguageTag::parse("en-US").unwrap(); /// builder.insert_header( /// AcceptLanguage(vec![ - /// qitem(langtag), + /// qitem("en-US".parse().unwrap()) /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, LanguageTag, QualityItem, q, qitem}; + /// use actix_web::http::header::{AcceptLanguage, QualityItem, q, qitem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptLanguage(vec![ - /// qitem(LanguageTag::parse("da").unwrap()), - /// QualityItem::new(LanguageTag::parse("en-GB").unwrap(), q(800)), - /// QualityItem::new(LanguageTag::parse("en").unwrap(), q(700)), + /// qitem("da".parse().unwrap()), + /// QualityItem::new("en-GB".parse().unwrap(), q(800)), + /// QualityItem::new("en".parse().unwrap(), q(700)), /// ]) /// ); /// ``` - (AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem)+ + (AcceptLanguage, header::ACCEPT_LANGUAGE) => (QualityItem>)* - test_accept_language { - // From the RFC - crate::http::header::common_header_test!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); - // Own test - crate::http::header::common_header_test!( - test2, vec![b"en-US, en; q=0.5, fr"], + parse_and_fmt_tests { + common_header_test!(no_headers, vec![b""; 0], Some(AcceptLanguage(vec![]))); + + common_header_test!(empty_header, vec![b""; 1], Some(AcceptLanguage(vec![]))); + + common_header_test!( + example_from_rfc, + vec![b"da, en-gb;q=0.8, en;q=0.7"] + ); + + common_header_test!( + not_ordered_by_weight, + vec![b"en-US, en; q=0.5, fr"], Some(AcceptLanguage(vec![ qitem("en-US".parse().unwrap()), QualityItem::new("en".parse().unwrap(), q(500)), qitem("fr".parse().unwrap()), - ]))); + ])) + ); + + common_header_test!( + has_wildcard, + vec![b"fr-CH, fr; q=0.9, en; q=0.8, de; q=0.7, *; q=0.5"], + Some(AcceptLanguage(vec![ + qitem("fr-CH".parse().unwrap()), + QualityItem::new("fr".parse().unwrap(), q(900)), + QualityItem::new("en".parse().unwrap(), q(800)), + QualityItem::new("de".parse().unwrap(), q(700)), + QualityItem::new("*".parse().unwrap(), q(500)), + ])) + ); + } +} + +impl AcceptLanguage { + /// Returns a sorted list of languages 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> { + if self.0.is_empty() { + return vec![]; + } + + 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().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 { + use actix_http::header::q; + + let mut max_item = None; + let mut max_pref = q(0); + + // 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)] +mod tests { + use super::*; + use crate::http::header::*; + + #[test] + fn ranking_precedence() { + let test = AcceptLanguage(vec![]); + assert!(test.ranked().is_empty()); + + let test = AcceptLanguage(vec![qitem("fr-CH".parse().unwrap())]); + assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap())); + + let test = AcceptLanguage(vec![ + QualityItem::new("fr".parse().unwrap(), q(900)), + QualityItem::new("fr-CH".parse().unwrap(), q(1000)), + QualityItem::new("en".parse().unwrap(), q(800)), + QualityItem::new("*".parse().unwrap(), q(500)), + QualityItem::new("de".parse().unwrap(), q(700)), + ]); + assert_eq!( + test.ranked(), + vec![ + "fr-CH".parse().unwrap(), + "fr".parse().unwrap(), + "en".parse().unwrap(), + "de".parse().unwrap(), + "*".parse().unwrap(), + ] + ); + + let test = AcceptLanguage(vec![ + qitem("fr".parse().unwrap()), + qitem("fr-CH".parse().unwrap()), + qitem("en".parse().unwrap()), + qitem("*".parse().unwrap()), + qitem("de".parse().unwrap()), + ]); + assert_eq!( + test.ranked(), + vec![ + "fr".parse().unwrap(), + "fr-CH".parse().unwrap(), + "en".parse().unwrap(), + "*".parse().unwrap(), + "de".parse().unwrap(), + ] + ); + } + + #[test] + fn preference_selection() { + let test = AcceptLanguage(vec![ + QualityItem::new("fr".parse().unwrap(), q(900)), + QualityItem::new("fr-CH".parse().unwrap(), q(1000)), + QualityItem::new("en".parse().unwrap(), q(800)), + QualityItem::new("*".parse().unwrap(), q(500)), + QualityItem::new("de".parse().unwrap(), q(700)), + ]); + assert_eq!( + test.preference(), + Preference::Specific("fr-CH".parse().unwrap()) + ); + + let test = AcceptLanguage(vec![ + qitem("fr".parse().unwrap()), + qitem("fr-CH".parse().unwrap()), + qitem("en".parse().unwrap()), + qitem("*".parse().unwrap()), + qitem("de".parse().unwrap()), + ]); + assert_eq!( + test.preference(), + Preference::Specific("fr".parse().unwrap()) + ); + + let test = AcceptLanguage(vec![]); + assert_eq!(test.preference(), Preference::Any); } } diff --git a/src/http/header/macros.rs b/src/http/header/macros.rs index 419d4fb6e..7fed7f286 100644 --- a/src/http/header/macros.rs +++ b/src/http/header/macros.rs @@ -1,33 +1,36 @@ +// TODO: replace with derive_more impl macro_rules! common_header_deref { ($from:ty => $to:ty) => { - impl ::std::ops::Deref for $from { + impl ::core::ops::Deref for $from { type Target = $to; #[inline] - fn deref(&self) -> &$to { + fn deref(&self) -> &Self::Target { &self.0 } } - impl ::std::ops::DerefMut for $from { + impl ::core::ops::DerefMut for $from { #[inline] - fn deref_mut(&mut self) -> &mut $to { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } }; } +/// Sets up a test module with some useful imports for use with [`common_header_test!`]. macro_rules! common_header_test_module { ($id:ident, $tm:ident{$($tf:item)*}) => { - #[allow(unused_imports)] #[cfg(test)] mod $tm { + #![allow(unused_imports)] + use std::str; use actix_http::http::Method; use mime::*; use $crate::http::header::*; - use super::$id as HeaderField; + use super::{$id as HeaderField, *}; $($tf)* } } @@ -42,14 +45,19 @@ macro_rules! common_header_test { let raw = $raw; let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); + let mut req = test::TestRequest::default(); + for item in a { req = req.insert_header((HeaderField::name(), item)).take(); } + let req = req.finish(); let value = HeaderField::parse(&req); + let result = format!("{}", value.unwrap()); let expected = String::from_utf8(raw[0].to_vec()).unwrap(); + let result_cmp: Vec = result .to_ascii_lowercase() .split(' ') @@ -60,10 +68,12 @@ macro_rules! common_header_test { .split(' ') .map(|x| x.to_owned()) .collect(); + assert_eq!(result_cmp.concat(), expected_cmp.concat()); } }; - ($id:ident, $raw:expr, $typed:expr) => { + + ($id:ident, $raw:expr, $exp:expr) => { #[test] fn $id() { use actix_http::test; @@ -75,26 +85,35 @@ macro_rules! common_header_test { } let req = req.finish(); let val = HeaderField::parse(&req); - let typed: Option = $typed; - // Test parsing - assert_eq!(val.ok(), typed); - // Test formatting - if typed.is_some() { + let exp: Option = $exp; + + println!("req: {:?}", &req); + println!("val: {:?}", &val); + println!("exp: {:?}", &exp); + + // test parsing + assert_eq!(val.ok(), exp); + + // test formatting + if let Some(exp) = exp { let raw = &($raw)[..]; let mut iter = raw.iter().map(|b| str::from_utf8(&b[..]).unwrap()); let mut joined = String::new(); - joined.push_str(iter.next().unwrap()); - for s in iter { - joined.push_str(", "); + if let Some(s) = iter.next() { joined.push_str(s); + for s in iter { + joined.push_str(", "); + joined.push_str(s); + } } - assert_eq!(format!("{}", typed.unwrap()), joined); + assert_eq!(format!("{}", exp), joined); } } }; } macro_rules! common_header { + // TODO: these docs are wrong, there's no $n or $nn // $a:meta: Attributes associated with the header item (usually docs) // $id:ident: Identifier of the header // $n:expr: Lowercase name of the header @@ -111,92 +130,100 @@ macro_rules! common_header { fn name() -> $crate::http::header::HeaderName { $name } + #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::http::header::from_comma_delimited( - msg.headers().get_all(Self::name())).map($id) + fn parse(msg: &M) -> Result { + let headers = msg.headers().get_all(Self::name()); + $crate::http::header::from_comma_delimited(headers).map($id) } } - impl std::fmt::Display for $id { + + impl ::core::fmt::Display for $id { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { $crate::http::header::fmt_comma_delimited(f, &self.0[..]) } } + impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { - use std::fmt::Write; + use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); let _ = write!(&mut writer, "{}", self); $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) } } }; + // List header, one or more items ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)+) => { $(#[$a])* #[derive(Clone, Debug, PartialEq)] pub struct $id(pub Vec<$item>); + crate::http::header::common_header_deref!($id => Vec<$item>); + impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { $name } #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { + fn parse(msg: &M) -> Result { $crate::http::header::from_comma_delimited( msg.headers().get_all(Self::name())).map($id) } } - impl std::fmt::Display for $id { + + impl ::core::fmt::Display for $id { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { $crate::http::header::fmt_comma_delimited(f, &self.0[..]) } } + impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { - use std::fmt::Write; + use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); let _ = write!(&mut writer, "{}", self); $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) } } }; + // Single value header ($(#[$a:meta])*($id:ident, $name:expr) => [$value:ty]) => { $(#[$a])* #[derive(Clone, Debug, PartialEq)] pub struct $id(pub $value); + crate::http::header::common_header_deref!($id => $value); + impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { $name } + #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::http::header::from_one_raw_str( - msg.headers().get(Self::name())).map($id) + fn parse(msg: &M) -> Result { + let header = msg.headers().get(Self::name()); + $crate::http::header::from_one_raw_str(header).map($id) } } - impl std::fmt::Display for $id { + + impl ::core::fmt::Display for $id { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + ::core::fmt::Display::fmt(&self.0, f) } } + impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; @@ -205,6 +232,7 @@ macro_rules! common_header { } } }; + // List header, one or more items with "*" option ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { $(#[$a])* @@ -215,42 +243,46 @@ macro_rules! common_header { /// Only the listed items are a match Items(Vec<$item>), } + impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { $name } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - let any = msg.headers().get(Self::name()).and_then(|hdr| { - hdr.to_str().ok().and_then(|hdr| Some(hdr.trim() == "*"))}); - if let Some(true) = any { + #[inline] + fn parse(msg: &M) -> Result { + let is_any = msg + .headers() + .get(Self::name()) + .and_then(|hdr| hdr.to_str().ok()) + .map(|hdr| hdr.trim() == "*"); + + if let Some(true) = is_any { Ok($id::Any) } else { - Ok($id::Items( - $crate::http::header::from_comma_delimited( - msg.headers().get_all(Self::name()))?)) + let headers = msg.headers().get_all(Self::name()); + Ok($id::Items($crate::http::header::from_comma_delimited(headers)?)) } } } - impl std::fmt::Display for $id { + + impl ::core::fmt::Display for $id { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { match *self { $id::Any => f.write_str("*"), - $id::Items(ref fields) => $crate::http::header::fmt_comma_delimited( - f, &fields[..]) + $id::Items(ref fields) => + $crate::http::header::fmt_comma_delimited(f, &fields[..]) } } } + impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { - use std::fmt::Write; + use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); let _ = write!(&mut writer, "{}", self); $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) diff --git a/src/http/header/mod.rs b/src/http/header/mod.rs index 79ba5772b..45d5b8d1a 100644 --- a/src/http/header/mod.rs +++ b/src/http/header/mod.rs @@ -1,15 +1,52 @@ //! A Collection of Header implementations for common HTTP Headers. //! -//! ## Mime -//! +//! ## Mime Types //! Several header fields use MIME values for their contents. Keeping with the strongly-typed theme, //! the [mime] crate is used in such headers as [`ContentType`] and [`Accept`]. -use bytes::{Bytes, BytesMut}; use std::fmt; -pub use self::accept_charset::AcceptCharset; +use bytes::{Bytes, BytesMut}; + +// re-export from actix-http +// - header name / value types +// - relevant traits for converting to header name / value +// - all const header names +// - header map +// - the few typed headers from actix-http +// - header parsing utils pub use actix_http::http::header::*; + +mod accept_charset; +// mod accept_encoding; +mod accept; +mod accept_language; +mod allow; +mod cache_control; +mod content_disposition; +mod content_language; +mod content_range; +mod content_type; +mod date; +mod encoding; +mod entity; +mod etag; +mod expires; +mod if_match; +mod if_modified_since; +mod if_none_match; +mod if_range; +mod if_unmodified_since; +mod last_modified; +mod macros; +mod preference; +// mod range; + +#[cfg(test)] +pub(crate) use macros::common_header_test; +pub(crate) use macros::{common_header, common_header_deref, common_header_test_module}; + +pub use self::accept_charset::AcceptCharset; //pub use self::accept_encoding::AcceptEncoding; pub use self::accept::Accept; pub use self::accept_language::AcceptLanguage; @@ -30,11 +67,10 @@ pub use self::if_none_match::IfNoneMatch; pub use self::if_range::IfRange; pub use self::if_unmodified_since::IfUnmodifiedSince; pub use self::last_modified::LastModified; +pub use self::preference::Preference; //pub use self::range::{Range, ByteRangeSpec}; -pub(crate) use actix_http::http::header::{ - fmt_comma_delimited, from_comma_delimited, from_one_raw_str, -}; +/// Format writer ([`fmt::Write`]) for a [`BytesMut`]. #[derive(Debug, Default)] struct Writer { buf: BytesMut, @@ -62,30 +98,3 @@ impl fmt::Write for Writer { fmt::write(self, args) } } - -mod accept_charset; -// mod accept_encoding; -mod accept; -mod accept_language; -mod allow; -mod cache_control; -mod content_disposition; -mod content_language; -mod content_range; -mod content_type; -mod date; -mod encoding; -mod entity; -mod etag; -mod expires; -mod if_match; -mod if_modified_since; -mod if_none_match; -mod if_range; -mod if_unmodified_since; -mod last_modified; - -mod macros; -#[cfg(test)] -pub(crate) use macros::common_header_test; -pub(crate) use macros::{common_header, common_header_deref, common_header_test_module}; diff --git a/src/http/header/preference.rs b/src/http/header/preference.rs new file mode 100644 index 000000000..979fc7720 --- /dev/null +++ b/src/http/header/preference.rs @@ -0,0 +1,70 @@ +use std::{ + fmt::{self, Write as _}, + str, +}; + +/// A wrapper for types used in header values where wildcard (`*`) items are allowed but the +/// underlying type does not support them. +/// +/// For example, we use the `language-tags` crate for the [`AcceptLanguage`](super::AcceptLanguage) +/// typed header but it does not parse `*` successfully. On the other hand, the `mime` crate, used +/// for [`Accept`](super::Accept), has first-party support for wildcard items so this wrapper is not +/// used in those header types. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)] +pub enum Preference { + /// A wildcard value. + Any, + + /// A valid `T`. + Specific(T), +} + +impl Preference { + /// Returns true if preference is the any/wildcard (`*`) value. + pub fn is_any(&self) -> bool { + matches!(self, Self::Any) + } + + /// Returns true if preference is the specific item (`T`) variant. + pub fn is_specific(&self) -> bool { + matches!(self, Self::Specific(_)) + } + + /// Returns reference to value in `Specific` variant, if it is set. + pub fn item(&self) -> Option<&T> { + match self { + Preference::Specific(ref item) => Some(item), + Preference::Any => None, + } + } + + /// Consumes the container, returning the value in the `Specific` variant, if it is set. + pub fn into_item(self) -> Option { + match self { + Preference::Specific(item) => Some(item), + Preference::Any => None, + } + } +} + +impl fmt::Display for Preference { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Preference::Any => f.write_char('*'), + Preference::Specific(item) => fmt::Display::fmt(item, f), + } + } +} + +impl str::FromStr for Preference { + type Err = T::Err; + + #[inline] + fn from_str(s: &str) -> Result { + match s.trim() { + "*" => Ok(Self::Any), + other => other.parse().map(Preference::Specific), + } + } +}