use winnow::{
combinator::{alt, delimited, fail, opt, peek, preceded, repeat},
error::ContextError,
prelude::*,
stream::{AsChar, Compare, Stream, StreamIsPartial},
token::{literal, one_of, take_while},
};
mod error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequestTarget<'a> {
Origin(RequestTargetOrigin<'a>),
Absolute(RequestTargetAbsolute<'a>),
Authority(RequestTargetAuthority<'a>),
Asterisk,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestTargetOrigin<'a> {
pub inner: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestTargetAbsolute<'a> {
pub inner: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestTargetAuthority<'a> {
pub inner: &'a [u8],
}
impl<'a> RequestTarget<'a> {
pub fn try_from_slice(
input: &'a [u8],
) -> Result<Self, winnow::error::ParseError<&'a [u8], ContextError>> {
alt((
parse_asterisk.value(RequestTarget::Asterisk),
parse_origin_form
.take()
.map(|inner| RequestTarget::Origin(RequestTargetOrigin { inner })),
parse_authority_form
.take()
.map(|inner| RequestTarget::Authority(RequestTargetAuthority { inner })),
parse_absolute_form
.take()
.map(|inner| RequestTarget::Absolute(RequestTargetAbsolute { inner })),
fail,
))
.parse(input)
}
}
fn parse_origin_form<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
(parse_path, opt((b'?', parse_query)))
.void()
.parse_next(input)
}
fn parse_path<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
peek(b'/').parse_next(input)?;
repeat(1.., (b'/', take_while(.., is_pchar)))
.map(|()| ())
.void()
.parse_next(input)
}
fn parse_query<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
I::Token: Clone + AsChar,
{
repeat(
..,
one_of((
is_pchar,
[b'/', b'?'],
)),
)
.map(|()| ())
.void()
.parse_next(input)
}
fn is_unreserved(char: char) -> bool {
matches!(char, '0'..='9' | 'A'..='Z' | 'a'..='z' | '-' | '.' | '_' | '~')
}
fn is_sub_delim(char: char) -> bool {
matches!(
char,
'!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '='
)
}
fn is_pchar(char: impl AsChar) -> bool {
let char = char.as_char();
is_unreserved(char)
|| is_sub_delim(char)
|| matches!(char, '%') || matches!(char, ':' | '@')
}
fn is_reg_name_char(char: impl AsChar) -> bool {
let char = char.as_char();
is_unreserved(char)
|| is_sub_delim(char)
|| matches!(char, '%') }
fn is_userinfo_char(char: impl AsChar) -> bool {
let char = char.as_char();
is_unreserved(char)
|| is_sub_delim(char)
|| matches!(char, '%') || matches!(char, ':')
}
fn is_ip_literal_char(char: impl AsChar) -> bool {
let char = char.as_char();
is_unreserved(char)
|| is_sub_delim(char)
|| matches!(char, ':')
}
fn is_ip_literal_body(bytes: &[u8]) -> bool {
bytes.contains(&b':') || matches!(bytes.first(), Some(b'v' | b'V')) && bytes.contains(&b'.')
}
fn parse_authority_form<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
(
parse_uri_host,
b':',
take_while(1.., |char: I::Token| matches!(char.as_char(), '0'..='9')),
)
.void()
.parse_next(input)
}
fn parse_absolute_form<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
(
parse_scheme,
b':',
(b'/', b'/'),
parse_authority,
parse_path_abempty,
opt((b'?', parse_query)),
)
.void()
.parse_next(input)
}
fn parse_asterisk<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
{
literal(b'*').void().parse_next(input)
}
fn parse_uri_host<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
alt((
parse_ip_literal,
parse_reg_name,
))
.void()
.parse_next(input)
}
fn parse_ip_literal<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
delimited(
b'[',
take_while(1.., is_ip_literal_char)
.verify(|slice: &I::Slice| is_ip_literal_body(slice.as_ref())),
b']',
)
.void()
.parse_next(input)
}
fn parse_reg_name<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
I::Token: Clone + AsChar,
{
take_while(1.., is_reg_name_char).void().parse_next(input)
}
fn is_scheme_start(char: impl AsChar) -> bool {
matches!(char.as_char(), 'A'..='Z' | 'a'..='z')
}
fn is_scheme_char(char: impl AsChar) -> bool {
matches!(char.as_char(), '0'..='9' | 'A'..='Z' | 'a'..='z' | '+' | '-' | '.')
}
fn parse_scheme<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
I::Token: Clone + AsChar,
{
preceded(one_of(is_scheme_start), take_while(.., is_scheme_char))
.void()
.parse_next(input)
}
fn parse_authority<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
(
opt((take_while(1.., is_userinfo_char), b'@')),
parse_uri_host,
opt((
b':',
take_while(1.., |char: I::Token| matches!(char.as_char(), '0'..='9')),
)),
)
.void()
.parse_next(input)
}
fn parse_path_abempty<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
repeat(.., (b'/', take_while(.., is_pchar)))
.map(|()| ())
.void()
.parse_next(input)
}
#[cfg(test)]
mod tests {
use winnow::{
BStr, Partial,
error::{ErrMode, Needed},
};
use super::*;
macro_rules! assert_backtrack {
($parser:expr, $input:expr $(,)?) => {
assert!(
matches!(
$parser.parse_peek(BStr::new($input)),
Err(ErrMode::Backtrack(_))
),
"assertion failed: parser did not backtrack for input {:?}: {:?}",
$input,
$parser.parse_peek(BStr::new($input)),
);
};
}
macro_rules! assert_ok_remaining {
($parser:expr, $input:expr, $remaining:expr $(,)?) => {
assert_eq!(
$parser.parse_peek(BStr::new($input)),
Ok((BStr::new($remaining), ())),
);
};
}
macro_rules! assert_partial_incomplete {
($parser:expr, $input:expr, $needed:expr $(,)?) => {
assert_eq!(
$parser.parse_peek(Partial::new(BStr::new($input))),
Err(ErrMode::Incomplete($needed)),
);
};
}
#[test]
fn validates_char_groups() {
assert!(!is_unreserved('/'));
assert!(is_unreserved('a'));
assert!(is_unreserved('Z'));
assert!(is_unreserved('0'));
assert!(is_unreserved('~'));
assert!(!is_sub_delim(':'));
assert!(is_sub_delim('!'));
assert!(is_sub_delim('='));
assert!(!is_pchar(b'/'));
assert!(is_pchar(b'='));
assert!(is_pchar(b'%'));
assert!(is_pchar(b':'));
assert!(is_pchar(b'@'));
assert!(!is_reg_name_char(b':'));
assert!(!is_reg_name_char(b'@'));
assert!(is_reg_name_char(b'%'));
assert!(is_reg_name_char(b'.'));
assert!(!is_ip_literal_char(b'['));
assert!(!is_ip_literal_char(b'/'));
assert!(is_ip_literal_char(b':'));
assert!(is_ip_literal_char(b'v'));
assert!(is_ip_literal_char(b'.'));
}
#[test]
fn validates_ip_literal_bodies() {
assert!(!is_ip_literal_body(b""));
assert!(!is_ip_literal_body(b"localhost"));
assert!(!is_ip_literal_body(b"v1"));
assert!(!is_ip_literal_body(b"v1/"));
assert!(is_ip_literal_body(b"::1"));
assert!(is_ip_literal_body(b"2001:db8::1"));
assert!(is_ip_literal_body(b"v1.future-host"));
assert!(is_ip_literal_body(b"Vf.token:more"));
}
#[test]
fn parses_reg_name() {
assert_backtrack!(parse_reg_name, b"");
assert_backtrack!(parse_reg_name, b"@localhost");
assert_ok_remaining!(parse_reg_name, b"localhost", b"");
assert_ok_remaining!(parse_reg_name, b"example.com:80", b":80");
assert_ok_remaining!(parse_reg_name, b"xn--hllo-bpa.example", b"");
}
#[test]
fn parses_ip_literal() {
assert_backtrack!(parse_ip_literal, b"");
assert_backtrack!(parse_ip_literal, b"[localhost]");
assert_backtrack!(parse_ip_literal, b"[::1");
assert_ok_remaining!(parse_ip_literal, b"[::1]", b"");
assert_ok_remaining!(parse_ip_literal, b"[2001:db8::1]:443", b":443");
assert_ok_remaining!(parse_ip_literal, b"[v1.future-host]", b"");
}
#[test]
fn parses_uri_host() {
assert_backtrack!(parse_uri_host, b"");
assert_backtrack!(parse_uri_host, b"@localhost:80");
assert_ok_remaining!(parse_uri_host, b"localhost:80", b":80");
assert_ok_remaining!(parse_uri_host, b"127.0.0.1:80", b":80");
assert_ok_remaining!(parse_uri_host, b"[::1]:80", b":80");
}
#[test]
fn parses_scheme() {
assert_backtrack!(parse_scheme, b"");
assert_backtrack!(parse_scheme, b"1http");
assert_partial_incomplete!(parse_scheme, b"", Needed::new(1));
assert_ok_remaining!(parse_scheme, b"http", b"");
assert_ok_remaining!(parse_scheme, b"https:", b":");
assert_ok_remaining!(parse_scheme, b"http+unix:", b":");
}
#[test]
fn parses_authority() {
assert_backtrack!(parse_authority, b"");
assert_backtrack!(parse_authority, b"@localhost");
assert_partial_incomplete!(parse_authority, b"", Needed::new(1));
assert_ok_remaining!(parse_authority, b"127.0.0.1:80", b"");
assert_ok_remaining!(parse_authority, b"user:pass@localhost:3000", b"");
assert_ok_remaining!(parse_authority, b"[::1]/path", b"/path");
}
#[test]
fn parses_path_abempty() {
assert_ok_remaining!(parse_path_abempty, b"", b"");
assert_ok_remaining!(parse_path_abempty, b"/", b"");
assert_ok_remaining!(parse_path_abempty, b"/foo/bar", b"");
assert_ok_remaining!(parse_path_abempty, b"?foo=bar", b"?foo=bar");
}
#[test]
fn parses_path() {
assert_backtrack!(parse_path, b"");
assert_partial_incomplete!(parse_path, b"", Needed::Unknown);
assert_backtrack!(parse_path, b"=");
assert_ok_remaining!(parse_path, b"/foo", b"");
assert_ok_remaining!(parse_path, b"/foo/bar", b"");
assert_ok_remaining!(parse_path, b"/foo/bar?baz", b"?baz");
}
#[test]
fn parses_query() {
assert_ok_remaining!(parse_query, b"", b"");
assert_ok_remaining!(parse_query, b"=", b"");
assert_ok_remaining!(parse_query, b"foo=bar", b"");
assert_ok_remaining!(parse_query, b"foo=bar&baz", b"");
}
#[test]
fn parses_authority_form() {
assert_backtrack!(parse_authority_form, b"");
assert_partial_incomplete!(parse_authority_form, b"", Needed::Unknown);
assert_backtrack!(parse_authority_form, b"localhost");
assert_backtrack!(parse_authority_form, b"user@localhost:3000");
assert_backtrack!(parse_authority_form, b"[::1]");
assert_ok_remaining!(parse_authority_form, b"localhost:3000", b"");
assert_ok_remaining!(parse_authority_form, b"127.0.0.1:80", b"");
assert_ok_remaining!(parse_authority_form, b"[::1]:443", b"");
}
#[test]
fn parses_absolute_form() {
assert_backtrack!(parse_absolute_form, b"");
assert_backtrack!(parse_absolute_form, b"/foo");
assert_backtrack!(parse_absolute_form, b"htt:p//host");
assert_partial_incomplete!(parse_absolute_form, b"", Needed::new(1));
assert_ok_remaining!(parse_absolute_form, b"http://127.0.0.1:61761/chunks", b"");
assert_ok_remaining!(parse_absolute_form, b"https://127.0.0.1:61761", b"");
assert_ok_remaining!(parse_absolute_form, b"http://127.0.0.1?foo=bar", b"");
assert_ok_remaining!(parse_absolute_form, b"git+http://example.com/repo", b"");
}
#[test]
fn parses_asterisk() {
assert_backtrack!(parse_asterisk, b"");
assert_partial_incomplete!(parse_asterisk, b"", Needed::Unknown);
assert_ok_remaining!(parse_asterisk, b"*", b"");
assert_ok_remaining!(parse_asterisk, b"**", b"*");
}
}