Skip to main content

winnow_rfc3986/
lib.rs

1//! Winnow parsers for reusable URI syntax productions from
2//! [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).
3
4#![cfg_attr(docsrs, feature(doc_cfg))]
5
6use core::{
7    net::{Ipv4Addr, Ipv6Addr},
8    str,
9};
10
11use winnow::{
12    combinator::{alt, opt, preceded, repeat, terminated},
13    prelude::*,
14    stream::{AsChar, Compare, Stream, StreamIsPartial},
15    token::{literal, one_of, take_while},
16};
17
18/// Parses an absolute path.
19///
20/// This is a compatibility alias for [`parse_path_absolute`].
21///
22/// # BNF
23///
24/// ```text
25/// absolute-path = 1*( "/" segment )
26/// segment       = *pchar
27/// ```
28///
29/// # Examples
30///
31/// ```
32/// use winnow::Parser as _;
33/// use winnow_rfc3986::parse_path;
34///
35/// let (rest, ()) = parse_path.parse_peek(&b"/a/b?c"[..]).unwrap();
36/// assert_eq!(rest, b"?c");
37/// ```
38///
39/// See:
40/// - [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
41/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
42#[inline]
43pub fn parse_path<I>(input: &mut I) -> ModalResult<()>
44where
45    I: Stream + StreamIsPartial + Compare<u8>,
46    I::Token: Clone + AsChar,
47{
48    parse_path_absolute.parse_next(input)
49}
50
51/// Parses a query string, without the leading `?`.
52///
53/// # BNF
54///
55/// ```text
56/// query = *( pchar / "/" / "?" )
57/// ```
58///
59/// # Examples
60///
61/// ```
62/// use winnow::Parser as _;
63/// use winnow_rfc3986::parse_query;
64///
65/// let (rest, ()) = parse_query.parse_peek(&b"foo=bar/baz?x#frag"[..]).unwrap();
66/// assert_eq!(rest, b"#frag");
67/// ```
68///
69/// See:
70/// - [RFC 3986 §3.4](https://datatracker.ietf.org/doc/html/rfc3986#section-3.4)
71/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
72#[inline]
73pub fn parse_query<I>(input: &mut I) -> ModalResult<()>
74where
75    I: Stream + StreamIsPartial + Compare<u8>,
76    I::Token: Clone + AsChar,
77{
78    repeat::<_, _, (), _, _>(.., parse_query_or_fragment_item)
79        .void()
80        .parse_next(input)
81}
82
83/// Parses a fragment string, without the leading `#`.
84///
85/// # BNF
86///
87/// ```text
88/// fragment = *( pchar / "/" / "?" )
89/// ```
90///
91/// # Examples
92///
93/// ```
94/// use winnow::Parser as _;
95/// use winnow_rfc3986::parse_fragment;
96///
97/// let (rest, ()) = parse_fragment.parse_peek(&b"section-2/part?a"[..]).unwrap();
98/// assert_eq!(rest, b"");
99/// ```
100///
101/// See:
102/// - [RFC 3986 §3.5](https://datatracker.ietf.org/doc/html/rfc3986#section-3.5)
103/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
104#[inline]
105pub fn parse_fragment<I>(input: &mut I) -> ModalResult<()>
106where
107    I: Stream + StreamIsPartial + Compare<u8>,
108    I::Token: Clone + AsChar,
109{
110    parse_query.parse_next(input)
111}
112
113/// Returns `true` if the given character is in the `unreserved` group.
114#[inline]
115pub fn is_unreserved(char: impl AsChar) -> bool {
116    matches!(
117        char.as_char(),
118        '0'..='9' | 'A'..='Z' | 'a'..='z' | '-' | '.' | '_' | '~'
119    )
120}
121
122/// Returns `true` if the given character is in the `sub-delims` group.
123#[inline]
124pub fn is_sub_delim(char: impl AsChar) -> bool {
125    matches!(
126        char.as_char(),
127        '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '='
128    )
129}
130
131/// Returns `true` if the given character is in the `gen-delims` group.
132#[inline]
133pub fn is_gen_delim(char: impl AsChar) -> bool {
134    matches!(char.as_char(), ':' | '/' | '?' | '#' | '[' | ']' | '@')
135}
136
137/// Returns `true` if the given character is in the `reserved` group.
138#[inline]
139pub fn is_reserved(char: impl AsChar) -> bool {
140    let char = char.as_char();
141
142    is_gen_delim(char) || is_sub_delim(char)
143}
144
145/// Returns `true` if the given character is an ASCII hexadecimal digit.
146#[inline]
147pub fn is_hexdig(char: impl AsChar) -> bool {
148    char.as_char().is_ascii_hexdigit()
149}
150
151/// Returns `true` if the given character is a valid `pchar`.
152#[inline]
153pub fn is_pchar(char: impl AsChar) -> bool {
154    let char = char.as_char();
155
156    is_unreserved(char) || is_sub_delim(char) || matches!(char, '%' | ':' | '@')
157}
158
159/// Returns `true` if the given character is a valid `segment-nz-nc` character.
160#[inline]
161pub fn is_pchar_nc(char: impl AsChar) -> bool {
162    let char = char.as_char();
163
164    is_unreserved(char) || is_sub_delim(char) || matches!(char, '%' | '@')
165}
166
167/// Returns `true` if the given character is valid in `reg-name`.
168#[inline]
169pub fn is_reg_name_char(char: impl AsChar) -> bool {
170    let char = char.as_char();
171
172    is_unreserved(char) || is_sub_delim(char) || matches!(char, '%')
173}
174
175/// Returns `true` if the given character is valid in `userinfo`.
176#[inline]
177pub fn is_userinfo_char(char: impl AsChar) -> bool {
178    let char = char.as_char();
179
180    is_unreserved(char) || is_sub_delim(char) || matches!(char, '%' | ':')
181}
182
183/// Returns `true` if the given character is valid within an `IPvFuture` tail.
184#[inline]
185pub fn is_ipvfuture_char(char: impl AsChar) -> bool {
186    let char = char.as_char();
187
188    is_unreserved(char) || is_sub_delim(char) || matches!(char, ':')
189}
190
191/// Returns `true` if the given character is valid within an `IP-literal` body.
192#[inline]
193pub fn is_ip_literal_char(char: impl AsChar) -> bool {
194    let char = char.as_char();
195
196    char.is_ascii_hexdigit() || is_ipvfuture_char(char) || matches!(char, '.')
197}
198
199/// Returns `true` if the given slice is a valid `IPv6address` or `IPvFuture` payload.
200///
201/// See: [RFC 3986 §3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2)
202#[inline]
203pub fn is_ip_literal_body(bytes: &[u8]) -> bool {
204    parse_ipv6address
205        .parse_peek(bytes)
206        .is_ok_and(|(rest, ())| rest.is_empty())
207        || parse_ipvfuture
208            .parse_peek(bytes)
209            .is_ok_and(|(rest, ())| rest.is_empty())
210}
211
212/// Parses `pct-encoded`.
213///
214/// # BNF
215///
216/// ```text
217/// pct-encoded = "%" HEXDIG HEXDIG
218/// ```
219///
220/// # Examples
221///
222/// ```
223/// use winnow::Parser as _;
224/// use winnow_rfc3986::parse_pct_encoded;
225///
226/// let (rest, ()) = parse_pct_encoded.parse_peek(&b"%20rest"[..]).unwrap();
227/// assert_eq!(rest, b"rest");
228/// ```
229///
230/// See:
231/// - [RFC 3986 §2.1](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1)
232/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
233#[inline]
234pub fn parse_pct_encoded<I>(input: &mut I) -> ModalResult<()>
235where
236    I: Stream + StreamIsPartial + Compare<u8>,
237    I::Token: Clone + AsChar,
238{
239    (b'%', take_while(2..=2, is_hexdig))
240        .void()
241        .parse_next(input)
242}
243
244/// Parses `userinfo`.
245///
246/// # BNF
247///
248/// ```text
249/// userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
250/// ```
251///
252/// # Examples
253///
254/// ```
255/// use winnow::Parser as _;
256/// use winnow_rfc3986::parse_userinfo;
257///
258/// let (rest, ()) = parse_userinfo.parse_peek(&b"user:pass@example.com"[..]).unwrap();
259/// assert_eq!(rest, b"@example.com");
260/// ```
261///
262/// See:
263/// - [RFC 3986 §3.2.1](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1)
264/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
265#[inline]
266pub fn parse_userinfo<I>(input: &mut I) -> ModalResult<()>
267where
268    I: Stream + StreamIsPartial + Compare<u8>,
269    I::Token: Clone + AsChar,
270{
271    repeat::<_, _, (), _, _>(.., parse_userinfo_item)
272        .void()
273        .parse_next(input)
274}
275
276/// Parses `host`.
277///
278/// # BNF
279///
280/// ```text
281/// host = IP-literal / IPv4address / reg-name
282/// ```
283///
284/// # Examples
285///
286/// ```
287/// use winnow::Parser as _;
288/// use winnow_rfc3986::parse_host;
289///
290/// let (rest, ()) = parse_host.parse_peek(&b"192.0.2.1:80"[..]).unwrap();
291/// assert_eq!(rest, b":80");
292/// ```
293///
294/// See:
295/// - [RFC 3986 §3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2)
296/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
297#[inline]
298pub fn parse_host<I>(input: &mut I) -> ModalResult<()>
299where
300    I: Stream + StreamIsPartial + Compare<u8>,
301    I::Token: Clone + AsChar,
302    I::Slice: AsRef<[u8]>,
303{
304    alt((parse_ip_literal, parse_ipv4address, parse_reg_name))
305        .void()
306        .parse_next(input)
307}
308
309/// Parses a `uri-host`.
310///
311/// This parser preserves the non-empty host behavior used by the HTTP request-target crate.
312///
313/// # BNF
314///
315/// ```text
316/// host = IP-literal / IPv4address / reg-name
317/// uri-host = <HTTP request-target use of URI host grammar>
318/// ```
319///
320/// # Examples
321///
322/// ```
323/// use winnow::Parser as _;
324/// use winnow_rfc3986::parse_uri_host;
325///
326/// let (rest, ()) = parse_uri_host.parse_peek(&b"[::1]:443"[..]).unwrap();
327/// assert_eq!(rest, b":443");
328/// ```
329///
330/// See:
331/// - [RFC 3986 §3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2)
332/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
333#[inline]
334pub fn parse_uri_host<I>(input: &mut I) -> ModalResult<()>
335where
336    I: Stream + StreamIsPartial + Compare<u8>,
337    I::Token: Clone + AsChar,
338    I::Slice: AsRef<[u8]>,
339{
340    alt((parse_ip_literal, parse_ipv4address, parse_reg_name_nz))
341        .void()
342        .parse_next(input)
343}
344
345/// Parses an `IP-literal`.
346///
347/// # BNF
348///
349/// ```text
350/// IP-literal = "[" ( IPv6address / IPvFuture ) "]"
351/// ```
352///
353/// # Examples
354///
355/// ```
356/// use winnow::Parser as _;
357/// use winnow_rfc3986::parse_ip_literal;
358///
359/// let (rest, ()) = parse_ip_literal.parse_peek(&b"[2001:db8::1]/"[..]).unwrap();
360/// assert_eq!(rest, b"/");
361/// ```
362///
363/// See:
364/// - [RFC 3986 §3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2)
365/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
366#[inline]
367pub fn parse_ip_literal<I>(input: &mut I) -> ModalResult<()>
368where
369    I: Stream + StreamIsPartial + Compare<u8>,
370    I::Token: Clone + AsChar,
371    I::Slice: AsRef<[u8]>,
372{
373    (b'[', alt((parse_ipv6address, parse_ipvfuture)), b']')
374        .void()
375        .parse_next(input)
376}
377
378/// Parses an `IPvFuture`.
379///
380/// # BNF
381///
382/// ```text
383/// IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
384/// ```
385///
386/// # Examples
387///
388/// ```
389/// use winnow::Parser as _;
390/// use winnow_rfc3986::parse_ipvfuture;
391///
392/// let (rest, ()) = parse_ipvfuture.parse_peek(&b"vF.token:part]"[..]).unwrap();
393/// assert_eq!(rest, b"]");
394/// ```
395///
396/// See:
397/// - [RFC 3986 §3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2)
398/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
399#[inline]
400pub fn parse_ipvfuture<I>(input: &mut I) -> ModalResult<()>
401where
402    I: Stream + StreamIsPartial + Compare<u8>,
403    I::Token: Clone + AsChar,
404{
405    (
406        one_of([b'v', b'V']),
407        take_while(1.., is_hexdig),
408        b'.',
409        repeat::<_, _, (), _, _>(1.., parse_ipvfuture_item),
410    )
411        .void()
412        .parse_next(input)
413}
414
415/// Parses an `IPv6address`.
416///
417/// # BNF
418///
419/// ```text
420/// IPv6address =                            6( h16 ":" ) ls32
421///             /                       "::" 5( h16 ":" ) ls32
422///             / [               h16 ] "::" 4( h16 ":" ) ls32
423///             / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
424///             / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
425///             / [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32
426///             / [ *4( h16 ":" ) h16 ] "::"              ls32
427///             / [ *5( h16 ":" ) h16 ] "::"              h16
428///             / [ *6( h16 ":" ) h16 ] "::"
429/// ```
430///
431/// See:
432/// - [RFC 3986 §3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2)
433/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
434#[inline]
435pub fn parse_ipv6address<I>(input: &mut I) -> ModalResult<()>
436where
437    I: Stream + StreamIsPartial,
438    I::Token: Clone + AsChar,
439    I::Slice: AsRef<[u8]>,
440{
441    take_while(1.., is_ipv6address_char)
442        .verify(|slice: &I::Slice| parse_ascii::<Ipv6Addr>(slice.as_ref()).is_some())
443        .void()
444        .parse_next(input)
445}
446
447/// Parses an `IPv4address`.
448///
449/// # BNF
450///
451/// ```text
452/// IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
453/// ```
454///
455/// # Examples
456///
457/// ```
458/// use winnow::Parser as _;
459/// use winnow_rfc3986::parse_ipv4address;
460///
461/// let (rest, ()) = parse_ipv4address.parse_peek(&b"127.0.0.1:80"[..]).unwrap();
462/// assert_eq!(rest, b":80");
463/// ```
464///
465/// See:
466/// - [RFC 3986 §3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2)
467/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
468#[inline]
469pub fn parse_ipv4address<I>(input: &mut I) -> ModalResult<()>
470where
471    I: Stream + StreamIsPartial,
472    I::Token: Clone + AsChar,
473    I::Slice: AsRef<[u8]>,
474{
475    take_while(1.., is_ipv4address_char)
476        .verify(|slice: &I::Slice| parse_ascii::<Ipv4Addr>(slice.as_ref()).is_some())
477        .void()
478        .parse_next(input)
479}
480
481/// Parses a `dec-octet`.
482///
483/// # BNF
484///
485/// ```text
486/// dec-octet = DIGIT
487///           / %x31-39 DIGIT
488///           / "1" 2DIGIT
489///           / "2" %x30-34 DIGIT
490///           / "25" %x30-35
491/// ```
492///
493/// See:
494/// - [RFC 3986 §3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2)
495/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
496#[inline]
497pub fn parse_dec_octet<I>(input: &mut I) -> ModalResult<()>
498where
499    I: Stream + StreamIsPartial,
500    I::Token: Clone + AsChar,
501    I::Slice: AsRef<[u8]>,
502{
503    take_while(1..=3, |char: I::Token| char.as_char().is_ascii_digit())
504        .verify(|slice: &I::Slice| is_dec_octet(slice.as_ref()))
505        .void()
506        .parse_next(input)
507}
508
509/// Parses a `reg-name`.
510///
511/// # BNF
512///
513/// ```text
514/// reg-name = *( unreserved / pct-encoded / sub-delims )
515/// ```
516///
517/// # Examples
518///
519/// ```
520/// use winnow::Parser as _;
521/// use winnow_rfc3986::parse_reg_name;
522///
523/// let (rest, ()) = parse_reg_name.parse_peek(&b"example.com:443"[..]).unwrap();
524/// assert_eq!(rest, b":443");
525/// ```
526///
527/// See:
528/// - [RFC 3986 §3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2)
529/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
530#[inline]
531pub fn parse_reg_name<I>(input: &mut I) -> ModalResult<()>
532where
533    I: Stream + StreamIsPartial + Compare<u8>,
534    I::Token: Clone + AsChar,
535{
536    repeat::<_, _, (), _, _>(.., parse_reg_name_item)
537        .void()
538        .parse_next(input)
539}
540
541/// Parses a `port`.
542///
543/// # BNF
544///
545/// ```text
546/// port = *DIGIT
547/// ```
548///
549/// # Examples
550///
551/// ```
552/// use winnow::Parser as _;
553/// use winnow_rfc3986::parse_port;
554///
555/// let (rest, ()) = parse_port.parse_peek(&b"8443/path"[..]).unwrap();
556/// assert_eq!(rest, b"/path");
557/// ```
558///
559/// See:
560/// - [RFC 3986 §3.2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3)
561/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
562#[inline]
563pub fn parse_port<I>(input: &mut I) -> ModalResult<()>
564where
565    I: Stream + StreamIsPartial,
566    I::Token: Clone + AsChar,
567{
568    take_while(.., |char: I::Token| char.as_char().is_ascii_digit())
569        .void()
570        .parse_next(input)
571}
572
573/// Returns `true` if the given character is a valid first character in `scheme`.
574#[inline]
575pub fn is_scheme_start(char: impl AsChar) -> bool {
576    char.is_alpha()
577}
578
579/// Returns `true` if the given character is valid in `scheme`.
580#[inline]
581pub fn is_scheme_char(char: impl AsChar) -> bool {
582    let char = char.as_char();
583    char.is_ascii_alphanumeric() || matches!(char, '+' | '-' | '.')
584}
585
586/// Parses `scheme`.
587///
588/// # BNF
589///
590/// ```text
591/// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
592/// ```
593///
594/// # Examples
595///
596/// ```
597/// use winnow::Parser as _;
598/// use winnow_rfc3986::parse_scheme;
599///
600/// let (rest, ()) = parse_scheme.parse_peek(&b"https://example.com"[..]).unwrap();
601/// assert_eq!(rest, b"://example.com");
602/// ```
603///
604/// See:
605/// - [RFC 3986 §3.1](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1)
606/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
607#[inline]
608pub fn parse_scheme<I>(input: &mut I) -> ModalResult<()>
609where
610    I: Stream + StreamIsPartial,
611    I::Token: Clone + AsChar,
612{
613    preceded(one_of(is_scheme_start), take_while(.., is_scheme_char))
614        .void()
615        .parse_next(input)
616}
617
618/// Parses `authority`.
619///
620/// This parser preserves the non-empty host behavior used by the HTTP request-target crate.
621///
622/// # BNF
623///
624/// ```text
625/// authority = [ userinfo "@" ] host [ ":" port ]
626/// ```
627///
628/// # Examples
629///
630/// ```
631/// use winnow::Parser as _;
632/// use winnow_rfc3986::parse_authority;
633///
634/// let (rest, ()) = parse_authority
635///     .parse_peek(&b"user:pass@example.com:443/path"[..])
636///     .unwrap();
637/// assert_eq!(rest, b"/path");
638/// ```
639///
640/// See:
641/// - [RFC 3986 §3.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2)
642/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
643#[inline]
644pub fn parse_authority<I>(input: &mut I) -> ModalResult<()>
645where
646    I: Stream + StreamIsPartial + Compare<u8>,
647    I::Token: Clone + AsChar,
648    I::Slice: AsRef<[u8]>,
649{
650    parse_authority_parts.void().parse_next(input)
651}
652
653/// Parses `authority` into `userinfo`, `host`, and optional `port`.
654///
655/// This parser preserves the non-empty host behavior used by the HTTP request-target crate.
656///
657/// # BNF
658///
659/// ```text
660/// authority = [ userinfo "@" ] host [ ":" port ]
661/// userinfo  = *( unreserved / pct-encoded / sub-delims / ":" )
662/// ```
663///
664/// # Examples
665///
666/// ```
667/// use winnow::Parser as _;
668/// use winnow_rfc3986::parse_authority_parts;
669///
670/// let (rest, ()) = parse_authority_parts
671///     .parse_peek(&b"user:pass@example.com:443/path"[..])
672///     .unwrap();
673/// assert_eq!(rest, b"/path");
674/// ```
675///
676/// See:
677/// - [RFC 3986 §3.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2)
678/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
679#[inline]
680pub fn parse_authority_parts<I>(input: &mut I) -> ModalResult<()>
681where
682    I: Stream + StreamIsPartial + Compare<u8>,
683    I::Token: Clone + AsChar,
684    I::Slice: AsRef<[u8]>,
685{
686    (
687        opt(terminated(parse_userinfo_nz, b'@')),
688        parse_uri_host,
689        opt((b':', parse_port)),
690    )
691        .void()
692        .parse_next(input)
693}
694
695/// Parses `path-abempty`.
696///
697/// # BNF
698///
699/// ```text
700/// path-abempty = *( "/" segment )
701/// ```
702///
703/// # Examples
704///
705/// ```
706/// use winnow::Parser as _;
707/// use winnow_rfc3986::parse_path_abempty;
708///
709/// let (rest, ()) = parse_path_abempty.parse_peek(&b"/a/b?x=1"[..]).unwrap();
710/// assert_eq!(rest, b"?x=1");
711/// ```
712///
713/// See:
714/// - [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
715/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
716#[inline]
717pub fn parse_path_abempty<I>(input: &mut I) -> ModalResult<()>
718where
719    I: Stream + StreamIsPartial + Compare<u8>,
720    I::Token: Clone + AsChar,
721{
722    repeat::<_, _, (), _, _>(.., (b'/', parse_segment))
723        .void()
724        .parse_next(input)
725}
726
727/// Parses `path-absolute`.
728///
729/// # BNF
730///
731/// ```text
732/// path-absolute = "/" [ segment-nz *( "/" segment ) ]
733/// ```
734///
735/// # Examples
736///
737/// ```
738/// use winnow::Parser as _;
739/// use winnow_rfc3986::parse_path_absolute;
740///
741/// let (rest, ()) = parse_path_absolute.parse_peek(&b"/a/b?x=1"[..]).unwrap();
742/// assert_eq!(rest, b"?x=1");
743/// ```
744///
745/// See:
746/// - [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
747/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
748#[inline]
749pub fn parse_path_absolute<I>(input: &mut I) -> ModalResult<()>
750where
751    I: Stream + StreamIsPartial + Compare<u8>,
752    I::Token: Clone + AsChar,
753{
754    (
755        b'/',
756        opt((
757            parse_segment_nz,
758            repeat::<_, _, (), _, _>(.., (b'/', parse_segment)),
759        )),
760    )
761        .void()
762        .parse_next(input)
763}
764
765/// Parses `path-noscheme`.
766///
767/// # BNF
768///
769/// ```text
770/// path-noscheme = segment-nz-nc *( "/" segment )
771/// ```
772///
773/// # Examples
774///
775/// ```
776/// use winnow::Parser as _;
777/// use winnow_rfc3986::parse_path_noscheme;
778///
779/// let (rest, ()) = parse_path_noscheme.parse_peek(&b"docs/latest?q=1"[..]).unwrap();
780/// assert_eq!(rest, b"?q=1");
781/// ```
782///
783/// See:
784/// - [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
785/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
786#[inline]
787pub fn parse_path_noscheme<I>(input: &mut I) -> ModalResult<()>
788where
789    I: Stream + StreamIsPartial + Compare<u8>,
790    I::Token: Clone + AsChar,
791{
792    (
793        parse_segment_nz_nc,
794        repeat::<_, _, (), _, _>(.., (b'/', parse_segment)),
795    )
796        .void()
797        .parse_next(input)
798}
799
800/// Parses `path-rootless`.
801///
802/// # BNF
803///
804/// ```text
805/// path-rootless = segment-nz *( "/" segment )
806/// ```
807///
808/// # Examples
809///
810/// ```
811/// use winnow::Parser as _;
812/// use winnow_rfc3986::parse_path_rootless;
813///
814/// let (rest, ()) = parse_path_rootless.parse_peek(&b"urn:isbn:0451450523?x"[..]).unwrap();
815/// assert_eq!(rest, b"?x");
816/// ```
817///
818/// See:
819/// - [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
820/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
821#[inline]
822pub fn parse_path_rootless<I>(input: &mut I) -> ModalResult<()>
823where
824    I: Stream + StreamIsPartial + Compare<u8>,
825    I::Token: Clone + AsChar,
826{
827    (
828        parse_segment_nz,
829        repeat::<_, _, (), _, _>(.., (b'/', parse_segment)),
830    )
831        .void()
832        .parse_next(input)
833}
834
835/// Parses `path-empty`.
836///
837/// # BNF
838///
839/// ```text
840/// path-empty = 0<pchar>
841/// ```
842///
843/// # Examples
844///
845/// ```
846/// use winnow::Parser as _;
847/// use winnow_rfc3986::parse_path_empty;
848///
849/// let (rest, ()) = parse_path_empty.parse_peek(&b"?q=1"[..]).unwrap();
850/// assert_eq!(rest, b"?q=1");
851/// ```
852///
853/// See:
854/// - [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
855/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
856#[inline]
857pub fn parse_path_empty<I>(_input: &mut I) -> ModalResult<()>
858where
859    I: Stream + StreamIsPartial,
860{
861    Ok(())
862}
863
864/// Parses `segment`.
865///
866/// # BNF
867///
868/// ```text
869/// segment = *pchar
870/// ```
871///
872/// See: [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
873#[inline]
874pub fn parse_segment<I>(input: &mut I) -> ModalResult<()>
875where
876    I: Stream + StreamIsPartial + Compare<u8>,
877    I::Token: Clone + AsChar,
878{
879    repeat::<_, _, (), _, _>(.., parse_pchar_item)
880        .void()
881        .parse_next(input)
882}
883
884/// Parses `segment-nz`.
885///
886/// # BNF
887///
888/// ```text
889/// segment-nz = 1*pchar
890/// ```
891///
892/// See: [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
893#[inline]
894pub fn parse_segment_nz<I>(input: &mut I) -> ModalResult<()>
895where
896    I: Stream + StreamIsPartial + Compare<u8>,
897    I::Token: Clone + AsChar,
898{
899    repeat::<_, _, (), _, _>(1.., parse_pchar_item)
900        .void()
901        .parse_next(input)
902}
903
904/// Parses `segment-nz-nc`.
905///
906/// # BNF
907///
908/// ```text
909/// segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
910/// ```
911///
912/// See: [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
913#[inline]
914pub fn parse_segment_nz_nc<I>(input: &mut I) -> ModalResult<()>
915where
916    I: Stream + StreamIsPartial + Compare<u8>,
917    I::Token: Clone + AsChar,
918{
919    repeat::<_, _, (), _, _>(1.., parse_pchar_nc_item)
920        .void()
921        .parse_next(input)
922}
923
924/// Parses `hier-part`.
925///
926/// # BNF
927///
928/// ```text
929/// hier-part = "//" authority path-abempty
930///           / path-absolute
931///           / path-rootless
932///           / path-empty
933/// ```
934///
935/// # Examples
936///
937/// ```
938/// use winnow::Parser as _;
939/// use winnow_rfc3986::parse_hier_part;
940///
941/// let (rest, ()) = parse_hier_part.parse_peek(&b"//example.com/a?b"[..]).unwrap();
942/// assert_eq!(rest, b"?b");
943/// ```
944///
945/// See:
946/// - [RFC 3986 §3](https://datatracker.ietf.org/doc/html/rfc3986#section-3)
947/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
948#[inline]
949pub fn parse_hier_part<I>(input: &mut I) -> ModalResult<()>
950where
951    I: Stream + StreamIsPartial + Compare<u8>,
952    I::Token: Clone + AsChar,
953    I::Slice: AsRef<[u8]>,
954{
955    alt((
956        ((b'/', b'/'), parse_generic_authority, parse_path_abempty).void(),
957        parse_path_absolute,
958        parse_path_rootless,
959        parse_path_empty,
960    ))
961    .parse_next(input)
962}
963
964/// Parses `relative-part`.
965///
966/// # BNF
967///
968/// ```text
969/// relative-part = "//" authority path-abempty
970///               / path-absolute
971///               / path-noscheme
972///               / path-empty
973/// ```
974///
975/// # Examples
976///
977/// ```
978/// use winnow::Parser as _;
979/// use winnow_rfc3986::parse_relative_part;
980///
981/// let (rest, ()) = parse_relative_part.parse_peek(&b"guides/setup#frag"[..]).unwrap();
982/// assert_eq!(rest, b"#frag");
983/// ```
984///
985/// See:
986/// - [RFC 3986 §4.2](https://datatracker.ietf.org/doc/html/rfc3986#section-4.2)
987/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
988#[inline]
989pub fn parse_relative_part<I>(input: &mut I) -> ModalResult<()>
990where
991    I: Stream + StreamIsPartial + Compare<u8>,
992    I::Token: Clone + AsChar,
993    I::Slice: AsRef<[u8]>,
994{
995    alt((
996        ((b'/', b'/'), parse_generic_authority, parse_path_abempty).void(),
997        parse_path_absolute,
998        parse_path_noscheme,
999        parse_path_empty,
1000    ))
1001    .parse_next(input)
1002}
1003
1004/// Parses `absolute-URI`.
1005///
1006/// # BNF
1007///
1008/// ```text
1009/// absolute-URI = scheme ":" hier-part [ "?" query ]
1010/// ```
1011///
1012/// # Examples
1013///
1014/// ```
1015/// use winnow::Parser as _;
1016/// use winnow_rfc3986::parse_absolute_uri;
1017///
1018/// let (rest, ()) = parse_absolute_uri
1019///     .parse_peek(&b"mailto:John.Doe@example.com?subject=Hi#frag"[..])
1020///     .unwrap();
1021/// assert_eq!(rest, b"#frag");
1022/// ```
1023///
1024/// See:
1025/// - [RFC 3986 §4.3](https://datatracker.ietf.org/doc/html/rfc3986#section-4.3)
1026/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
1027#[inline]
1028pub fn parse_absolute_uri<I>(input: &mut I) -> ModalResult<()>
1029where
1030    I: Stream + StreamIsPartial + Compare<u8>,
1031    I::Token: Clone + AsChar,
1032    I::Slice: AsRef<[u8]>,
1033{
1034    (
1035        parse_scheme,
1036        b':',
1037        parse_hier_part,
1038        opt((b'?', parse_query)),
1039    )
1040        .void()
1041        .parse_next(input)
1042}
1043
1044/// Parses `relative-ref`.
1045///
1046/// # BNF
1047///
1048/// ```text
1049/// relative-ref = relative-part [ "?" query ] [ "#" fragment ]
1050/// ```
1051///
1052/// # Examples
1053///
1054/// ```
1055/// use winnow::Parser as _;
1056/// use winnow_rfc3986::parse_relative_ref;
1057///
1058/// let (rest, ()) = parse_relative_ref
1059///     .parse_peek(&b"../images/logo.svg?v=2#hero"[..])
1060///     .unwrap();
1061/// assert_eq!(rest, b"");
1062/// ```
1063///
1064/// See:
1065/// - [RFC 3986 §4.2](https://datatracker.ietf.org/doc/html/rfc3986#section-4.2)
1066/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
1067#[inline]
1068pub fn parse_relative_ref<I>(input: &mut I) -> ModalResult<()>
1069where
1070    I: Stream + StreamIsPartial + Compare<u8>,
1071    I::Token: Clone + AsChar,
1072    I::Slice: AsRef<[u8]>,
1073{
1074    (
1075        parse_relative_part,
1076        opt((b'?', parse_query)),
1077        opt((b'#', parse_fragment)),
1078    )
1079        .void()
1080        .parse_next(input)
1081}
1082
1083/// Parses `URI`.
1084///
1085/// # BNF
1086///
1087/// ```text
1088/// URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
1089/// ```
1090///
1091/// # Examples
1092///
1093/// ```
1094/// use winnow::Parser as _;
1095/// use winnow_rfc3986::parse_uri;
1096///
1097/// let (rest, ()) = parse_uri
1098///     .parse_peek(&b"https://example.com/a/b?q=1#frag"[..])
1099///     .unwrap();
1100/// assert_eq!(rest, b"");
1101/// ```
1102///
1103/// See:
1104/// - [RFC 3986 §3](https://datatracker.ietf.org/doc/html/rfc3986#section-3)
1105/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
1106#[inline]
1107pub fn parse_uri<I>(input: &mut I) -> ModalResult<()>
1108where
1109    I: Stream + StreamIsPartial + Compare<u8>,
1110    I::Token: Clone + AsChar,
1111    I::Slice: AsRef<[u8]>,
1112{
1113    (
1114        parse_scheme,
1115        b':',
1116        parse_hier_part,
1117        opt((b'?', parse_query)),
1118        opt((b'#', parse_fragment)),
1119    )
1120        .void()
1121        .parse_next(input)
1122}
1123
1124/// Parses `URI-reference`.
1125///
1126/// # BNF
1127///
1128/// ```text
1129/// URI-reference = URI / relative-ref
1130/// ```
1131///
1132/// # Examples
1133///
1134/// ```
1135/// use winnow::Parser as _;
1136/// use winnow_rfc3986::parse_uri_reference;
1137///
1138/// let (rest, ()) = parse_uri_reference.parse_peek(&b"//example.com/path"[..]).unwrap();
1139/// assert_eq!(rest, b"");
1140/// ```
1141///
1142/// See:
1143/// - [RFC 3986 §4.1](https://datatracker.ietf.org/doc/html/rfc3986#section-4.1)
1144/// - [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
1145#[inline]
1146pub fn parse_uri_reference<I>(input: &mut I) -> ModalResult<()>
1147where
1148    I: Stream + StreamIsPartial + Compare<u8>,
1149    I::Token: Clone + AsChar,
1150    I::Slice: AsRef<[u8]>,
1151{
1152    alt((parse_uri, parse_relative_ref)).parse_next(input)
1153}
1154
1155/// Parses `h16`.
1156///
1157/// # BNF
1158///
1159/// ```text
1160/// h16 = 1*4HEXDIG
1161/// ```
1162///
1163/// See: [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
1164#[inline]
1165pub fn parse_h16<I>(input: &mut I) -> ModalResult<()>
1166where
1167    I: Stream + StreamIsPartial,
1168    I::Token: Clone + AsChar,
1169{
1170    take_while(1..=4, is_hexdig).void().parse_next(input)
1171}
1172
1173/// Parses `ls32`.
1174///
1175/// # BNF
1176///
1177/// ```text
1178/// ls32 = ( h16 ":" h16 ) / IPv4address
1179/// ```
1180///
1181/// See: [RFC 3986 Appendix A](https://datatracker.ietf.org/doc/html/rfc3986#appendix-A)
1182#[inline]
1183pub fn parse_ls32<I>(input: &mut I) -> ModalResult<()>
1184where
1185    I: Stream + StreamIsPartial + Compare<u8>,
1186    I::Token: Clone + AsChar,
1187    I::Slice: AsRef<[u8]>,
1188{
1189    alt((parse_ipv4address, (parse_h16, b':', parse_h16).void())).parse_next(input)
1190}
1191
1192#[inline]
1193fn parse_unreserved_item<I>(input: &mut I) -> ModalResult<()>
1194where
1195    I: Stream + StreamIsPartial,
1196    I::Token: Clone + AsChar,
1197{
1198    take_while(1..=1, is_unreserved).void().parse_next(input)
1199}
1200
1201#[inline]
1202fn parse_sub_delim_item<I>(input: &mut I) -> ModalResult<()>
1203where
1204    I: Stream + StreamIsPartial,
1205    I::Token: Clone + AsChar,
1206{
1207    take_while(1..=1, is_sub_delim).void().parse_next(input)
1208}
1209
1210#[inline]
1211fn parse_reg_name_item<I>(input: &mut I) -> ModalResult<()>
1212where
1213    I: Stream + StreamIsPartial + Compare<u8>,
1214    I::Token: Clone + AsChar,
1215{
1216    alt((
1217        parse_pct_encoded,
1218        parse_unreserved_item,
1219        parse_sub_delim_item,
1220    ))
1221    .parse_next(input)
1222}
1223
1224#[inline]
1225fn parse_reg_name_nz<I>(input: &mut I) -> ModalResult<()>
1226where
1227    I: Stream + StreamIsPartial + Compare<u8>,
1228    I::Token: Clone + AsChar,
1229{
1230    repeat::<_, _, (), _, _>(1.., parse_reg_name_item)
1231        .void()
1232        .parse_next(input)
1233}
1234
1235#[inline]
1236fn parse_userinfo_item<I>(input: &mut I) -> ModalResult<()>
1237where
1238    I: Stream + StreamIsPartial + Compare<u8>,
1239    I::Token: Clone + AsChar,
1240{
1241    alt((
1242        parse_pct_encoded,
1243        parse_unreserved_item,
1244        parse_sub_delim_item,
1245        literal(b':').void(),
1246    ))
1247    .parse_next(input)
1248}
1249
1250#[inline]
1251fn parse_userinfo_nz<I>(input: &mut I) -> ModalResult<()>
1252where
1253    I: Stream + StreamIsPartial + Compare<u8>,
1254    I::Token: Clone + AsChar,
1255{
1256    repeat::<_, _, (), _, _>(1.., parse_userinfo_item)
1257        .void()
1258        .parse_next(input)
1259}
1260
1261#[inline]
1262fn parse_pchar_item<I>(input: &mut I) -> ModalResult<()>
1263where
1264    I: Stream + StreamIsPartial + Compare<u8>,
1265    I::Token: Clone + AsChar,
1266{
1267    alt((
1268        parse_pct_encoded,
1269        parse_unreserved_item,
1270        parse_sub_delim_item,
1271        one_of([b':', b'@']).void(),
1272    ))
1273    .parse_next(input)
1274}
1275
1276#[inline]
1277fn parse_pchar_nc_item<I>(input: &mut I) -> ModalResult<()>
1278where
1279    I: Stream + StreamIsPartial + Compare<u8>,
1280    I::Token: Clone + AsChar,
1281{
1282    alt((
1283        parse_pct_encoded,
1284        parse_unreserved_item,
1285        parse_sub_delim_item,
1286        literal(b'@').void(),
1287    ))
1288    .parse_next(input)
1289}
1290
1291#[inline]
1292fn parse_query_or_fragment_item<I>(input: &mut I) -> ModalResult<()>
1293where
1294    I: Stream + StreamIsPartial + Compare<u8>,
1295    I::Token: Clone + AsChar,
1296{
1297    alt((parse_pchar_item, one_of([b'/', b'?']).void())).parse_next(input)
1298}
1299
1300#[inline]
1301fn parse_ipvfuture_item<I>(input: &mut I) -> ModalResult<()>
1302where
1303    I: Stream + StreamIsPartial,
1304    I::Token: Clone + AsChar,
1305{
1306    take_while(1..=1, is_ipvfuture_char)
1307        .void()
1308        .parse_next(input)
1309}
1310
1311#[inline]
1312fn parse_generic_authority<I>(input: &mut I) -> ModalResult<()>
1313where
1314    I: Stream + StreamIsPartial + Compare<u8>,
1315    I::Token: Clone + AsChar,
1316    I::Slice: AsRef<[u8]>,
1317{
1318    (
1319        opt(terminated(parse_userinfo, b'@')),
1320        parse_host,
1321        opt((b':', parse_port)),
1322    )
1323        .void()
1324        .parse_next(input)
1325}
1326
1327#[inline]
1328fn is_ipv4address_char(char: impl AsChar) -> bool {
1329    matches!(char.as_char(), '0'..='9' | '.')
1330}
1331
1332#[inline]
1333fn is_ipv6address_char(char: impl AsChar) -> bool {
1334    let char = char.as_char();
1335
1336    char.is_ascii_hexdigit() || matches!(char, ':' | '.')
1337}
1338
1339#[inline]
1340fn parse_ascii<T>(bytes: &[u8]) -> Option<T>
1341where
1342    T: str::FromStr,
1343{
1344    str::from_utf8(bytes).ok()?.parse().ok()
1345}
1346
1347#[inline]
1348fn is_dec_octet(bytes: &[u8]) -> bool {
1349    matches!(bytes, [b'0'..=b'9'])
1350        || matches!(bytes, [b'1'..=b'9', b'0'..=b'9'])
1351        || matches!(bytes, [b'1', b'0'..=b'9', b'0'..=b'9'])
1352        || matches!(bytes, [b'2', b'0'..=b'4', b'0'..=b'9'])
1353        || matches!(bytes, [b'2', b'5', b'0'..=b'5'])
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358    use winnow::{
1359        BStr, Partial,
1360        error::{ErrMode, Needed},
1361    };
1362
1363    use super::*;
1364
1365    macro_rules! assert_backtrack {
1366        ($parser:expr, $input:expr $(,)?) => {
1367            assert!(
1368                matches!(
1369                    $parser.parse_peek(BStr::new($input)),
1370                    Err(ErrMode::Backtrack(_))
1371                ),
1372                "assertion failed: parser did not backtrack for input {:?}: {:?}",
1373                $input,
1374                $parser.parse_peek(BStr::new($input)),
1375            );
1376        };
1377    }
1378
1379    macro_rules! assert_ok_remaining {
1380        ($parser:expr, $input:expr, $remaining:expr $(,)?) => {
1381            assert!(
1382                matches!(
1383                    $parser.parse_peek(BStr::new($input)),
1384                    Ok((remaining, _)) if remaining == BStr::new($remaining)
1385                ),
1386                "assertion failed: parser did not leave expected remaining input {:?}: {:?}",
1387                $remaining,
1388                $parser.parse_peek(BStr::new($input)),
1389            );
1390        };
1391    }
1392
1393    macro_rules! assert_partial_incomplete {
1394        ($parser:expr, $input:expr, $needed:expr $(,)?) => {
1395            assert_eq!(
1396                $parser.parse_peek(Partial::new(BStr::new($input))),
1397                Err(ErrMode::Incomplete($needed)),
1398            );
1399        };
1400    }
1401
1402    #[test]
1403    fn validates_char_groups() {
1404        assert!(is_unreserved('a'));
1405        assert!(!is_unreserved('/'));
1406        assert!(is_sub_delim('!'));
1407        assert!(!is_sub_delim('/'));
1408        assert!(is_gen_delim('/'));
1409        assert!(is_reserved('/'));
1410        assert!(is_hexdig('F'));
1411        assert!(!is_hexdig('g'));
1412        assert!(is_pchar('%'));
1413        assert!(is_pchar_nc('@'));
1414        assert!(!is_pchar_nc(':'));
1415        assert!(is_reg_name_char('%'));
1416        assert!(is_userinfo_char(':'));
1417        assert!(is_ipvfuture_char(':'));
1418        assert!(is_ip_literal_char('.'));
1419    }
1420
1421    #[test]
1422    fn validates_ip_literal_bodies() {
1423        assert!(!is_ip_literal_body(b""));
1424        assert!(!is_ip_literal_body(b"localhost"));
1425        assert!(!is_ip_literal_body(b"v1"));
1426        assert!(is_ip_literal_body(b"::1"));
1427        assert!(is_ip_literal_body(b"2001:db8::1"));
1428        assert!(is_ip_literal_body(b"v1.future-host"));
1429    }
1430
1431    #[test]
1432    fn parses_pct_encoded() {
1433        assert_backtrack!(parse_pct_encoded, b"");
1434        assert_backtrack!(parse_pct_encoded, b"%");
1435        assert_backtrack!(parse_pct_encoded, b"%2G");
1436        assert_partial_incomplete!(parse_pct_encoded, b"%2", Needed::new(1));
1437
1438        assert_ok_remaining!(parse_pct_encoded, b"%20rest", b"rest");
1439    }
1440
1441    #[test]
1442    fn parses_userinfo_and_reg_name() {
1443        assert_ok_remaining!(parse_userinfo, b"", b"");
1444        assert_ok_remaining!(parse_userinfo, b"user:pass@", b"@");
1445        assert_ok_remaining!(parse_reg_name, b"", b"");
1446        assert_ok_remaining!(parse_reg_name, b"example.com:80", b":80");
1447        assert_backtrack!(parse_uri_host, b"");
1448        assert_ok_remaining!(parse_uri_host, b"example.com:80", b":80");
1449    }
1450
1451    #[test]
1452    fn parses_ipv4_address_and_dec_octet() {
1453        assert_ok_remaining!(parse_dec_octet, b"0.", b".");
1454        assert_ok_remaining!(parse_dec_octet, b"255.", b".");
1455        assert_backtrack!(parse_dec_octet, b"256");
1456        assert_backtrack!(parse_dec_octet, b"01");
1457
1458        assert_ok_remaining!(parse_ipv4address, b"127.0.0.1:80", b":80");
1459        assert_backtrack!(parse_ipv4address, b"256.0.0.1");
1460        assert_backtrack!(parse_ipv4address, b"127.0.0");
1461    }
1462
1463    #[test]
1464    fn parses_ip_literal_variants() {
1465        assert_ok_remaining!(parse_h16, b"abcd:", b":");
1466        assert_ok_remaining!(parse_ls32, b"abcd:ef01", b"");
1467        assert_ok_remaining!(parse_ls32, b"192.0.2.1", b"");
1468
1469        assert_ok_remaining!(parse_ipv6address, b"2001:db8::1]", b"]");
1470        assert_ok_remaining!(parse_ipvfuture, b"vF.token:part]", b"]");
1471        assert_backtrack!(parse_ipvfuture, b"v.");
1472        assert_ok_remaining!(parse_ip_literal, b"[::1]:443", b":443");
1473        assert_ok_remaining!(parse_ip_literal, b"[vF.token:part]/", b"/");
1474        assert_backtrack!(parse_ip_literal, b"[localhost]");
1475    }
1476
1477    #[test]
1478    fn parses_host_and_authority() {
1479        assert_ok_remaining!(parse_host, b":80", b":80");
1480        assert_ok_remaining!(parse_host, b"127.0.0.1:80", b":80");
1481        assert_ok_remaining!(parse_host, b"[::1]:80", b":80");
1482
1483        assert_backtrack!(parse_authority, b"");
1484        assert_ok_remaining!(parse_authority, b"user:pass@example.com:443/path", b"/path");
1485        assert_ok_remaining!(parse_authority, b"[::1]/path", b"/path");
1486    }
1487
1488    #[test]
1489    fn parses_path_variants() {
1490        assert_ok_remaining!(parse_segment, b":@", b"");
1491        assert_backtrack!(parse_segment_nz, b"");
1492        assert_backtrack!(parse_segment_nz_nc, b":");
1493        assert_ok_remaining!(parse_segment_nz_nc, b"abc@/rest", b"/rest");
1494
1495        assert_ok_remaining!(parse_path_abempty, b"/a/b?x=1", b"?x=1");
1496        assert_ok_remaining!(parse_path_absolute, b"/a/b?x=1", b"?x=1");
1497        assert_backtrack!(parse_path_absolute, b"foo/bar");
1498        assert_ok_remaining!(parse_path_noscheme, b"docs/latest?q=1", b"?q=1");
1499        assert_ok_remaining!(parse_path_noscheme, b"urn:ietf", b":ietf");
1500        assert_ok_remaining!(parse_path_rootless, b"urn:ietf:rfc:3986#frag", b"#frag");
1501        assert_ok_remaining!(parse_path_empty, b"?q=1", b"?q=1");
1502        assert_ok_remaining!(parse_path, b"/foo%20bar?baz", b"?baz");
1503        assert_ok_remaining!(parse_path, b"/foo%", b"%");
1504    }
1505
1506    #[test]
1507    fn parses_query_and_fragment() {
1508        assert_ok_remaining!(parse_query, b"foo=bar/baz?x#frag", b"#frag");
1509        assert_ok_remaining!(parse_query, b"foo%", b"%");
1510        assert_ok_remaining!(parse_fragment, b"section-2/part?a", b"");
1511    }
1512
1513    #[test]
1514    fn parses_hier_part_and_relative_part() {
1515        assert_ok_remaining!(parse_hier_part, b"//example.com/a?b", b"?b");
1516        assert_ok_remaining!(parse_hier_part, b"/a/b?c", b"?c");
1517        assert_ok_remaining!(parse_hier_part, b"mailto:John.Doe@example.com", b"");
1518
1519        assert_ok_remaining!(parse_relative_part, b"//example.com/a?b", b"?b");
1520        assert_ok_remaining!(parse_relative_part, b"/a/b?b", b"?b");
1521        assert_ok_remaining!(parse_relative_part, b"guides/setup#frag", b"#frag");
1522    }
1523
1524    #[test]
1525    fn parses_uri_forms() {
1526        assert_partial_incomplete!(parse_scheme, b"", Needed::new(1));
1527
1528        assert_ok_remaining!(
1529            parse_absolute_uri,
1530            b"mailto:John.Doe@example.com?subject=Hi#frag",
1531            b"#frag"
1532        );
1533        assert_ok_remaining!(parse_relative_ref, b"../images/logo.svg?v=2#hero", b"");
1534        assert_ok_remaining!(parse_uri, b"https://example.com/a/b?q=1#frag", b"");
1535        assert_ok_remaining!(parse_uri_reference, b"//example.com/path", b"");
1536        assert_ok_remaining!(parse_uri_reference, b"urn:ietf:rfc:3986", b"");
1537
1538        assert_backtrack!(parse_uri, b"//example.com/path");
1539        assert_ok_remaining!(parse_uri_reference, b"http://example.com/%", b"%");
1540    }
1541}