mirror of https://github.com/chipsenkbeil/distant
Rewrite Destination to no longer use uriparse library
parent
86b34d23c6
commit
768dbdc053
@ -0,0 +1,313 @@
|
||||
use crate::serde_str::{deserialize_from_str, serialize_to_str};
|
||||
use derive_more::{Display, Error, From};
|
||||
use serde::{de::Deserializer, ser::Serializer, Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt,
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// Represents the host of a destination
|
||||
#[derive(Clone, Debug, From, Display, Hash, PartialEq, Eq)]
|
||||
pub enum Host {
|
||||
Ipv4(Ipv4Addr),
|
||||
Ipv6(Ipv6Addr),
|
||||
|
||||
/// Represents a hostname that follows the
|
||||
/// [DoD Internet Host Table Specification](https://www.ietf.org/rfc/rfc0952.txt):
|
||||
///
|
||||
/// * Hostname can be a maximum of 253 characters including '.'
|
||||
/// * Each label is a-zA-Z0-9 alongside hyphen ('-') and a maximum size of 63 characters
|
||||
/// * Labels can be segmented by periods ('.')
|
||||
Name(String),
|
||||
}
|
||||
|
||||
impl Host {
|
||||
/// Indicates whether the host destination is globally routable
|
||||
pub const fn is_global(&self) -> bool {
|
||||
match self {
|
||||
Self::Ipv4(x) => {
|
||||
!(x.is_broadcast()
|
||||
|| x.is_documentation()
|
||||
|| x.is_link_local()
|
||||
|| x.is_loopback()
|
||||
|| x.is_private()
|
||||
|| x.is_unspecified())
|
||||
}
|
||||
Self::Ipv6(x) => {
|
||||
// NOTE: 14 is the global flag
|
||||
x.is_multicast() && (x.segments()[0] & 0x000f == 14)
|
||||
}
|
||||
Self::Name(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Error, Hash, PartialEq, Eq)]
|
||||
pub enum HostParseError {
|
||||
EmptyLabel,
|
||||
EndsWithHyphen,
|
||||
EndsWithPeriod,
|
||||
InvalidLabel,
|
||||
LargeLabel,
|
||||
LargeName,
|
||||
StartsWithHyphen,
|
||||
StartsWithPeriod,
|
||||
}
|
||||
|
||||
impl HostParseError {
|
||||
/// Returns a static `str` describing the error
|
||||
pub const fn into_static_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::EmptyLabel => "Hostname cannot have an empty label",
|
||||
Self::EndsWithHyphen => "Hostname cannot end with hyphen ('-')",
|
||||
Self::EndsWithPeriod => "Hostname cannot end with period ('.')",
|
||||
Self::InvalidLabel => "Hostname label can only be a-zA-Z0-9 or hyphen ('-')",
|
||||
Self::LargeLabel => "Hostname label larger cannot be larger than 63 characters",
|
||||
Self::LargeName => "Hostname cannot be larger than 253 characters",
|
||||
Self::StartsWithHyphen => "Hostname cannot start with hyphen ('-')",
|
||||
Self::StartsWithPeriod => "Hostname cannot start with period ('.')",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HostParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.into_static_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Host {
|
||||
type Err = HostParseError;
|
||||
|
||||
/// Parses a host from a str
|
||||
///
|
||||
/// ### Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use distant_core::Host;
|
||||
/// # use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
/// // IPv4 address
|
||||
/// assert_eq!("127.0.0.1".parse(), Ok(Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1))));
|
||||
///
|
||||
/// // IPv6 address
|
||||
/// assert_eq!("::1".parse(), Ok(Host::Ipv6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))));
|
||||
///
|
||||
/// // Valid hostname
|
||||
/// assert_eq!("localhost".parse(), Ok(Host::Name("localhost".to_string())));
|
||||
///
|
||||
/// // Invalid hostname
|
||||
/// assert!("local_host".parse::<Host>().is_err());
|
||||
/// ```
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// Check if the str is a valid Ipv4 or Ipv6 address first
|
||||
if let Ok(x) = s.parse::<Ipv4Addr>() {
|
||||
return Ok(Self::Ipv4(x));
|
||||
} else if let Ok(x) = s.parse::<Ipv6Addr>() {
|
||||
return Ok(Self::Ipv6(x));
|
||||
}
|
||||
|
||||
// NOTE: We have to catch an empty string seprately from invalid label checks
|
||||
if s.is_empty() {
|
||||
return Err(HostParseError::InvalidLabel);
|
||||
}
|
||||
|
||||
// Since it is not, we need to validate the string as a hostname
|
||||
let mut label_size_cnt = 0;
|
||||
let mut last_char = None;
|
||||
for (i, c) in s.char_indices() {
|
||||
if i >= 253 {
|
||||
return Err(HostParseError::LargeName);
|
||||
}
|
||||
|
||||
// Dot and hyphen cannot be first character
|
||||
if i == 0 && c == '.' {
|
||||
return Err(HostParseError::StartsWithPeriod);
|
||||
} else if i == 0 && c == '-' {
|
||||
return Err(HostParseError::StartsWithHyphen);
|
||||
}
|
||||
|
||||
if c.is_alphanumeric() {
|
||||
label_size_cnt += 1;
|
||||
if label_size_cnt > 63 {
|
||||
return Err(HostParseError::LargeLabel);
|
||||
}
|
||||
} else if c == '.' {
|
||||
// Back-to-back dots are not allowed (would indicate an empty label, which is
|
||||
// reserved)
|
||||
if label_size_cnt == 0 {
|
||||
return Err(HostParseError::EmptyLabel);
|
||||
}
|
||||
|
||||
label_size_cnt = 0;
|
||||
} else if c != '-' {
|
||||
return Err(HostParseError::InvalidLabel);
|
||||
}
|
||||
|
||||
last_char = Some(c);
|
||||
}
|
||||
|
||||
if last_char == Some('.') {
|
||||
return Err(HostParseError::EndsWithPeriod);
|
||||
} else if last_char == Some('-') {
|
||||
return Err(HostParseError::EndsWithHyphen);
|
||||
}
|
||||
|
||||
Ok(Self::Name(s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for Host {
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
match self {
|
||||
Self::Ipv4(x) => x.to_string() == other,
|
||||
Self::Ipv6(x) => x.to_string() == other,
|
||||
Self::Name(x) => x == other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<&'a str> for Host {
|
||||
fn eq(&self, other: &&'a str) -> bool {
|
||||
match self {
|
||||
Self::Ipv4(x) => x.to_string() == *other,
|
||||
Self::Ipv6(x) => x.to_string() == *other,
|
||||
Self::Name(x) => x == other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Host {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serialize_to_str(self, serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Host {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_from_str(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn display_should_output_ipv4_correctly() {
|
||||
let host = Host::Ipv4(Ipv4Addr::LOCALHOST);
|
||||
assert_eq!(host.to_string(), "127.0.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_should_output_ipv6_correctly() {
|
||||
let host = Host::Ipv6(Ipv6Addr::LOCALHOST);
|
||||
assert_eq!(host.to_string(), "::1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_should_output_hostname_verbatim() {
|
||||
let host = Host::Name("localhost".to_string());
|
||||
assert_eq!(host.to_string(), "localhost");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_fail_if_str_is_empty() {
|
||||
let err = "".parse::<Host>().unwrap_err();
|
||||
assert_eq!(err, HostParseError::InvalidLabel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_fail_if_str_is_larger_than_253_characters() {
|
||||
// 63 + 1 + 63 + 1 + 63 + 1 + 62 = 254 characters
|
||||
let long_name = format!(
|
||||
"{}.{}.{}.{}",
|
||||
"a".repeat(63),
|
||||
"a".repeat(63),
|
||||
"a".repeat(63),
|
||||
"a".repeat(62)
|
||||
);
|
||||
let err = long_name.parse::<Host>().unwrap_err();
|
||||
assert_eq!(err, HostParseError::LargeName);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_fail_if_str_starts_with_period() {
|
||||
let err = ".localhost".parse::<Host>().unwrap_err();
|
||||
assert_eq!(err, HostParseError::StartsWithPeriod);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_fail_if_str_ends_with_period() {
|
||||
let err = "localhost.".parse::<Host>().unwrap_err();
|
||||
assert_eq!(err, HostParseError::EndsWithPeriod);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_fail_if_str_starts_with_hyphen() {
|
||||
let err = "-localhost".parse::<Host>().unwrap_err();
|
||||
assert_eq!(err, HostParseError::StartsWithHyphen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_fail_if_str_ends_with_hyphen() {
|
||||
let err = "localhost-".parse::<Host>().unwrap_err();
|
||||
assert_eq!(err, HostParseError::EndsWithHyphen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_fail_if_str_has_a_label_larger_than_63_characters() {
|
||||
let long_label = format!("{}.com", "a".repeat(64));
|
||||
let err = long_label.parse::<Host>().unwrap_err();
|
||||
assert_eq!(err, HostParseError::LargeLabel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_fail_if_str_has_empty_label() {
|
||||
let err = "example..com".parse::<Host>().unwrap_err();
|
||||
assert_eq!(err, HostParseError::EmptyLabel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_fail_if_str_has_invalid_label() {
|
||||
let err = "www.exa_mple.com".parse::<Host>().unwrap_err();
|
||||
assert_eq!(err, HostParseError::InvalidLabel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_succeed_if_valid_ipv4_address() {
|
||||
let host = "127.0.0.1".parse::<Host>().unwrap();
|
||||
assert_eq!(host, Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_succeed_if_valid_ipv6_address() {
|
||||
let host = "::1".parse::<Host>().unwrap();
|
||||
assert_eq!(host, Host::Ipv6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_should_succeed_if_valid_hostname() {
|
||||
let host = "localhost".parse::<Host>().unwrap();
|
||||
assert_eq!(host, Host::Name("localhost".to_string()));
|
||||
|
||||
let host = "example.com".parse::<Host>().unwrap();
|
||||
assert_eq!(host, Host::Name("example.com".to_string()));
|
||||
|
||||
let host = "w-w-w.example.com".parse::<Host>().unwrap();
|
||||
assert_eq!(host, Host::Name("w-w-w.example.com".to_string()));
|
||||
|
||||
let host = "w3.example.com".parse::<Host>().unwrap();
|
||||
assert_eq!(host, Host::Name("w3.example.com".to_string()));
|
||||
|
||||
// Revision of RFC-952 via RFC-1123 allows digit at start of label
|
||||
let host = "3.example.com".parse::<Host>().unwrap();
|
||||
assert_eq!(host, Host::Name("3.example.com".to_string()));
|
||||
}
|
||||
}
|
@ -0,0 +1,642 @@
|
||||
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<Destination, &'static str> {
|
||||
let (s, scheme) = maybe(parse_scheme)(s)?;
|
||||
let (s, username_password) = maybe(parse_username_password)(s)?;
|
||||
let (s, host) = 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<Host> {
|
||||
let host = s.parse::<Host>().map_err(HostParseError::into_static_str)?;
|
||||
Ok(("", host))
|
||||
}
|
||||
|
||||
fn parse_port(s: &str) -> PResult<u16> {
|
||||
let port = s
|
||||
.parse::<u16>()
|
||||
.map_err(|_| "Not an unsigned 16-bit integer")?;
|
||||
|
||||
Ok(("", port))
|
||||
}
|
||||
|
||||
/// 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<T>> {
|
||||
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<char> {
|
||||
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_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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue