1pub const MAX_METHOD_BYTES: usize = 32;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum MethodError {
9 Empty,
11 TooLong,
13 NonCanonical,
15 KnownMethod,
17 DeniedMethod,
19}
20
21impl_static_error!(MethodError,
22 Self::Empty => "HTTP extension method is empty",
23 Self::TooLong => "HTTP extension method exceeds the length limit",
24 Self::NonCanonical => "HTTP extension method is not a canonical uppercase token",
25 Self::KnownMethod => "known HTTP method must use its dedicated constant",
26 Self::DeniedMethod => "HTTP method is denied by the transport contract",
27);
28
29#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub struct Method {
50 token: &'static str,
51}
52
53#[allow(non_upper_case_globals)]
54impl Method {
55 pub const Get: Self = Self::known("GET");
57 pub const Post: Self = Self::known("POST");
59 pub const Put: Self = Self::known("PUT");
61 pub const Delete: Self = Self::known("DELETE");
63 pub const Patch: Self = Self::known("PATCH");
65 pub const Head: Self = Self::known("HEAD");
67 pub const Options: Self = Self::known("OPTIONS");
69
70 pub const fn extension(token: &'static str) -> Result<Self, MethodError> {
76 let bytes = token.as_bytes();
77 if bytes.is_empty() {
78 return Err(MethodError::Empty);
79 }
80 if bytes.len() > MAX_METHOD_BYTES {
81 return Err(MethodError::TooLong);
82 }
83 if token_is(bytes, b"CONNECT") || token_is(bytes, b"TRACE") {
84 return Err(MethodError::DeniedMethod);
85 }
86 if is_known(bytes) {
87 return Err(MethodError::KnownMethod);
88 }
89
90 let mut remaining = bytes;
91 while let [byte, tail @ ..] = remaining {
92 if !is_canonical_token_byte(*byte) {
93 return Err(MethodError::NonCanonical);
94 }
95 remaining = tail;
96 }
97 Ok(Self { token })
98 }
99
100 #[must_use]
102 pub const fn as_str(self) -> &'static str {
103 self.token
104 }
105
106 const fn known(token: &'static str) -> Self {
107 Self { token }
108 }
109}
110
111const fn is_known(token: &[u8]) -> bool {
112 token_is(token, b"GET")
113 || token_is(token, b"POST")
114 || token_is(token, b"PUT")
115 || token_is(token, b"DELETE")
116 || token_is(token, b"PATCH")
117 || token_is(token, b"HEAD")
118 || token_is(token, b"OPTIONS")
119}
120
121const fn token_is(left: &[u8], right: &[u8]) -> bool {
122 let mut left_remaining = left;
123 let mut right_remaining = right;
124 loop {
125 match (left_remaining, right_remaining) {
126 ([], []) => return true,
127 ([left_byte, left_tail @ ..], [right_byte, right_tail @ ..]) => {
128 if *left_byte != *right_byte {
129 return false;
130 }
131 left_remaining = left_tail;
132 right_remaining = right_tail;
133 }
134 _ => return false,
135 }
136 }
137}
138
139const fn is_canonical_token_byte(byte: u8) -> bool {
140 byte.is_ascii_uppercase()
141 || byte.is_ascii_digit()
142 || matches!(
143 byte,
144 b'!' | b'#'
145 | b'$'
146 | b'%'
147 | b'&'
148 | b'\''
149 | b'*'
150 | b'+'
151 | b'-'
152 | b'.'
153 | b'^'
154 | b'_'
155 | b'`'
156 | b'|'
157 | b'~'
158 )
159}
160
161#[cfg(test)]
162mod tests {
163 use super::{MAX_METHOD_BYTES, Method, MethodError};
164 use crate::transport::{RequestTarget, RequestTargetError, TransportRequest};
165
166 const PURGE: Method = match Method::extension("PURGE") {
167 Ok(method) => method,
168 Err(_) => panic!("valid extension method"),
169 };
170
171 #[test]
172 fn exposes_every_admitted_known_method() {
173 assert_eq!(
174 [
175 Method::Get.as_str(),
176 Method::Post.as_str(),
177 Method::Put.as_str(),
178 Method::Delete.as_str(),
179 Method::Patch.as_str(),
180 Method::Head.as_str(),
181 Method::Options.as_str(),
182 ],
183 ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
184 );
185 }
186
187 #[test]
188 fn admits_bounded_canonical_extension_tokens() {
189 for token in [
190 "PURGE",
191 "PROPFIND",
192 "M-SEARCH",
193 "VERSION-CONTROL",
194 "A!#$%&'*+-.^_`|~9",
195 ] {
196 assert_eq!(Method::extension(token).map(Method::as_str), Ok(token));
197 }
198 assert_eq!(PURGE.as_str(), "PURGE");
199 assert_eq!(
200 Method::extension("A2345678901234567890123456789012").map(Method::as_str),
201 Ok("A2345678901234567890123456789012")
202 );
203 }
204
205 #[test]
206 fn rejects_empty_oversized_noncanonical_and_alias_tokens() {
207 assert_eq!(Method::extension(""), Err(MethodError::Empty));
208 assert_eq!(
209 Method::extension("A23456789012345678901234567890123"),
210 Err(MethodError::TooLong)
211 );
212 assert_eq!("A2345678901234567890123456789012".len(), MAX_METHOD_BYTES);
213
214 for alias in ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"] {
215 assert_eq!(Method::extension(alias), Err(MethodError::KnownMethod));
216 }
217 for invalid in [
218 "get",
219 "Get",
220 "Purge",
221 "M SEARCH",
222 "M\tSEARCH",
223 "M\r\nSEARCH",
224 "M/SEARCH",
225 "M:SEARCH",
226 "M\\SEARCH",
227 "M{SEARCH}",
228 "MÜNCHEN",
229 ] {
230 assert_eq!(Method::extension(invalid), Err(MethodError::NonCanonical));
231 }
232 }
233
234 #[test]
235 fn denies_connect_trace_and_non_origin_options() {
236 assert_eq!(Method::extension("CONNECT"), Err(MethodError::DeniedMethod));
237 assert_eq!(Method::extension("TRACE"), Err(MethodError::DeniedMethod));
238 assert_eq!(
239 RequestTarget::new("*"),
240 Err(RequestTargetError::NotOriginForm)
241 );
242
243 let target = RequestTarget::new("/").unwrap_or_else(|_| unreachable!());
244 let request = TransportRequest::new(Method::Options, target);
245 assert_eq!(request.method(), Method::Options);
246 assert_eq!(request.target().as_str(), "/");
247 }
248}