url_lite/
lib.rs

1/*!
2Port of the URL parser from
3[nodejs/http-parser](https://github.com/nodejs/http-parser) to Rust
4
5# Examples
6
7## Invalid input
8
9```rust
10use url_lite::{Url, ParseError};
11
12// Note that ParseError doesn't implement the Error trait unless the `unstable`
13// feature is enabled
14assert!(Url::parse("not-an-url") == Err(ParseError::Invalid))
15```
16
17## Valid input
18
19```rust
20use url_lite::Url;
21
22let input = "https://usr:pass@example.com:8080/some%20path?foo=bar#zzz";
23let url = Url::parse(input).expect("Invalid URL");
24
25assert_eq!(url.schema, Some("https"));
26assert_eq!(url.host, Some("example.com"));
27assert_eq!(url.port, Some("8080"));
28assert_eq!(url.path, Some("/some%20path"));
29assert_eq!(url.query, Some("foo=bar"));
30assert_eq!(url.fragment, Some("zzz"));
31assert_eq!(url.userinfo, Some("usr:pass"));
32```
33
34# Feature: `unstable`
35
36Implements [`core::error::Error`] for [`ParseError`]. Requires nightly due to
37[`error_in_core`](https://doc.rust-lang.org/unstable-book/library-features/error-in-core.html)
38
39# Caveats
40
41Although this is a port of the URL parser from http-parser and it passes all
42the tests, it has not been used in production. It is also not a generic parser,
43may not support all URLs, only returns slices, and performs no decoding.
44
45If you need a robust URL parser and are okay with std/alloc dependency, use
46[servo/rust-url](https://crates.io/crates/url) instead.
47
48*/
49
50#![forbid(unsafe_code)]
51#![cfg_attr(not(test), no_std)]
52#![cfg_attr(feature = "unstable", feature(error_in_core))]
53
54use core::fmt;
55
56#[derive(Debug, PartialEq, Eq)]
57pub enum ParseError {
58    InvalidConnect,
59    EmptyInput,
60    Whitespace,
61    NoHost,
62    Invalid,
63}
64
65impl fmt::Display for ParseError {
66    #[cfg(not(tarpaulin_include))]
67    #[cfg_attr(feature = "_nopanic", no_panic::no_panic)]
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            ParseError::InvalidConnect => {
71                write!(f, "CONNECT requests can only contain \"hostname:port\"")
72            }
73            ParseError::EmptyInput => write!(f, "Empty input"),
74            ParseError::Whitespace => write!(f, "Whitespace"),
75            ParseError::NoHost => {
76                write!(f, "host must be present if there is a schema")
77            }
78            ParseError::Invalid => write!(f, "Invalid URL"),
79        }
80    }
81}
82
83#[cfg(feature = "unstable")]
84impl core::error::Error for ParseError {}
85
86#[derive(Debug, PartialEq, Eq)]
87enum State {
88    SchemaSlash,
89    SchemaSlashSlash,
90    ServerStart,
91    QueryStringStart,
92    FragmentStart,
93    Schema,
94    ServerWithAt,
95    Server,
96    Path,
97    QueryString,
98    Fragment,
99}
100
101#[derive(Debug, PartialEq, Eq)]
102enum UrlFields {
103    Schema,
104    Host,
105    Path,
106    Query,
107    Fragment,
108}
109
110#[derive(Debug, PartialEq, Eq)]
111/// A parsed URL
112pub struct Url<'a> {
113    pub schema: Option<&'a str>,
114    pub host: Option<&'a str>,
115    pub port: Option<&'a str>,
116    pub path: Option<&'a str>,
117    pub query: Option<&'a str>,
118    pub fragment: Option<&'a str>,
119    pub userinfo: Option<&'a str>,
120}
121
122impl<'a> Url<'a> {
123    #[cfg_attr(feature = "_nopanic", no_panic::no_panic)]
124    /// Parse a URL from a string
125    ///
126    /// # Examples
127    ///
128    /// ```rust
129    /// use url_lite::Url;
130    /// # use url_lite::ParseError;
131    ///
132    /// # fn run() -> Result<(), ParseError> {
133    /// let url = Url::parse("http://example.com").expect("Invalid URL");
134    /// # Ok(())
135    /// # }
136    /// # run().unwrap();
137    /// ```
138    pub fn parse(buf: &'a str) -> Result<Url<'a>, ParseError> {
139        parse_url(buf, false)
140    }
141
142    /// Parse as a HTTP CONNECT method URL
143    ///
144    /// Will return an error if the URL contains anything other than hostname
145    /// and port
146    pub fn parse_connect(buf: &'a str) -> Result<Url<'a>, ParseError> {
147        parse_url(buf, true)
148    }
149}
150
151#[cfg_attr(feature = "_nopanic", no_panic::no_panic)]
152fn parse_url(buf: &str, is_connect: bool) -> Result<Url, ParseError> {
153    if buf.is_empty() {
154        return Err(ParseError::EmptyInput);
155    }
156
157    let mut url = Url {
158        schema: None,
159        host: None,
160        port: None,
161        path: None,
162        query: None,
163        fragment: None,
164        userinfo: None,
165    };
166
167    let mut state = State::ServerStart;
168    let mut old_uf: Option<UrlFields> = None;
169    let mut found_at = false;
170
171    let mut len = 0;
172    let mut off = 0;
173
174    for (i, p) in buf.chars().enumerate() {
175        let uf: UrlFields;
176
177        if p.is_whitespace() {
178            return Err(ParseError::Whitespace);
179        }
180
181        if i == 0 && !is_connect {
182            state = parse_url_start(p)?;
183        } else {
184            state = parse_url_char(state, p)?;
185        }
186
187        // Figure out the next field that we're operating on
188        match state {
189            // Skip delimeters
190            State::SchemaSlash
191            | State::SchemaSlashSlash
192            | State::ServerStart
193            | State::QueryStringStart
194            | State::FragmentStart => {
195                continue;
196            }
197            State::Schema => {
198                uf = UrlFields::Schema;
199            }
200            State::ServerWithAt => {
201                found_at = true;
202                uf = UrlFields::Host;
203            }
204            State::Server => {
205                uf = UrlFields::Host;
206            }
207            State::Path => {
208                uf = UrlFields::Path;
209            }
210            State::QueryString => {
211                uf = UrlFields::Query;
212            }
213            State::Fragment => {
214                uf = UrlFields::Fragment;
215            }
216        }
217
218        off += 1;
219        len += 1;
220
221        // Nothing's changed; soldier on
222        if old_uf.as_ref() == Some(&uf) {
223            continue;
224        }
225
226        if let Some(old_uf) = old_uf {
227            let value =
228                Some(buf.get(off - len..off).ok_or(ParseError::Invalid)?);
229            set_url_field(&old_uf, &mut url, value)
230        }
231        old_uf = Some(uf);
232        len = 0;
233        off = i;
234    }
235
236    if let Some(old_uf) = old_uf {
237        let value =
238            Some(buf.get(off - len..off + 1).ok_or(ParseError::Invalid)?);
239        set_url_field(&old_uf, &mut url, value)
240    }
241
242    // host must be present if there is a schema
243    // parsing http:///toto will fail
244    if url.schema.is_some() && url.host.is_none() {
245        return Err(ParseError::NoHost);
246    }
247
248    if let Some(host_buf) = url.host.take() {
249        url.host = None;
250
251        let mut host_state = if found_at {
252            HttpHostState::UserinfoStart
253        } else {
254            HttpHostState::HostStart
255        };
256
257        let mut off = 0;
258        let mut len = 0;
259
260        for (i, p) in host_buf.chars().enumerate() {
261            let new_host_state = parse_host_char(&host_state, p)?;
262
263            match new_host_state {
264                HttpHostState::Host => {
265                    if host_state != HttpHostState::Host {
266                        off = i;
267                        len = 0;
268                    }
269                    len += 1;
270                    url.host = Some(
271                        host_buf
272                            .get(off..off + len)
273                            .ok_or(ParseError::Invalid)?,
274                    );
275                }
276                HttpHostState::Hostv6 => {
277                    if host_state != HttpHostState::Hostv6 {
278                        off = i;
279                    }
280                    len += 1;
281                    url.host = Some(
282                        host_buf
283                            .get(off..off + len)
284                            .ok_or(ParseError::Invalid)?,
285                    );
286                }
287                HttpHostState::Hostv6ZoneStart | HttpHostState::Hostv6Zone => {
288                    len += 1;
289                    url.host = Some(
290                        host_buf
291                            .get(off..off + len)
292                            .ok_or(ParseError::Invalid)?,
293                    );
294                }
295                HttpHostState::HostPort => {
296                    if host_state != HttpHostState::HostPort {
297                        off = i;
298                        len = 0;
299                    }
300                    len += 1;
301                    url.port = Some(
302                        host_buf
303                            .get(off..off + len)
304                            .ok_or(ParseError::Invalid)?,
305                    );
306                }
307                HttpHostState::Userinfo => {
308                    if host_state != HttpHostState::Userinfo {
309                        off = i;
310                        len = 0;
311                    }
312                    len += 1;
313                    url.userinfo = Some(
314                        host_buf
315                            .get(off..off + len)
316                            .ok_or(ParseError::Invalid)?,
317                    );
318                }
319                _ => {}
320            }
321            host_state = new_host_state;
322        }
323
324        // Make sure we don't end somewhere unexpected
325        match host_state {
326            HttpHostState::HostStart
327            | HttpHostState::Hostv6Start
328            | HttpHostState::Hostv6
329            | HttpHostState::Hostv6ZoneStart
330            | HttpHostState::Hostv6Zone
331            | HttpHostState::HostPortStart
332            | HttpHostState::Userinfo
333            | HttpHostState::UserinfoStart => {
334                return Err(ParseError::Invalid);
335            }
336            _ => {}
337        }
338    }
339
340    if is_connect
341        && (url.schema.is_some()
342            || url.path.is_some()
343            || url.query.is_some()
344            || url.fragment.is_some()
345            || url.userinfo.is_some())
346    {
347        return Err(ParseError::InvalidConnect);
348    }
349
350    Ok(url)
351}
352
353#[cfg_attr(feature = "_nopanic", no_panic::no_panic)]
354fn set_url_field<'a>(
355    uf: &UrlFields,
356    mut url: &mut Url<'a>,
357    value: Option<&'a str>,
358) {
359    match uf {
360        UrlFields::Schema => url.schema = value,
361        UrlFields::Host => url.host = value,
362        UrlFields::Path => url.path = value,
363        UrlFields::Query => url.query = value,
364        UrlFields::Fragment => url.fragment = value,
365    };
366}
367
368#[cfg_attr(feature = "_nopanic", no_panic::no_panic)]
369fn is_mark(c: char) -> bool {
370    c == '-'
371        || c == '_'
372        || c == '.'
373        || c == '!'
374        || c == '~'
375        || c == '*'
376        || c == '\''
377        || c == '('
378        || c == ')'
379}
380
381#[cfg_attr(feature = "_nopanic", no_panic::no_panic)]
382fn is_userinfo_char(c: char) -> bool {
383    c.is_ascii_alphanumeric()
384        || is_mark(c)
385        || c == '%'
386        || c == ';'
387        || c == ':'
388        || c == '&'
389        || c == '='
390        || c == '+'
391        || c == '$'
392        || c == ','
393}
394
395#[cfg_attr(feature = "_nopanic", no_panic::no_panic)]
396fn is_url_char(c: char) -> bool {
397    !matches!(c, '\0'..='\u{001F}' | '#' | '?' | '\x7F')
398}
399
400#[cfg_attr(feature = "_nopanic", no_panic::no_panic)]
401fn parse_url_start(ch: char) -> Result<State, ParseError> {
402    // Proxied requests are followed by scheme of an absolute URI (alpha).
403    // All methods except CONNECT are followed by '/' or '*'.
404    if ch == '/' || ch == '*' {
405        return Ok(State::Path);
406    }
407
408    if ch.is_ascii_alphabetic() {
409        return Ok(State::Schema);
410    }
411
412    Err(ParseError::Invalid)
413}
414
415#[cfg_attr(feature = "_nopanic", no_panic::no_panic)]
416fn parse_url_char(state: State, ch: char) -> Result<State, ParseError> {
417    match state {
418        State::Schema => {
419            if ch.is_ascii_alphabetic() {
420                return Ok(state);
421            }
422
423            if ch == ':' {
424                return Ok(State::SchemaSlash);
425            }
426        }
427        State::SchemaSlash => {
428            if ch == '/' {
429                return Ok(State::SchemaSlashSlash);
430            }
431        }
432        State::SchemaSlashSlash => {
433            if ch == '/' {
434                return Ok(State::ServerStart);
435            }
436        }
437        State::ServerWithAt | State::ServerStart | State::Server => {
438            if state == State::ServerWithAt && ch == '@' {
439                return Err(ParseError::Invalid);
440            }
441
442            if ch == '/' {
443                return Ok(State::Path);
444            }
445
446            if ch == '?' {
447                return Ok(State::QueryStringStart);
448            }
449
450            if ch == '@' {
451                return Ok(State::ServerWithAt);
452            }
453
454            if is_userinfo_char(ch) || ch == '[' || ch == ']' {
455                return Ok(State::Server);
456            }
457        }
458        State::Path => {
459            if is_url_char(ch) {
460                return Ok(state);
461            }
462
463            if ch == '?' {
464                return Ok(State::QueryStringStart);
465            }
466
467            if ch == '#' {
468                return Ok(State::FragmentStart);
469            }
470        }
471        State::QueryStringStart | State::QueryString => {
472            if is_url_char(ch) {
473                return Ok(State::QueryString);
474            }
475
476            if ch == '?' {
477                // allow extra '?' in query string
478                return Ok(State::QueryString);
479            }
480
481            if ch == '#' {
482                return Ok(State::FragmentStart);
483            }
484        }
485        State::FragmentStart => {
486            if is_url_char(ch) {
487                return Ok(State::Fragment);
488            }
489        }
490        State::Fragment => {
491            if is_url_char(ch) {
492                return Ok(state);
493            }
494        }
495    };
496
497    // We should never fall out of the switch above unless there's an error
498    Err(ParseError::Invalid)
499}
500
501#[derive(Debug, PartialEq, Eq)]
502enum HttpHostState {
503    UserinfoStart,
504    Userinfo,
505    HostStart,
506    Hostv6Start,
507    Host,
508    Hostv6,
509    Hostv6End,
510    Hostv6ZoneStart,
511    Hostv6Zone,
512    HostPortStart,
513    HostPort,
514}
515
516#[cfg_attr(feature = "_nopanic", no_panic::no_panic)]
517fn is_host_char(c: char) -> bool {
518    c.is_ascii_alphanumeric() || c == '.' || c == '-'
519}
520
521#[cfg_attr(feature = "_nopanic", no_panic::no_panic)]
522fn parse_host_char(
523    s: &HttpHostState,
524    ch: char,
525) -> Result<HttpHostState, ParseError> {
526    match s {
527        HttpHostState::Userinfo | HttpHostState::UserinfoStart => {
528            if ch == '@' {
529                return Ok(HttpHostState::HostStart);
530            }
531
532            if is_userinfo_char(ch) {
533                return Ok(HttpHostState::Userinfo);
534            }
535        }
536        HttpHostState::HostStart => {
537            if ch == '[' {
538                return Ok(HttpHostState::Hostv6Start);
539            }
540
541            if is_host_char(ch) {
542                return Ok(HttpHostState::Host);
543            }
544        }
545        HttpHostState::Host => {
546            if is_host_char(ch) {
547                return Ok(HttpHostState::Host);
548            }
549            if ch == ':' {
550                return Ok(HttpHostState::HostPortStart);
551            }
552        }
553        HttpHostState::Hostv6End => {
554            if ch == ':' {
555                return Ok(HttpHostState::HostPortStart);
556            }
557        }
558        HttpHostState::Hostv6 | HttpHostState::Hostv6Start => {
559            if s == &HttpHostState::Hostv6 && ch == ']' {
560                return Ok(HttpHostState::Hostv6End);
561            }
562
563            if ch.is_ascii_hexdigit() || ch == ':' || ch == '.' {
564                return Ok(HttpHostState::Hostv6);
565            }
566
567            if s == &HttpHostState::Hostv6 && ch == '%' {
568                return Ok(HttpHostState::Hostv6ZoneStart);
569            }
570        }
571        HttpHostState::Hostv6Zone | HttpHostState::Hostv6ZoneStart => {
572            if s == &HttpHostState::Hostv6Zone && ch == ']' {
573                return Ok(HttpHostState::Hostv6End);
574            }
575
576            // RFC 6874 Zone ID consists of 1*( unreserved / pct-encoded)
577            if ch.is_ascii_alphanumeric()
578                || ch == '%'
579                || ch == '.'
580                || ch == '-'
581                || ch == '_'
582                || ch == '~'
583            {
584                return Ok(HttpHostState::Hostv6Zone);
585            }
586        }
587        HttpHostState::HostPort | HttpHostState::HostPortStart => {
588            if ch.is_ascii_digit() {
589                return Ok(HttpHostState::HostPort);
590            }
591        }
592    }
593
594    Err(ParseError::Invalid)
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use alloc_counter::{no_alloc, AllocCounterSystem};
601
602    #[global_allocator]
603    static A: AllocCounterSystem = AllocCounterSystem;
604
605    #[test]
606    #[no_alloc(forbid)]
607    #[should_panic]
608    /// Tests that the no_alloc attribute works
609    fn test_no_alloc() {
610        let _ = std::boxed::Box::new(8);
611    }
612
613    #[test]
614    #[no_alloc(forbid)]
615    fn test_proxy_request() {
616        let result = Url::parse("http://hostname/").unwrap();
617        let expected = Url {
618            schema: Some("http"),
619            host: Some("hostname"),
620            port: None,
621            path: Some("/"),
622            query: None,
623            fragment: None,
624            userinfo: None,
625        };
626        assert_eq!(expected, result);
627    }
628
629    #[test]
630    #[no_alloc(forbid)]
631    fn test_proxy_request_with_port() {
632        let result = Url::parse("http://hostname:444/").unwrap();
633        let expected = Url {
634            schema: Some("http"),
635            host: Some("hostname"),
636            port: Some("444"),
637            path: Some("/"),
638            query: None,
639            fragment: None,
640            userinfo: None,
641        };
642        assert_eq!(expected, result);
643    }
644
645    #[test]
646    #[no_alloc(forbid)]
647    fn test_connect_request() {
648        let result = Url::parse_connect("hostname:443").unwrap();
649        let expected = Url {
650            schema: None,
651            host: Some("hostname"),
652            port: Some("443"),
653            path: None,
654            query: None,
655            fragment: None,
656            userinfo: None,
657        };
658        assert_eq!(expected, result);
659    }
660
661    #[test]
662    #[no_alloc(forbid)]
663    fn test_proxy_ipv6_request() {
664        let result = Url::parse("http://[1:2::3:4]/").unwrap();
665        let expected = Url {
666            schema: Some("http"),
667            host: Some("1:2::3:4"),
668            port: None,
669            path: Some("/"),
670            query: None,
671            fragment: None,
672            userinfo: None,
673        };
674        assert_eq!(expected, result);
675    }
676
677    #[test]
678    #[no_alloc(forbid)]
679    fn test_proxy_ipv6_request_with_port() {
680        let result = Url::parse("http://[1:2::3:4]:67/").unwrap();
681        let expected = Url {
682            schema: Some("http"),
683            host: Some("1:2::3:4"),
684            port: Some("67"),
685            path: Some("/"),
686            query: None,
687            fragment: None,
688            userinfo: None,
689        };
690        assert_eq!(expected, result);
691    }
692
693    #[test]
694    #[no_alloc(forbid)]
695    fn test_connect_ipv6_address() {
696        let result = Url::parse_connect("[1:2::3:4]:443").unwrap();
697        let expected = Url {
698            schema: None,
699            host: Some("1:2::3:4"),
700            port: Some("443"),
701            path: None,
702            query: None,
703            fragment: None,
704            userinfo: None,
705        };
706        assert_eq!(expected, result);
707    }
708
709    #[test]
710    #[no_alloc(forbid)]
711    fn test_ipv4_in_ipv6_address() {
712        let result =
713            Url::parse("http://[2001:0000:0000:0000:0000:0000:1.9.1.1]/")
714                .unwrap();
715        let expected = Url {
716            schema: Some("http"),
717            host: Some("2001:0000:0000:0000:0000:0000:1.9.1.1"),
718            port: None,
719            path: Some("/"),
720            query: None,
721            fragment: None,
722            userinfo: None,
723        };
724        assert_eq!(expected, result);
725    }
726
727    #[test]
728    #[no_alloc(forbid)]
729    fn test_extra_question_in_query_string() {
730        let result = Url::parse(
731            "http://a.tbcdn.cn/p/fp/2010c/??fp-header-min\
732            .css,fp-base-min.css,fp-channel-min.css,fp-product-min.css,\
733            fp-mall-min.css,fp-category-min.css,fp-sub-min.css,fp-gdp4p-min\
734            .css,fp-css3-min.css,fp-misc-min.css?t=20101022.css",
735        )
736        .unwrap();
737        let expected = Url {
738            schema: Some("http"),
739            host: Some("a.tbcdn.cn"),
740            port: None,
741            path: Some("/p/fp/2010c/"),
742            query: Some(
743                "?fp-header-min.css,fp-base-min.css,fp-channel-min.css,\
744            fp-product-min.css,fp-mall-min.css,fp-category-min.css,\
745            fp-sub-min.css,fp-gdp4p-min.css,fp-css3-min.css,\
746            fp-misc-min.css?t=20101022.css",
747            ),
748            fragment: None,
749            userinfo: None,
750        };
751        assert_eq!(expected, result);
752    }
753
754    #[test]
755    #[no_alloc(forbid)]
756    fn test_space_url_encoded() {
757        let result = Url::parse("/toto.html?toto=a%20b").unwrap();
758        let expected = Url {
759            schema: None,
760            host: None,
761            port: None,
762            path: Some("/toto.html"),
763            query: Some("toto=a%20b"),
764            fragment: None,
765            userinfo: None,
766        };
767        assert_eq!(expected, result);
768    }
769
770    #[test]
771    #[no_alloc(forbid)]
772    fn test_url_fragment() {
773        let result = Url::parse("/toto.html#titi").unwrap();
774        let expected = Url {
775            schema: None,
776            host: None,
777            port: None,
778            path: Some("/toto.html"),
779            query: None,
780            fragment: Some("titi"),
781            userinfo: None,
782        };
783        assert_eq!(expected, result);
784    }
785
786    #[test]
787    #[no_alloc(forbid)]
788    fn test_complex_url_fragment() {
789        let result = Url::parse(
790            "http://www.webmasterworld.com/r.cgi?f=21&d=8405\
791            &url=http://www.example.com/index.html?foo=bar&hello=world#midpage",
792        )
793        .unwrap();
794        let expected = Url {
795            schema: Some("http"),
796            host: Some("www.webmasterworld.com"),
797            port: None,
798            path: Some("/r.cgi"),
799            query: Some(
800                "f=21&d=8405&url=http://www.example.com/index.html\
801                 ?foo=bar&hello=world",
802            ),
803            fragment: Some("midpage"),
804            userinfo: None,
805        };
806        assert_eq!(expected, result);
807    }
808
809    #[test]
810    #[no_alloc(forbid)]
811    fn test_complex_url_from_node_js_url_parser_doc() {
812        let result =
813            Url::parse("http://host.com:8080/p/a/t/h?query=string#hash")
814                .unwrap();
815        let expected = Url {
816            schema: Some("http"),
817            host: Some("host.com"),
818            port: Some("8080"),
819            path: Some("/p/a/t/h"),
820            query: Some("query=string"),
821            fragment: Some("hash"),
822            userinfo: None,
823        };
824        assert_eq!(expected, result);
825    }
826
827    #[test]
828    #[no_alloc(forbid)]
829    fn test_complex_url_with_basic_auth_from_node_js_url_parser_doc() {
830        let result =
831            Url::parse("http://a:b@host.com:8080/p/a/t/h?query=string#hash")
832                .unwrap();
833        let expected = Url {
834            schema: Some("http"),
835            host: Some("host.com"),
836            port: Some("8080"),
837            path: Some("/p/a/t/h"),
838            query: Some("query=string"),
839            fragment: Some("hash"),
840            userinfo: Some("a:b"),
841        };
842        assert_eq!(expected, result);
843    }
844
845    #[test]
846    #[no_alloc(forbid)]
847    fn test_proxy_basic_auth_with_space_url_encoded() {
848        let result = Url::parse("http://a%20:b@host.com/").unwrap();
849        let expected = Url {
850            schema: Some("http"),
851            host: Some("host.com"),
852            port: None,
853            path: Some("/"),
854            query: None,
855            fragment: None,
856            userinfo: Some("a%20:b"),
857        };
858        assert_eq!(expected, result);
859    }
860
861    #[test]
862    #[no_alloc(forbid)]
863    fn test_proxy_basic_auth_with_double_colon() {
864        let result = Url::parse("http://a::b@host.com/").unwrap();
865        let expected = Url {
866            schema: Some("http"),
867            host: Some("host.com"),
868            port: None,
869            path: Some("/"),
870            query: None,
871            fragment: None,
872            userinfo: Some("a::b"),
873        };
874        assert_eq!(expected, result);
875    }
876
877    #[test]
878    #[no_alloc(forbid)]
879    fn test_proxy_empty_basic_auth() {
880        let result = Url::parse("http://@hostname/fo").unwrap();
881        let expected = Url {
882            schema: Some("http"),
883            host: Some("hostname"),
884            port: None,
885            path: Some("/fo"),
886            query: None,
887            fragment: None,
888            userinfo: None,
889        };
890        assert_eq!(expected, result);
891    }
892
893    #[test]
894    #[no_alloc(forbid)]
895    fn test_proxy_basic_auth_with_unreservedchars() {
896        let result = Url::parse("http://a!;-_!=+$@host.com/").unwrap();
897        let expected = Url {
898            schema: Some("http"),
899            host: Some("host.com"),
900            port: None,
901            path: Some("/"),
902            query: None,
903            fragment: None,
904            userinfo: Some("a!;-_!=+$"),
905        };
906        assert_eq!(expected, result);
907    }
908
909    #[test]
910    #[no_alloc(forbid)]
911    fn test_ipv6_address_with_zone_id() {
912        let result = Url::parse("http://[fe80::a%25eth0]/").unwrap();
913        let expected = Url {
914            schema: Some("http"),
915            host: Some("fe80::a%25eth0"),
916            port: None,
917            path: Some("/"),
918            query: None,
919            fragment: None,
920            userinfo: None,
921        };
922        assert_eq!(expected, result);
923    }
924
925    #[test]
926    #[no_alloc(forbid)]
927    fn test_ipv6_address_with_zone_id_but_percent_is_not_percent_encoded() {
928        let result = Url::parse("http://[fe80::a%eth0]/").unwrap();
929        let expected = Url {
930            schema: Some("http"),
931            host: Some("fe80::a%eth0"),
932            port: None,
933            path: Some("/"),
934            query: None,
935            fragment: None,
936            userinfo: None,
937        };
938        assert_eq!(expected, result);
939    }
940
941    #[test]
942    #[no_alloc(forbid)]
943    fn test_double_at() {
944        let error = Url::parse("http://a:b@@hostname:443/").unwrap_err();
945        assert_eq!(error, ParseError::Invalid);
946    }
947
948    #[test]
949    #[no_alloc(forbid)]
950    fn test_proxy_empty_host() {
951        let error = Url::parse("http://:443/").unwrap_err();
952        assert_eq!(error, ParseError::Invalid);
953    }
954
955    #[test]
956    #[no_alloc(forbid)]
957    fn test_proxy_empty_port() {
958        let error = Url::parse("http://hostname:/").unwrap_err();
959        assert_eq!(error, ParseError::Invalid);
960    }
961
962    #[test]
963    #[no_alloc(forbid)]
964    fn test_connect_with_basic_auth() {
965        let error = Url::parse_connect("a:b@hostname:443").unwrap_err();
966        assert_eq!(error, ParseError::InvalidConnect);
967    }
968
969    #[test]
970    #[no_alloc(forbid)]
971    fn test_connect_empty_host() {
972        let error = Url::parse_connect(":443").unwrap_err();
973        assert_eq!(error, ParseError::Invalid);
974    }
975
976    #[test]
977    #[no_alloc(forbid)]
978    fn test_connect_empty_port() {
979        let error = Url::parse_connect("hostname:").unwrap_err();
980        assert_eq!(error, ParseError::Invalid);
981    }
982
983    #[test]
984    #[no_alloc(forbid)]
985    fn test_connect_with_extra_bits() {
986        let error = Url::parse_connect("hostname:443/").unwrap_err();
987        assert_eq!(error, ParseError::InvalidConnect);
988    }
989
990    #[test]
991    #[no_alloc(forbid)]
992    fn test_space_in_url() {
993        let error = Url::parse("/foo bar/").unwrap_err();
994        assert_eq!(error, ParseError::Whitespace);
995    }
996
997    #[test]
998    #[no_alloc(forbid)]
999    fn test_carriage_return_in_url() {
1000        let error = Url::parse("/foo\rbar/").unwrap_err();
1001        assert_eq!(error, ParseError::Whitespace);
1002    }
1003
1004    #[test]
1005    #[no_alloc(forbid)]
1006    fn test_proxy_double_colon_in_url() {
1007        let error = Url::parse("http://hostname::443/").unwrap_err();
1008        assert_eq!(error, ParseError::Invalid);
1009    }
1010
1011    #[test]
1012    #[no_alloc(forbid)]
1013    fn test_line_feed_in_url() {
1014        let error = Url::parse("/foo\nbar/").unwrap_err();
1015        assert_eq!(error, ParseError::Whitespace);
1016    }
1017
1018    #[test]
1019    #[no_alloc(forbid)]
1020    fn test_proxy_line_feed_in_hostname() {
1021        let error = Url::parse("http://host\name/fo").unwrap_err();
1022        assert_eq!(error, ParseError::Whitespace);
1023    }
1024
1025    #[test]
1026    #[no_alloc(forbid)]
1027    fn test_proxy_percentage_in_hostname() {
1028        let error = Url::parse("http://host%name/fo").unwrap_err();
1029        assert_eq!(error, ParseError::Invalid);
1030    }
1031
1032    #[test]
1033    #[no_alloc(forbid)]
1034    fn test_proxy_semicolon_in_hostname() {
1035        let error = Url::parse("http://host;ame/fo").unwrap_err();
1036        assert_eq!(error, ParseError::Invalid);
1037    }
1038
1039    #[test]
1040    #[no_alloc(forbid)]
1041    fn test_proxy_only_empty_basic_auth() {
1042        let error = Url::parse("http://@/fo").unwrap_err();
1043        assert_eq!(error, ParseError::Invalid);
1044    }
1045
1046    #[test]
1047    #[no_alloc(forbid)]
1048    fn test_proxy_only_basic_auth() {
1049        let error = Url::parse("http://toto@/fo").unwrap_err();
1050        assert_eq!(error, ParseError::Invalid);
1051    }
1052
1053    #[test]
1054    #[no_alloc(forbid)]
1055    fn test_proxy_emtpy_hostname() {
1056        let error = Url::parse("http:///fo").unwrap_err();
1057        assert_eq!(error, ParseError::NoHost);
1058    }
1059
1060    #[test]
1061    #[no_alloc(forbid)]
1062    fn test_proxy_equal_in_url() {
1063        let error = Url::parse("http://host=ame/fo").unwrap_err();
1064        assert_eq!(error, ParseError::Invalid);
1065    }
1066
1067    #[test]
1068    #[no_alloc(forbid)]
1069    fn test_ipv6_address_ending_with() {
1070        let error = Url::parse("http://[fe80::a%]/").unwrap_err();
1071        assert_eq!(error, ParseError::Invalid);
1072    }
1073
1074    #[test]
1075    #[no_alloc(forbid)]
1076    fn test_ipv6_address_with_zone_id_including_bad_character() {
1077        let error = Url::parse("http://[fe80::a%$HOME]/").unwrap_err();
1078        assert_eq!(error, ParseError::Invalid);
1079    }
1080
1081    #[test]
1082    #[no_alloc(forbid)]
1083    fn test_just_ipv6_zone_id() {
1084        let error = Url::parse("http://[%eth0]/").unwrap_err();
1085        assert_eq!(error, ParseError::Invalid);
1086    }
1087
1088    #[test]
1089    #[no_alloc(forbid)]
1090    fn test_empty_url() {
1091        let error = Url::parse("").unwrap_err();
1092        assert_eq!(error, ParseError::EmptyInput);
1093    }
1094
1095    #[test]
1096    #[no_alloc(forbid)]
1097    fn test_full_of_spaces_url() {
1098        let error = Url::parse("  ").unwrap_err();
1099        assert_eq!(error, ParseError::Whitespace);
1100    }
1101
1102    #[test]
1103    #[no_alloc(forbid)]
1104    fn test_tab_in_url() {
1105        let error = Url::parse("/foo	bar/").unwrap_err();
1106        assert_eq!(error, ParseError::Whitespace);
1107    }
1108
1109    #[test]
1110    #[no_alloc(forbid)]
1111    fn test_form_feed_in_url() {
1112        let error = Url::parse("/foo\x0cbar/").unwrap_err();
1113        assert_eq!(error, ParseError::Whitespace);
1114    }
1115
1116    #[test]
1117    #[no_alloc(forbid)]
1118    fn test_char_boundary_path_1() {
1119        let error = Url::parse("http://www.example.com/你好你好").unwrap_err();
1120        assert_eq!(error, ParseError::Invalid);
1121        let error =
1122            Url::parse("http://www.example.com/?q=你好#foo").unwrap_err();
1123        assert_eq!(error, ParseError::Invalid);
1124        let error = Url::parse("http://www.example.com/FOO/?foo=%A1%C1")
1125            .unwrap_err();
1126        assert_eq!(error, ParseError::Invalid);
1127        let error = Url::parse("http://www.example.com/FOO/?foo=%A1%C1")
1128            .unwrap_err();
1129        assert_eq!(error, ParseError::Invalid);
1130    }
1131
1132    #[test]
1133    #[no_alloc(forbid)]
1134    fn test_query_after_at() {
1135        let error = Url::parse("http://a:b@?foo").unwrap_err();
1136        assert_eq!(error, ParseError::Invalid);
1137    }
1138
1139    #[test]
1140    #[no_alloc(forbid)]
1141    fn test_invalid_fragment() {
1142        let error = Url::parse("http://hostname#你").unwrap_err();
1143        assert_eq!(error, ParseError::Invalid);
1144    }
1145
1146    #[test]
1147    #[no_alloc(forbid)]
1148    fn test_invalid_start() {
1149        let error = Url::parse("#").unwrap_err();
1150        assert_eq!(error, ParseError::Invalid);
1151    }
1152}