1use winnow::{
5 combinator::{alt, delimited, fail, opt, peek, preceded, repeat},
6 error::ContextError,
7 prelude::*,
8 stream::{AsChar, Compare, Stream, StreamIsPartial},
9 token::{literal, one_of, take_while},
10};
11
12mod error;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum RequestTarget<'a> {
19 Origin(RequestTargetOrigin<'a>),
21
22 Absolute(RequestTargetAbsolute<'a>),
27
28 Authority(RequestTargetAuthority<'a>),
33
34 Asterisk,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct RequestTargetOrigin<'a> {
46 pub inner: &'a [u8],
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct RequestTargetAbsolute<'a> {
53 pub inner: &'a [u8],
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct RequestTargetAuthority<'a> {
60 pub inner: &'a [u8],
62}
63
64impl<'a> RequestTarget<'a> {
65 pub fn try_from_slice(
67 input: &'a [u8],
68 ) -> Result<Self, winnow::error::ParseError<&'a [u8], ContextError>> {
69 alt((
73 parse_asterisk.value(RequestTarget::Asterisk),
74 parse_origin_form
75 .take()
76 .map(|inner| RequestTarget::Origin(RequestTargetOrigin { inner })),
77 parse_authority_form
78 .take()
79 .map(|inner| RequestTarget::Authority(RequestTargetAuthority { inner })),
80 parse_absolute_form
81 .take()
82 .map(|inner| RequestTarget::Absolute(RequestTargetAbsolute { inner })),
83 fail,
84 ))
85 .parse(input)
86 }
87}
88
89fn parse_origin_form<I>(input: &mut I) -> ModalResult<()>
108where
109 I: Stream + StreamIsPartial + Compare<u8>,
110 I::Token: Clone + AsChar,
111{
112 (parse_path, opt((b'?', parse_query)))
113 .void()
114 .parse_next(input)
115}
116
117fn parse_path<I>(input: &mut I) -> ModalResult<()>
126where
127 I: Stream + StreamIsPartial + Compare<u8>,
128 I::Token: Clone + AsChar,
129{
130 peek(b'/').parse_next(input)?;
131
132 repeat(1.., (b'/', take_while(.., is_pchar)))
134 .map(|()| ())
135 .void()
136 .parse_next(input)
137}
138
139fn parse_query<I>(input: &mut I) -> ModalResult<()>
148where
149 I: Stream + StreamIsPartial,
150 I::Token: Clone + AsChar,
151{
152 repeat(
153 ..,
154 one_of((
155 is_pchar,
156 [b'/', b'?'],
158 )),
159 )
160 .map(|()| ())
161 .void()
162 .parse_next(input)
163}
164
165fn is_unreserved(char: char) -> bool {
169 matches!(char, '0'..='9' | 'A'..='Z' | 'a'..='z' | '-' | '.' | '_' | '~')
170}
171
172fn is_sub_delim(char: char) -> bool {
176 matches!(
177 char,
178 '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '='
179 )
180}
181
182fn is_pchar(char: impl AsChar) -> bool {
186 let char = char.as_char();
187
188 is_unreserved(char)
189 || is_sub_delim(char)
190 || matches!(char, '%') || matches!(char, ':' | '@')
194}
195
196fn is_reg_name_char(char: impl AsChar) -> bool {
200 let char = char.as_char();
201
202 is_unreserved(char)
203 || is_sub_delim(char)
204 || matches!(char, '%') }
207
208fn is_userinfo_char(char: impl AsChar) -> bool {
212 let char = char.as_char();
213
214 is_unreserved(char)
215 || is_sub_delim(char)
216 || matches!(char, '%') || matches!(char, ':')
219}
220
221fn is_ip_literal_char(char: impl AsChar) -> bool {
226 let char = char.as_char();
227
228 is_unreserved(char)
229 || is_sub_delim(char)
230 || matches!(char, ':')
232}
233
234fn is_ip_literal_body(bytes: &[u8]) -> bool {
236 bytes.contains(&b':') || matches!(bytes.first(), Some(b'v' | b'V')) && bytes.contains(&b'.')
237}
238
239fn parse_authority_form<I>(input: &mut I) -> ModalResult<()>
245where
246 I: Stream + StreamIsPartial + Compare<u8>,
247 I::Token: Clone + AsChar,
248 I::Slice: AsRef<[u8]>,
249{
250 (
251 parse_uri_host,
252 b':',
253 take_while(1.., |char: I::Token| matches!(char.as_char(), '0'..='9')),
254 )
255 .void()
256 .parse_next(input)
257}
258
259fn parse_absolute_form<I>(input: &mut I) -> ModalResult<()>
277where
278 I: Stream + StreamIsPartial + Compare<u8>,
279 I::Token: Clone + AsChar,
280 I::Slice: AsRef<[u8]>,
281{
282 (
283 parse_scheme,
284 b':',
285 (b'/', b'/'),
286 parse_authority,
287 parse_path_abempty,
288 opt((b'?', parse_query)),
289 )
290 .void()
291 .parse_next(input)
292}
293
294fn parse_asterisk<I>(input: &mut I) -> ModalResult<()>
300where
301 I: Stream + StreamIsPartial + Compare<u8>,
302{
303 literal(b'*').void().parse_next(input)
304}
305
306fn parse_uri_host<I>(input: &mut I) -> ModalResult<()>
311where
312 I: Stream + StreamIsPartial + Compare<u8>,
313 I::Token: Clone + AsChar,
314 I::Slice: AsRef<[u8]>,
315{
316 alt((
317 parse_ip_literal,
318 parse_reg_name,
320 ))
321 .void()
322 .parse_next(input)
323}
324
325fn parse_ip_literal<I>(input: &mut I) -> ModalResult<()>
330where
331 I: Stream + StreamIsPartial + Compare<u8>,
332 I::Token: Clone + AsChar,
333 I::Slice: AsRef<[u8]>,
334{
335 delimited(
336 b'[',
337 take_while(1.., is_ip_literal_char)
338 .verify(|slice: &I::Slice| is_ip_literal_body(slice.as_ref())),
339 b']',
340 )
341 .void()
342 .parse_next(input)
343}
344
345fn parse_reg_name<I>(input: &mut I) -> ModalResult<()>
350where
351 I: Stream + StreamIsPartial,
352 I::Token: Clone + AsChar,
353{
354 take_while(1.., is_reg_name_char).void().parse_next(input)
355}
356
357fn is_scheme_start(char: impl AsChar) -> bool {
361 matches!(char.as_char(), 'A'..='Z' | 'a'..='z')
362}
363
364fn is_scheme_char(char: impl AsChar) -> bool {
368 matches!(char.as_char(), '0'..='9' | 'A'..='Z' | 'a'..='z' | '+' | '-' | '.')
369}
370
371fn parse_scheme<I>(input: &mut I) -> ModalResult<()>
375where
376 I: Stream + StreamIsPartial,
377 I::Token: Clone + AsChar,
378{
379 preceded(one_of(is_scheme_start), take_while(.., is_scheme_char))
380 .void()
381 .parse_next(input)
382}
383
384fn parse_authority<I>(input: &mut I) -> ModalResult<()>
388where
389 I: Stream + StreamIsPartial + Compare<u8>,
390 I::Token: Clone + AsChar,
391 I::Slice: AsRef<[u8]>,
392{
393 (
394 opt((take_while(1.., is_userinfo_char), b'@')),
395 parse_uri_host,
396 opt((
397 b':',
398 take_while(1.., |char: I::Token| matches!(char.as_char(), '0'..='9')),
399 )),
400 )
401 .void()
402 .parse_next(input)
403}
404
405fn parse_path_abempty<I>(input: &mut I) -> ModalResult<()>
409where
410 I: Stream + StreamIsPartial + Compare<u8>,
411 I::Token: Clone + AsChar,
412{
413 repeat(.., (b'/', take_while(.., is_pchar)))
414 .map(|()| ())
415 .void()
416 .parse_next(input)
417}
418
419#[cfg(test)]
420mod tests {
421 use winnow::{
422 BStr, Partial,
423 error::{ErrMode, Needed},
424 };
425
426 use super::*;
427
428 macro_rules! assert_backtrack {
429 ($parser:expr, $input:expr $(,)?) => {
430 assert!(
431 matches!(
432 $parser.parse_peek(BStr::new($input)),
433 Err(ErrMode::Backtrack(_))
434 ),
435 "assertion failed: parser did not backtrack for input {:?}: {:?}",
436 $input,
437 $parser.parse_peek(BStr::new($input)),
438 );
439 };
440 }
441
442 macro_rules! assert_ok_remaining {
443 ($parser:expr, $input:expr, $remaining:expr $(,)?) => {
444 assert_eq!(
445 $parser.parse_peek(BStr::new($input)),
446 Ok((BStr::new($remaining), ())),
447 );
448 };
449 }
450
451 macro_rules! assert_partial_incomplete {
452 ($parser:expr, $input:expr, $needed:expr $(,)?) => {
453 assert_eq!(
454 $parser.parse_peek(Partial::new(BStr::new($input))),
455 Err(ErrMode::Incomplete($needed)),
456 );
457 };
458 }
459
460 #[test]
461 fn validates_char_groups() {
462 assert!(!is_unreserved('/'));
463 assert!(is_unreserved('a'));
464 assert!(is_unreserved('Z'));
465 assert!(is_unreserved('0'));
466 assert!(is_unreserved('~'));
467
468 assert!(!is_sub_delim(':'));
469 assert!(is_sub_delim('!'));
470 assert!(is_sub_delim('='));
471
472 assert!(!is_pchar(b'/'));
473 assert!(is_pchar(b'='));
474 assert!(is_pchar(b'%'));
475 assert!(is_pchar(b':'));
476 assert!(is_pchar(b'@'));
477
478 assert!(!is_reg_name_char(b':'));
479 assert!(!is_reg_name_char(b'@'));
480 assert!(is_reg_name_char(b'%'));
481 assert!(is_reg_name_char(b'.'));
482
483 assert!(!is_ip_literal_char(b'['));
484 assert!(!is_ip_literal_char(b'/'));
485 assert!(is_ip_literal_char(b':'));
486 assert!(is_ip_literal_char(b'v'));
487 assert!(is_ip_literal_char(b'.'));
488 }
489
490 #[test]
491 fn validates_ip_literal_bodies() {
492 assert!(!is_ip_literal_body(b""));
493 assert!(!is_ip_literal_body(b"localhost"));
494 assert!(!is_ip_literal_body(b"v1"));
495 assert!(!is_ip_literal_body(b"v1/"));
496
497 assert!(is_ip_literal_body(b"::1"));
498 assert!(is_ip_literal_body(b"2001:db8::1"));
499 assert!(is_ip_literal_body(b"v1.future-host"));
500 assert!(is_ip_literal_body(b"Vf.token:more"));
501 }
502
503 #[test]
504 fn parses_reg_name() {
505 assert_backtrack!(parse_reg_name, b"");
506 assert_backtrack!(parse_reg_name, b"@localhost");
507
508 assert_ok_remaining!(parse_reg_name, b"localhost", b"");
509 assert_ok_remaining!(parse_reg_name, b"example.com:80", b":80");
510 assert_ok_remaining!(parse_reg_name, b"xn--hllo-bpa.example", b"");
511 }
512
513 #[test]
514 fn parses_ip_literal() {
515 assert_backtrack!(parse_ip_literal, b"");
516 assert_backtrack!(parse_ip_literal, b"[localhost]");
517 assert_backtrack!(parse_ip_literal, b"[::1");
518
519 assert_ok_remaining!(parse_ip_literal, b"[::1]", b"");
520 assert_ok_remaining!(parse_ip_literal, b"[2001:db8::1]:443", b":443");
521 assert_ok_remaining!(parse_ip_literal, b"[v1.future-host]", b"");
522 }
523
524 #[test]
525 fn parses_uri_host() {
526 assert_backtrack!(parse_uri_host, b"");
527 assert_backtrack!(parse_uri_host, b"@localhost:80");
528
529 assert_ok_remaining!(parse_uri_host, b"localhost:80", b":80");
530 assert_ok_remaining!(parse_uri_host, b"127.0.0.1:80", b":80");
531 assert_ok_remaining!(parse_uri_host, b"[::1]:80", b":80");
532 }
533
534 #[test]
535 fn parses_scheme() {
536 assert_backtrack!(parse_scheme, b"");
537 assert_backtrack!(parse_scheme, b"1http");
538 assert_partial_incomplete!(parse_scheme, b"", Needed::new(1));
539
540 assert_ok_remaining!(parse_scheme, b"http", b"");
541 assert_ok_remaining!(parse_scheme, b"https:", b":");
542 assert_ok_remaining!(parse_scheme, b"http+unix:", b":");
543 }
544
545 #[test]
546 fn parses_authority() {
547 assert_backtrack!(parse_authority, b"");
548 assert_backtrack!(parse_authority, b"@localhost");
549 assert_partial_incomplete!(parse_authority, b"", Needed::new(1));
550
551 assert_ok_remaining!(parse_authority, b"127.0.0.1:80", b"");
552 assert_ok_remaining!(parse_authority, b"user:pass@localhost:3000", b"");
553 assert_ok_remaining!(parse_authority, b"[::1]/path", b"/path");
554 }
555
556 #[test]
557 fn parses_path_abempty() {
558 assert_ok_remaining!(parse_path_abempty, b"", b"");
559 assert_ok_remaining!(parse_path_abempty, b"/", b"");
560 assert_ok_remaining!(parse_path_abempty, b"/foo/bar", b"");
561 assert_ok_remaining!(parse_path_abempty, b"?foo=bar", b"?foo=bar");
562 }
563
564 #[test]
565 fn parses_path() {
566 assert_backtrack!(parse_path, b"");
567 assert_partial_incomplete!(parse_path, b"", Needed::Unknown);
568 assert_backtrack!(parse_path, b"=");
569
570 assert_ok_remaining!(parse_path, b"/foo", b"");
571 assert_ok_remaining!(parse_path, b"/foo/bar", b"");
572
573 assert_ok_remaining!(parse_path, b"/foo/bar?baz", b"?baz");
575 }
576
577 #[test]
578 fn parses_query() {
579 assert_ok_remaining!(parse_query, b"", b"");
580 assert_ok_remaining!(parse_query, b"=", b"");
581 assert_ok_remaining!(parse_query, b"foo=bar", b"");
582 assert_ok_remaining!(parse_query, b"foo=bar&baz", b"");
583 }
584
585 #[test]
586 fn parses_authority_form() {
587 assert_backtrack!(parse_authority_form, b"");
588 assert_partial_incomplete!(parse_authority_form, b"", Needed::Unknown);
589
590 assert_backtrack!(parse_authority_form, b"localhost");
591 assert_backtrack!(parse_authority_form, b"user@localhost:3000");
592 assert_backtrack!(parse_authority_form, b"[::1]");
593
594 assert_ok_remaining!(parse_authority_form, b"localhost:3000", b"");
595 assert_ok_remaining!(parse_authority_form, b"127.0.0.1:80", b"");
596 assert_ok_remaining!(parse_authority_form, b"[::1]:443", b"");
597 }
598
599 #[test]
600 fn parses_absolute_form() {
601 assert_backtrack!(parse_absolute_form, b"");
602 assert_backtrack!(parse_absolute_form, b"/foo");
603 assert_backtrack!(parse_absolute_form, b"htt:p//host");
604 assert_partial_incomplete!(parse_absolute_form, b"", Needed::new(1));
605
606 assert_ok_remaining!(parse_absolute_form, b"http://127.0.0.1:61761/chunks", b"");
607 assert_ok_remaining!(parse_absolute_form, b"https://127.0.0.1:61761", b"");
608 assert_ok_remaining!(parse_absolute_form, b"http://127.0.0.1?foo=bar", b"");
609 assert_ok_remaining!(parse_absolute_form, b"git+http://example.com/repo", b"");
610 }
611
612 #[test]
613 fn parses_asterisk() {
614 assert_backtrack!(parse_asterisk, b"");
615 assert_partial_incomplete!(parse_asterisk, b"", Needed::Unknown);
616
617 assert_ok_remaining!(parse_asterisk, b"*", b"");
618 assert_ok_remaining!(parse_asterisk, b"**", b"*");
619 }
620}