Rust's str::parse accepts and rejects different inputs than Zig's std.fmt.parseInt/parseFloat (underscores, base prefixes, signs). The config layer already used the Zig ports; now the protocol parsers do too (call site -> Zig reference -> status in final-Z12.md): - zig_fmt moves into the leaf crate ghostty-zigfmt (ghostty-vt cannot depend on ghostty-os); ghostty-os re-exports it unchanged. - ghostty-vt: kitty graphics keys, kitty text sizing, OSC 4/5/10-19 color indices, hex color components and intensities, OSC 9 sleep and progress, OSC 133 exit codes, OSC 3008 fields, kitty color palette. - ghostty-terminal: tmux layout, format variables and pane tabs. - ghostty-os: URI ports and %XX decoding; ghostty-bench CLI arguments. Expected values were cross-checked against the real Zig toolchain. Test Plan: - cargo test --workspace --no-fail-fast - just check-macos (both), just check-windows, just check-wasm
609 lines
21 KiB
Rust
609 lines
21 KiB
Rust
//! URI parsing. Extends a faithful port of Zig's `std.Uri.parse` with the
|
|
//! additional [`ParseOptions`] from `os/uri.zig`.
|
|
|
|
use std::{borrow::Cow, fmt::Write as _};
|
|
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
pub struct ParseOptions {
|
|
/// Parse MAC addresses in the host component.
|
|
///
|
|
/// This is useful when the "Private Wi-Fi address" is enabled on macOS,
|
|
/// which sets the hostname to a rotating MAC address (12:34:56:ab:cd:ef).
|
|
pub mac_address: bool,
|
|
|
|
/// Return the full, raw, unencoded path string. Any query and fragment
|
|
/// values will be return as part of the path instead of as distinct
|
|
/// fields.
|
|
pub raw_path: bool,
|
|
}
|
|
|
|
/// Errors of `std.Uri.ParseError` plus `InvalidMacAddress`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
|
pub enum ParseError {
|
|
#[error("unexpected character")]
|
|
UnexpectedCharacter,
|
|
#[error("invalid format")]
|
|
InvalidFormat,
|
|
#[error("invalid port")]
|
|
InvalidPort,
|
|
#[error("invalid mac address")]
|
|
InvalidMacAddress,
|
|
}
|
|
|
|
/// `std.Uri.Component`.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum Component<'a> {
|
|
/// Unencoded text.
|
|
Raw(Cow<'a, str>),
|
|
/// Percent-encoded text.
|
|
PercentEncoded(&'a str),
|
|
}
|
|
|
|
impl Component<'_> {
|
|
pub fn is_empty(&self) -> bool {
|
|
match self {
|
|
Component::Raw(s) => s.is_empty(),
|
|
Component::PercentEncoded(s) => s.is_empty(),
|
|
}
|
|
}
|
|
|
|
/// The raw (percent-decoded) value (`toRaw`) as bytes. Invalid escapes
|
|
/// are left as-is, matching `std.Uri.percentDecodeInPlace`. The decoded
|
|
/// bytes are NOT required to be UTF-8 (e.g. non-UTF-8 file names).
|
|
pub fn to_raw_bytes(&self) -> Cow<'_, [u8]> {
|
|
match self {
|
|
Component::Raw(s) => Cow::Borrowed(s.as_bytes()),
|
|
Component::PercentEncoded(s) => percent_decode(s),
|
|
}
|
|
}
|
|
|
|
/// Lossy (U+FFFD) UTF-8 view of [`Self::to_raw_bytes`], for values such
|
|
/// as host names that are expected to be text. Use `to_raw_bytes` for
|
|
/// paths.
|
|
pub fn to_raw(&self) -> Cow<'_, str> {
|
|
match self.to_raw_bytes() {
|
|
Cow::Borrowed(b) => String::from_utf8_lossy(b),
|
|
Cow::Owned(b) => Cow::Owned(String::from_utf8_lossy(&b).into_owned()),
|
|
}
|
|
}
|
|
|
|
/// The percent-encoded value as it appears in the source, or the raw
|
|
/// text for `Raw` components.
|
|
pub fn as_str(&self) -> &str {
|
|
match self {
|
|
Component::Raw(s) => s,
|
|
Component::PercentEncoded(s) => s,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Lenient percent decoding like `std.Uri.percentDecodeBackwards`/InPlace:
|
|
/// valid `%XX` sequences are decoded, everything else passes through.
|
|
fn percent_decode(s: &str) -> Cow<'_, [u8]> {
|
|
if !s.contains('%') {
|
|
return Cow::Borrowed(s.as_bytes());
|
|
}
|
|
let b = s.as_bytes();
|
|
let mut out = Vec::with_capacity(b.len());
|
|
let mut i = 0;
|
|
while i < b.len() {
|
|
if b[i] == b'%'
|
|
&& i + 2 < b.len()
|
|
// `std.fmt.parseInt(u8, "XX", 16)`: accepts a sign and `_`.
|
|
&& let Ok(v) = crate::zig_fmt::parse_int::<u8>(&b[i + 1..i + 3], 16)
|
|
{
|
|
out.push(v);
|
|
i += 3;
|
|
continue;
|
|
}
|
|
out.push(b[i]);
|
|
i += 1;
|
|
}
|
|
Cow::Owned(out)
|
|
}
|
|
|
|
/// `std.Uri`.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Uri<'a> {
|
|
pub scheme: &'a str,
|
|
pub user: Option<Component<'a>>,
|
|
pub password: Option<Component<'a>>,
|
|
pub host: Option<Component<'a>>,
|
|
pub port: Option<u16>,
|
|
pub path: Component<'a>,
|
|
pub query: Option<Component<'a>>,
|
|
pub fragment: Option<Component<'a>>,
|
|
}
|
|
|
|
/// Port of `std.Uri.parse`.
|
|
fn std_parse(text: &str) -> Result<Uri<'_>, ParseError> {
|
|
let Some(colon) = text.find(':') else {
|
|
return Err(ParseError::UnexpectedCharacter);
|
|
};
|
|
let scheme = &text[..colon];
|
|
parse_after_scheme(scheme, &text[colon + 1..])
|
|
}
|
|
|
|
/// Port of `std.Uri.parseAfterScheme`.
|
|
fn parse_after_scheme<'a>(scheme: &'a str, text: &'a str) -> Result<Uri<'a>, ParseError> {
|
|
let mut uri = Uri {
|
|
scheme,
|
|
user: None,
|
|
password: None,
|
|
host: None,
|
|
port: None,
|
|
path: Component::PercentEncoded(""),
|
|
query: None,
|
|
fragment: None,
|
|
};
|
|
let mut rest = text;
|
|
|
|
if let Some(after) = rest.strip_prefix("//") {
|
|
// authority part
|
|
let end = after.find(['/', '?', '#']).unwrap_or(after.len());
|
|
let authority = &after[..end];
|
|
rest = &after[end..];
|
|
|
|
if authority.is_empty() {
|
|
if !rest.starts_with('/') {
|
|
return Err(ParseError::InvalidFormat);
|
|
}
|
|
} else {
|
|
parse_authority(authority, &mut uri)?;
|
|
}
|
|
}
|
|
|
|
let path_end = rest.find(['?', '#']).unwrap_or(rest.len());
|
|
uri.path = Component::PercentEncoded(&rest[..path_end]);
|
|
rest = &rest[path_end..];
|
|
if let Some(after) = rest.strip_prefix('?') {
|
|
// query part
|
|
let end = after.find('#').unwrap_or(after.len());
|
|
uri.query = Some(Component::PercentEncoded(&after[..end]));
|
|
rest = &after[end..];
|
|
}
|
|
if let Some(after) = rest.strip_prefix('#') {
|
|
uri.fragment = Some(Component::PercentEncoded(after));
|
|
}
|
|
Ok(uri)
|
|
}
|
|
|
|
fn parse_authority<'a>(authority: &'a str, uri: &mut Uri<'a>) -> Result<(), ParseError> {
|
|
let mut start_of_host = 0;
|
|
if let Some(index) = authority.find('@') {
|
|
start_of_host = index + 1;
|
|
let user_info = &authority[..index];
|
|
|
|
if let Some(idx) = user_info.find(':') {
|
|
uri.user = Some(Component::PercentEncoded(&user_info[..idx]));
|
|
if idx + 1 < user_info.len() {
|
|
// empty password is also "no password"
|
|
uri.password = Some(Component::PercentEncoded(&user_info[idx + 1..]));
|
|
}
|
|
} else {
|
|
uri.user = Some(Component::PercentEncoded(user_info));
|
|
uri.password = None;
|
|
}
|
|
}
|
|
|
|
// only possible if uri consists of only `userinfo@`
|
|
if start_of_host >= authority.len() {
|
|
return Ok(());
|
|
}
|
|
|
|
let mut end_of_host = authority.len();
|
|
let ab = authority.as_bytes();
|
|
|
|
// if we see `]` first without `@`
|
|
if ab[start_of_host] == b']' {
|
|
return Err(ParseError::InvalidFormat);
|
|
}
|
|
|
|
if ab[start_of_host] == b'[' {
|
|
// IPv6
|
|
end_of_host = authority.rfind(']').ok_or(ParseError::InvalidFormat)? + 1;
|
|
|
|
if let Some(index) = authority.rfind(':')
|
|
&& index >= end_of_host
|
|
{
|
|
// if not part of the V6 address field
|
|
end_of_host = end_of_host.min(index);
|
|
uri.port = Some(
|
|
crate::zig_fmt::parse_int::<u16>(&authority.as_bytes()[index + 1..], 10)
|
|
.map_err(|_| ParseError::InvalidPort)?,
|
|
);
|
|
}
|
|
} else if let Some(index) = authority.rfind(':')
|
|
&& index >= start_of_host
|
|
{
|
|
// if not part of the userinfo field
|
|
end_of_host = end_of_host.min(index);
|
|
uri.port = Some(
|
|
crate::zig_fmt::parse_int::<u16>(&authority.as_bytes()[index + 1..], 10)
|
|
.map_err(|_| ParseError::InvalidPort)?,
|
|
);
|
|
}
|
|
|
|
uri.host = Some(Component::PercentEncoded(
|
|
&authority[start_of_host..end_of_host],
|
|
));
|
|
Ok(())
|
|
}
|
|
|
|
/// Byte offset of `inner` within `outer` (both must be sub-slices of the
|
|
/// same allocation; here `inner` always comes from parsing `outer`).
|
|
fn offset_in(outer: &str, inner: &str) -> usize {
|
|
inner.as_ptr() as usize - outer.as_ptr() as usize
|
|
}
|
|
|
|
/// Parses a URI from the given string.
|
|
///
|
|
/// This extends the standard URI parse with some additional ParseOptions.
|
|
pub fn parse(text: &str, options: ParseOptions) -> Result<Uri<'_>, ParseError> {
|
|
let mut uri = match std_parse(text) {
|
|
Ok(uri) => uri,
|
|
Err(err) => {
|
|
// We can attempt to re-parse the text as a URI that has a MAC address
|
|
// in its host field (which tripped up the standard port parsing):
|
|
//
|
|
// file://12:34:56:78:90:aa/path/to/file
|
|
// ^^ InvalidPort
|
|
//
|
|
if err != ParseError::InvalidPort || !options.mac_address {
|
|
return Err(err);
|
|
}
|
|
|
|
// We can assume that the initial parse already validated the
|
|
// scheme, so we only need to find its bounds within the string.
|
|
let scheme_end = text.find("://").ok_or(ParseError::InvalidFormat)?;
|
|
let scheme = &text[..scheme_end];
|
|
|
|
// We similarly find the bounds of the host component by looking
|
|
// for the first slash (/) after the scheme. This is all we need
|
|
// for this case because the resulting slice can be unambiguously
|
|
// determined to be a MAC address (or not).
|
|
let host_start = scheme_end + "://".len();
|
|
let host_end = text[host_start..]
|
|
.find('/')
|
|
.map_or(text.len(), |i| host_start + i);
|
|
let mac_address = &text[host_start..host_end];
|
|
if !is_valid_mac_address(mac_address) {
|
|
return Err(ParseError::InvalidMacAddress);
|
|
}
|
|
|
|
// Parse the rest of the text (starting with the path component) as a
|
|
// partial URI and then add our MAC address as its host component.
|
|
let mut uri = parse_after_scheme(scheme, &text[host_end..])?;
|
|
uri.host = Some(Component::PercentEncoded(mac_address));
|
|
uri
|
|
}
|
|
};
|
|
|
|
// When MAC address parsing is enabled, we need to handle the case where
|
|
// the standard parse parsed the address's last octet as a numeric port number.
|
|
// We use a few heuristics to identify this case (14 characters, 4 colons)
|
|
// and then "repair" the result by reassign the .host component to the full
|
|
// MAC address and clearing the .port component.
|
|
//
|
|
// 12:34:56:78:90:99 -> [12:34:56:78:90, 99] -> 12:34:56:78:90:99
|
|
// (original host) (parsed host + port) (restored host)
|
|
//
|
|
if options.mac_address
|
|
&& let Some(Component::PercentEncoded(host)) = uri.host.clone()
|
|
&& host.len() == 14
|
|
&& host.matches(':').count() == 4
|
|
&& let Some(port) = uri.port
|
|
&& port <= 99
|
|
{
|
|
// The parse returns slices pointing into the original text string.
|
|
let host_start = offset_in(text, host);
|
|
let path_start = offset_in(text, uri.path.as_str());
|
|
let mac_address = &text[host_start..path_start];
|
|
if !is_valid_mac_address(mac_address) {
|
|
return Err(ParseError::InvalidMacAddress);
|
|
}
|
|
|
|
uri.host = Some(Component::PercentEncoded(mac_address));
|
|
uri.port = None;
|
|
}
|
|
|
|
// When the raw_path option is active, return everything after the authority
|
|
// (host) in the .path component, including any query and fragment values.
|
|
if options.raw_path {
|
|
let path_start = offset_in(text, uri.path.as_str());
|
|
uri.path = Component::Raw(Cow::Borrowed(&text[path_start..]));
|
|
uri.query = None;
|
|
uri.fragment = None;
|
|
}
|
|
|
|
Ok(uri)
|
|
}
|
|
|
|
/// The pieces of a parsed URI that are needed by byte-oriented callers.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ParsedBytes {
|
|
pub scheme: String,
|
|
/// Percent-decoded host (lossy; host names are text).
|
|
pub host: Option<String>,
|
|
/// Percent-decoded path bytes (or the full raw tail with
|
|
/// [`ParseOptions::raw_path`]). Not required to be UTF-8.
|
|
pub path: Vec<u8>,
|
|
}
|
|
|
|
/// Parse a URI given as arbitrary bytes without losing any path bytes.
|
|
///
|
|
/// Bytes that are not valid UTF-8 are percent-escaped before parsing (so
|
|
/// they decode back to exactly the same bytes), mirroring Zig's byte-string
|
|
/// `std.Uri.parse`. With `raw_path` the original bytes are sliced directly.
|
|
pub fn parse_bytes(text: &[u8], options: ParseOptions) -> Result<ParsedBytes, ParseError> {
|
|
// Escape invalid bytes, remembering the offset mapping.
|
|
let mut escaped = String::with_capacity(text.len());
|
|
for chunk in text.utf8_chunks() {
|
|
escaped.push_str(chunk.valid());
|
|
for b in chunk.invalid() {
|
|
let _ = write!(escaped, "%{b:02X}");
|
|
}
|
|
}
|
|
|
|
let uri = parse(&escaped, options)?;
|
|
let path = if options.raw_path {
|
|
let esc_start = offset_in(&escaped, uri.path.as_str());
|
|
// Map the escaped offset back to the original byte offset. The path
|
|
// never begins inside an escape (it starts at a delimiter).
|
|
let mut orig_start = esc_start;
|
|
let mut esc_pos = 0usize;
|
|
let mut orig_pos = 0usize;
|
|
for chunk in text.utf8_chunks() {
|
|
let v = chunk.valid().len();
|
|
if esc_start <= esc_pos + v {
|
|
orig_start = orig_pos + (esc_start - esc_pos);
|
|
break;
|
|
}
|
|
esc_pos += v;
|
|
orig_pos += v;
|
|
let inv = chunk.invalid().len();
|
|
esc_pos += inv * 3;
|
|
orig_pos += inv;
|
|
orig_start = orig_pos;
|
|
}
|
|
text[orig_start.min(text.len())..].to_vec()
|
|
} else {
|
|
uri.path.to_raw_bytes().into_owned()
|
|
};
|
|
Ok(ParsedBytes {
|
|
scheme: uri.scheme.to_owned(),
|
|
host: uri.host.as_ref().map(|h| h.to_raw().into_owned()),
|
|
path,
|
|
})
|
|
}
|
|
|
|
/// Checks if a string represents a valid MAC address, e.g. 12:34:56:ab:cd:ef.
|
|
fn is_valid_mac_address(s: &str) -> bool {
|
|
let b = s.as_bytes();
|
|
if b.len() != 17 {
|
|
return false;
|
|
}
|
|
|
|
for (i, &c) in b.iter().enumerate() {
|
|
if i % 3 == 2 {
|
|
if c != b':' {
|
|
return false;
|
|
}
|
|
} else if !c.is_ascii_hexdigit() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn zig_parse_int_port_and_percent() {
|
|
let u = parse("http://host:8_0/p", ParseOptions::default()).unwrap();
|
|
assert_eq!(u.port, Some(80));
|
|
let u = parse("http://host:+80/p", ParseOptions::default()).unwrap();
|
|
assert_eq!(u.port, Some(80));
|
|
assert!(parse("http://host:0x50/p", ParseOptions::default()).is_err());
|
|
assert!(parse("http://host:65536/p", ParseOptions::default()).is_err());
|
|
assert!(parse("http://host:-1/p", ParseOptions::default()).is_err());
|
|
// `%XX` is decoded with `parseInt(u8, XX, 16)`: sign and nothing else.
|
|
let u = parse(
|
|
"file://localhost/a%+Ab%-0c%_1d%1_e%41",
|
|
ParseOptions::default(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&*u.path.to_raw_bytes(), b"/a\x0ab\x00c%_1d%1_eA");
|
|
}
|
|
|
|
const MAC: ParseOptions = ParseOptions {
|
|
mac_address: true,
|
|
raw_path: false,
|
|
};
|
|
|
|
fn host<'a>(u: &'a Uri<'_>) -> &'a str {
|
|
match u.host.as_ref().unwrap() {
|
|
Component::PercentEncoded(s) => s,
|
|
Component::Raw(_) => panic!("raw host"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_mac_address() {
|
|
// Numeric MAC address without a port
|
|
let uri1 = parse("file://00:12:34:56:78:90/path", MAC).unwrap();
|
|
assert_eq!("file", uri1.scheme);
|
|
assert_eq!("00:12:34:56:78:90", host(&uri1));
|
|
assert_eq!("/path", uri1.path.as_str());
|
|
assert_eq!(None, uri1.port);
|
|
|
|
// Numeric MAC address with a port
|
|
let uri2 = parse("file://00:12:34:56:78:90:999/path", MAC).unwrap();
|
|
assert_eq!("file", uri2.scheme);
|
|
assert_eq!("00:12:34:56:78:90", host(&uri2));
|
|
assert_eq!("/path", uri2.path.as_str());
|
|
assert_eq!(Some(999), uri2.port);
|
|
|
|
// Alphabetic MAC address without a port
|
|
let uri3 = parse("file://ab:cd:ef:ab:cd:ef/path", MAC).unwrap();
|
|
assert_eq!("file", uri3.scheme);
|
|
assert_eq!("ab:cd:ef:ab:cd:ef", host(&uri3));
|
|
assert_eq!("/path", uri3.path.as_str());
|
|
assert_eq!(None, uri3.port);
|
|
|
|
// Alphabetic MAC address with a port
|
|
let uri4 = parse("file://ab:cd:ef:ab:cd:ef:999/path", MAC).unwrap();
|
|
assert_eq!("file", uri4.scheme);
|
|
assert_eq!("ab:cd:ef:ab:cd:ef", host(&uri4));
|
|
assert_eq!("/path", uri4.path.as_str());
|
|
assert_eq!(Some(999), uri4.port);
|
|
|
|
// Numeric MAC address without a path component
|
|
let uri5 = parse("file://00:12:34:56:78:90", MAC).unwrap();
|
|
assert_eq!("file", uri5.scheme);
|
|
assert_eq!("00:12:34:56:78:90", host(&uri5));
|
|
assert!(uri5.path.is_empty());
|
|
|
|
// Alphabetic MAC address without a path component
|
|
let uri6 = parse("file://ab:cd:ef:ab:cd:ef", MAC).unwrap();
|
|
assert_eq!("file", uri6.scheme);
|
|
assert_eq!("ab:cd:ef:ab:cd:ef", host(&uri6));
|
|
assert!(uri6.path.is_empty());
|
|
|
|
// Invalid MAC addresses
|
|
assert_eq!(
|
|
Err(ParseError::InvalidMacAddress),
|
|
parse("file://zz:zz:zz:zz:zz:00/path", MAC)
|
|
);
|
|
assert_eq!(
|
|
Err(ParseError::InvalidMacAddress),
|
|
parse("file://zz:zz:zz:zz:zz:zz/path", MAC)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn percent_decode_preserves_non_utf8_bytes() {
|
|
let u = parse("file://localhost/a%FFb%e2%82", ParseOptions::default()).unwrap();
|
|
assert_eq!(&*u.path.to_raw_bytes(), b"/a\xffb\xe2\x82");
|
|
// The lossy text view still works for text values.
|
|
assert_eq!(u.path.to_raw(), "/a\u{fffd}b\u{fffd}");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_bytes_non_utf8() {
|
|
let p = parse_bytes(b"file://localhost/a\xffb%FEc", ParseOptions::default()).unwrap();
|
|
assert_eq!(p.scheme, "file");
|
|
assert_eq!(p.host.as_deref(), Some("localhost"));
|
|
assert_eq!(p.path, b"/a\xffb\xfec");
|
|
|
|
let raw = ParseOptions {
|
|
raw_path: true,
|
|
mac_address: true,
|
|
};
|
|
let p = parse_bytes(b"kitty-shell-cwd://localhost/a\xff%41?x", raw).unwrap();
|
|
assert_eq!(p.path, b"/a\xff%41?x");
|
|
let p = parse_bytes(b"kitty-shell-cwd://\xfflo/a", raw).ok();
|
|
assert!(p.is_none() || p.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_raw_path() {
|
|
let text = "file://localhost/path??#fragment";
|
|
|
|
let uri1 = parse(
|
|
text,
|
|
ParseOptions {
|
|
raw_path: false,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!("file", uri1.scheme);
|
|
assert_eq!("localhost", host(&uri1));
|
|
assert_eq!("/path", uri1.path.to_raw());
|
|
assert_eq!("?", uri1.query.as_ref().unwrap().as_str());
|
|
assert_eq!("fragment", uri1.fragment.as_ref().unwrap().as_str());
|
|
|
|
let uri2 = parse(
|
|
text,
|
|
ParseOptions {
|
|
raw_path: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!("file", uri2.scheme);
|
|
assert_eq!("localhost", host(&uri2));
|
|
assert_eq!("/path??#fragment", uri2.path.to_raw());
|
|
assert_eq!(None, uri2.query);
|
|
assert_eq!(None, uri2.fragment);
|
|
|
|
let uri3 = parse(
|
|
"file://localhost",
|
|
ParseOptions {
|
|
raw_path: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!("file", uri3.scheme);
|
|
assert_eq!("localhost", host(&uri3));
|
|
assert!(uri3.path.is_empty());
|
|
assert_eq!(None, uri3.query);
|
|
assert_eq!(None, uri3.fragment);
|
|
}
|
|
|
|
#[test]
|
|
fn is_valid_mac_address_cases() {
|
|
assert!(is_valid_mac_address("01:23:45:67:89:Aa"));
|
|
assert!(is_valid_mac_address("Aa:Bb:Cc:Dd:Ee:Ff"));
|
|
|
|
assert!(!is_valid_mac_address(""));
|
|
assert!(!is_valid_mac_address("00:23:45"));
|
|
assert!(!is_valid_mac_address("00:23:45:Xx:Yy:Zz"));
|
|
assert!(!is_valid_mac_address("01-23-45-67-89-Aa"));
|
|
assert!(!is_valid_mac_address("01:23:45:67:89:Aa:Bb"));
|
|
}
|
|
|
|
#[test]
|
|
fn std_parse_basics() {
|
|
let u = parse(
|
|
"https://user:pw@example.com:8080/a%20b?q=1#frag",
|
|
ParseOptions::default(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(u.scheme, "https");
|
|
assert_eq!(u.user.as_ref().unwrap().as_str(), "user");
|
|
assert_eq!(u.password.as_ref().unwrap().as_str(), "pw");
|
|
assert_eq!(host(&u), "example.com");
|
|
assert_eq!(u.port, Some(8080));
|
|
assert_eq!(u.path.to_raw(), "/a b");
|
|
assert_eq!(u.query.as_ref().unwrap().as_str(), "q=1");
|
|
assert_eq!(u.fragment.as_ref().unwrap().as_str(), "frag");
|
|
|
|
let u = parse("file:///tmp/x", ParseOptions::default()).unwrap();
|
|
assert_eq!(u.host, None);
|
|
assert_eq!(u.path.as_str(), "/tmp/x");
|
|
|
|
let u = parse("http://[::1]:80/", ParseOptions::default()).unwrap();
|
|
assert_eq!(host(&u), "[::1]");
|
|
assert_eq!(u.port, Some(80));
|
|
|
|
assert_eq!(
|
|
parse("nocolon", ParseOptions::default()),
|
|
Err(ParseError::UnexpectedCharacter)
|
|
);
|
|
assert_eq!(
|
|
parse("file://a:b/", ParseOptions::default()),
|
|
Err(ParseError::InvalidPort)
|
|
);
|
|
assert_eq!(
|
|
parse("file://?x", ParseOptions::default()),
|
|
Err(ParseError::InvalidFormat)
|
|
);
|
|
}
|
|
}
|