diff --git a/crates/net/src/http/method.rs b/crates/net/src/http/method.rs index 86f76472..ef87a70b 100644 --- a/crates/net/src/http/method.rs +++ b/crates/net/src/http/method.rs @@ -1,471 +1,99 @@ -//! The HTTP request method -//! -//! This module contains HTTP-method related structs and errors and such. The -//! main type of this module, `Method`, is also reexported at the root of the -//! crate as `http::Method` and is intended for import through that location -//! primarily. -//! -//! # Examples -//! -//! ``` -//! use gloo_net::http::Method; -//! -//! assert_eq!(Method::GET, Method::from_bytes(b"GET").unwrap()); -//! assert!(Method::GET.is_idempotent()); -//! assert_eq!(Method::POST.as_str(), "POST"); -//! ``` +use std::{fmt::Display, str::FromStr}; -use self::extension::{AllocatedExtension, InlineExtension}; -use self::Inner::*; - -use std::convert::AsRef; -use std::convert::TryFrom; -use std::error::Error; -use std::str::FromStr; -use std::{fmt, str}; - -/// The Request Method (VERB) -/// -/// This type also contains constants for a number of common HTTP methods such -/// as GET, POST, etc. -/// -/// Currently includes 8 variants representing the 8 methods defined in -/// [RFC 7230](https://tools.ietf.org/html/rfc7231#section-4.1), plus PATCH, -/// and an Extension variant for all extensions. -/// -/// # Examples -/// -/// ``` -/// use gloo_net::http::Method; -/// -/// assert_eq!(Method::GET, Method::from_bytes(b"GET").unwrap()); -/// assert!(Method::GET.is_idempotent()); -/// assert_eq!(Method::POST.as_str(), "POST"); -/// ``` -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct Method(Inner); - -/// A possible error value when converting `Method` from bytes. -#[derive(Clone, Copy)] -pub struct InvalidMethod { - _priv: (), -} - -#[derive(Clone, PartialEq, Eq, Hash)] -enum Inner { +/// HTTP methods that can be used in a request. +/// The methods as defined by the [fetch spec](https://fetch.spec.whatwg.org/#methods). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Method { + /// The OPTIONS method represents a request for information about the communication options Options, + /// The GET method means retrieve whatever information (in the form of an entity) is Get, + /// The POST method is used to request that the origin server accept the entity enclosed Post, + /// The PUT method requests that the enclosed entity be stored under the supplied Request-URI. Put, + /// The DELETE method requests that the origin server delete the resource identified by the Request-URI. Delete, + /// The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response. Head, - Trace, - Connect, + /// The PATCH method requests that a set of changes described in the Patch, - // If the extension is short enough, store it inline - ExtensionInline(InlineExtension), - // Otherwise, allocate it - ExtensionAllocated(AllocatedExtension), } impl Method { - /// GET - pub const GET: Method = Method(Get); - - /// POST - pub const POST: Method = Method(Post); - - /// PUT - pub const PUT: Method = Method(Put); - - /// DELETE - pub const DELETE: Method = Method(Delete); - - /// HEAD - pub const HEAD: Method = Method(Head); - - /// OPTIONS - pub const OPTIONS: Method = Method(Options); - - /// CONNECT - pub const CONNECT: Method = Method(Connect); - - /// PATCH - pub const PATCH: Method = Method(Patch); - - /// TRACE - pub const TRACE: Method = Method(Trace); - - /// Converts a slice of bytes to an HTTP method. - pub fn from_bytes(src: &[u8]) -> Result { - match src.len() { - 0 => Err(InvalidMethod::new()), - 3 => match src { - b"GET" => Ok(Method(Get)), - b"PUT" => Ok(Method(Put)), - _ => Method::extension_inline(src), - }, - 4 => match src { - b"POST" => Ok(Method(Post)), - b"HEAD" => Ok(Method(Head)), - _ => Method::extension_inline(src), - }, - 5 => match src { - b"PATCH" => Ok(Method(Patch)), - b"TRACE" => Ok(Method(Trace)), - _ => Method::extension_inline(src), - }, - 6 => match src { - b"DELETE" => Ok(Method(Delete)), - _ => Method::extension_inline(src), - }, - 7 => match src { - b"OPTIONS" => Ok(Method(Options)), - b"CONNECT" => Ok(Method(Connect)), - _ => Method::extension_inline(src), - }, - _ => { - if src.len() < InlineExtension::MAX { - Method::extension_inline(src) - } else { - let allocated = AllocatedExtension::new(src)?; - - Ok(Method(ExtensionAllocated(allocated))) - } - } - } - } - - fn extension_inline(src: &[u8]) -> Result { - let inline = InlineExtension::new(src)?; - - Ok(Method(ExtensionInline(inline))) - } - - /// Whether a method is considered "safe", meaning the request is - /// essentially read-only. - /// - /// See [the spec](https://tools.ietf.org/html/rfc7231#section-4.2.1) - /// for more words. - pub fn is_safe(&self) -> bool { - matches!(self.0, Get | Head | Options | Trace) - } - - /// Whether a method is considered "idempotent", meaning the request has - /// the same result if executed multiple times. - /// - /// See [the spec](https://tools.ietf.org/html/rfc7231#section-4.2.2) for - /// more words. - pub fn is_idempotent(&self) -> bool { - match self.0 { - Put | Delete => true, - _ => self.is_safe(), + /// Returns a `Method` from the given bytes. + pub fn from_bytes(src: &[u8]) -> Option { + match src { + b"OPTIONS" => Some(Method::Options), + b"GET" => Some(Method::Get), + b"POST" => Some(Method::Post), + b"PUT" => Some(Method::Put), + b"DELETE" => Some(Method::Delete), + b"HEAD" => Some(Method::Head), + b"PATCH" => Some(Method::Patch), + _ => None, } } - /// Return a &str representation of the HTTP method - #[inline] - pub fn as_str(&self) -> &str { - match self.0 { - Options => "OPTIONS", - Get => "GET", - Post => "POST", - Put => "PUT", - Delete => "DELETE", - Head => "HEAD", - Trace => "TRACE", - Connect => "CONNECT", - Patch => "PATCH", - ExtensionInline(ref inline) => inline.as_str(), - ExtensionAllocated(ref allocated) => allocated.as_str(), - } - } + /// The OPTIONS method represents a request for information about the communication options + pub const OPTIONS: Method = Method::Options; + /// The GET method means retrieve whatever information (in the form of an entity) is + /// identified by the Request-URI. + pub const GET: Method = Method::Get; + /// The POST method is used to request that the origin server accept the entity enclosed + /// in the request as a new subordinate of the resource identified by the Request-URI + pub const POST: Method = Method::Post; + /// The PUT method requests that the enclosed entity be stored under the supplied Request-URI. + pub const PUT: Method = Method::Put; + /// The DELETE method requests that the origin server delete the resource identified by the Request-URI. + pub const DELETE: Method = Method::Delete; + /// The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response. + pub const HEAD: Method = Method::Head; + /// The PATCH method requests that a set of changes described in the + /// request entity be applied to the resource identified by the Request-URI. + pub const PATCH: Method = Method::Patch; } impl AsRef for Method { - #[inline] fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl<'a> PartialEq<&'a Method> for Method { - #[inline] - fn eq(&self, other: &&'a Method) -> bool { - self == *other - } -} - -impl<'a> PartialEq for &'a Method { - #[inline] - fn eq(&self, other: &Method) -> bool { - *self == other - } -} - -impl PartialEq for Method { - #[inline] - fn eq(&self, other: &str) -> bool { - self.as_ref() == other - } -} - -impl PartialEq for str { - #[inline] - fn eq(&self, other: &Method) -> bool { - self == other.as_ref() - } -} - -impl<'a> PartialEq<&'a str> for Method { - #[inline] - fn eq(&self, other: &&'a str) -> bool { - self.as_ref() == *other - } -} - -impl<'a> PartialEq for &'a str { - #[inline] - fn eq(&self, other: &Method) -> bool { - *self == other.as_ref() - } -} - -impl fmt::Debug for Method { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_ref()) - } -} - -impl fmt::Display for Method { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.write_str(self.as_ref()) - } -} - -impl Default for Method { - #[inline] - fn default() -> Method { - Method::GET - } -} - -impl<'a> From<&'a Method> for Method { - #[inline] - fn from(t: &'a Method) -> Self { - t.clone() - } -} - -impl<'a> TryFrom<&'a [u8]> for Method { - type Error = InvalidMethod; - - #[inline] - fn try_from(t: &'a [u8]) -> Result { - Method::from_bytes(t) - } -} - -impl<'a> TryFrom<&'a str> for Method { - type Error = InvalidMethod; - - #[inline] - fn try_from(t: &'a str) -> Result { - TryFrom::try_from(t.as_bytes()) + match *self { + Method::Options => "OPTIONS", + Method::Get => "GET", + Method::Post => "POST", + Method::Put => "PUT", + Method::Delete => "DELETE", + Method::Head => "HEAD", + Method::Patch => "PATCH", + } } } impl FromStr for Method { - type Err = InvalidMethod; - - #[inline] - fn from_str(t: &str) -> Result { - TryFrom::try_from(t) - } -} - -impl InvalidMethod { - fn new() -> InvalidMethod { - InvalidMethod { _priv: () } - } -} - -impl fmt::Debug for InvalidMethod { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("InvalidMethod") - // skip _priv noise - .finish() + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "OPTIONS" => Ok(Method::Options), + "GET" => Ok(Method::Get), + "POST" => Ok(Method::Post), + "PUT" => Ok(Method::Put), + "DELETE" => Ok(Method::Delete), + "HEAD" => Ok(Method::Head), + "PATCH" => Ok(Method::Patch), + _ => Err(Error::InvalidMethod), + } } } -impl fmt::Display for InvalidMethod { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("invalid HTTP method") - } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + InvalidMethod, } -impl Error for InvalidMethod {} - -mod extension { - use super::InvalidMethod; - use std::str; - - #[derive(Clone, PartialEq, Eq, Hash)] - // Invariant: the first self.1 bytes of self.0 are valid UTF-8. - pub struct InlineExtension([u8; InlineExtension::MAX], u8); - - #[derive(Clone, PartialEq, Eq, Hash)] - // Invariant: self.0 contains valid UTF-8. - pub struct AllocatedExtension(Box<[u8]>); - - impl InlineExtension { - // Method::from_bytes() assumes this is at least 7 - pub const MAX: usize = 15; - - pub fn new(src: &[u8]) -> Result { - let mut data: [u8; InlineExtension::MAX] = Default::default(); - - write_checked(src, &mut data)?; - - // Invariant: write_checked ensures that the first src.len() bytes - // of data are valid UTF-8. - Ok(InlineExtension(data, src.len() as u8)) +impl Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match *self { + Error::InvalidMethod => write!(f, "Invalid method"), } - - pub fn as_str(&self) -> &str { - let InlineExtension(ref data, len) = self; - // Safety: the invariant of InlineExtension ensures that the first - // len bytes of data contain valid UTF-8. - unsafe { str::from_utf8_unchecked(&data[..*len as usize]) } - } - } - - impl AllocatedExtension { - pub fn new(src: &[u8]) -> Result { - let mut data: Vec = vec![0; src.len()]; - - write_checked(src, &mut data)?; - - // Invariant: data is exactly src.len() long and write_checked - // ensures that the first src.len() bytes of data are valid UTF-8. - Ok(AllocatedExtension(data.into_boxed_slice())) - } - - pub fn as_str(&self) -> &str { - // Safety: the invariant of AllocatedExtension ensures that self.0 - // contains valid UTF-8. - unsafe { str::from_utf8_unchecked(&self.0) } - } - } - - // From the HTTP spec section 5.1.1, the HTTP method is case-sensitive and can - // contain the following characters: - // - // ``` - // method = token - // token = 1*tchar - // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / - // "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA - // ``` - // - // https://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01#Method - // - // Note that this definition means that any &[u8] that consists solely of valid - // characters is also valid UTF-8 because the valid method characters are a - // subset of the valid 1 byte UTF-8 encoding. - #[rustfmt::skip] - const METHOD_CHARS: [u8; 256] = [ - // 0 1 2 3 4 5 6 7 8 9 - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 1x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 2x - b'\0', b'\0', b'\0', b'!', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 3x - b'\0', b'\0', b'*', b'+', b'\0', b'-', b'.', b'\0', b'0', b'1', // 4x - b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'\0', b'\0', // 5x - b'\0', b'\0', b'\0', b'\0', b'\0', b'A', b'B', b'C', b'D', b'E', // 6x - b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', // 7x - b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', // 8x - b'Z', b'\0', b'\0', b'\0', b'^', b'_', b'`', b'a', b'b', b'c', // 9x - b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', // 10x - b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', // 11x - b'x', b'y', b'z', b'\0', b'|', b'\0', b'~', b'\0', b'\0', b'\0', // 12x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 13x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 14x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 15x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 16x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 17x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 18x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 19x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 20x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 21x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 22x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 23x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 24x - b'\0', b'\0', b'\0', b'\0', b'\0', b'\0' // 25x - ]; - - // write_checked ensures (among other things) that the first src.len() bytes - // of dst are valid UTF-8 - fn write_checked(src: &[u8], dst: &mut [u8]) -> Result<(), InvalidMethod> { - for (i, &b) in src.iter().enumerate() { - let b = METHOD_CHARS[b as usize]; - - if b == 0 { - return Err(InvalidMethod::new()); - } - - dst[i] = b; - } - - Ok(()) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_method_eq() { - assert_eq!(Method::GET, Method::GET); - assert_eq!(Method::GET, "GET"); - assert_eq!(&Method::GET, "GET"); - - assert_eq!("GET", Method::GET); - assert_eq!("GET", &Method::GET); - - assert_eq!(&Method::GET, Method::GET); - assert_eq!(Method::GET, &Method::GET); - } - - #[test] - fn test_invalid_method() { - assert!(Method::from_str("").is_err()); - assert!(Method::from_bytes(b"").is_err()); - assert!(Method::from_bytes(&[0xC0]).is_err()); // invalid utf-8 - assert!(Method::from_bytes(&[0x10]).is_err()); // invalid method characters - } - - #[test] - fn test_is_idempotent() { - assert!(Method::OPTIONS.is_idempotent()); - assert!(Method::GET.is_idempotent()); - assert!(Method::PUT.is_idempotent()); - assert!(Method::DELETE.is_idempotent()); - assert!(Method::HEAD.is_idempotent()); - assert!(Method::TRACE.is_idempotent()); - - assert!(!Method::POST.is_idempotent()); - assert!(!Method::CONNECT.is_idempotent()); - assert!(!Method::PATCH.is_idempotent()); - } - - #[test] - fn test_extension_method() { - assert_eq!(Method::from_str("WOW").unwrap(), "WOW"); - assert_eq!(Method::from_str("wOw!!").unwrap(), "wOw!!"); - - let long_method = "This_is_a_very_long_method.It_is_valid_but_unlikely."; - assert_eq!(Method::from_str(long_method).unwrap(), long_method); } }