Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
pub mod method;
pub mod version;

pub use method::Method;
286 changes: 286 additions & 0 deletions src/method.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
//! The HTTP request method

use self::Inner::*;

use std::{fmt, str};
use std::convert::AsRef;
use std::error::Error;

/// The Request Method (VERB)
///
/// 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.
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the derived PartailEq and Hash may also be incorrect in the presence of from_bytes_unchecked (similar to below) as it's possible to have Inner::Get and Inner::ExtensionInline(b"GET")

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that is the point! We want to avoid string comparisons if we don't have to, and from_bytes_unchecked is unsafe. So, if you do it wrong, that is on you :)

pub struct Method(Inner);

#[derive(Debug)]
pub struct FromBytesError {
_priv: (),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I again wonder if InvalidMethodError is more descriptive..

}

#[derive(Clone, PartialEq, Eq, Hash, Debug)]
enum Inner {
Options,
Get,
Post,
Put,
Delete,
Head,
Trace,
Connect,
Patch,
// If the extension is short enough, store it inline
ExtensionInline([u8; MAX_INLINE], u8),
// Otherwise, allocate it
ExtensionAllocated(Box<[u8]>),
}

const MAX_INLINE: usize = 15;

// 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
//
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
];

/// 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);

impl Method {
/// Converts a slice of bytes to an HTTP method.
pub fn from_bytes(src: &[u8]) -> Result<Method, FromBytesError> {
match src.len() {
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() < MAX_INLINE {
Method::extension_inline(src)
} else {
let mut data: Vec<u8> = vec![0; src.len()];

try!(write_checked(src, &mut data));

Ok(Method(ExtensionAllocated(data.into_boxed_slice())))
}
}
}
}

fn extension_inline(src: &[u8]) -> Result<Method, FromBytesError> {
let mut data: [u8; MAX_INLINE] = Default::default();

try!(write_checked(src, &mut data));

Ok(Method(ExtensionInline(data, src.len() as u8)))
}

/// 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 {
match self.0 {
Get | Head | Options | Trace => true,
_ => false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may be incorrect in the face of from_bytes_unchecked because we could hav ExtensionInline(b"GET"), right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

}
}

/// 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 {
if self.is_safe() {
true
} else {
match self.0 {
Put | Delete => true,
_ => false
}
}
}

/// Return a &str representation of the HTTP method
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 data, len) => {
unsafe {
str::from_utf8_unchecked(&data[..len as usize])
}
}
ExtensionAllocated(ref data) => {
unsafe {
str::from_utf8_unchecked(data)
}
}
}
}
}

fn write_checked(src: &[u8], dst: &mut [u8]) -> Result<(), FromBytesError> {
for (i, &b) in src.iter().enumerate() {
let b = METHOD_CHARS[b as usize];

if b == 0 {
return Err(FromBytesError::new());
}

dst[i] = b;
}

Ok(())
}

impl AsRef<str> for Method {
fn as_ref(&self) -> &str {
self.as_str()
}
}

impl PartialEq<str> for Method {
fn eq(&self, other: &str) -> bool {
self.as_ref() == other
}
}

impl<'a> PartialEq<&'a str> for Method {
fn eq(&self, other: &&'a str) -> bool {
self.as_ref() == *other
}
}

impl fmt::Display for Method {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(self.as_ref())
}
}

impl Default for Method {
fn default() -> Method {
GET
}
}

impl FromBytesError {
fn new() -> FromBytesError {
FromBytesError {
_priv: (),
}
}
}

impl fmt::Display for FromBytesError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.description())
}
}

impl Error for FromBytesError {
fn description(&self) -> &str {
"invalid HTTP method"
}
}