eggserve_core/primitives/
request_target.rs1use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum RequestTargetError {
12 Empty,
14 NotOriginForm,
16 ContainsWhitespace,
18 AbsoluteUri,
20 AuthorityForm,
22 AsteriskForm,
24}
25
26impl fmt::Display for RequestTargetError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::Empty => write!(f, "request target is empty"),
30 Self::NotOriginForm => write!(f, "request target must start with '/'"),
31 Self::ContainsWhitespace => write!(f, "request target contains whitespace"),
32 Self::AbsoluteUri => write!(f, "absolute URI not supported"),
33 Self::AuthorityForm => write!(f, "authority-form URI not supported"),
34 Self::AsteriskForm => write!(f, "asterisk-form URI not supported"),
35 }
36 }
37}
38
39impl std::error::Error for RequestTargetError {}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct RequestTarget {
54 raw: String,
55 path: String,
56 query: Option<String>,
57}
58
59impl RequestTarget {
60 pub fn parse(raw: impl Into<String>) -> Result<Self, RequestTargetError> {
66 let raw = raw.into();
67 if raw.is_empty() {
68 return Err(RequestTargetError::Empty);
69 }
70 if raw == "*" {
71 return Err(RequestTargetError::AsteriskForm);
72 }
73 if raw.contains(char::is_whitespace) {
74 return Err(RequestTargetError::ContainsWhitespace);
75 }
76 if raw.starts_with('/') {
77 if raw.starts_with("//") {
78 return Err(RequestTargetError::AuthorityForm);
79 }
80 return Self::parse_origin_form(raw);
81 }
82 if raw.contains("://") {
83 return Err(RequestTargetError::AbsoluteUri);
84 }
85 if raw.contains('@') || raw.contains(':') {
86 return Err(RequestTargetError::AuthorityForm);
87 }
88 Err(RequestTargetError::NotOriginForm)
90 }
91
92 fn parse_origin_form(raw: String) -> Result<Self, RequestTargetError> {
93 debug_assert!(raw.starts_with('/'));
94 let (path, query) = match raw.find('?') {
95 Some(pos) => {
96 let path = raw[..pos].to_string();
97 let q = &raw[pos + 1..];
98 if q.is_empty() {
99 (path, None)
100 } else {
101 (path, Some(q.to_string()))
102 }
103 }
104 None => (raw.clone(), None),
105 };
106
107 Ok(Self { raw, path, query })
108 }
109
110 pub fn raw(&self) -> &str {
112 &self.raw
113 }
114
115 pub fn path(&self) -> &str {
117 &self.path
118 }
119
120 pub fn query(&self) -> Option<&str> {
122 self.query.as_deref()
123 }
124
125 pub fn path_and_query(&self) -> &str {
127 &self.raw
128 }
129}
130
131impl fmt::Display for RequestTarget {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 f.write_str(&self.raw)
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn root_path() {
143 let t = RequestTarget::parse("/").unwrap();
144 assert_eq!(t.raw(), "/");
145 assert_eq!(t.path(), "/");
146 assert!(t.query().is_none());
147 }
148
149 #[test]
150 fn network_path_reference_is_rejected() {
151 assert_eq!(
152 RequestTarget::parse("//example.com/file").unwrap_err(),
153 RequestTargetError::AuthorityForm
154 );
155 }
156
157 #[test]
158 fn path_with_query() {
159 let t = RequestTarget::parse("/foo?bar=baz").unwrap();
160 assert_eq!(t.path(), "/foo");
161 assert_eq!(t.query(), Some("bar=baz"));
162 }
163
164 #[test]
165 fn path_with_empty_query() {
166 let t = RequestTarget::parse("/foo?").unwrap();
167 assert_eq!(t.path(), "/foo");
168 assert!(t.query().is_none());
169 }
170
171 #[test]
172 fn path_with_multiple_query_params() {
173 let t = RequestTarget::parse("/a?b=1&c=2").unwrap();
174 assert_eq!(t.path(), "/a");
175 assert_eq!(t.query(), Some("b=1&c=2"));
176 }
177
178 #[test]
179 fn complex_path() {
180 let t = RequestTarget::parse("/foo/bar/file.txt?x=1&y=2").unwrap();
181 assert_eq!(t.path(), "/foo/bar/file.txt");
182 assert_eq!(t.query(), Some("x=1&y=2"));
183 }
184
185 #[test]
186 fn reject_empty() {
187 assert_eq!(
188 RequestTarget::parse("").unwrap_err(),
189 RequestTargetError::Empty
190 );
191 }
192
193 #[test]
194 fn reject_no_slash_prefix() {
195 assert_eq!(
196 RequestTarget::parse("foo").unwrap_err(),
197 RequestTargetError::NotOriginForm
198 );
199 }
200
201 #[test]
202 fn reject_absolute_uri() {
203 assert_eq!(
204 RequestTarget::parse("http://example.com/").unwrap_err(),
205 RequestTargetError::AbsoluteUri
206 );
207 }
208
209 #[test]
210 fn reject_authority_form() {
211 assert_eq!(
212 RequestTarget::parse("example.com:443").unwrap_err(),
213 RequestTargetError::AuthorityForm
214 );
215 }
216
217 #[test]
218 fn reject_asterisk_form() {
219 assert_eq!(
220 RequestTarget::parse("*").unwrap_err(),
221 RequestTargetError::AsteriskForm
222 );
223 }
224
225 #[test]
226 fn reject_whitespace() {
227 assert_eq!(
228 RequestTarget::parse("/foo bar").unwrap_err(),
229 RequestTargetError::ContainsWhitespace
230 );
231 assert_eq!(
232 RequestTarget::parse("/foo\tbar").unwrap_err(),
233 RequestTargetError::ContainsWhitespace
234 );
235 }
236
237 #[test]
238 fn path_and_query_combined() {
239 let t = RequestTarget::parse("/foo?bar").unwrap();
240 assert_eq!(t.path_and_query(), "/foo?bar");
241 }
242
243 #[test]
244 fn display() {
245 let t = RequestTarget::parse("/foo?bar").unwrap();
246 assert_eq!(format!("{t}"), "/foo?bar");
247 }
248
249 #[test]
250 fn error_display() {
251 assert!(!RequestTargetError::Empty.to_string().is_empty());
252 assert!(!RequestTargetError::NotOriginForm.to_string().is_empty());
253 assert!(!RequestTargetError::AbsoluteUri.to_string().is_empty());
254 assert!(!RequestTargetError::AuthorityForm.to_string().is_empty());
255 assert!(!RequestTargetError::AsteriskForm.to_string().is_empty());
256 assert!(!RequestTargetError::ContainsWhitespace
257 .to_string()
258 .is_empty());
259 }
260
261 #[test]
262 fn error_is_error() {
263 let err: &dyn std::error::Error = &RequestTargetError::Empty;
264 assert!(!err.to_string().is_empty());
265 }
266
267 #[test]
268 fn percent_encoded_path() {
269 let t = RequestTarget::parse("/foo%20bar").unwrap();
270 assert_eq!(t.raw(), "/foo%20bar");
271 assert_eq!(t.path(), "/foo%20bar");
272 assert!(t.query().is_none());
273 }
274
275 #[test]
276 fn percent_encoded_slash() {
277 let t = RequestTarget::parse("/foo%2Fbar").unwrap();
278 assert_eq!(t.raw(), "/foo%2Fbar");
279 assert_eq!(t.path(), "/foo%2Fbar");
280 assert!(t.query().is_none());
281 }
282
283 #[test]
284 fn dot_segment_paths() {
285 let t = RequestTarget::parse("/./foo").unwrap();
286 assert_eq!(t.path(), "/./foo");
287 assert!(t.query().is_none());
288 }
289
290 #[test]
291 fn dot_segment_parent() {
292 let t = RequestTarget::parse("/foo/../bar").unwrap();
293 assert_eq!(t.path(), "/foo/../bar");
294 assert!(t.query().is_none());
295 }
296
297 #[test]
298 fn backslash_in_path() {
299 let t = RequestTarget::parse("/foo\\bar").unwrap();
300 assert_eq!(t.path(), "/foo\\bar");
301 assert!(t.query().is_none());
302 }
303
304 #[test]
305 fn non_ascii_bytes() {
306 let t = RequestTarget::parse("/foo\u{00E9}\u{00FF}").unwrap();
307 assert_eq!(t.raw(), "/foo\u{00E9}\u{00FF}");
308 assert_eq!(t.path(), "/foo\u{00E9}\u{00FF}");
309 assert!(t.query().is_none());
310 }
311
312 #[test]
313 fn origin_form_only_enforced() {
314 assert_eq!(
315 RequestTarget::parse("foo").unwrap_err(),
316 RequestTargetError::NotOriginForm
317 );
318 }
319
320 #[test]
321 fn query_with_equals() {
322 let t = RequestTarget::parse("/path?key=val=ue").unwrap();
323 assert_eq!(t.path(), "/path");
324 assert_eq!(t.query(), Some("key=val=ue"));
325 }
326
327 #[test]
328 fn query_with_encoded_chars() {
329 let t = RequestTarget::parse("/path?key=hello%20world").unwrap();
330 assert_eq!(t.path(), "/path");
331 assert_eq!(t.query(), Some("key=hello%20world"));
332 }
333
334 #[test]
335 fn multiple_question_marks() {
336 let t = RequestTarget::parse("/path?a=1?b=2").unwrap();
337 assert_eq!(t.path(), "/path");
338 assert_eq!(t.query(), Some("a=1?b=2"));
339 }
340}