From 64f270cae5e8322f0ec88a5ef0c3d2eddeec7119 Mon Sep 17 00:00:00 2001 From: brightly-salty Date: Sat, 19 Dec 2020 17:33:30 -0600 Subject: [PATCH 1/5] Make trailers module a submodule of transfer --- src/lib.rs | 10 +++++++++- src/request.rs | 2 +- src/response.rs | 2 +- src/transfer/mod.rs | 2 ++ src/{ => transfer}/trailers.rs | 0 5 files changed, 13 insertions(+), 3 deletions(-) rename src/{ => transfer}/trailers.rs (100%) diff --git a/src/lib.rs b/src/lib.rs index 2c4f33fd..f7966e5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -152,11 +152,19 @@ pub use status::Status; pub use status_code::StatusCode; pub use version::Version; +#[doc(inline)] +pub use transfer::trailers::Trailers; + +#[doc(inline)] +pub use mime::Mime; + +#[doc(inline)] +pub use headers::Headers; + #[doc(inline)] pub use crate::url::Url; pub mod security; -pub mod trailers; #[cfg(feature = "hyperium_http")] mod hyperium_http; diff --git a/src/request.rs b/src/request.rs index e10ed479..0f87cf9d 100644 --- a/src/request.rs +++ b/src/request.rs @@ -12,7 +12,7 @@ use crate::headers::{ CONTENT_TYPE, }; use crate::mime::Mime; -use crate::trailers::{self, Trailers}; +use crate::transfer::{trailers, Trailers}; use crate::{Body, Extensions, Method, StatusCode, Url, Version}; pin_project_lite::pin_project! { diff --git a/src/response.rs b/src/response.rs index 64d4312b..19a6b11c 100644 --- a/src/response.rs +++ b/src/response.rs @@ -13,7 +13,7 @@ use crate::headers::{ CONTENT_TYPE, }; use crate::mime::Mime; -use crate::trailers::{self, Trailers}; +use crate::transfer::{trailers, Trailers}; use crate::upgrade; use crate::{Body, Extensions, StatusCode, Version}; diff --git a/src/transfer/mod.rs b/src/transfer/mod.rs index 38cc6753..7ba59164 100644 --- a/src/transfer/mod.rs +++ b/src/transfer/mod.rs @@ -5,9 +5,11 @@ mod encoding; mod encoding_proposal; mod te; +pub mod trailers; mod transfer_encoding; pub use encoding::Encoding; pub use encoding_proposal::EncodingProposal; pub use te::TE; +pub use trailers::Trailers; pub use transfer_encoding::TransferEncoding; diff --git a/src/trailers.rs b/src/transfer/trailers.rs similarity index 100% rename from src/trailers.rs rename to src/transfer/trailers.rs From 71a3944d047ed2ac3ae16fbf1c28bff1528fee32 Mon Sep 17 00:00:00 2001 From: Caden Haustein Date: Sun, 24 Jan 2021 11:27:30 -0600 Subject: [PATCH 2/5] Revert "Merge branch 'main' into move-trailers" This reverts commit 65cd9b780cd718594a149b065d44c684a40e1078, reversing changes made to cbc6faf82520516ba0bdbde01d739ea83a98f664. --- .travis.yml | 13 +++ Cargo.toml | 9 +- src/conditional/etag.rs | 2 +- src/headers/header_name.rs | 1 - src/headers/header_value.rs | 1 - src/lib.rs | 8 +- src/mime/constants.rs | 41 ++++++--- src/mime/mod.rs | 116 ++++++++++++++++++------ src/mime/parse.rs | 85 +++++++++--------- src/other/mod.rs | 4 - src/other/referer.rs | 161 ---------------------------------- src/other/retry_after.rs | 170 ------------------------------------ src/other/source_map.rs | 4 +- src/request.rs | 112 +++++++++++++----------- src/status_code.rs | 10 +-- tests/mime.rs | 35 ++++---- 16 files changed, 261 insertions(+), 511 deletions(-) create mode 100644 .travis.yml delete mode 100644 src/other/referer.rs delete mode 100644 src/other/retry_after.rs diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..c70fb67f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: rust +rust: + - stable + +before_script: | + rustup component add rustfmt-preview && + rustup component add clippy-preview +script: | + cargo fmt -- --check && + cargo clippy -- -D clippy && + cargo build --verbose && + cargo test --verbose +cache: cargo diff --git a/Cargo.toml b/Cargo.toml index a78e1879..f16c7eb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http-types" -version = "3.0.0" +version = "2.9.0" license = "MIT OR Apache-2.0" repository = "https://github.com/http-rs/http-types" documentation = "https://docs.rs/http-types" @@ -21,8 +21,7 @@ docs = ["unstable"] unstable = [] hyperium_http = ["http"] async_std = ["fs"] -cookies = ["cookie"] -cookie-secure = ["cookies", "cookie/secure"] +cookie-secure = ["cookie/secure"] fs = ["async-std"] [dependencies] @@ -35,9 +34,7 @@ async-channel = "1.5.1" http = { version = "0.2.0", optional = true } anyhow = "1.0.26" - -# features: cookies -cookie = { version = "0.14.0", features = ["percent-encode"], optional = true } +cookie = { version = "0.14.0", features = ["percent-encode"] } infer = "0.2.3" pin-project-lite = "0.2.0" url = { version = "2.1.1", features = ["serde"] } diff --git a/src/conditional/etag.rs b/src/conditional/etag.rs index 548e4bdb..00d59194 100644 --- a/src/conditional/etag.rs +++ b/src/conditional/etag.rs @@ -117,7 +117,7 @@ impl ETag { if !s .bytes() - .all(|c| c == 0x21 || (0x23..=0x7E).contains(&c) || c >= 0x80) + .all(|c| c == 0x21 || (c >= 0x23 && c <= 0x7E) || c >= 0x80) { return Err(Error::from_str( StatusCode::BadRequest, diff --git a/src/headers/header_name.rs b/src/headers/header_name.rs index d4eb21be..2d584e3d 100644 --- a/src/headers/header_name.rs +++ b/src/headers/header_name.rs @@ -135,7 +135,6 @@ mod tests { use super::*; #[test] - #[allow(clippy::eq_op)] fn test_header_name_static_non_static() { let static_header = HeaderName::from_lowercase_str("hello"); let non_static_header = HeaderName::from_str("hello").unwrap(); diff --git a/src/headers/header_value.rs b/src/headers/header_value.rs index c8287a1c..bd51c78b 100644 --- a/src/headers/header_value.rs +++ b/src/headers/header_value.rs @@ -56,7 +56,6 @@ impl From for HeaderValue { } } -#[cfg(feature = "cookies")] impl From> for HeaderValue { fn from(cookie: Cookie<'_>) -> Self { HeaderValue { diff --git a/src/lib.rs b/src/lib.rs index f7966e5d..4e6071fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,9 +8,9 @@ //! ``` //! # fn main() -> Result<(), http_types::url::ParseError> { //! # -//! use http_types::{Method, Request, Response, StatusCode}; +//! use http_types::{Url, Method, Request, Response, StatusCode}; //! -//! let mut req = Request::new(Method::Get, "https://example.com"); +//! let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); //! req.set_body("Hello, Nori!"); //! //! let mut res = Response::new(StatusCode::Ok); @@ -102,7 +102,6 @@ #![doc(html_logo_url = "https://yoshuawuyts.com/assets/http-rs/logo-rounded.png")] /// HTTP cookies. -#[cfg(feature = "cookies")] pub mod cookies { pub use cookie::*; } @@ -164,6 +163,9 @@ pub use headers::Headers; #[doc(inline)] pub use crate::url::Url; +#[doc(inline)] +pub use crate::cookies::Cookie; + pub mod security; #[cfg(feature = "hyperium_http")] diff --git a/src/mime/constants.rs b/src/mime/constants.rs index 56827f00..0b8ce05d 100644 --- a/src/mime/constants.rs +++ b/src/mime/constants.rs @@ -1,5 +1,5 @@ -use crate::mime::Mime; -use std::borrow::Cow; +use super::ParamKind; +use crate::Mime; macro_rules! utf8_mime_const { ($name:ident, $desc:expr, $base:expr, $sub:expr) => { @@ -9,34 +9,47 @@ macro_rules! utf8_mime_const { $desc, $base, $sub, - true, + Some(ParamKind::Utf8), ";charset=utf-8" ); }; } macro_rules! mime_const { ($name:ident, $desc:expr, $base:expr, $sub:expr) => { - mime_const!(with_params, $name, $desc, $base, $sub, false, ""); + mime_const!(with_params, $name, $desc, $base, $sub, None, ""); }; - (with_params, $name:ident, $desc:expr, $base:expr, $sub:expr, $is_utf8:expr, $doccomment:expr) => { - mime_const!(doc_expanded, $name, $desc, $base, $sub, $is_utf8, - concat!( + (with_params, $name:ident, $desc:expr, $base:expr, $sub:expr, $params:expr, $doccomment:expr) => { + mime_const!( + doc_expanded, + $name, + $desc, + $base, + $sub, + $params, + concat!( "Content-Type for ", $desc, ".\n\n# Mime Type\n\n```text\n", - $base, "/", $sub, $doccomment, "\n```") + $base, + "/", + $sub, + $doccomment, + "\n```" + ) ); }; - (doc_expanded, $name:ident, $desc:expr, $base:expr, $sub:expr, $is_utf8:expr, $doccomment:expr) => { + (doc_expanded, $name:ident, $desc:expr, $base:expr, $sub:expr, $params:expr, $doccomment:expr) => { #[doc = $doccomment] pub const $name: Mime = Mime { - essence: Cow::Borrowed(concat!($base, "/", $sub)), - basetype: Cow::Borrowed($base), - subtype: Cow::Borrowed($sub), - is_utf8: $is_utf8, - params: vec![], + essence: String::new(), + basetype: String::new(), + subtype: String::new(), + params: $params, + static_essence: Some(concat!($base, "/", $sub)), + static_basetype: Some($base), + static_subtype: Some($sub), }; }; } diff --git a/src/mime/mod.rs b/src/mime/mod.rs index 3d99123b..6b5dcf26 100644 --- a/src/mime/mod.rs +++ b/src/mime/mod.rs @@ -28,15 +28,15 @@ use infer::Infer; /// ``` // NOTE: we cannot statically initialize Strings with values yet, so we keep dedicated static // fields for the static strings. -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone)] pub struct Mime { - pub(crate) essence: Cow<'static, str>, - pub(crate) basetype: Cow<'static, str>, - pub(crate) subtype: Cow<'static, str>, - // NOTE(yosh): this is a hack because we can't populate vecs in const yet. - // This enables us to encode media types as utf-8 at compilation. - pub(crate) is_utf8: bool, - pub(crate) params: Vec<(ParamName, ParamValue)>, + pub(crate) essence: String, + pub(crate) basetype: String, + pub(crate) subtype: String, + pub(crate) static_essence: Option<&'static str>, + pub(crate) static_basetype: Option<&'static str>, + pub(crate) static_subtype: Option<&'static str>, + pub(crate) params: Option, } impl Mime { @@ -68,40 +68,87 @@ impl Mime { /// According to the spec this method should be named `type`, but that's a reserved keyword in /// Rust so hence prefix with `base` instead. pub fn basetype(&self) -> &str { - &self.basetype + if let Some(basetype) = self.static_basetype { + &basetype + } else { + &self.basetype + } } /// Access the Mime's `subtype` value. pub fn subtype(&self) -> &str { - &self.subtype + if let Some(subtype) = self.static_subtype { + &subtype + } else { + &self.subtype + } } /// Access the Mime's `essence` value. pub fn essence(&self) -> &str { - &self.essence + if let Some(essence) = self.static_essence { + &essence + } else { + &self.essence + } } /// Get a reference to a param. pub fn param(&self, name: impl Into) -> Option<&ParamValue> { let name: ParamName = name.into(); - if name.as_str() == "charset" && self.is_utf8 { - return Some(&ParamValue(Cow::Borrowed("utf-8"))); - } - - self.params.iter().find(|(k, _)| k == &name).map(|(_, v)| v) + self.params + .as_ref() + .map(|inner| match inner { + ParamKind::Vec(v) => v + .iter() + .find_map(|(k, v)| if k == &name { Some(v) } else { None }), + ParamKind::Utf8 => match name { + ParamName(Cow::Borrowed("charset")) => Some(&ParamValue(Cow::Borrowed("utf8"))), + _ => None, + }, + }) + .flatten() } /// Remove a param from the set. Returns the `ParamValue` if it was contained within the set. pub fn remove_param(&mut self, name: impl Into) -> Option { let name: ParamName = name.into(); - if name.as_str() == "charset" && self.is_utf8 { - self.is_utf8 = false; - return Some(ParamValue(Cow::Borrowed("utf-8"))); + let mut unset_params = false; + let ret = self + .params + .as_mut() + .map(|inner| match inner { + ParamKind::Vec(v) => match v.iter().position(|(k, _)| k == &name) { + Some(index) => Some(v.remove(index).1), + None => None, + }, + ParamKind::Utf8 => match name { + ParamName(Cow::Borrowed("charset")) => { + unset_params = true; + Some(ParamValue(Cow::Borrowed("utf8"))) + } + _ => None, + }, + }) + .flatten(); + if unset_params { + self.params = None; } - self.params - .iter() - .position(|(k, _)| k == &name) - .map(|pos| self.params.remove(pos).1) + ret + } +} + +impl PartialEq for Mime { + fn eq(&self, other: &Mime) -> bool { + let left = match self.static_essence { + Some(essence) => essence, + None => &self.essence, + }; + let right = match other.static_essence { + Some(essence) => essence, + None => &other.essence, + }; + left == right } } @@ -111,11 +158,15 @@ impl Display for Mime { } } -// impl Debug for Mime { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// Debug::fmt(&self.essence, f) -// } -// } +impl Debug for Mime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(essence) = self.static_essence { + Debug::fmt(essence, f) + } else { + Debug::fmt(&self.essence, f) + } + } +} impl FromStr for Mime { type Err = crate::Error; @@ -145,7 +196,6 @@ impl ToHeaderValues for Mime { Ok(header.to_header_values().unwrap()) } } - /// A parameter name. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ParamName(Cow<'static, str>); @@ -209,3 +259,11 @@ impl PartialEq for ParamValue { self.0 == other } } + +/// This is a hack that allows us to mark a trait as utf8 during compilation. We +/// can remove this once we can construct HashMap during compilation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ParamKind { + Utf8, + Vec(Vec<(ParamName, ParamValue)>), +} diff --git a/src/mime/parse.rs b/src/mime/parse.rs index 4c02ec4a..a8b8c48e 100644 --- a/src/mime/parse.rs +++ b/src/mime/parse.rs @@ -1,7 +1,6 @@ -use std::borrow::Cow; use std::fmt; -use super::{Mime, ParamName, ParamValue}; +use super::{Mime, ParamKind, ParamName, ParamValue}; /// Parse a string into a mime type. /// Follows the [WHATWG MIME parsing algorithm](https://mimesniff.spec.whatwg.org/#parsing-a-mime-type) @@ -41,8 +40,7 @@ pub(crate) fn parse(input: &str) -> crate::Result { // 10. let basetype = basetype.to_ascii_lowercase(); let subtype = subtype.to_ascii_lowercase(); - let mut params = vec![]; - let mut is_utf8 = false; + let mut params = None; // 11. let mut input = input; @@ -93,14 +91,13 @@ pub(crate) fn parse(input: &str) -> crate::Result { }; // 10. - if parameter_name == "charset" && parameter_value == "utf-8" { - is_utf8 = true; - } else if !parameter_name.is_empty() + if !parameter_name.is_empty() && parameter_name.chars().all(is_http_token_code_point) && parameter_value .chars() .all(is_http_quoted_string_token_code_point) { + let params = params.get_or_insert_with(Vec::new); let name = ParamName(parameter_name.into()); let value = ParamValue(parameter_value.into()); if !params.iter().any(|(k, _)| k == &name) { @@ -110,11 +107,13 @@ pub(crate) fn parse(input: &str) -> crate::Result { } Ok(Mime { - essence: Cow::Owned(format!("{}/{}", &basetype, &subtype)), - basetype: basetype.into(), - subtype: subtype.into(), - params, - is_utf8, + essence: format!("{}/{}", &basetype, &subtype), + basetype, + subtype, + params: params.map(ParamKind::Vec), + static_essence: None, + static_basetype: None, + static_subtype: None, }) } @@ -206,23 +205,39 @@ fn collect_http_quoted_string(mut input: &str) -> (String, &str) { /// Implementation of [WHATWG MIME serialization algorithm](https://mimesniff.spec.whatwg.org/#serializing-a-mime-type) pub(crate) fn format(mime_type: &Mime, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", &mime_type.essence)?; - if mime_type.is_utf8 { - write!(f, ";charset=utf-8")?; + if let Some(essence) = mime_type.static_essence { + write!(f, "{}", essence)? + } else { + write!(f, "{}", &mime_type.essence)? } - for (name, value) in mime_type.params.iter() { - if value.0.chars().all(is_http_token_code_point) && !value.0.is_empty() { - write!(f, ";{}={}", name, value)?; - } else { - let value = value - .0 - .chars() - .flat_map(|c| match c { - '"' | '\\' => EscapeMimeValue::backslash(c), - c => EscapeMimeValue::char(c), - }) - .collect::(); - write!(f, ";{}=\"{}\"", name, value)?; + if let Some(params) = &mime_type.params { + match params { + ParamKind::Utf8 => write!(f, ";charset=utf-8")?, + ParamKind::Vec(params) => { + for (name, value) in params { + if value.0.chars().all(is_http_token_code_point) && !value.0.is_empty() { + write!(f, ";{}={}", name, value)?; + } else { + write!( + f, + ";{}=\"{}\"", + name, + value + .0 + .chars() + .flat_map(|c| match c { + '"' | '\\' => EscapeMimeValue { + state: EscapeMimeValueState::Backslash(c) + }, + c => EscapeMimeValue { + state: EscapeMimeValueState::Char(c) + }, + }) + .collect::() + )?; + } + } + } } } Ok(()) @@ -232,20 +247,6 @@ struct EscapeMimeValue { state: EscapeMimeValueState, } -impl EscapeMimeValue { - fn backslash(c: char) -> Self { - EscapeMimeValue { - state: EscapeMimeValueState::Backslash(c), - } - } - - fn char(c: char) -> Self { - EscapeMimeValue { - state: EscapeMimeValueState::Char(c), - } - } -} - #[derive(Clone, Debug)] enum EscapeMimeValueState { Done, diff --git a/src/other/mod.rs b/src/other/mod.rs index a1dc7f1e..4cb2ba7b 100644 --- a/src/other/mod.rs +++ b/src/other/mod.rs @@ -2,12 +2,8 @@ mod date; mod expect; -mod referer; -mod retry_after; mod source_map; pub use date::Date; pub use expect::Expect; -pub use referer::Referer; -pub use retry_after::RetryAfter; pub use source_map::SourceMap; diff --git a/src/other/referer.rs b/src/other/referer.rs deleted file mode 100644 index 10510b1b..00000000 --- a/src/other/referer.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::headers::{HeaderName, HeaderValue, Headers, REFERER}; -use crate::{bail_status as bail, Status, Url}; - -use std::convert::TryInto; - -/// Contains the address of the page making the request. -/// -/// __Important__: Although this header has many innocent uses it can have -/// undesirable consequences for user security and privacy. -/// -/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) -/// -/// # Specifications -/// -/// - [RFC 7231, section 5.5.2: Referer](https://tools.ietf.org/html/rfc7231#section-5.5.2) -/// -/// # Examples -/// -/// ``` -/// # fn main() -> http_types::Result<()> { -/// # -/// use http_types::{Response, Url}; -/// use http_types::other::Referer; -/// -/// let referer = Referer::new(Url::parse("https://example.net/")?); -/// -/// let mut res = Response::new(200); -/// referer.apply(&mut res); -/// -/// let base_url = Url::parse("https://example.net/")?; -/// let referer = Referer::from_headers(base_url, res)?.unwrap(); -/// assert_eq!(referer.location(), &Url::parse("https://example.net/")?); -/// # -/// # Ok(()) } -/// ``` -#[derive(Debug)] -pub struct Referer { - location: Url, -} - -impl Referer { - /// Create a new instance of `Referer` header. - pub fn new(location: Url) -> Self { - Self { location } - } - - /// Create a new instance from headers. - pub fn from_headers(base_url: U, headers: impl AsRef) -> crate::Result> - where - U: TryInto, - U::Error: std::fmt::Debug, - { - let headers = match headers.as_ref().get(REFERER) { - Some(headers) => headers, - None => return Ok(None), - }; - - // If we successfully parsed the header then there's always at least one - // entry. We want the last entry. - let header_value = headers.iter().last().unwrap(); - - let url = match Url::parse(header_value.as_str()) { - Ok(url) => url, - Err(_) => match base_url.try_into() { - Ok(base_url) => base_url.join(header_value.as_str().trim()).status(500)?, - Err(_) => bail!(500, "Invalid base url provided"), - }, - }; - - Ok(Some(Self { location: url })) - } - - /// Sets the header. - pub fn apply(&self, mut headers: impl AsMut) { - headers.as_mut().insert(self.name(), self.value()); - } - - /// Get the `HeaderName`. - pub fn name(&self) -> HeaderName { - REFERER - } - - /// Get the `HeaderValue`. - pub fn value(&self) -> HeaderValue { - let output = self.location.to_string(); - - // SAFETY: the internal string is validated to be ASCII. - unsafe { HeaderValue::from_bytes_unchecked(output.into()) } - } - - /// Get the url. - pub fn location(&self) -> &Url { - &self.location - } - - /// Set the url. - pub fn set_location(&mut self, location: U) -> Result<(), U::Error> - where - U: TryInto, - U::Error: std::fmt::Debug, - { - self.location = location.try_into()?; - Ok(()) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::headers::Headers; - - #[test] - fn smoke() -> crate::Result<()> { - let referer = Referer::new(Url::parse("https://example.net/test.json")?); - - let mut headers = Headers::new(); - referer.apply(&mut headers); - - let base_url = Url::parse("https://example.net/")?; - let referer = Referer::from_headers(base_url, headers)?.unwrap(); - assert_eq!( - referer.location(), - &Url::parse("https://example.net/test.json")? - ); - Ok(()) - } - - #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { - let mut headers = Headers::new(); - headers.insert(REFERER, "htt://"); - let err = - Referer::from_headers(Url::parse("https://example.net").unwrap(), headers).unwrap_err(); - assert_eq!(err.status(), 500); - Ok(()) - } - - #[test] - fn fallback_works() -> crate::Result<()> { - let mut headers = Headers::new(); - headers.insert(REFERER, "/test.json"); - - let base_url = Url::parse("https://fallback.net/")?; - let referer = Referer::from_headers(base_url, headers)?.unwrap(); - assert_eq!( - referer.location(), - &Url::parse("https://fallback.net/test.json")? - ); - - let mut headers = Headers::new(); - headers.insert(REFERER, "https://example.com/test.json"); - - let base_url = Url::parse("https://fallback.net/")?; - let referer = Referer::from_headers(base_url, headers)?.unwrap(); - assert_eq!( - referer.location(), - &Url::parse("https://example.com/test.json")? - ); - Ok(()) - } -} diff --git a/src/other/retry_after.rs b/src/other/retry_after.rs deleted file mode 100644 index bfe81f22..00000000 --- a/src/other/retry_after.rs +++ /dev/null @@ -1,170 +0,0 @@ -use std::time::{Duration, SystemTime, SystemTimeError}; - -use crate::headers::{HeaderName, HeaderValue, Headers, RETRY_AFTER}; -use crate::utils::{fmt_http_date, parse_http_date}; - -/// Indicate how long the user agent should wait before making a follow-up request. -/// -/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) -/// -/// # Specifications -/// -/// - [RFC 7231, section 3.1.4.2: Retry-After](https://tools.ietf.org/html/rfc7231#section-3.1.4.2) -/// -/// # Examples -/// -/// ```no_run -/// # fn main() -> http_types::Result<()> { -/// # -/// use http_types::other::RetryAfter; -/// use http_types::Response; -/// use std::time::{SystemTime, Duration}; -/// use async_std::task; -/// -/// let retry = RetryAfter::new(Duration::from_secs(10)); -/// -/// let mut headers = Response::new(429); -/// retry.apply(&mut headers); -/// -/// // Sleep for the duration, then try the task again. -/// let retry = RetryAfter::from_headers(headers)?.unwrap(); -/// task::sleep(retry.duration_since(SystemTime::now())?); -/// # -/// # Ok(()) } -/// ``` -#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct RetryAfter { - inner: RetryDirective, -} - -#[allow(clippy::len_without_is_empty)] -impl RetryAfter { - /// Create a new instance from a `Duration`. - /// - /// This value will be encoded over the wire as a relative offset in seconds. - pub fn new(dur: Duration) -> Self { - Self { - inner: RetryDirective::Duration(dur), - } - } - - /// Create a new instance from a `SystemTime` instant. - /// - /// This value will be encoded a specific `Date` over the wire. - pub fn new_at(at: SystemTime) -> Self { - Self { - inner: RetryDirective::SystemTime(at), - } - } - - /// Create a new instance from headers. - pub fn from_headers(headers: impl AsRef) -> crate::Result> { - let header = match headers.as_ref().get(RETRY_AFTER) { - Some(headers) => headers.last(), - None => return Ok(None), - }; - - let inner = match header.as_str().parse::() { - Ok(dur) => RetryDirective::Duration(Duration::from_secs(dur)), - Err(_) => { - let at = parse_http_date(header.as_str())?; - RetryDirective::SystemTime(at) - } - }; - Ok(Some(Self { inner })) - } - - /// Returns the amount of time elapsed from an earlier point in time. - /// - /// # Errors - /// - /// This may return an error if the `earlier` time was after the current time. - pub fn duration_since(&self, earlier: SystemTime) -> Result { - let at = match self.inner { - RetryDirective::Duration(dur) => SystemTime::now() + dur, - RetryDirective::SystemTime(at) => at, - }; - - at.duration_since(earlier) - } - - /// Sets the header. - pub fn apply(&self, mut headers: impl AsMut) { - headers.as_mut().insert(self.name(), self.value()); - } - - /// Get the `HeaderName`. - pub fn name(&self) -> HeaderName { - RETRY_AFTER - } - - /// Get the `HeaderValue`. - pub fn value(&self) -> HeaderValue { - let output = match self.inner { - RetryDirective::Duration(dur) => format!("{}", dur.as_secs()), - RetryDirective::SystemTime(at) => fmt_http_date(at), - }; - // SAFETY: the internal string is validated to be ASCII. - unsafe { HeaderValue::from_bytes_unchecked(output.into()) } - } -} - -impl From for SystemTime { - fn from(retry_after: RetryAfter) -> Self { - match retry_after.inner { - RetryDirective::Duration(dur) => SystemTime::now() + dur, - RetryDirective::SystemTime(at) => at, - } - } -} - -/// What value are we decoding into? -/// -/// This value is intionally never exposes; all end-users want is a `Duration` -/// value that tells them how long to wait for before trying again. -#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] -enum RetryDirective { - Duration(Duration), - SystemTime(SystemTime), -} - -#[cfg(test)] -mod test { - use super::*; - use crate::headers::Headers; - - #[test] - fn smoke() -> crate::Result<()> { - let retry = RetryAfter::new(Duration::from_secs(10)); - - let mut headers = Headers::new(); - retry.apply(&mut headers); - - // `SystemTime::now` uses sub-second precision which means there's some - // offset that's not encoded. - let now = SystemTime::now(); - let retry = RetryAfter::from_headers(headers)?.unwrap(); - assert_eq!( - retry.duration_since(now)?.as_secs(), - Duration::from_secs(10).as_secs() - ); - Ok(()) - } - - #[test] - fn new_at() -> crate::Result<()> { - let now = SystemTime::now(); - let retry = RetryAfter::new_at(now + Duration::from_secs(10)); - - let mut headers = Headers::new(); - retry.apply(&mut headers); - - // `SystemTime::now` uses sub-second precision which means there's some - // offset that's not encoded. - let retry = RetryAfter::from_headers(headers)?.unwrap(); - let delta = retry.duration_since(now)?; - assert!(delta >= Duration::from_secs(9)); - assert!(delta <= Duration::from_secs(10)); - Ok(()) - } -} diff --git a/src/other/source_map.rs b/src/other/source_map.rs index 349a99a2..e0e94355 100644 --- a/src/other/source_map.rs +++ b/src/other/source_map.rs @@ -59,7 +59,7 @@ impl SourceMap { let url = match Url::parse(header_value.as_str()) { Ok(url) => url, Err(_) => match base_url.try_into() { - Ok(base_url) => base_url.join(header_value.as_str().trim()).status(500)?, + Ok(base_url) => base_url.join(header_value.as_str().trim()).status(400)?, Err(_) => bail!(500, "Invalid base url provided"), }, }; @@ -128,7 +128,7 @@ mod test { headers.insert(SOURCE_MAP, "htt://"); let err = SourceMap::from_headers(Url::parse("https://example.net").unwrap(), headers) .unwrap_err(); - assert_eq!(err.status(), 500); + assert_eq!(err.status(), 400); Ok(()) } diff --git a/src/request.rs b/src/request.rs index 0f87cf9d..8eabcf5b 100644 --- a/src/request.rs +++ b/src/request.rs @@ -21,9 +21,9 @@ pin_project_lite::pin_project! { /// # Examples /// /// ``` - /// use http_types::Request; + /// use http_types::{Url, Method, Request}; /// - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); /// req.set_body("Hello, Nori!"); /// ``` #[derive(Debug)] @@ -157,8 +157,8 @@ impl Request { /// ``` /// # fn main() -> Result<(), http_types::Error> { /// # - /// use http_types::{Request, Response, StatusCode}; - /// let mut req = Request::get("https://example.com"); + /// use http_types::{Method, Request, Response, StatusCode, Url}; + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); /// assert_eq!(req.url().scheme(), "https"); /// # /// # Ok(()) } @@ -175,7 +175,7 @@ impl Request { /// # fn main() -> Result<(), http_types::Error> { /// # /// use http_types::{Method, Request, Response, StatusCode, Url}; - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); /// req.url_mut().set_scheme("http"); /// assert_eq!(req.url().scheme(), "http"); /// # @@ -190,9 +190,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request}; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); /// req.set_body("Hello, Nori!"); /// ``` pub fn set_body(&mut self, body: impl Into) { @@ -208,9 +208,9 @@ impl Request { /// # use async_std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// # - /// use http_types::{Body, Method, Request}; + /// use http_types::{Body, Method, Request, Url}; /// - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); /// req.set_body("Hello, Nori!"); /// let mut body: Body = req.replace_body("Hello, Chashu!"); /// @@ -234,9 +234,9 @@ impl Request { /// # use async_std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// # - /// use http_types::{Body, Request}; + /// use http_types::{Body, Method, Request, Url}; /// - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); /// req.set_body("Hello, Nori!"); /// let mut body = "Hello, Chashu!".into(); /// req.swap_body(&mut body); @@ -260,9 +260,9 @@ impl Request { /// # use async_std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// # - /// use http_types::{Body, Request}; + /// use http_types::{Body, Method, Request, Url}; /// - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); /// req.set_body("Hello, Nori!"); /// let mut body: Body = req.take_body(); /// @@ -293,9 +293,9 @@ impl Request { /// # use std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// use async_std::io::Cursor; - /// use http_types::{Body, Request}; + /// use http_types::{Body, Method, Request, Url}; /// - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); /// /// let cursor = Cursor::new("Hello Nori"); /// let body = Body::from_reader(cursor, None); @@ -319,10 +319,10 @@ impl Request { /// /// ``` /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { - /// use http_types::{Body, Request}; + /// use http_types::{Body, Method, Request, Url}; /// /// let bytes = vec![1, 2, 3]; - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); /// req.set_body(Body::from_bytes(bytes)); /// /// let bytes = req.body_bytes().await?; @@ -346,7 +346,7 @@ impl Request { /// ``` /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// use http_types::convert::{Deserialize, Serialize}; - /// use http_types::{Body, Request}; + /// use http_types::{Body, Method, Request, Url}; /// /// #[derive(Debug, Serialize, Deserialize)] /// struct Cat { @@ -356,7 +356,7 @@ impl Request { /// let cat = Cat { /// name: String::from("chashu"), /// }; - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); /// req.set_body(Body::from_json(&cat)?); /// /// let cat: Cat = req.body_json().await?; @@ -380,7 +380,7 @@ impl Request { /// ``` /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// use http_types::convert::{Deserialize, Serialize}; - /// use http_types::{Body, Request}; + /// use http_types::{Body, Method, Request, Url}; /// /// #[derive(Debug, Serialize, Deserialize)] /// struct Cat { @@ -390,7 +390,7 @@ impl Request { /// let cat = Cat { /// name: String::from("chashu"), /// }; - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); /// req.set_body(Body::from_form(&cat)?); /// /// let cat: Cat = req.body_form().await?; @@ -424,9 +424,9 @@ impl Request { /// ``` /// # fn main() -> Result<(), Box> { /// # - /// use http_types::Request; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); /// req.insert_header("Content-Type", "text/plain"); /// # /// # Ok(()) } @@ -450,9 +450,9 @@ impl Request { /// ``` /// # fn main() -> Result<(), Box> { /// # - /// use http_types::Request; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); /// req.append_header("Content-Type", "text/plain"); /// # /// # Ok(()) } @@ -503,11 +503,11 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Request, Version}; + /// use http_types::{Method, Request, Url, Version}; /// /// # fn main() -> Result<(), http_types::Error> { /// # - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); /// assert_eq!(req.version(), None); /// /// req.set_version(Some(Version::Http2_0)); @@ -524,11 +524,11 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Request, Version}; + /// use http_types::{Method, Request, Url, Version}; /// /// # fn main() -> Result<(), http_types::Error> { /// # - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); /// req.set_version(Some(Version::Http2_0)); /// # /// # Ok(()) } @@ -594,9 +594,9 @@ impl Request { /// ``` /// # fn main() -> Result<(), http_types::Error> { /// # - /// use http_types::{Request, Version}; + /// use http_types::{Method, Request, Url, Version}; /// - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); /// req.ext_mut().insert("hello from the extension"); /// assert_eq!(req.ext().get(), Some(&"hello from the extension")); /// # @@ -612,7 +612,7 @@ impl Request { /// /// ``` /// use http_types::convert::Deserialize; - /// use http_types::Request; + /// use http_types::{Method, Request, Url}; /// use std::collections::HashMap; /// /// #[derive(Deserialize)] @@ -621,7 +621,10 @@ impl Request { /// selections: HashMap, /// } /// - /// let mut req = Request::get("https://httpbin.org/get?page=2&selections[width]=narrow&selections[height]=tall"); + /// let req = Request::new( + /// Method::Get, + /// Url::parse("https://httpbin.org/get?page=2&selections[width]=narrow&selections[height]=tall").unwrap(), + /// ); /// let Index { page, selections } = req.query().unwrap(); /// assert_eq!(page, 2); /// assert_eq!(selections["width"], "narrow"); @@ -645,7 +648,7 @@ impl Request { /// /// ``` /// use http_types::convert::Serialize; - /// use http_types::{Method, Request}; + /// use http_types::{Method, Request, Url}; /// use std::collections::HashMap; /// /// #[derive(Serialize)] @@ -655,7 +658,10 @@ impl Request { /// } /// /// let query = Index { page: 2, topics: vec!["rust", "crabs", "crustaceans"] }; - /// let mut req = Request::get("https://httpbin.org/get"); + /// let mut req = Request::new( + /// Method::Get, + /// Url::parse("https://httpbin.org/get").unwrap(), + /// ); /// req.set_query(&query).unwrap(); /// assert_eq!(req.url().query(), Some("page=2&topics[0]=rust&topics[1]=crabs&topics[2]=crustaceans")); /// ``` @@ -674,9 +680,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request}; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::get("https://example.com"); + /// let mut req = Request::get(Url::parse("https://example.com").unwrap()); /// req.set_body("Hello, Nori!"); /// assert_eq!(req.method(), Method::Get); /// ``` @@ -696,9 +702,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request}; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::head("https://example.com"); + /// let mut req = Request::head(Url::parse("https://example.com").unwrap()); /// assert_eq!(req.method(), Method::Head); /// ``` pub fn head(url: U) -> Self @@ -717,9 +723,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request}; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::post("https://example.com"); + /// let mut req = Request::post(Url::parse("https://example.com").unwrap()); /// assert_eq!(req.method(), Method::Post); /// ``` pub fn post(url: U) -> Self @@ -738,9 +744,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request}; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::put("https://example.com"); + /// let mut req = Request::put(Url::parse("https://example.com").unwrap()); /// assert_eq!(req.method(), Method::Put); /// ``` pub fn put(url: U) -> Self @@ -758,9 +764,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request}; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::delete("https://example.com"); + /// let mut req = Request::delete(Url::parse("https://example.com").unwrap()); /// assert_eq!(req.method(), Method::Delete); /// ``` pub fn delete(url: U) -> Self @@ -779,9 +785,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request}; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::connect("https://example.com"); + /// let mut req = Request::connect(Url::parse("https://example.com").unwrap()); /// assert_eq!(req.method(), Method::Connect); /// ``` pub fn connect(url: U) -> Self @@ -800,9 +806,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request}; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::options("https://example.com"); + /// let mut req = Request::options(Url::parse("https://example.com").unwrap()); /// assert_eq!(req.method(), Method::Options); /// ``` pub fn options(url: U) -> Self @@ -821,9 +827,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request}; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::trace("https://example.com"); + /// let mut req = Request::trace(Url::parse("https://example.com").unwrap()); /// assert_eq!(req.method(), Method::Trace); /// ``` pub fn trace(url: U) -> Self @@ -841,9 +847,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request}; + /// use http_types::{Method, Request, Url}; /// - /// let mut req = Request::patch("https://example.com"); + /// let mut req = Request::patch(Url::parse("https://example.com").unwrap()); /// assert_eq!(req.method(), Method::Patch); /// ``` pub fn patch(url: U) -> Self diff --git a/src/status_code.rs b/src/status_code.rs index 86f14d1a..c10c4a2f 100644 --- a/src/status_code.rs +++ b/src/status_code.rs @@ -434,7 +434,7 @@ impl StatusCode { /// continuing process. pub fn is_informational(&self) -> bool { let num: u16 = self.clone().into(); - (100..200).contains(&num) + num >= 100 && num < 200 } /// Returns `true` if the status code is the `2xx` range. @@ -443,7 +443,7 @@ impl StatusCode { /// received, understood, and accepted. pub fn is_success(&self) -> bool { let num: u16 = self.clone().into(); - (200..300).contains(&num) + num >= 200 && num < 300 } /// Returns `true` if the status code is the `3xx` range. @@ -452,7 +452,7 @@ impl StatusCode { /// taken in order to complete the request. pub fn is_redirection(&self) -> bool { let num: u16 = self.clone().into(); - (300..400).contains(&num) + num >= 300 && num < 400 } /// Returns `true` if the status code is the `4xx` range. @@ -461,7 +461,7 @@ impl StatusCode { /// or cannot be fulfilled. pub fn is_client_error(&self) -> bool { let num: u16 = self.clone().into(); - (400..500).contains(&num) + num >= 400 && num < 500 } /// Returns `true` if the status code is the `5xx` range. @@ -470,7 +470,7 @@ impl StatusCode { /// apparently valid request. pub fn is_server_error(&self) -> bool { let num: u16 = self.clone().into(); - (500..600).contains(&num) + num >= 500 && num < 600 } /// The canonical reason for a given status code diff --git a/tests/mime.rs b/tests/mime.rs index 2488ef4f..b408748e 100644 --- a/tests/mime.rs +++ b/tests/mime.rs @@ -1,8 +1,6 @@ -#[cfg(features = "fs")] -mod tests { - use async_std::fs; - use async_std::io; - use http_types::{mime, Body, Response}; +use async_std::fs; +use async_std::io; +use http_types::{mime, Body, Response}; #[async_std::test] async fn guess_plain_text_mime() -> io::Result<()> { @@ -20,12 +18,12 @@ mod tests { res.set_body(body); assert_eq!(res.content_type(), Some(mime::PNG)); - // Assert the file is correctly reset after we've peeked the bytes - let left = fs::read("tests/fixtures/nori.png").await?; - let right = res.body_bytes().await?; - assert_eq!(left, right); - Ok(()) - } +#[async_std::test] +async fn guess_binary_mime() -> http_types::Result<()> { + let body = Body::from_file("tests/fixtures/nori.png").await?; + let mut res = Response::new(200); + res.set_body(body); + assert_eq!(res.content_type(), Some(mime::PNG)); #[async_std::test] async fn guess_mime_fallback() -> io::Result<()> { @@ -45,12 +43,11 @@ mod tests { Ok(()) } - // #[test] - // fn match_mime_types() { - // let req = Request::get("https://example.com"); - // match req.content_type() { - // Some(mime::JSON) => {} - // _ => {} - // } - // } +#[async_std::test] +async fn parse_empty_files() -> http_types::Result<()> { + let body = Body::from_file("tests/fixtures/empty.custom").await?; + let mut res = Response::new(200); + res.set_body(body); + assert_eq!(res.content_type(), Some(mime::BYTE_STREAM)); + Ok(()) } From a581d3b75a0a6a6fe1d1202af7faa6862573060a Mon Sep 17 00:00:00 2001 From: Caden Haustein Date: Wed, 27 Jan 2021 15:12:22 -0600 Subject: [PATCH 3/5] Delete .travis.yml --- .travis.yml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c70fb67f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: rust -rust: - - stable - -before_script: | - rustup component add rustfmt-preview && - rustup component add clippy-preview -script: | - cargo fmt -- --check && - cargo clippy -- -D clippy && - cargo build --verbose && - cargo test --verbose -cache: cargo From 5dfea58992e399aa292302032d539c112affb229 Mon Sep 17 00:00:00 2001 From: Caden Haustein Date: Wed, 27 Jan 2021 17:26:42 -0600 Subject: [PATCH 4/5] Revert "Revert "Merge branch 'main' into move-trailers"" This reverts commit 71a3944d047ed2ac3ae16fbf1c28bff1528fee32. --- Cargo.toml | 9 +- src/conditional/etag.rs | 2 +- src/headers/header_name.rs | 1 + src/headers/header_value.rs | 1 + src/lib.rs | 8 +- src/mime/constants.rs | 41 +++------ src/mime/mod.rs | 116 ++++++------------------ src/mime/parse.rs | 85 +++++++++--------- src/other/mod.rs | 4 + src/other/referer.rs | 161 ++++++++++++++++++++++++++++++++++ src/other/retry_after.rs | 170 ++++++++++++++++++++++++++++++++++++ src/other/source_map.rs | 4 +- src/request.rs | 112 +++++++++++------------- src/status_code.rs | 10 +-- tests/mime.rs | 35 ++++---- 15 files changed, 511 insertions(+), 248 deletions(-) create mode 100644 src/other/referer.rs create mode 100644 src/other/retry_after.rs diff --git a/Cargo.toml b/Cargo.toml index f16c7eb5..a78e1879 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http-types" -version = "2.9.0" +version = "3.0.0" license = "MIT OR Apache-2.0" repository = "https://github.com/http-rs/http-types" documentation = "https://docs.rs/http-types" @@ -21,7 +21,8 @@ docs = ["unstable"] unstable = [] hyperium_http = ["http"] async_std = ["fs"] -cookie-secure = ["cookie/secure"] +cookies = ["cookie"] +cookie-secure = ["cookies", "cookie/secure"] fs = ["async-std"] [dependencies] @@ -34,7 +35,9 @@ async-channel = "1.5.1" http = { version = "0.2.0", optional = true } anyhow = "1.0.26" -cookie = { version = "0.14.0", features = ["percent-encode"] } + +# features: cookies +cookie = { version = "0.14.0", features = ["percent-encode"], optional = true } infer = "0.2.3" pin-project-lite = "0.2.0" url = { version = "2.1.1", features = ["serde"] } diff --git a/src/conditional/etag.rs b/src/conditional/etag.rs index 00d59194..548e4bdb 100644 --- a/src/conditional/etag.rs +++ b/src/conditional/etag.rs @@ -117,7 +117,7 @@ impl ETag { if !s .bytes() - .all(|c| c == 0x21 || (c >= 0x23 && c <= 0x7E) || c >= 0x80) + .all(|c| c == 0x21 || (0x23..=0x7E).contains(&c) || c >= 0x80) { return Err(Error::from_str( StatusCode::BadRequest, diff --git a/src/headers/header_name.rs b/src/headers/header_name.rs index 2d584e3d..d4eb21be 100644 --- a/src/headers/header_name.rs +++ b/src/headers/header_name.rs @@ -135,6 +135,7 @@ mod tests { use super::*; #[test] + #[allow(clippy::eq_op)] fn test_header_name_static_non_static() { let static_header = HeaderName::from_lowercase_str("hello"); let non_static_header = HeaderName::from_str("hello").unwrap(); diff --git a/src/headers/header_value.rs b/src/headers/header_value.rs index bd51c78b..c8287a1c 100644 --- a/src/headers/header_value.rs +++ b/src/headers/header_value.rs @@ -56,6 +56,7 @@ impl From for HeaderValue { } } +#[cfg(feature = "cookies")] impl From> for HeaderValue { fn from(cookie: Cookie<'_>) -> Self { HeaderValue { diff --git a/src/lib.rs b/src/lib.rs index 4e6071fe..f7966e5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,9 +8,9 @@ //! ``` //! # fn main() -> Result<(), http_types::url::ParseError> { //! # -//! use http_types::{Url, Method, Request, Response, StatusCode}; +//! use http_types::{Method, Request, Response, StatusCode}; //! -//! let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); +//! let mut req = Request::new(Method::Get, "https://example.com"); //! req.set_body("Hello, Nori!"); //! //! let mut res = Response::new(StatusCode::Ok); @@ -102,6 +102,7 @@ #![doc(html_logo_url = "https://yoshuawuyts.com/assets/http-rs/logo-rounded.png")] /// HTTP cookies. +#[cfg(feature = "cookies")] pub mod cookies { pub use cookie::*; } @@ -163,9 +164,6 @@ pub use headers::Headers; #[doc(inline)] pub use crate::url::Url; -#[doc(inline)] -pub use crate::cookies::Cookie; - pub mod security; #[cfg(feature = "hyperium_http")] diff --git a/src/mime/constants.rs b/src/mime/constants.rs index 0b8ce05d..56827f00 100644 --- a/src/mime/constants.rs +++ b/src/mime/constants.rs @@ -1,5 +1,5 @@ -use super::ParamKind; -use crate::Mime; +use crate::mime::Mime; +use std::borrow::Cow; macro_rules! utf8_mime_const { ($name:ident, $desc:expr, $base:expr, $sub:expr) => { @@ -9,47 +9,34 @@ macro_rules! utf8_mime_const { $desc, $base, $sub, - Some(ParamKind::Utf8), + true, ";charset=utf-8" ); }; } macro_rules! mime_const { ($name:ident, $desc:expr, $base:expr, $sub:expr) => { - mime_const!(with_params, $name, $desc, $base, $sub, None, ""); + mime_const!(with_params, $name, $desc, $base, $sub, false, ""); }; - (with_params, $name:ident, $desc:expr, $base:expr, $sub:expr, $params:expr, $doccomment:expr) => { - mime_const!( - doc_expanded, - $name, - $desc, - $base, - $sub, - $params, - concat!( + (with_params, $name:ident, $desc:expr, $base:expr, $sub:expr, $is_utf8:expr, $doccomment:expr) => { + mime_const!(doc_expanded, $name, $desc, $base, $sub, $is_utf8, + concat!( "Content-Type for ", $desc, ".\n\n# Mime Type\n\n```text\n", - $base, - "/", - $sub, - $doccomment, - "\n```" - ) + $base, "/", $sub, $doccomment, "\n```") ); }; - (doc_expanded, $name:ident, $desc:expr, $base:expr, $sub:expr, $params:expr, $doccomment:expr) => { + (doc_expanded, $name:ident, $desc:expr, $base:expr, $sub:expr, $is_utf8:expr, $doccomment:expr) => { #[doc = $doccomment] pub const $name: Mime = Mime { - essence: String::new(), - basetype: String::new(), - subtype: String::new(), - params: $params, - static_essence: Some(concat!($base, "/", $sub)), - static_basetype: Some($base), - static_subtype: Some($sub), + essence: Cow::Borrowed(concat!($base, "/", $sub)), + basetype: Cow::Borrowed($base), + subtype: Cow::Borrowed($sub), + is_utf8: $is_utf8, + params: vec![], }; }; } diff --git a/src/mime/mod.rs b/src/mime/mod.rs index 6b5dcf26..3d99123b 100644 --- a/src/mime/mod.rs +++ b/src/mime/mod.rs @@ -28,15 +28,15 @@ use infer::Infer; /// ``` // NOTE: we cannot statically initialize Strings with values yet, so we keep dedicated static // fields for the static strings. -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq, Debug)] pub struct Mime { - pub(crate) essence: String, - pub(crate) basetype: String, - pub(crate) subtype: String, - pub(crate) static_essence: Option<&'static str>, - pub(crate) static_basetype: Option<&'static str>, - pub(crate) static_subtype: Option<&'static str>, - pub(crate) params: Option, + pub(crate) essence: Cow<'static, str>, + pub(crate) basetype: Cow<'static, str>, + pub(crate) subtype: Cow<'static, str>, + // NOTE(yosh): this is a hack because we can't populate vecs in const yet. + // This enables us to encode media types as utf-8 at compilation. + pub(crate) is_utf8: bool, + pub(crate) params: Vec<(ParamName, ParamValue)>, } impl Mime { @@ -68,87 +68,40 @@ impl Mime { /// According to the spec this method should be named `type`, but that's a reserved keyword in /// Rust so hence prefix with `base` instead. pub fn basetype(&self) -> &str { - if let Some(basetype) = self.static_basetype { - &basetype - } else { - &self.basetype - } + &self.basetype } /// Access the Mime's `subtype` value. pub fn subtype(&self) -> &str { - if let Some(subtype) = self.static_subtype { - &subtype - } else { - &self.subtype - } + &self.subtype } /// Access the Mime's `essence` value. pub fn essence(&self) -> &str { - if let Some(essence) = self.static_essence { - &essence - } else { - &self.essence - } + &self.essence } /// Get a reference to a param. pub fn param(&self, name: impl Into) -> Option<&ParamValue> { let name: ParamName = name.into(); - self.params - .as_ref() - .map(|inner| match inner { - ParamKind::Vec(v) => v - .iter() - .find_map(|(k, v)| if k == &name { Some(v) } else { None }), - ParamKind::Utf8 => match name { - ParamName(Cow::Borrowed("charset")) => Some(&ParamValue(Cow::Borrowed("utf8"))), - _ => None, - }, - }) - .flatten() + if name.as_str() == "charset" && self.is_utf8 { + return Some(&ParamValue(Cow::Borrowed("utf-8"))); + } + + self.params.iter().find(|(k, _)| k == &name).map(|(_, v)| v) } /// Remove a param from the set. Returns the `ParamValue` if it was contained within the set. pub fn remove_param(&mut self, name: impl Into) -> Option { let name: ParamName = name.into(); - let mut unset_params = false; - let ret = self - .params - .as_mut() - .map(|inner| match inner { - ParamKind::Vec(v) => match v.iter().position(|(k, _)| k == &name) { - Some(index) => Some(v.remove(index).1), - None => None, - }, - ParamKind::Utf8 => match name { - ParamName(Cow::Borrowed("charset")) => { - unset_params = true; - Some(ParamValue(Cow::Borrowed("utf8"))) - } - _ => None, - }, - }) - .flatten(); - if unset_params { - self.params = None; + if name.as_str() == "charset" && self.is_utf8 { + self.is_utf8 = false; + return Some(ParamValue(Cow::Borrowed("utf-8"))); } - ret - } -} - -impl PartialEq for Mime { - fn eq(&self, other: &Mime) -> bool { - let left = match self.static_essence { - Some(essence) => essence, - None => &self.essence, - }; - let right = match other.static_essence { - Some(essence) => essence, - None => &other.essence, - }; - left == right + self.params + .iter() + .position(|(k, _)| k == &name) + .map(|pos| self.params.remove(pos).1) } } @@ -158,15 +111,11 @@ impl Display for Mime { } } -impl Debug for Mime { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(essence) = self.static_essence { - Debug::fmt(essence, f) - } else { - Debug::fmt(&self.essence, f) - } - } -} +// impl Debug for Mime { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// Debug::fmt(&self.essence, f) +// } +// } impl FromStr for Mime { type Err = crate::Error; @@ -196,6 +145,7 @@ impl ToHeaderValues for Mime { Ok(header.to_header_values().unwrap()) } } + /// A parameter name. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ParamName(Cow<'static, str>); @@ -259,11 +209,3 @@ impl PartialEq for ParamValue { self.0 == other } } - -/// This is a hack that allows us to mark a trait as utf8 during compilation. We -/// can remove this once we can construct HashMap during compilation. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum ParamKind { - Utf8, - Vec(Vec<(ParamName, ParamValue)>), -} diff --git a/src/mime/parse.rs b/src/mime/parse.rs index a8b8c48e..4c02ec4a 100644 --- a/src/mime/parse.rs +++ b/src/mime/parse.rs @@ -1,6 +1,7 @@ +use std::borrow::Cow; use std::fmt; -use super::{Mime, ParamKind, ParamName, ParamValue}; +use super::{Mime, ParamName, ParamValue}; /// Parse a string into a mime type. /// Follows the [WHATWG MIME parsing algorithm](https://mimesniff.spec.whatwg.org/#parsing-a-mime-type) @@ -40,7 +41,8 @@ pub(crate) fn parse(input: &str) -> crate::Result { // 10. let basetype = basetype.to_ascii_lowercase(); let subtype = subtype.to_ascii_lowercase(); - let mut params = None; + let mut params = vec![]; + let mut is_utf8 = false; // 11. let mut input = input; @@ -91,13 +93,14 @@ pub(crate) fn parse(input: &str) -> crate::Result { }; // 10. - if !parameter_name.is_empty() + if parameter_name == "charset" && parameter_value == "utf-8" { + is_utf8 = true; + } else if !parameter_name.is_empty() && parameter_name.chars().all(is_http_token_code_point) && parameter_value .chars() .all(is_http_quoted_string_token_code_point) { - let params = params.get_or_insert_with(Vec::new); let name = ParamName(parameter_name.into()); let value = ParamValue(parameter_value.into()); if !params.iter().any(|(k, _)| k == &name) { @@ -107,13 +110,11 @@ pub(crate) fn parse(input: &str) -> crate::Result { } Ok(Mime { - essence: format!("{}/{}", &basetype, &subtype), - basetype, - subtype, - params: params.map(ParamKind::Vec), - static_essence: None, - static_basetype: None, - static_subtype: None, + essence: Cow::Owned(format!("{}/{}", &basetype, &subtype)), + basetype: basetype.into(), + subtype: subtype.into(), + params, + is_utf8, }) } @@ -205,39 +206,23 @@ fn collect_http_quoted_string(mut input: &str) -> (String, &str) { /// Implementation of [WHATWG MIME serialization algorithm](https://mimesniff.spec.whatwg.org/#serializing-a-mime-type) pub(crate) fn format(mime_type: &Mime, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(essence) = mime_type.static_essence { - write!(f, "{}", essence)? - } else { - write!(f, "{}", &mime_type.essence)? + write!(f, "{}", &mime_type.essence)?; + if mime_type.is_utf8 { + write!(f, ";charset=utf-8")?; } - if let Some(params) = &mime_type.params { - match params { - ParamKind::Utf8 => write!(f, ";charset=utf-8")?, - ParamKind::Vec(params) => { - for (name, value) in params { - if value.0.chars().all(is_http_token_code_point) && !value.0.is_empty() { - write!(f, ";{}={}", name, value)?; - } else { - write!( - f, - ";{}=\"{}\"", - name, - value - .0 - .chars() - .flat_map(|c| match c { - '"' | '\\' => EscapeMimeValue { - state: EscapeMimeValueState::Backslash(c) - }, - c => EscapeMimeValue { - state: EscapeMimeValueState::Char(c) - }, - }) - .collect::() - )?; - } - } - } + for (name, value) in mime_type.params.iter() { + if value.0.chars().all(is_http_token_code_point) && !value.0.is_empty() { + write!(f, ";{}={}", name, value)?; + } else { + let value = value + .0 + .chars() + .flat_map(|c| match c { + '"' | '\\' => EscapeMimeValue::backslash(c), + c => EscapeMimeValue::char(c), + }) + .collect::(); + write!(f, ";{}=\"{}\"", name, value)?; } } Ok(()) @@ -247,6 +232,20 @@ struct EscapeMimeValue { state: EscapeMimeValueState, } +impl EscapeMimeValue { + fn backslash(c: char) -> Self { + EscapeMimeValue { + state: EscapeMimeValueState::Backslash(c), + } + } + + fn char(c: char) -> Self { + EscapeMimeValue { + state: EscapeMimeValueState::Char(c), + } + } +} + #[derive(Clone, Debug)] enum EscapeMimeValueState { Done, diff --git a/src/other/mod.rs b/src/other/mod.rs index 4cb2ba7b..a1dc7f1e 100644 --- a/src/other/mod.rs +++ b/src/other/mod.rs @@ -2,8 +2,12 @@ mod date; mod expect; +mod referer; +mod retry_after; mod source_map; pub use date::Date; pub use expect::Expect; +pub use referer::Referer; +pub use retry_after::RetryAfter; pub use source_map::SourceMap; diff --git a/src/other/referer.rs b/src/other/referer.rs new file mode 100644 index 00000000..10510b1b --- /dev/null +++ b/src/other/referer.rs @@ -0,0 +1,161 @@ +use crate::headers::{HeaderName, HeaderValue, Headers, REFERER}; +use crate::{bail_status as bail, Status, Url}; + +use std::convert::TryInto; + +/// Contains the address of the page making the request. +/// +/// __Important__: Although this header has many innocent uses it can have +/// undesirable consequences for user security and privacy. +/// +/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) +/// +/// # Specifications +/// +/// - [RFC 7231, section 5.5.2: Referer](https://tools.ietf.org/html/rfc7231#section-5.5.2) +/// +/// # Examples +/// +/// ``` +/// # fn main() -> http_types::Result<()> { +/// # +/// use http_types::{Response, Url}; +/// use http_types::other::Referer; +/// +/// let referer = Referer::new(Url::parse("https://example.net/")?); +/// +/// let mut res = Response::new(200); +/// referer.apply(&mut res); +/// +/// let base_url = Url::parse("https://example.net/")?; +/// let referer = Referer::from_headers(base_url, res)?.unwrap(); +/// assert_eq!(referer.location(), &Url::parse("https://example.net/")?); +/// # +/// # Ok(()) } +/// ``` +#[derive(Debug)] +pub struct Referer { + location: Url, +} + +impl Referer { + /// Create a new instance of `Referer` header. + pub fn new(location: Url) -> Self { + Self { location } + } + + /// Create a new instance from headers. + pub fn from_headers(base_url: U, headers: impl AsRef) -> crate::Result> + where + U: TryInto, + U::Error: std::fmt::Debug, + { + let headers = match headers.as_ref().get(REFERER) { + Some(headers) => headers, + None => return Ok(None), + }; + + // If we successfully parsed the header then there's always at least one + // entry. We want the last entry. + let header_value = headers.iter().last().unwrap(); + + let url = match Url::parse(header_value.as_str()) { + Ok(url) => url, + Err(_) => match base_url.try_into() { + Ok(base_url) => base_url.join(header_value.as_str().trim()).status(500)?, + Err(_) => bail!(500, "Invalid base url provided"), + }, + }; + + Ok(Some(Self { location: url })) + } + + /// Sets the header. + pub fn apply(&self, mut headers: impl AsMut) { + headers.as_mut().insert(self.name(), self.value()); + } + + /// Get the `HeaderName`. + pub fn name(&self) -> HeaderName { + REFERER + } + + /// Get the `HeaderValue`. + pub fn value(&self) -> HeaderValue { + let output = self.location.to_string(); + + // SAFETY: the internal string is validated to be ASCII. + unsafe { HeaderValue::from_bytes_unchecked(output.into()) } + } + + /// Get the url. + pub fn location(&self) -> &Url { + &self.location + } + + /// Set the url. + pub fn set_location(&mut self, location: U) -> Result<(), U::Error> + where + U: TryInto, + U::Error: std::fmt::Debug, + { + self.location = location.try_into()?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::headers::Headers; + + #[test] + fn smoke() -> crate::Result<()> { + let referer = Referer::new(Url::parse("https://example.net/test.json")?); + + let mut headers = Headers::new(); + referer.apply(&mut headers); + + let base_url = Url::parse("https://example.net/")?; + let referer = Referer::from_headers(base_url, headers)?.unwrap(); + assert_eq!( + referer.location(), + &Url::parse("https://example.net/test.json")? + ); + Ok(()) + } + + #[test] + fn bad_request_on_parse_error() -> crate::Result<()> { + let mut headers = Headers::new(); + headers.insert(REFERER, "htt://"); + let err = + Referer::from_headers(Url::parse("https://example.net").unwrap(), headers).unwrap_err(); + assert_eq!(err.status(), 500); + Ok(()) + } + + #[test] + fn fallback_works() -> crate::Result<()> { + let mut headers = Headers::new(); + headers.insert(REFERER, "/test.json"); + + let base_url = Url::parse("https://fallback.net/")?; + let referer = Referer::from_headers(base_url, headers)?.unwrap(); + assert_eq!( + referer.location(), + &Url::parse("https://fallback.net/test.json")? + ); + + let mut headers = Headers::new(); + headers.insert(REFERER, "https://example.com/test.json"); + + let base_url = Url::parse("https://fallback.net/")?; + let referer = Referer::from_headers(base_url, headers)?.unwrap(); + assert_eq!( + referer.location(), + &Url::parse("https://example.com/test.json")? + ); + Ok(()) + } +} diff --git a/src/other/retry_after.rs b/src/other/retry_after.rs new file mode 100644 index 00000000..bfe81f22 --- /dev/null +++ b/src/other/retry_after.rs @@ -0,0 +1,170 @@ +use std::time::{Duration, SystemTime, SystemTimeError}; + +use crate::headers::{HeaderName, HeaderValue, Headers, RETRY_AFTER}; +use crate::utils::{fmt_http_date, parse_http_date}; + +/// Indicate how long the user agent should wait before making a follow-up request. +/// +/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) +/// +/// # Specifications +/// +/// - [RFC 7231, section 3.1.4.2: Retry-After](https://tools.ietf.org/html/rfc7231#section-3.1.4.2) +/// +/// # Examples +/// +/// ```no_run +/// # fn main() -> http_types::Result<()> { +/// # +/// use http_types::other::RetryAfter; +/// use http_types::Response; +/// use std::time::{SystemTime, Duration}; +/// use async_std::task; +/// +/// let retry = RetryAfter::new(Duration::from_secs(10)); +/// +/// let mut headers = Response::new(429); +/// retry.apply(&mut headers); +/// +/// // Sleep for the duration, then try the task again. +/// let retry = RetryAfter::from_headers(headers)?.unwrap(); +/// task::sleep(retry.duration_since(SystemTime::now())?); +/// # +/// # Ok(()) } +/// ``` +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct RetryAfter { + inner: RetryDirective, +} + +#[allow(clippy::len_without_is_empty)] +impl RetryAfter { + /// Create a new instance from a `Duration`. + /// + /// This value will be encoded over the wire as a relative offset in seconds. + pub fn new(dur: Duration) -> Self { + Self { + inner: RetryDirective::Duration(dur), + } + } + + /// Create a new instance from a `SystemTime` instant. + /// + /// This value will be encoded a specific `Date` over the wire. + pub fn new_at(at: SystemTime) -> Self { + Self { + inner: RetryDirective::SystemTime(at), + } + } + + /// Create a new instance from headers. + pub fn from_headers(headers: impl AsRef) -> crate::Result> { + let header = match headers.as_ref().get(RETRY_AFTER) { + Some(headers) => headers.last(), + None => return Ok(None), + }; + + let inner = match header.as_str().parse::() { + Ok(dur) => RetryDirective::Duration(Duration::from_secs(dur)), + Err(_) => { + let at = parse_http_date(header.as_str())?; + RetryDirective::SystemTime(at) + } + }; + Ok(Some(Self { inner })) + } + + /// Returns the amount of time elapsed from an earlier point in time. + /// + /// # Errors + /// + /// This may return an error if the `earlier` time was after the current time. + pub fn duration_since(&self, earlier: SystemTime) -> Result { + let at = match self.inner { + RetryDirective::Duration(dur) => SystemTime::now() + dur, + RetryDirective::SystemTime(at) => at, + }; + + at.duration_since(earlier) + } + + /// Sets the header. + pub fn apply(&self, mut headers: impl AsMut) { + headers.as_mut().insert(self.name(), self.value()); + } + + /// Get the `HeaderName`. + pub fn name(&self) -> HeaderName { + RETRY_AFTER + } + + /// Get the `HeaderValue`. + pub fn value(&self) -> HeaderValue { + let output = match self.inner { + RetryDirective::Duration(dur) => format!("{}", dur.as_secs()), + RetryDirective::SystemTime(at) => fmt_http_date(at), + }; + // SAFETY: the internal string is validated to be ASCII. + unsafe { HeaderValue::from_bytes_unchecked(output.into()) } + } +} + +impl From for SystemTime { + fn from(retry_after: RetryAfter) -> Self { + match retry_after.inner { + RetryDirective::Duration(dur) => SystemTime::now() + dur, + RetryDirective::SystemTime(at) => at, + } + } +} + +/// What value are we decoding into? +/// +/// This value is intionally never exposes; all end-users want is a `Duration` +/// value that tells them how long to wait for before trying again. +#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] +enum RetryDirective { + Duration(Duration), + SystemTime(SystemTime), +} + +#[cfg(test)] +mod test { + use super::*; + use crate::headers::Headers; + + #[test] + fn smoke() -> crate::Result<()> { + let retry = RetryAfter::new(Duration::from_secs(10)); + + let mut headers = Headers::new(); + retry.apply(&mut headers); + + // `SystemTime::now` uses sub-second precision which means there's some + // offset that's not encoded. + let now = SystemTime::now(); + let retry = RetryAfter::from_headers(headers)?.unwrap(); + assert_eq!( + retry.duration_since(now)?.as_secs(), + Duration::from_secs(10).as_secs() + ); + Ok(()) + } + + #[test] + fn new_at() -> crate::Result<()> { + let now = SystemTime::now(); + let retry = RetryAfter::new_at(now + Duration::from_secs(10)); + + let mut headers = Headers::new(); + retry.apply(&mut headers); + + // `SystemTime::now` uses sub-second precision which means there's some + // offset that's not encoded. + let retry = RetryAfter::from_headers(headers)?.unwrap(); + let delta = retry.duration_since(now)?; + assert!(delta >= Duration::from_secs(9)); + assert!(delta <= Duration::from_secs(10)); + Ok(()) + } +} diff --git a/src/other/source_map.rs b/src/other/source_map.rs index e0e94355..349a99a2 100644 --- a/src/other/source_map.rs +++ b/src/other/source_map.rs @@ -59,7 +59,7 @@ impl SourceMap { let url = match Url::parse(header_value.as_str()) { Ok(url) => url, Err(_) => match base_url.try_into() { - Ok(base_url) => base_url.join(header_value.as_str().trim()).status(400)?, + Ok(base_url) => base_url.join(header_value.as_str().trim()).status(500)?, Err(_) => bail!(500, "Invalid base url provided"), }, }; @@ -128,7 +128,7 @@ mod test { headers.insert(SOURCE_MAP, "htt://"); let err = SourceMap::from_headers(Url::parse("https://example.net").unwrap(), headers) .unwrap_err(); - assert_eq!(err.status(), 400); + assert_eq!(err.status(), 500); Ok(()) } diff --git a/src/request.rs b/src/request.rs index 8eabcf5b..0f87cf9d 100644 --- a/src/request.rs +++ b/src/request.rs @@ -21,9 +21,9 @@ pin_project_lite::pin_project! { /// # Examples /// /// ``` - /// use http_types::{Url, Method, Request}; + /// use http_types::Request; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); /// ``` #[derive(Debug)] @@ -157,8 +157,8 @@ impl Request { /// ``` /// # fn main() -> Result<(), http_types::Error> { /// # - /// use http_types::{Method, Request, Response, StatusCode, Url}; - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// use http_types::{Request, Response, StatusCode}; + /// let mut req = Request::get("https://example.com"); /// assert_eq!(req.url().scheme(), "https"); /// # /// # Ok(()) } @@ -175,7 +175,7 @@ impl Request { /// # fn main() -> Result<(), http_types::Error> { /// # /// use http_types::{Method, Request, Response, StatusCode, Url}; - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// req.url_mut().set_scheme("http"); /// assert_eq!(req.url().scheme(), "http"); /// # @@ -190,9 +190,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); /// ``` pub fn set_body(&mut self, body: impl Into) { @@ -208,9 +208,9 @@ impl Request { /// # use async_std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// # - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Method, Request}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); /// let mut body: Body = req.replace_body("Hello, Chashu!"); /// @@ -234,9 +234,9 @@ impl Request { /// # use async_std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// # - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); /// let mut body = "Hello, Chashu!".into(); /// req.swap_body(&mut body); @@ -260,9 +260,9 @@ impl Request { /// # use async_std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// # - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); /// let mut body: Body = req.take_body(); /// @@ -293,9 +293,9 @@ impl Request { /// # use std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// use async_std::io::Cursor; - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// /// let cursor = Cursor::new("Hello Nori"); /// let body = Body::from_reader(cursor, None); @@ -319,10 +319,10 @@ impl Request { /// /// ``` /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// /// let bytes = vec![1, 2, 3]; - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body(Body::from_bytes(bytes)); /// /// let bytes = req.body_bytes().await?; @@ -346,7 +346,7 @@ impl Request { /// ``` /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// use http_types::convert::{Deserialize, Serialize}; - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// /// #[derive(Debug, Serialize, Deserialize)] /// struct Cat { @@ -356,7 +356,7 @@ impl Request { /// let cat = Cat { /// name: String::from("chashu"), /// }; - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body(Body::from_json(&cat)?); /// /// let cat: Cat = req.body_json().await?; @@ -380,7 +380,7 @@ impl Request { /// ``` /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// use http_types::convert::{Deserialize, Serialize}; - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// /// #[derive(Debug, Serialize, Deserialize)] /// struct Cat { @@ -390,7 +390,7 @@ impl Request { /// let cat = Cat { /// name: String::from("chashu"), /// }; - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body(Body::from_form(&cat)?); /// /// let cat: Cat = req.body_form().await?; @@ -424,9 +424,9 @@ impl Request { /// ``` /// # fn main() -> Result<(), Box> { /// # - /// use http_types::{Method, Request, Url}; + /// use http_types::Request; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// req.insert_header("Content-Type", "text/plain"); /// # /// # Ok(()) } @@ -450,9 +450,9 @@ impl Request { /// ``` /// # fn main() -> Result<(), Box> { /// # - /// use http_types::{Method, Request, Url}; + /// use http_types::Request; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// req.append_header("Content-Type", "text/plain"); /// # /// # Ok(()) } @@ -503,11 +503,11 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url, Version}; + /// use http_types::{Request, Version}; /// /// # fn main() -> Result<(), http_types::Error> { /// # - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// assert_eq!(req.version(), None); /// /// req.set_version(Some(Version::Http2_0)); @@ -524,11 +524,11 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url, Version}; + /// use http_types::{Request, Version}; /// /// # fn main() -> Result<(), http_types::Error> { /// # - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// req.set_version(Some(Version::Http2_0)); /// # /// # Ok(()) } @@ -594,9 +594,9 @@ impl Request { /// ``` /// # fn main() -> Result<(), http_types::Error> { /// # - /// use http_types::{Method, Request, Url, Version}; + /// use http_types::{Request, Version}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// req.ext_mut().insert("hello from the extension"); /// assert_eq!(req.ext().get(), Some(&"hello from the extension")); /// # @@ -612,7 +612,7 @@ impl Request { /// /// ``` /// use http_types::convert::Deserialize; - /// use http_types::{Method, Request, Url}; + /// use http_types::Request; /// use std::collections::HashMap; /// /// #[derive(Deserialize)] @@ -621,10 +621,7 @@ impl Request { /// selections: HashMap, /// } /// - /// let req = Request::new( - /// Method::Get, - /// Url::parse("https://httpbin.org/get?page=2&selections[width]=narrow&selections[height]=tall").unwrap(), - /// ); + /// let mut req = Request::get("https://httpbin.org/get?page=2&selections[width]=narrow&selections[height]=tall"); /// let Index { page, selections } = req.query().unwrap(); /// assert_eq!(page, 2); /// assert_eq!(selections["width"], "narrow"); @@ -648,7 +645,7 @@ impl Request { /// /// ``` /// use http_types::convert::Serialize; - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// use std::collections::HashMap; /// /// #[derive(Serialize)] @@ -658,10 +655,7 @@ impl Request { /// } /// /// let query = Index { page: 2, topics: vec!["rust", "crabs", "crustaceans"] }; - /// let mut req = Request::new( - /// Method::Get, - /// Url::parse("https://httpbin.org/get").unwrap(), - /// ); + /// let mut req = Request::get("https://httpbin.org/get"); /// req.set_query(&query).unwrap(); /// assert_eq!(req.url().query(), Some("page=2&topics[0]=rust&topics[1]=crabs&topics[2]=crustaceans")); /// ``` @@ -680,9 +674,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// - /// let mut req = Request::get(Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); /// assert_eq!(req.method(), Method::Get); /// ``` @@ -702,9 +696,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// - /// let mut req = Request::head(Url::parse("https://example.com").unwrap()); + /// let mut req = Request::head("https://example.com"); /// assert_eq!(req.method(), Method::Head); /// ``` pub fn head(url: U) -> Self @@ -723,9 +717,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// - /// let mut req = Request::post(Url::parse("https://example.com").unwrap()); + /// let mut req = Request::post("https://example.com"); /// assert_eq!(req.method(), Method::Post); /// ``` pub fn post(url: U) -> Self @@ -744,9 +738,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// - /// let mut req = Request::put(Url::parse("https://example.com").unwrap()); + /// let mut req = Request::put("https://example.com"); /// assert_eq!(req.method(), Method::Put); /// ``` pub fn put(url: U) -> Self @@ -764,9 +758,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// - /// let mut req = Request::delete(Url::parse("https://example.com").unwrap()); + /// let mut req = Request::delete("https://example.com"); /// assert_eq!(req.method(), Method::Delete); /// ``` pub fn delete(url: U) -> Self @@ -785,9 +779,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// - /// let mut req = Request::connect(Url::parse("https://example.com").unwrap()); + /// let mut req = Request::connect("https://example.com"); /// assert_eq!(req.method(), Method::Connect); /// ``` pub fn connect(url: U) -> Self @@ -806,9 +800,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// - /// let mut req = Request::options(Url::parse("https://example.com").unwrap()); + /// let mut req = Request::options("https://example.com"); /// assert_eq!(req.method(), Method::Options); /// ``` pub fn options(url: U) -> Self @@ -827,9 +821,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// - /// let mut req = Request::trace(Url::parse("https://example.com").unwrap()); + /// let mut req = Request::trace("https://example.com"); /// assert_eq!(req.method(), Method::Trace); /// ``` pub fn trace(url: U) -> Self @@ -847,9 +841,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// - /// let mut req = Request::patch(Url::parse("https://example.com").unwrap()); + /// let mut req = Request::patch("https://example.com"); /// assert_eq!(req.method(), Method::Patch); /// ``` pub fn patch(url: U) -> Self diff --git a/src/status_code.rs b/src/status_code.rs index c10c4a2f..86f14d1a 100644 --- a/src/status_code.rs +++ b/src/status_code.rs @@ -434,7 +434,7 @@ impl StatusCode { /// continuing process. pub fn is_informational(&self) -> bool { let num: u16 = self.clone().into(); - num >= 100 && num < 200 + (100..200).contains(&num) } /// Returns `true` if the status code is the `2xx` range. @@ -443,7 +443,7 @@ impl StatusCode { /// received, understood, and accepted. pub fn is_success(&self) -> bool { let num: u16 = self.clone().into(); - num >= 200 && num < 300 + (200..300).contains(&num) } /// Returns `true` if the status code is the `3xx` range. @@ -452,7 +452,7 @@ impl StatusCode { /// taken in order to complete the request. pub fn is_redirection(&self) -> bool { let num: u16 = self.clone().into(); - num >= 300 && num < 400 + (300..400).contains(&num) } /// Returns `true` if the status code is the `4xx` range. @@ -461,7 +461,7 @@ impl StatusCode { /// or cannot be fulfilled. pub fn is_client_error(&self) -> bool { let num: u16 = self.clone().into(); - num >= 400 && num < 500 + (400..500).contains(&num) } /// Returns `true` if the status code is the `5xx` range. @@ -470,7 +470,7 @@ impl StatusCode { /// apparently valid request. pub fn is_server_error(&self) -> bool { let num: u16 = self.clone().into(); - num >= 500 && num < 600 + (500..600).contains(&num) } /// The canonical reason for a given status code diff --git a/tests/mime.rs b/tests/mime.rs index b408748e..2488ef4f 100644 --- a/tests/mime.rs +++ b/tests/mime.rs @@ -1,6 +1,8 @@ -use async_std::fs; -use async_std::io; -use http_types::{mime, Body, Response}; +#[cfg(features = "fs")] +mod tests { + use async_std::fs; + use async_std::io; + use http_types::{mime, Body, Response}; #[async_std::test] async fn guess_plain_text_mime() -> io::Result<()> { @@ -18,12 +20,12 @@ use http_types::{mime, Body, Response}; res.set_body(body); assert_eq!(res.content_type(), Some(mime::PNG)); -#[async_std::test] -async fn guess_binary_mime() -> http_types::Result<()> { - let body = Body::from_file("tests/fixtures/nori.png").await?; - let mut res = Response::new(200); - res.set_body(body); - assert_eq!(res.content_type(), Some(mime::PNG)); + // Assert the file is correctly reset after we've peeked the bytes + let left = fs::read("tests/fixtures/nori.png").await?; + let right = res.body_bytes().await?; + assert_eq!(left, right); + Ok(()) + } #[async_std::test] async fn guess_mime_fallback() -> io::Result<()> { @@ -43,11 +45,12 @@ async fn guess_binary_mime() -> http_types::Result<()> { Ok(()) } -#[async_std::test] -async fn parse_empty_files() -> http_types::Result<()> { - let body = Body::from_file("tests/fixtures/empty.custom").await?; - let mut res = Response::new(200); - res.set_body(body); - assert_eq!(res.content_type(), Some(mime::BYTE_STREAM)); - Ok(()) + // #[test] + // fn match_mime_types() { + // let req = Request::get("https://example.com"); + // match req.content_type() { + // Some(mime::JSON) => {} + // _ => {} + // } + // } } From dca360e9b48d42b5d04a454136df268e4eb835b0 Mon Sep 17 00:00:00 2001 From: Caden Haustein Date: Wed, 27 Jan 2021 17:52:37 -0600 Subject: [PATCH 5/5] Fix doctests --- src/transfer/trailers.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/transfer/trailers.rs b/src/transfer/trailers.rs index d77daf4e..220fb620 100644 --- a/src/transfer/trailers.rs +++ b/src/transfer/trailers.rs @@ -23,7 +23,7 @@ //! # async_std::task::block_on(async { //! # //! use http_types::{Url, Method, Request}; -//! use http_types::trailers::Trailers; +//! use http_types::transfer::Trailers; //! use http_types::headers::{HeaderName, HeaderValue}; //! use async_std::task; //! use std::str::FromStr; @@ -80,7 +80,7 @@ impl Trailers { /// ``` /// # fn main() -> Result<(), Box> { /// # - /// use http_types::trailers::Trailers; + /// use http_types::transfer::Trailers; /// /// let mut trailers = Trailers::new(); /// trailers.insert("Content-Type", "text/plain"); @@ -105,7 +105,7 @@ impl Trailers { /// ``` /// # fn main() -> Result<(), Box> { /// # - /// use http_types::trailers::Trailers; + /// use http_types::transfer::Trailers; /// /// let mut trailers = Trailers::new(); /// trailers.append("Content-Type", "text/plain");