#![cfg_attr(docsrs, feature(doc_cfg))]
use core::{
net::{Ipv4Addr, Ipv6Addr},
str,
};
use winnow::{
combinator::{alt, opt, preceded, repeat, terminated},
prelude::*,
stream::{AsChar, Compare, Stream, StreamIsPartial},
token::{literal, one_of, take_while},
};
#[inline]
pub fn parse_path<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
parse_path_absolute.parse_next(input)
}
#[inline]
pub fn parse_query<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
repeat::<_, _, (), _, _>(.., parse_query_or_fragment_item)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_fragment<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
parse_query.parse_next(input)
}
#[inline]
pub fn is_unreserved(char: impl AsChar) -> bool {
matches!(
char.as_char(),
'0'..='9' | 'A'..='Z' | 'a'..='z' | '-' | '.' | '_' | '~'
)
}
#[inline]
pub fn is_sub_delim(char: impl AsChar) -> bool {
matches!(
char.as_char(),
'!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '='
)
}
#[inline]
pub fn is_gen_delim(char: impl AsChar) -> bool {
matches!(char.as_char(), ':' | '/' | '?' | '#' | '[' | ']' | '@')
}
#[inline]
pub fn is_reserved(char: impl AsChar) -> bool {
let char = char.as_char();
is_gen_delim(char) || is_sub_delim(char)
}
#[inline]
pub fn is_hexdig(char: impl AsChar) -> bool {
char.as_char().is_ascii_hexdigit()
}
#[inline]
pub fn is_pchar(char: impl AsChar) -> bool {
let char = char.as_char();
is_unreserved(char) || is_sub_delim(char) || matches!(char, '%' | ':' | '@')
}
#[inline]
pub fn is_pchar_nc(char: impl AsChar) -> bool {
let char = char.as_char();
is_unreserved(char) || is_sub_delim(char) || matches!(char, '%' | '@')
}
#[inline]
pub fn is_reg_name_char(char: impl AsChar) -> bool {
let char = char.as_char();
is_unreserved(char) || is_sub_delim(char) || matches!(char, '%')
}
#[inline]
pub fn is_userinfo_char(char: impl AsChar) -> bool {
let char = char.as_char();
is_unreserved(char) || is_sub_delim(char) || matches!(char, '%' | ':')
}
#[inline]
pub fn is_ipvfuture_char(char: impl AsChar) -> bool {
let char = char.as_char();
is_unreserved(char) || is_sub_delim(char) || matches!(char, ':')
}
#[inline]
pub fn is_ip_literal_char(char: impl AsChar) -> bool {
let char = char.as_char();
char.is_ascii_hexdigit() || is_ipvfuture_char(char) || matches!(char, '.')
}
#[inline]
pub fn is_ip_literal_body(bytes: &[u8]) -> bool {
parse_ipv6address
.parse_peek(bytes)
.is_ok_and(|(rest, ())| rest.is_empty())
|| parse_ipvfuture
.parse_peek(bytes)
.is_ok_and(|(rest, ())| rest.is_empty())
}
#[inline]
pub fn parse_pct_encoded<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
(b'%', take_while(2..=2, is_hexdig))
.void()
.parse_next(input)
}
#[inline]
pub fn parse_userinfo<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
repeat::<_, _, (), _, _>(.., parse_userinfo_item)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_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_ipv4address, parse_reg_name))
.void()
.parse_next(input)
}
#[inline]
pub 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_ipv4address, parse_reg_name_nz))
.void()
.parse_next(input)
}
#[inline]
pub fn parse_ip_literal<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
(b'[', alt((parse_ipv6address, parse_ipvfuture)), b']')
.void()
.parse_next(input)
}
#[inline]
pub fn parse_ipvfuture<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
(
one_of([b'v', b'V']),
take_while(1.., is_hexdig),
b'.',
repeat::<_, _, (), _, _>(1.., parse_ipvfuture_item),
)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_ipv6address<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
take_while(1.., is_ipv6address_char)
.verify(|slice: &I::Slice| parse_ascii::<Ipv6Addr>(slice.as_ref()).is_some())
.void()
.parse_next(input)
}
#[inline]
pub fn parse_ipv4address<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
take_while(1.., is_ipv4address_char)
.verify(|slice: &I::Slice| parse_ascii::<Ipv4Addr>(slice.as_ref()).is_some())
.void()
.parse_next(input)
}
#[inline]
pub fn parse_dec_octet<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
take_while(1..=3, |char: I::Token| char.as_char().is_ascii_digit())
.verify(|slice: &I::Slice| is_dec_octet(slice.as_ref()))
.void()
.parse_next(input)
}
#[inline]
pub fn parse_reg_name<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
repeat::<_, _, (), _, _>(.., parse_reg_name_item)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_port<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
I::Token: Clone + AsChar,
{
take_while(.., |char: I::Token| char.as_char().is_ascii_digit())
.void()
.parse_next(input)
}
#[inline]
pub fn is_scheme_start(char: impl AsChar) -> bool {
char.is_alpha()
}
#[inline]
pub fn is_scheme_char(char: impl AsChar) -> bool {
let char = char.as_char();
char.is_ascii_alphanumeric() || matches!(char, '+' | '-' | '.')
}
#[inline]
pub 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)
}
#[inline]
pub fn parse_authority<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
parse_authority_parts.void().parse_next(input)
}
#[inline]
pub fn parse_authority_parts<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
(
opt(terminated(parse_userinfo_nz, b'@')),
parse_uri_host,
opt((b':', parse_port)),
)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_path_abempty<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
repeat::<_, _, (), _, _>(.., (b'/', parse_segment))
.void()
.parse_next(input)
}
#[inline]
pub fn parse_path_absolute<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
(
b'/',
opt((
parse_segment_nz,
repeat::<_, _, (), _, _>(.., (b'/', parse_segment)),
)),
)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_path_noscheme<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
(
parse_segment_nz_nc,
repeat::<_, _, (), _, _>(.., (b'/', parse_segment)),
)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_path_rootless<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
(
parse_segment_nz,
repeat::<_, _, (), _, _>(.., (b'/', parse_segment)),
)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_path_empty<I>(_input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
{
Ok(())
}
#[inline]
pub fn parse_segment<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
repeat::<_, _, (), _, _>(.., parse_pchar_item)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_segment_nz<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
repeat::<_, _, (), _, _>(1.., parse_pchar_item)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_segment_nz_nc<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
repeat::<_, _, (), _, _>(1.., parse_pchar_nc_item)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_hier_part<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
alt((
((b'/', b'/'), parse_generic_authority, parse_path_abempty).void(),
parse_path_absolute,
parse_path_rootless,
parse_path_empty,
))
.parse_next(input)
}
#[inline]
pub fn parse_relative_part<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
alt((
((b'/', b'/'), parse_generic_authority, parse_path_abempty).void(),
parse_path_absolute,
parse_path_noscheme,
parse_path_empty,
))
.parse_next(input)
}
#[inline]
pub fn parse_absolute_uri<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
(
parse_scheme,
b':',
parse_hier_part,
opt((b'?', parse_query)),
)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_relative_ref<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
(
parse_relative_part,
opt((b'?', parse_query)),
opt((b'#', parse_fragment)),
)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_uri<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
(
parse_scheme,
b':',
parse_hier_part,
opt((b'?', parse_query)),
opt((b'#', parse_fragment)),
)
.void()
.parse_next(input)
}
#[inline]
pub fn parse_uri_reference<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
alt((parse_uri, parse_relative_ref)).parse_next(input)
}
#[inline]
pub fn parse_h16<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
I::Token: Clone + AsChar,
{
take_while(1..=4, is_hexdig).void().parse_next(input)
}
#[inline]
pub fn parse_ls32<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
alt((parse_ipv4address, (parse_h16, b':', parse_h16).void())).parse_next(input)
}
#[inline]
fn parse_unreserved_item<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
I::Token: Clone + AsChar,
{
take_while(1..=1, is_unreserved).void().parse_next(input)
}
#[inline]
fn parse_sub_delim_item<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
I::Token: Clone + AsChar,
{
take_while(1..=1, is_sub_delim).void().parse_next(input)
}
#[inline]
fn parse_reg_name_item<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
alt((
parse_pct_encoded,
parse_unreserved_item,
parse_sub_delim_item,
))
.parse_next(input)
}
#[inline]
fn parse_reg_name_nz<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
repeat::<_, _, (), _, _>(1.., parse_reg_name_item)
.void()
.parse_next(input)
}
#[inline]
fn parse_userinfo_item<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
alt((
parse_pct_encoded,
parse_unreserved_item,
parse_sub_delim_item,
literal(b':').void(),
))
.parse_next(input)
}
#[inline]
fn parse_userinfo_nz<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
repeat::<_, _, (), _, _>(1.., parse_userinfo_item)
.void()
.parse_next(input)
}
#[inline]
fn parse_pchar_item<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
alt((
parse_pct_encoded,
parse_unreserved_item,
parse_sub_delim_item,
one_of([b':', b'@']).void(),
))
.parse_next(input)
}
#[inline]
fn parse_pchar_nc_item<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
alt((
parse_pct_encoded,
parse_unreserved_item,
parse_sub_delim_item,
literal(b'@').void(),
))
.parse_next(input)
}
#[inline]
fn parse_query_or_fragment_item<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
{
alt((parse_pchar_item, one_of([b'/', b'?']).void())).parse_next(input)
}
#[inline]
fn parse_ipvfuture_item<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial,
I::Token: Clone + AsChar,
{
take_while(1..=1, is_ipvfuture_char)
.void()
.parse_next(input)
}
#[inline]
fn parse_generic_authority<I>(input: &mut I) -> ModalResult<()>
where
I: Stream + StreamIsPartial + Compare<u8>,
I::Token: Clone + AsChar,
I::Slice: AsRef<[u8]>,
{
(
opt(terminated(parse_userinfo, b'@')),
parse_host,
opt((b':', parse_port)),
)
.void()
.parse_next(input)
}
#[inline]
fn is_ipv4address_char(char: impl AsChar) -> bool {
matches!(char.as_char(), '0'..='9' | '.')
}
#[inline]
fn is_ipv6address_char(char: impl AsChar) -> bool {
let char = char.as_char();
char.is_ascii_hexdigit() || matches!(char, ':' | '.')
}
#[inline]
fn parse_ascii<T>(bytes: &[u8]) -> Option<T>
where
T: str::FromStr,
{
str::from_utf8(bytes).ok()?.parse().ok()
}
#[inline]
fn is_dec_octet(bytes: &[u8]) -> bool {
matches!(bytes, [b'0'..=b'9'])
|| matches!(bytes, [b'1'..=b'9', b'0'..=b'9'])
|| matches!(bytes, [b'1', b'0'..=b'9', b'0'..=b'9'])
|| matches!(bytes, [b'2', b'0'..=b'4', b'0'..=b'9'])
|| matches!(bytes, [b'2', b'5', b'0'..=b'5'])
}
#[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!(
matches!(
$parser.parse_peek(BStr::new($input)),
Ok((remaining, _)) if remaining == BStr::new($remaining)
),
"assertion failed: parser did not leave expected remaining input {:?}: {:?}",
$remaining,
$parser.parse_peek(BStr::new($input)),
);
};
}
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('a'));
assert!(!is_unreserved('/'));
assert!(is_sub_delim('!'));
assert!(!is_sub_delim('/'));
assert!(is_gen_delim('/'));
assert!(is_reserved('/'));
assert!(is_hexdig('F'));
assert!(!is_hexdig('g'));
assert!(is_pchar('%'));
assert!(is_pchar_nc('@'));
assert!(!is_pchar_nc(':'));
assert!(is_reg_name_char('%'));
assert!(is_userinfo_char(':'));
assert!(is_ipvfuture_char(':'));
assert!(is_ip_literal_char('.'));
}
#[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"::1"));
assert!(is_ip_literal_body(b"2001:db8::1"));
assert!(is_ip_literal_body(b"v1.future-host"));
}
#[test]
fn parses_pct_encoded() {
assert_backtrack!(parse_pct_encoded, b"");
assert_backtrack!(parse_pct_encoded, b"%");
assert_backtrack!(parse_pct_encoded, b"%2G");
assert_partial_incomplete!(parse_pct_encoded, b"%2", Needed::new(1));
assert_ok_remaining!(parse_pct_encoded, b"%20rest", b"rest");
}
#[test]
fn parses_userinfo_and_reg_name() {
assert_ok_remaining!(parse_userinfo, b"", b"");
assert_ok_remaining!(parse_userinfo, b"user:pass@", b"@");
assert_ok_remaining!(parse_reg_name, b"", b"");
assert_ok_remaining!(parse_reg_name, b"example.com:80", b":80");
assert_backtrack!(parse_uri_host, b"");
assert_ok_remaining!(parse_uri_host, b"example.com:80", b":80");
}
#[test]
fn parses_ipv4_address_and_dec_octet() {
assert_ok_remaining!(parse_dec_octet, b"0.", b".");
assert_ok_remaining!(parse_dec_octet, b"255.", b".");
assert_backtrack!(parse_dec_octet, b"256");
assert_backtrack!(parse_dec_octet, b"01");
assert_ok_remaining!(parse_ipv4address, b"127.0.0.1:80", b":80");
assert_backtrack!(parse_ipv4address, b"256.0.0.1");
assert_backtrack!(parse_ipv4address, b"127.0.0");
}
#[test]
fn parses_ip_literal_variants() {
assert_ok_remaining!(parse_h16, b"abcd:", b":");
assert_ok_remaining!(parse_ls32, b"abcd:ef01", b"");
assert_ok_remaining!(parse_ls32, b"192.0.2.1", b"");
assert_ok_remaining!(parse_ipv6address, b"2001:db8::1]", b"]");
assert_ok_remaining!(parse_ipvfuture, b"vF.token:part]", b"]");
assert_backtrack!(parse_ipvfuture, b"v.");
assert_ok_remaining!(parse_ip_literal, b"[::1]:443", b":443");
assert_ok_remaining!(parse_ip_literal, b"[vF.token:part]/", b"/");
assert_backtrack!(parse_ip_literal, b"[localhost]");
}
#[test]
fn parses_host_and_authority() {
assert_ok_remaining!(parse_host, b":80", b":80");
assert_ok_remaining!(parse_host, b"127.0.0.1:80", b":80");
assert_ok_remaining!(parse_host, b"[::1]:80", b":80");
assert_backtrack!(parse_authority, b"");
assert_ok_remaining!(parse_authority, b"user:pass@example.com:443/path", b"/path");
assert_ok_remaining!(parse_authority, b"[::1]/path", b"/path");
}
#[test]
fn parses_path_variants() {
assert_ok_remaining!(parse_segment, b":@", b"");
assert_backtrack!(parse_segment_nz, b"");
assert_backtrack!(parse_segment_nz_nc, b":");
assert_ok_remaining!(parse_segment_nz_nc, b"abc@/rest", b"/rest");
assert_ok_remaining!(parse_path_abempty, b"/a/b?x=1", b"?x=1");
assert_ok_remaining!(parse_path_absolute, b"/a/b?x=1", b"?x=1");
assert_backtrack!(parse_path_absolute, b"foo/bar");
assert_ok_remaining!(parse_path_noscheme, b"docs/latest?q=1", b"?q=1");
assert_ok_remaining!(parse_path_noscheme, b"urn:ietf", b":ietf");
assert_ok_remaining!(parse_path_rootless, b"urn:ietf:rfc:3986#frag", b"#frag");
assert_ok_remaining!(parse_path_empty, b"?q=1", b"?q=1");
assert_ok_remaining!(parse_path, b"/foo%20bar?baz", b"?baz");
assert_ok_remaining!(parse_path, b"/foo%", b"%");
}
#[test]
fn parses_query_and_fragment() {
assert_ok_remaining!(parse_query, b"foo=bar/baz?x#frag", b"#frag");
assert_ok_remaining!(parse_query, b"foo%", b"%");
assert_ok_remaining!(parse_fragment, b"section-2/part?a", b"");
}
#[test]
fn parses_hier_part_and_relative_part() {
assert_ok_remaining!(parse_hier_part, b"//example.com/a?b", b"?b");
assert_ok_remaining!(parse_hier_part, b"/a/b?c", b"?c");
assert_ok_remaining!(parse_hier_part, b"mailto:John.Doe@example.com", b"");
assert_ok_remaining!(parse_relative_part, b"//example.com/a?b", b"?b");
assert_ok_remaining!(parse_relative_part, b"/a/b?b", b"?b");
assert_ok_remaining!(parse_relative_part, b"guides/setup#frag", b"#frag");
}
#[test]
fn parses_uri_forms() {
assert_partial_incomplete!(parse_scheme, b"", Needed::new(1));
assert_ok_remaining!(
parse_absolute_uri,
b"mailto:John.Doe@example.com?subject=Hi#frag",
b"#frag"
);
assert_ok_remaining!(parse_relative_ref, b"../images/logo.svg?v=2#hero", b"");
assert_ok_remaining!(parse_uri, b"https://example.com/a/b?q=1#frag", b"");
assert_ok_remaining!(parse_uri_reference, b"//example.com/path", b"");
assert_ok_remaining!(parse_uri_reference, b"urn:ietf:rfc:3986", b"");
assert_backtrack!(parse_uri, b"//example.com/path");
assert_ok_remaining!(parse_uri_reference, b"http://example.com/%", b"%");
}
}