use crate::errors::{error, nil, New};
use crate::types::{int, map, slice, string};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct URL {
pub Scheme: string,
pub Opaque: string,
pub User: Option<Userinfo>,
pub Host: string,
pub Path: string,
pub RawPath: string,
pub RawQuery: string,
pub Fragment: string,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Userinfo {
pub username: string,
pub password: Option<string>,
}
impl Userinfo {
pub fn Username(&self) -> string { self.username.clone() }
pub fn Password(&self) -> (string, bool) {
match &self.password {
Some(p) => (p.clone(), true),
None => (String::new(), false),
}
}
pub fn String(&self) -> string {
match &self.password {
Some(p) => format!("{}:{}", QueryEscape(&self.username), QueryEscape(p)),
None => QueryEscape(&self.username),
}
}
}
impl URL {
pub fn String(&self) -> string {
let mut out = String::new();
if !self.Scheme.is_empty() {
out.push_str(&self.Scheme);
out.push(':');
}
if !self.Opaque.is_empty() {
out.push_str(&self.Opaque);
} else {
if !self.Host.is_empty() || self.User.is_some()
|| self.Scheme == "http" || self.Scheme == "https"
|| self.Scheme == "ws" || self.Scheme == "wss" || self.Scheme == "ftp"
{
out.push_str("//");
if let Some(u) = &self.User {
out.push_str(&u.String());
out.push('@');
}
out.push_str(&self.Host);
}
out.push_str(&self.Path);
}
if !self.RawQuery.is_empty() {
out.push('?');
out.push_str(&self.RawQuery);
}
if !self.Fragment.is_empty() {
out.push('#');
out.push_str(&self.Fragment);
}
out
}
pub fn Query(&self) -> Values {
ParseQuery(&self.RawQuery).0
}
pub fn IsAbs(&self) -> bool { !self.Scheme.is_empty() }
pub fn Hostname(&self) -> string {
let h = &self.Host;
if h.starts_with('[') {
if let Some(end) = h.find(']') { return h[1..end].to_string(); }
}
match h.rsplit_once(':') {
Some((host, _)) => host.to_string(),
None => h.to_string(),
}
}
pub fn RequestURI(&self) -> string {
let mut out = String::new();
if !self.Opaque.is_empty() {
out.push_str(&self.Opaque);
} else {
if self.Path.is_empty() {
out.push('/');
} else {
out.push_str(&self.Path);
}
}
if !self.RawQuery.is_empty() {
out.push('?');
out.push_str(&self.RawQuery);
}
out
}
pub fn JoinPath(&self, elem: &[impl AsRef<str>]) -> URL {
let mut joined = self.Path.clone();
for e in elem {
let s = e.as_ref();
if s.is_empty() { continue; }
if !joined.ends_with('/') && !s.starts_with('/') { joined.push('/'); }
joined.push_str(s);
}
let cleaned = path_clean(&joined);
let mut u = self.clone();
u.Path = cleaned;
u.RawPath = String::new();
u
}
pub fn Port(&self) -> string {
let h = &self.Host;
if h.starts_with('[') {
if let Some(end) = h.find(']') {
if h.len() > end + 1 && &h[end + 1..end + 2] == ":" {
return h[end + 2..].to_string();
}
return String::new();
}
}
match h.rsplit_once(':') {
Some((_, p)) => p.to_string(),
None => String::new(),
}
}
}
#[allow(non_snake_case)]
pub fn Parse(raw: impl AsRef<str>) -> (URL, error) {
let s = raw.as_ref();
let mut u = URL::default();
let mut rest = s;
if let Some(i) = rest.find('#') {
u.Fragment = rest[i + 1..].to_string();
rest = &rest[..i];
}
if let Some(i) = rest.find('?') {
u.RawQuery = rest[i + 1..].to_string();
rest = &rest[..i];
}
if let Some(i) = find_scheme_end(rest) {
u.Scheme = rest[..i].to_ascii_lowercase();
rest = &rest[i + 1..];
}
if rest.starts_with("//") {
rest = &rest[2..];
let authority_end = rest.find('/').unwrap_or(rest.len());
let authority = &rest[..authority_end];
rest = &rest[authority_end..];
let (userinfo_opt, host) = match authority.rfind('@') {
Some(i) => (Some(&authority[..i]), &authority[i + 1..]),
None => (None, authority),
};
if let Some(ui) = userinfo_opt {
let (user, pwd) = match ui.find(':') {
Some(i) => {
let (u, p) = ui.split_at(i);
(QueryUnescape(u).0, Some(QueryUnescape(&p[1..]).0))
}
None => (QueryUnescape(ui).0, None),
};
u.User = Some(Userinfo { username: user, password: pwd });
}
u.Host = host.to_string();
} else if !u.Scheme.is_empty() && !rest.starts_with('/') {
u.Opaque = rest.to_string();
return (u, nil);
}
u.RawPath = rest.to_string();
let (decoded, err) = PathUnescape(rest);
if err != nil {
return (u, New(&format!("parse {:?}: invalid URL escape", s)));
}
u.Path = decoded;
(u, nil)
}
fn find_scheme_end(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
if bytes.is_empty() || !bytes[0].is_ascii_alphabetic() { return None; }
for (i, &b) in bytes.iter().enumerate() {
if b == b':' { return Some(i); }
if i == 0 {
if !b.is_ascii_alphabetic() { return None; }
} else if !(b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || b == b'.') {
return None;
}
}
None
}
#[derive(Debug, Clone, Default)]
pub struct Values {
inner: map<string, slice<string>>,
}
impl Values {
pub fn new() -> Self { Values { inner: HashMap::new() } }
pub fn Get(&self, key: impl AsRef<str>) -> string {
self.inner.get(key.as_ref())
.and_then(|v| v.first())
.cloned()
.unwrap_or_default()
}
pub fn Set(&mut self, key: impl AsRef<str>, value: impl AsRef<str>) {
self.inner.insert(key.as_ref().to_string(), vec![value.as_ref().to_string()]);
}
pub fn Add(&mut self, key: impl AsRef<str>, value: impl AsRef<str>) {
self.inner.entry(key.as_ref().to_string()).or_default()
.push(value.as_ref().to_string());
}
pub fn Del(&mut self, key: impl AsRef<str>) {
self.inner.remove(key.as_ref());
}
pub fn Has(&self, key: impl AsRef<str>) -> bool {
self.inner.contains_key(key.as_ref())
}
pub fn Encode(&self) -> string {
let mut keys: Vec<&String> = self.inner.keys().collect();
keys.sort();
let mut out = String::new();
for k in keys {
let enc_k = QueryEscape(k);
for v in &self.inner[k] {
if !out.is_empty() { out.push('&'); }
out.push_str(&enc_k);
out.push('=');
out.push_str(&QueryEscape(v));
}
}
out
}
pub fn Len(&self) -> int { self.inner.len() as int }
pub fn Values(&self, key: impl AsRef<str>) -> slice<string> {
self.inner.get(key.as_ref()).cloned().unwrap_or_default()
}
}
#[allow(non_snake_case)]
pub fn ParseQuery(s: impl AsRef<str>) -> (Values, error) {
let mut v = Values::new();
let mut err: error = nil;
for part in s.as_ref().split('&') {
if part.is_empty() { continue; }
let (k, val) = match part.find('=') {
Some(i) => (&part[..i], &part[i + 1..]),
None => (part, ""),
};
let (key, e1) = QueryUnescape(k);
let (value, e2) = QueryUnescape(val);
if e1 != nil { err = e1; }
if e2 != nil { err = e2; }
v.Add(key, value);
}
(v, err)
}
#[allow(non_snake_case)]
pub fn QueryEscape(s: impl AsRef<str>) -> string {
escape(s.as_ref(), true)
}
#[allow(non_snake_case)]
pub fn PathEscape(s: impl AsRef<str>) -> string {
escape(s.as_ref(), false)
}
fn escape(s: &str, is_query: bool) -> string {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
if should_not_escape(b, is_query) {
out.push(b as char);
} else if b == b' ' && is_query {
out.push('+');
} else {
out.push('%');
out.push(hex_digit(b >> 4));
out.push(hex_digit(b & 0xf));
}
}
out
}
fn should_not_escape(b: u8, is_query: bool) -> bool {
if b.is_ascii_alphanumeric() { return true; }
match b {
b'-' | b'.' | b'_' | b'~' => true,
b'$' | b'&' | b'+' | b',' | b'/' | b':' | b';' | b'=' | b'?' | b'@' => !is_query && b != b'?',
_ => false,
}
}
fn hex_digit(n: u8) -> char {
match n {
0..=9 => (b'0' + n) as char,
10..=15 => (b'A' + (n - 10)) as char,
_ => '?',
}
}
#[allow(non_snake_case)]
pub fn QueryUnescape(s: impl AsRef<str>) -> (string, error) {
unescape(s.as_ref(), true)
}
#[allow(non_snake_case)]
pub fn PathUnescape(s: impl AsRef<str>) -> (string, error) {
unescape(s.as_ref(), false)
}
fn unescape(s: &str, is_query: bool) -> (string, error) {
let mut out = Vec::<u8>::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'%' {
if i + 2 >= bytes.len() {
return (String::new(), New("invalid URL escape"));
}
let hi = hex_val(bytes[i + 1]);
let lo = hex_val(bytes[i + 2]);
if hi < 0 || lo < 0 {
return (String::new(), New("invalid URL escape"));
}
out.push(((hi as u8) << 4) | (lo as u8));
i += 3;
} else if b == b'+' && is_query {
out.push(b' ');
i += 1;
} else {
out.push(b);
i += 1;
}
}
(String::from_utf8_lossy(&out).into_owned(), nil)
}
fn path_clean(p: &str) -> String {
if p.is_empty() { return ".".to_string(); }
let absolute = p.starts_with('/');
let mut stack: Vec<&str> = Vec::new();
for part in p.split('/') {
match part {
"" | "." => continue,
".." => {
if stack.last().map_or(false, |t| *t != "..") && !stack.is_empty() {
stack.pop();
} else if !absolute {
stack.push("..");
}
}
other => stack.push(other),
}
}
let joined = stack.join("/");
if absolute { format!("/{}", joined) }
else if joined.is_empty() { ".".to_string() }
else { joined }
}
#[allow(non_snake_case)]
pub fn JoinPath(base: impl AsRef<str>, elem: &[impl AsRef<str>]) -> (string, error) {
let (u, err) = Parse(base);
if err != nil { return (String::new(), err); }
let out = u.JoinPath(elem);
(out.String(), nil)
}
#[allow(non_snake_case)]
pub fn ParseRequestURI(raw: impl AsRef<str>) -> (URL, error) {
Parse(raw)
}
fn hex_val(b: u8) -> i32 {
match b {
b'0'..=b'9' => (b - b'0') as i32,
b'a'..=b'f' => (b - b'a' + 10) as i32,
b'A'..=b'F' => (b - b'A' + 10) as i32,
_ => -1,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_http() {
let (u, err) = Parse("http://example.com/foo/bar");
assert_eq!(err, nil);
assert_eq!(u.Scheme, "http");
assert_eq!(u.Host, "example.com");
assert_eq!(u.Path, "/foo/bar");
}
#[test]
fn parse_with_userinfo_and_port() {
let (u, err) = Parse("https://alice:s3cret@api.example.com:8443/v1/ping?x=1&y=2#section");
assert_eq!(err, nil);
assert_eq!(u.Scheme, "https");
assert_eq!(u.Host, "api.example.com:8443");
assert_eq!(u.Hostname(), "api.example.com");
assert_eq!(u.Port(), "8443");
assert_eq!(u.Path, "/v1/ping");
assert_eq!(u.RawQuery, "x=1&y=2");
assert_eq!(u.Fragment, "section");
let ui = u.User.as_ref().unwrap();
assert_eq!(ui.username, "alice");
assert_eq!(ui.password, Some("s3cret".to_string()));
}
#[test]
fn parse_ipv6_host() {
let (u, err) = Parse("http://[::1]:8080/path");
assert_eq!(err, nil);
assert_eq!(u.Hostname(), "::1");
assert_eq!(u.Port(), "8080");
}
#[test]
fn parse_opaque_mailto() {
let (u, err) = Parse("mailto:alice@example.com");
assert_eq!(err, nil);
assert_eq!(u.Scheme, "mailto");
assert_eq!(u.Opaque, "alice@example.com");
}
#[test]
fn url_string_roundtrip() {
let (u, _) = Parse("http://example.com/foo?bar=1#x");
assert_eq!(u.String(), "http://example.com/foo?bar=1#x");
}
#[test]
fn query_escape_and_unescape() {
assert_eq!(QueryEscape("hello world/+?"), "hello+world%2F%2B%3F");
let (v, err) = QueryUnescape("hello+world%2F");
assert_eq!(err, nil);
assert_eq!(v, "hello world/");
}
#[test]
fn path_escape_preserves_slashes() {
assert_eq!(PathEscape("a b/c"), "a%20b/c");
}
#[test]
fn values_encode_sorts_keys() {
let mut v = Values::new();
v.Set("name", "alice smith");
v.Add("tag", "a");
v.Add("tag", "b");
assert_eq!(v.Encode(), "name=alice+smith&tag=a&tag=b");
}
#[test]
fn parse_query_round_trip() {
let (v, err) = ParseQuery("a=1&a=2&b=three");
assert_eq!(err, nil);
assert_eq!(v.Values("a"), vec!["1", "2"]);
assert_eq!(v.Get("b"), "three");
}
#[test]
fn bad_escape_is_error() {
let (_, err) = QueryUnescape("%ZZ");
assert!(err != nil);
}
}