use super::{Destination, Host, HostParseError}; type PResult<'a, T> = Result<(&'a str, T), PError>; type PError = &'static str; /// Parses `s` into a [`Destination`] pub fn parse(s: &str) -> Result { let (s, scheme) = maybe(parse_scheme)(s)?; let (s, username_password) = maybe(parse_username_password)(s)?; // NOTE: We can have a host or host/port in a couple of different ways // // 1. IPv4 - 127.0.0.1 or 127.0.0.1:1234 // 2. IPv6 - ::1 or [::1]:1234 // 3. Name - localhost or localhost:1234 // // To determine path to take, we count the colons. If there is more than 1, we can assume IPv6 // is involved and can try to parse entirely as IPv6, or if that fails split off one colon and // try a second time. Otherwise, we assume that it is not IPv6 and can parse with a single // colon at most for a port. let colon_cnt = s.chars().filter(|c| *c == ':').count(); let (s, host) = if colon_cnt > 1 { // Either the host is [{}] with a port following, or the host is everything either( delimited( parse_char('['), parse_and_then(parse_until(|c| c == ']'), parse_host), parse_char(']'), ), parse_host, )(s)? } else { parse_and_then(parse_until(|c| c == ':'), parse_host)(s)? }; let (s, port) = maybe(prefixed(parse_char(':'), parse_port))(s)?; if !s.is_empty() { return Err("Str has more characters after destination"); } Ok(Destination { scheme: scheme.map(ToString::to_string), username: username_password .as_ref() .and_then(|up| up.0) .map(ToString::to_string), password: username_password .as_ref() .and_then(|up| up.1) .map(ToString::to_string), host, port, }) } fn parse_scheme(s: &str) -> PResult<&str> { let (scheme, remaining) = s.split_once("://").ok_or("Scheme missing ://")?; if scheme .chars() .all(|c| c.is_alphanumeric() || c == '+' || c == '.' || c == '-') { Ok((remaining, scheme)) } else { Err("Invalid scheme") } } fn parse_username_password(s: &str) -> PResult<(Option<&str>, Option<&str>)> { let (auth, remaining) = s.split_once('@').ok_or("Auth missing @")?; let (auth, username) = maybe(parse_until(|c| !c.is_alphanumeric()))(auth)?; let (auth, password) = maybe(prefixed( parse_char(':'), parse_until(|c| !c.is_alphanumeric()), ))(auth)?; if !auth.is_empty() { return Err("Dangling characters after username/password"); } Ok((remaining, (username, password))) } fn parse_host(s: &str) -> PResult { let host = s.parse::().map_err(HostParseError::into_static_str)?; Ok(("", host)) } fn parse_port(s: &str) -> PResult { let port = s .parse::() .map_err(|_| "Not an unsigned 16-bit integer")?; Ok(("", port)) } /// Execute parsers in order from left to right, returning the result of the first that succeeds fn either<'a, T>( left: impl Fn(&'a str) -> PResult<'a, T>, right: impl Fn(&'a str) -> PResult<'a, T>, ) -> impl Fn(&'a str) -> PResult<'a, T> { move |s: &str| { if let Ok((s, value)) = left(s) { Ok((s, value)) } else { right(s) } } } /// Execute three parsers in a row, failing if any fails, and returns second parser's result fn delimited<'a, T1, T2, T3>( p1: impl Fn(&'a str) -> PResult<'a, T1>, p2: impl Fn(&'a str) -> PResult<'a, T2>, p3: impl Fn(&'a str) -> PResult<'a, T3>, ) -> impl Fn(&'a str) -> PResult<'a, T2> { move |s: &str| { let (s, _) = p1(s)?; let (s, value) = p2(s)?; let (s, _) = p3(s)?; Ok((s, value)) } } /// Execute two parsers in a row, failing if either fails, and returns second parser's result fn prefixed<'a, T1, T2>( prefix_parser: impl Fn(&'a str) -> PResult<'a, T1>, parser: impl Fn(&'a str) -> PResult<'a, T2>, ) -> impl Fn(&'a str) -> PResult<'a, T2> { move |s: &str| { let (s, _) = prefix_parser(s)?; let (s, value) = parser(s)?; Ok((s, value)) } } /// Execute a parser, returning Some(value) if succeeds and None if fails fn maybe<'a, T>( parser: impl Fn(&'a str) -> PResult<'a, T>, ) -> impl Fn(&'a str) -> PResult<'a, Option> { move |s: &str| match parser(s) { Ok((remaining, value)) => Ok((remaining, Some(value))), Err(_) => Ok((s, None)), } } /// Parses using `first`, and then feeds result into `second`, failing if `second` does not fully /// parse the result of `first` fn parse_and_then<'a, T>( first: impl Fn(&'a str) -> PResult<'a, &'a str>, second: impl Fn(&'a str) -> PResult<'a, T>, ) -> impl Fn(&'a str) -> PResult<'a, T> { move |s: &str| { let (s, first_s) = first(s)?; let (first_s, value) = second(first_s)?; if !first_s.is_empty() { return Err("Second parser did not fully consume results of first parser"); } Ok((s, value)) } } /// Parse str until predicate returns true, failing if nothing parsed fn parse_until(predicate: impl Fn(char) -> bool) -> impl Fn(&str) -> PResult<&str> { move |s: &str| { if s.is_empty() { return Err("Empty str"); } let (s, value) = match s.char_indices().find(|(_, c)| predicate(*c)) { // Position represents the first character (at boundary) that is not alphanumeric Some((i, _)) => (&s[i..], &s[..i]), // No position means that the remainder of the str was alphanumeric None => ("", s), }; if value.is_empty() { return Err("Predicate immediately returned true"); } Ok((s, value)) } } /// Parse a single character fn parse_char(c: char) -> impl Fn(&str) -> PResult { move |s: &str| { if s.is_empty() { return Err("Empty str"); } if s.starts_with(c) { Ok((&s[1..], c)) } else { Err("Wrong char") } } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_should_fail_if_string_is_only_whitespace() { let _ = parse("").unwrap_err(); let _ = parse(" ").unwrap_err(); let _ = parse("\t").unwrap_err(); let _ = parse("\n").unwrap_err(); let _ = parse("\r").unwrap_err(); let _ = parse("\r\n").unwrap_err(); } #[test] fn parse_should_succeed_when_parsing_valid_destination() { // Minimal example let destination = parse("example.com").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username, None); assert_eq!(destination.password, None); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, None); // Full example let destination = parse("scheme://username:password@example.com:22").unwrap(); assert_eq!(destination.scheme.as_deref(), Some("scheme")); assert_eq!(destination.username.as_deref(), Some("username")); assert_eq!(destination.password.as_deref(), Some("password")); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, Some(22)); } #[test] fn parse_should_fail_if_given_path() { let _ = parse("/").unwrap_err(); let _ = parse("/localhost").unwrap_err(); let _ = parse("my/path").unwrap_err(); let _ = parse("/my/path").unwrap_err(); let _ = parse("//localhost").unwrap_err(); } mod parsers { use super::*; fn parse_fail(_: &str) -> PResult<&str> { Err("bad parser") } fn parse_all(s: &str) -> PResult<&str> { Ok(("", s)) } fn parse_cnt(cnt: usize) -> impl Fn(&str) -> PResult<&str> { move |s: &str| match s.char_indices().nth(cnt) { Some((i, _)) => Ok((&s[i..], &s[..i])), None => Err("Not enough characters"), } } mod parse_scheme { use super::*; #[test] fn should_fail_if_not_ending_properly() { let _ = parse_scheme("scheme").unwrap_err(); } #[test] fn should_fail_if_scheme_has_invalid_character() { let _ = parse_scheme("sche_me://").unwrap_err(); } #[test] fn should_return_scheme_if_valid() { let (s, scheme) = parse_scheme("scheme+.-://").unwrap(); assert_eq!(s, ""); assert_eq!(scheme, "scheme+.-"); } #[test] fn should_consume_up_to_the_ending_sequence() { let (s, scheme) = parse_scheme("scheme+.-://example.com").unwrap(); assert_eq!(s, "example.com"); assert_eq!(scheme, "scheme+.-"); } } mod parse_username_password { use super::*; #[test] fn should_fail_if_not_ending_properly() { let _ = parse_username_password("username:password").unwrap_err(); } #[test] fn should_fail_if_username_not_alphanumeric() { let _ = parse_username_password("us\x1bername:password@").unwrap_err(); } #[test] fn should_fail_if_password_not_alphanumeric() { let _ = parse_username_password("username:pas\x1bsword@").unwrap_err(); } #[test] fn should_return_username_if_available() { let (s, username_password) = parse_username_password("username@").unwrap(); assert_eq!(s, ""); assert_eq!(username_password.0, Some("username")); assert_eq!(username_password.1, None); } #[test] fn should_return_password_if_available() { let (s, username_password) = parse_username_password(":password@").unwrap(); assert_eq!(s, ""); assert_eq!(username_password.0, None); assert_eq!(username_password.1, Some("password")); } #[test] fn should_return_username_and_password_if_available() { let (s, username_password) = parse_username_password("username:password@").unwrap(); assert_eq!(s, ""); assert_eq!(username_password.0, Some("username")); assert_eq!(username_password.1, Some("password")); } #[test] fn should_consume_up_to_the_ending_sequence() { let (s, username_password) = parse_username_password("username:password@example.com").unwrap(); assert_eq!(s, "example.com"); assert_eq!(username_password.0, Some("username")); assert_eq!(username_password.1, Some("password")); } } mod parse_host { use super::*; use std::net::{Ipv4Addr, Ipv6Addr}; #[test] fn should_fail_if_domain_name_is_invalid() { let _ = parse_host("").unwrap_err(); let _ = parse_host(".").unwrap_err(); } #[test] fn should_succeed_if_ipv4_address() { let (s, host) = parse_host("127.0.0.1").unwrap(); assert_eq!(s, ""); assert_eq!(host, Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1))); } #[test] fn should_succeed_if_ipv6_address() { let (s, host) = parse_host("::1").unwrap(); assert_eq!(s, ""); assert_eq!(host, Host::Ipv6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))); } #[test] fn should_succeed_if_domain_name_is_valid() { let (s, host) = parse_host("example.com").unwrap(); assert_eq!(s, ""); assert_eq!(host, Host::Name("example.com".to_string())); } } mod parse_port { use super::*; #[test] fn should_fail_if_input_cannot_be_parsed_as_a_u16() { let _ = parse_port("").unwrap_err(); let _ = parse_port("a").unwrap_err(); let _ = parse_port("-1").unwrap_err(); let _ = parse_port("0.1").unwrap_err(); let _ = parse_port(&(u16::MAX as u32 + 1u32).to_string()).unwrap_err(); } #[test] fn should_succeed_if_input_can_be_parsed_as_a_u16() { let (s, value) = parse_port("12345").unwrap(); assert_eq!(s, ""); assert_eq!(value, 12345); } } mod prefixed { use super::*; #[test] fn should_fail_if_prefix_parser_fails() { let _ = prefixed(parse_fail, parse_all)("abc").unwrap_err(); } #[test] fn should_fail_if_main_parser_fails() { let _ = prefixed(parse_cnt(1), parse_fail)("abc").unwrap_err(); } #[test] fn should_return_value_of_main_parser_when_succeeds() { let (s, value) = prefixed(parse_cnt(1), parse_cnt(1))("abc").unwrap(); assert_eq!(s, "c"); assert_eq!(value, "b"); } } mod maybe { use super::*; #[test] fn should_return_some_value_if_wrapped_parser_succeeds() { let (s, value) = maybe(parse_cnt(2))("abc").unwrap(); assert_eq!(s, "c"); assert_eq!(value, Some("ab")); } #[test] fn should_return_none_if_wrapped_parser_fails() { let (s, value) = maybe(parse_fail)("abc").unwrap(); assert_eq!(s, "abc"); assert_eq!(value, None); } } mod parse_and_then { use super::*; #[test] fn should_fail_if_first_parser_fails() { let _ = parse_and_then(parse_fail, parse_all)("abc").unwrap_err(); } #[test] fn should_fail_if_second_parser_fails() { let _ = parse_and_then(parse_all, parse_fail)("abc").unwrap_err(); } #[test] fn should_fail_if_second_parser_does_not_fully_consume_first_parser_output() { let _ = parse_and_then(parse_all, parse_cnt(2))("abc").unwrap_err(); } #[test] fn should_consume_with_first_parser_and_then_return_results_of_feeding_into_second_parser( ) { let (s, text) = parse_and_then(parse_cnt(2), parse_all)("abc").unwrap(); assert_eq!(s, "c"); assert_eq!(text, "ab"); } } mod parse_until { use super::*; #[test] fn should_consume_until_predicate_matches() { let (s, text) = parse_until(|c| c == 'b')("abc").unwrap(); assert_eq!(s, "bc"); assert_eq!(text, "a"); } #[test] fn should_consume_completely_if_predicate_never_matches() { let (s, text) = parse_until(|c| c == 'z')("abc").unwrap(); assert_eq!(s, ""); assert_eq!(text, "abc"); } #[test] fn should_fail_if_nothing_consumed() { let _ = parse_until(|c| c == 'a')("abc").unwrap_err(); } #[test] fn should_fail_if_input_is_empty() { let _ = parse_until(|c| c == 'a')("").unwrap_err(); } } mod parse_char { use super::*; #[test] fn should_succeed_if_next_char_matches() { let (s, c) = parse_char('a')("abc").unwrap(); assert_eq!(s, "bc"); assert_eq!(c, 'a'); } #[test] fn should_fail_if_next_char_does_not_match() { let _ = parse_char('b')("abc").unwrap_err(); } #[test] fn should_fail_if_input_is_empty() { let _ = parse_char('a')("").unwrap_err(); } } } mod examples { use super::*; #[test] fn parse_should_succeed_if_given_just_host() { let destination = parse("example.com").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username, None); assert_eq!(destination.password, None); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, None); } #[test] fn parse_should_succeed_if_given_scheme_and_host() { let destination = parse("scheme://example.com").unwrap(); assert_eq!(destination.scheme.as_deref(), Some("scheme")); assert_eq!(destination.username, None); assert_eq!(destination.password, None); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, None); } #[test] fn parse_should_succeed_if_given_username_and_host() { let destination = parse("username@example.com").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username.as_deref(), Some("username")); assert_eq!(destination.password, None); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, None); } #[test] fn parse_should_succeed_if_given_password_and_host() { let destination = parse(":password@example.com").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username, None); assert_eq!(destination.password.as_deref(), Some("password")); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, None); } #[test] fn parse_should_succeed_if_given_host_and_port() { let destination = parse("example.com:22").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username, None); assert_eq!(destination.password, None); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, Some(22)); } #[test] fn parse_should_succeed_if_given_scheme_username_and_host() { let destination = parse("scheme://username@example.com").unwrap(); assert_eq!(destination.scheme.as_deref(), Some("scheme")); assert_eq!(destination.username.as_deref(), Some("username")); assert_eq!(destination.password, None); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, None); } #[test] fn parse_should_succeed_if_given_scheme_password_and_host() { let destination = parse("scheme://:password@example.com").unwrap(); assert_eq!(destination.scheme.as_deref(), Some("scheme")); assert_eq!(destination.username, None); assert_eq!(destination.password.as_deref(), Some("password")); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, None); } #[test] fn parse_should_succeed_if_given_scheme_host_and_port() { let destination = parse("scheme://example.com:22").unwrap(); assert_eq!(destination.scheme.as_deref(), Some("scheme")); assert_eq!(destination.username, None); assert_eq!(destination.password, None); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, Some(22)); } #[test] fn parse_should_succeed_if_given_scheme_username_password_and_host() { let destination = parse("scheme://username:password@example.com").unwrap(); assert_eq!(destination.scheme.as_deref(), Some("scheme")); assert_eq!(destination.username.as_deref(), Some("username")); assert_eq!(destination.password.as_deref(), Some("password")); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, None); } #[test] fn parse_should_succeed_if_given_scheme_username_host_and_port() { let destination = parse("scheme://username@example.com:22").unwrap(); assert_eq!(destination.scheme.as_deref(), Some("scheme")); assert_eq!(destination.username.as_deref(), Some("username")); assert_eq!(destination.password, None); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, Some(22)); } #[test] fn parse_should_succeed_if_given_scheme_password_host_and_port() { let destination = parse("scheme://:password@example.com:22").unwrap(); assert_eq!(destination.scheme.as_deref(), Some("scheme")); assert_eq!(destination.username, None); assert_eq!(destination.password.as_deref(), Some("password")); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, Some(22)); } #[test] fn parse_should_succeed_if_given_scheme_username_password_host_and_port() { let destination = parse("scheme://username:password@example.com:22").unwrap(); assert_eq!(destination.scheme.as_deref(), Some("scheme")); assert_eq!(destination.username.as_deref(), Some("username")); assert_eq!(destination.password.as_deref(), Some("password")); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, Some(22)); } #[test] fn parse_should_succeed_if_given_username_password_and_host() { let destination = parse("username:password@example.com").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username.as_deref(), Some("username")); assert_eq!(destination.password.as_deref(), Some("password")); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, None); } #[test] fn parse_should_succeed_if_given_username_host_and_port() { let destination = parse("username@example.com:22").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username.as_deref(), Some("username")); assert_eq!(destination.password, None); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, Some(22)); } #[test] fn parse_should_succeed_if_given_password_host_and_port() { let destination = parse(":password@example.com:22").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username, None); assert_eq!(destination.password.as_deref(), Some("password")); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, Some(22)); } #[test] fn parse_should_succeed_if_given_username_password_host_and_port() { let destination = parse("username:password@example.com:22").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username.as_deref(), Some("username")); assert_eq!(destination.password.as_deref(), Some("password")); assert_eq!(destination.host, "example.com"); assert_eq!(destination.port, Some(22)); } #[test] fn parse_should_succeed_if_given_ipv4_host() { let destination = parse("127.0.0.1").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username, None); assert_eq!(destination.password, None); assert_eq!(destination.host, "127.0.0.1"); assert_eq!(destination.port, None); } #[test] fn parse_should_succeed_if_given_ipv4_host_and_port() { let destination = parse("127.0.0.1:12345").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username, None); assert_eq!(destination.password, None); assert_eq!(destination.host, "127.0.0.1"); assert_eq!(destination.port, Some(12345)); } #[test] fn parse_should_succeed_if_given_ipv6_host() { let destination = parse("::1").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username, None); assert_eq!(destination.password, None); assert_eq!(destination.host, "::1"); assert_eq!(destination.port, None); } #[test] fn parse_should_succeed_if_given_ipv6_host_and_port() { let destination = parse("[::1]:12345").unwrap(); assert_eq!(destination.scheme, None); assert_eq!(destination.username, None); assert_eq!(destination.password, None); assert_eq!(destination.host, "::1"); assert_eq!(destination.port, Some(12345)); } #[test] fn parse_should_succeed_with_distant_server_output() { // This is an example of what a server might output that includes a 32-byte key let destination = parse(concat!( "distant://", ":d561d38251700a5ac0b162c19e0c961832a64990ee19e33f7a5728f0615b2013@", "localhost", ":59699", )) .unwrap(); assert_eq!(destination.scheme.as_deref(), Some("distant")); assert_eq!(destination.username.as_deref(), None); assert_eq!( destination.password.as_deref(), Some("d561d38251700a5ac0b162c19e0c961832a64990ee19e33f7a5728f0615b2013") ); assert_eq!(destination.host, "localhost"); assert_eq!(destination.port, Some(59699)); } } }