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 pub(crate) const fn permits_direct_read_only(self) -> bool {
107 token_is(self.token.as_bytes(), b"GET") || token_is(self.token.as_bytes(), b"HEAD")
108 }
109
110 const fn known(token: &'static str) -> Self {
111 Self { token }
112 }
113}
114
115const fn is_known(token: &[u8]) -> bool {
116 token_is(token, b"GET")
117 || token_is(token, b"POST")
118 || token_is(token, b"PUT")
119 || token_is(token, b"DELETE")
120 || token_is(token, b"PATCH")
121 || token_is(token, b"HEAD")
122 || token_is(token, b"OPTIONS")
123}
124
125const fn token_is(left: &[u8], right: &[u8]) -> bool {
126 let mut left_remaining = left;
127 let mut right_remaining = right;
128 loop {
129 match (left_remaining, right_remaining) {
130 ([], []) => return true,
131 ([left_byte, left_tail @ ..], [right_byte, right_tail @ ..]) => {
132 if *left_byte != *right_byte {
133 return false;
134 }
135 left_remaining = left_tail;
136 right_remaining = right_tail;
137 }
138 _ => return false,
139 }
140 }
141}
142
143const fn is_canonical_token_byte(byte: u8) -> bool {
144 byte.is_ascii_uppercase()
145 || byte.is_ascii_digit()
146 || matches!(
147 byte,
148 b'!' | b'#'
149 | b'$'
150 | b'%'
151 | b'&'
152 | b'\''
153 | b'*'
154 | b'+'
155 | b'-'
156 | b'.'
157 | b'^'
158 | b'_'
159 | b'`'
160 | b'|'
161 | b'~'
162 )
163}
164
165#[cfg(test)]
166mod tests {
167 use super::{MAX_METHOD_BYTES, Method, MethodError};
168 use crate::transport::{RequestTarget, RequestTargetError, TransportRequest};
169
170 const PURGE: Method = match Method::extension("PURGE") {
171 Ok(method) => method,
172 Err(_) => panic!("valid extension method"),
173 };
174
175 #[test]
176 fn exposes_every_admitted_known_method() {
177 assert_eq!(
178 [
179 Method::Get.as_str(),
180 Method::Post.as_str(),
181 Method::Put.as_str(),
182 Method::Delete.as_str(),
183 Method::Patch.as_str(),
184 Method::Head.as_str(),
185 Method::Options.as_str(),
186 ],
187 ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
188 );
189 }
190
191 #[test]
192 fn only_get_and_head_admit_direct_read_only_execution() {
193 assert!(Method::Get.permits_direct_read_only());
194 assert!(Method::Head.permits_direct_read_only());
195 for method in [
196 Method::Post,
197 Method::Put,
198 Method::Delete,
199 Method::Patch,
200 Method::Options,
201 PURGE,
202 ] {
203 assert!(!method.permits_direct_read_only());
204 }
205 }
206
207 #[test]
208 fn admits_bounded_canonical_extension_tokens() {
209 for token in [
210 "PURGE",
211 "PROPFIND",
212 "M-SEARCH",
213 "VERSION-CONTROL",
214 "A!#$%&'*+-.^_`|~9",
215 ] {
216 assert_eq!(Method::extension(token).map(Method::as_str), Ok(token));
217 }
218 assert_eq!(PURGE.as_str(), "PURGE");
219 assert_eq!(
220 Method::extension("A2345678901234567890123456789012").map(Method::as_str),
221 Ok("A2345678901234567890123456789012")
222 );
223 }
224
225 #[test]
226 fn rejects_empty_oversized_noncanonical_and_alias_tokens() {
227 assert_eq!(Method::extension(""), Err(MethodError::Empty));
228 assert_eq!(
229 Method::extension("A23456789012345678901234567890123"),
230 Err(MethodError::TooLong)
231 );
232 assert_eq!("A2345678901234567890123456789012".len(), MAX_METHOD_BYTES);
233
234 for alias in ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"] {
235 assert_eq!(Method::extension(alias), Err(MethodError::KnownMethod));
236 }
237 for invalid in [
238 "get",
239 "Get",
240 "Purge",
241 "M SEARCH",
242 "M\tSEARCH",
243 "M\r\nSEARCH",
244 "M/SEARCH",
245 "M:SEARCH",
246 "M\\SEARCH",
247 "M{SEARCH}",
248 "MÜNCHEN",
249 ] {
250 assert_eq!(Method::extension(invalid), Err(MethodError::NonCanonical));
251 }
252 }
253
254 #[test]
255 fn denies_connect_trace_and_non_origin_options() {
256 assert_eq!(Method::extension("CONNECT"), Err(MethodError::DeniedMethod));
257 assert_eq!(Method::extension("TRACE"), Err(MethodError::DeniedMethod));
258 assert_eq!(
259 RequestTarget::new("*"),
260 Err(RequestTargetError::Path(
261 crate::transport::RequestPathError::NotOriginForm
262 ))
263 );
264
265 let target = RequestTarget::new("/").unwrap_or_else(|_| unreachable!());
266 let request = TransportRequest::new(Method::Options, target);
267 assert_eq!(request.method(), Method::Options);
268 assert_eq!(request.target().as_str(), "/");
269 }
270}