///|
/// RFC 3986 URI reference.
pub struct Uri {
scheme : String
userinfo : String
host : String
port : String
path : String
query : String
fragment : String
has_authority : Bool
}
///|
pub fn Uri::scheme(self : Uri) -> String {
self.scheme
}
///|
pub fn Uri::userinfo(self : Uri) -> String {
self.userinfo
}
///|
pub fn Uri::host(self : Uri) -> String {
self.host
}
///|
pub fn Uri::port(self : Uri) -> String {
self.port
}
///|
pub fn Uri::path(self : Uri) -> String {
self.path
}
///|
pub fn Uri::query(self : Uri) -> String {
self.query
}
///|
pub fn Uri::fragment(self : Uri) -> String {
self.fragment
}
///|
pub fn Uri::has_authority(self : Uri) -> Bool {
self.has_authority
}
///|
pub fn parse_uri(input : String) -> Result[Uri, String] {
if !valid_percent(input) {
return Err("uri.percent")
}
let frag = split_once(input, "#")
let qy = split_once(frag.0, "?")
let (scheme, hier) = take_scheme(qy.0)
let has_authority = hier.has_prefix("//")
let (userinfo, host, port, path) = if has_authority {
parse_hier(hier)
} else {
("", "", "", hier)
}
if has_authority && host.has_prefix("[") && !host.has_suffix("]") {
return Err("uri.host")
}
if port.length() > 0 && !all_digits(port) {
return Err("uri.port")
}
if port.length() > 0 && !port_in_range(port) {
return Err("uri.port")
}
if has_authority && host.contains(":") && !host.has_prefix("[") {
return Err("uri.port")
}
Ok({
scheme,
userinfo,
host,
port,
path,
query: qy.1,
fragment: frag.1,
has_authority,
})
}
///|
fn take_scheme(input : String) -> (String, String) {
match input.find(":") {
Some(i) => {
let candidate = input.sub(end=i).to_owned()
if valid_scheme(candidate) {
(candidate, input.sub(start=i + 1).to_owned())
} else {
("", input)
}
}
None => ("", input)
}
}
///|
fn parse_hier(hier : String) -> (String, String, String, String) {
let rest = hier.sub(start=2).to_owned()
let end = authority_end(rest)
let authority = rest.sub(end~).to_owned()
let path = rest.sub(start=end).to_owned()
let ui = split_last(authority, "@")
let userinfo = ui.0
let hostport = ui.1
if hostport.has_prefix("[") {
match hostport.find("]") {
Some(i) => {
let host = hostport.sub(end=i + 1).to_owned()
let port = if i + 1 < hostport.length() &&
hostport.sub(start=i + 1, end=i + 2).to_owned() == ":" {
hostport.sub(start=i + 2).to_owned()
} else {
""
}
(userinfo, host, port, path)
}
None => (userinfo, hostport, "", path)
}
} else {
let hp = split_last(hostport, ":")
if hp.0.length() > 0 && all_digits(hp.1) {
(userinfo, hp.0, hp.1, path)
} else {
(userinfo, hostport, "", path)
}
}
}
///|
fn authority_end(s : String) -> Int {
match s.find("/") {
Some(i) => i
None => s.length()
}
}
///|
fn split_once(input : String, sep : String) -> (String, String) {
match input.find(sep) {
Some(i) =>
(
input.sub(end=i).to_owned(),
input.sub(start=i + sep.length()).to_owned(),
)
None => (input, "")
}
}
///|
fn split_last(input : String, sep : String) -> (String, String) {
match input.rev_find(sep) {
Some(i) =>
(
input.sub(end=i).to_owned(),
input.sub(start=i + sep.length()).to_owned(),
)
None => ("", input)
}
}
///|
fn valid_scheme(s : String) -> Bool {
if s.length() == 0 {
return false
}
let mut first = true
for c in s {
if first {
if !is_alpha(c) {
return false
}
first = false
} else if !(is_alpha(c) || is_digit(c) || c == '+' || c == '-' || c == '.') {
return false
}
}
true
}
///|
fn is_alpha(c : Char) -> Bool {
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
///|
fn is_digit(c : Char) -> Bool {
c >= '0' && c <= '9'
}
///|
fn all_digits(s : String) -> Bool {
if s.length() == 0 {
return false
}
for c in s {
if !is_digit(c) {
return false
}
}
true
}
///|
fn port_in_range(s : String) -> Bool {
let mut n = 0
for c in s {
n = n * 10 + (c.to_int() - 48)
if n > 65535 {
return false
}
}
true
}
///|
fn is_hex(c : Char) -> Bool {
is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
///|
fn valid_percent(s : String) -> Bool {
let chars = s.to_array()
let mut i = 0
while i < chars.length() {
if chars[i] == '%' {
if i + 2 >= chars.length() ||
!is_hex(chars[i + 1]) ||
!is_hex(chars[i + 2]) {
return false
}
i = i + 3
} else {
i = i + 1
}
}
true
}
///|
pub fn to_string(uri : Uri) -> String {
let mut out = ""
if uri.scheme.length() > 0 {
out = out + uri.scheme + ":"
}
if uri.has_authority {
out = out + "//"
if uri.userinfo.length() > 0 {
out = out + uri.userinfo + "@"
}
out = out + uri.host
if uri.port.length() > 0 {
out = out + ":" + uri.port
}
}
out = out + uri.path
if uri.query.length() > 0 {
out = out + "?" + uri.query
}
if uri.fragment.length() > 0 {
out = out + "#" + uri.fragment
}
out
}
///|
pub fn encode_component(input : String) -> String {
let mut out = ""
for c in input {
if is_unreserved(c) {
out = out + String::from_array([c])
} else {
let n = c.to_int()
out = out + "%" + hex_digit(n / 16) + hex_digit(n % 16)
}
}
out
}
///|
fn is_unreserved(c : Char) -> Bool {
is_alpha(c) || is_digit(c) || c == '-' || c == '.' || c == '_' || c == '~'
}
///|
fn hex_digit(n : Int) -> String {
let table = "0123456789ABCDEF"
table.sub(start=n, end=n + 1).to_owned()
}
///|
pub fn decode_component(input : String) -> Result[String, String] {
if !valid_percent(input) {
return Err("uri.percent")
}
let chars = input.to_array()
let mut out = ""
let mut i = 0
while i < chars.length() {
if chars[i] == '%' {
let v = hex_val(chars[i + 1]) * 16 + hex_val(chars[i + 2])
out = out + String::from_array([v.unsafe_to_char()])
i = i + 3
} else {
out = out + String::from_array([chars[i]])
i = i + 1
}
}
Ok(out)
}
///|
fn hex_val(c : Char) -> Int {
if is_digit(c) {
c.to_int() - 48
} else if c >= 'a' && c <= 'f' {
c.to_int() - 87
} else {
c.to_int() - 55
}
}
///|
///|
fn plus_to_space(s : String) -> String {
let mut out = ""
for c in s {
if c == '+' {
out = out + " "
} else {
out = out + String::from_array([c])
}
}
out
}
///|
pub fn parse_query(input : String) -> Result[Array[(String, String)], String] {
let result : Array[(String, String)] = []
if input.length() == 0 {
return Ok(result)
}
for item in input.split("&") {
let text = item.to_owned()
if text.length() == 0 {
continue
}
match text.split_once("=") {
Some((k, v)) =>
match decode_component(plus_to_space(k.to_owned())) {
Ok(dk) =>
match decode_component(plus_to_space(v.to_owned())) {
Ok(dv) => result.push((dk, dv))
Err(e) => return Err(e)
}
Err(e) => return Err(e)
}
None =>
match decode_component(plus_to_space(text)) {
Ok(dk) => result.push((dk, ""))
Err(e) => return Err(e)
}
}
}
Ok(result)
}
///|
pub fn query_get_all(
input : String,
key : String,
) -> Result[Array[String], String] {
let values : Array[String] = []
match parse_query(input) {
Err(e) => return Err(e)
Ok(pairs) =>
for pair in pairs {
if pair.0 == key {
values.push(pair.1)
}
}
}
Ok(values)
}
///|
pub fn remove_dot_segments(path : String) -> String {
let segs = path.split("/")
let out : Array[String] = []
let abs = path.has_prefix("/")
for seg in segs {
let s = seg.to_owned()
if s == "" || s == "." {
continue
} else if s == ".." {
if out.length() > 0 {
ignore(out.pop())
}
} else {
out.push(s)
}
}
let mut joined = if abs { "/" } else { "" }
let mut i = 0
while i < out.length() {
if i > 0 {
joined = joined + "/"
}
joined = joined + out[i]
i = i + 1
}
if path.has_suffix("/") && !joined.has_suffix("/") && joined != "/" {
joined = joined + "/"
}
joined
}
///|
pub fn normalize_uri(uri : Uri) -> Uri {
let scheme = uri.scheme.to_lower()
let port = if uri.port == default_port(scheme) { "" } else { uri.port }
{
scheme,
userinfo: uri.userinfo,
host: uri.host.to_lower(),
port,
path: remove_dot_segments(uri.path),
query: uri.query,
fragment: uri.fragment,
has_authority: uri.has_authority,
}
}
///|
fn default_port(scheme : String) -> String {
if scheme == "http" || scheme == "ws" {
"80"
} else if scheme == "https" || scheme == "wss" {
"443"
} else if scheme == "ftp" {
"21"
} else {
""
}
}
///|
pub fn resolve_reference(base : Uri, reference : Uri) -> Uri {
if reference.scheme.length() > 0 {
{
scheme: reference.scheme,
userinfo: reference.userinfo,
host: reference.host,
port: reference.port,
path: remove_dot_segments(reference.path),
query: reference.query,
fragment: reference.fragment,
has_authority: reference.has_authority,
}
} else if reference.has_authority {
{
scheme: base.scheme,
userinfo: reference.userinfo,
host: reference.host,
port: reference.port,
path: remove_dot_segments(reference.path),
query: reference.query,
fragment: reference.fragment,
has_authority: true,
}
} else {
let path = if reference.path.length() == 0 {
base.path
} else if reference.path.has_prefix("/") {
remove_dot_segments(reference.path)
} else {
merge_path(base, reference.path)
}
let query = if reference.path.length() == 0 && reference.query.length() == 0 {
base.query
} else if reference.path.length() == 0 {
reference.query
} else {
reference.query
}
{
scheme: base.scheme,
userinfo: base.userinfo,
host: base.host,
port: base.port,
path,
query,
fragment: reference.fragment,
has_authority: base.has_authority,
}
}
}
///|
fn merge_path(base : Uri, rel : String) -> String {
if base.has_authority && base.path.length() == 0 {
remove_dot_segments("/" + rel)
} else {
match base.path.rev_find("/") {
Some(i) => remove_dot_segments(base.path.sub(end=i + 1).to_owned() + rel)
None => remove_dot_segments(rel)
}
}
}
///|
/// Match `/users/{id}` style path templates. Literal segments must equal.
pub fn match_path(
pattern : String,
path : String,
) -> Result[Array[(String, String)], String] {
let p = segs(pattern)
let s = segs(path)
if p.length() != s.length() {
return Err("uri.path.mismatch")
}
let out : Array[(String, String)] = []
let mut i = 0
while i < p.length() {
let pat = p[i]
if pat.has_prefix("{") && pat.has_suffix("}") && pat.length() > 2 {
match decode_component(s[i]) {
Ok(val) =>
out.push((pat.sub(start=1, end=pat.length() - 1).to_owned(), val))
Err(e) => return Err(e)
}
} else if pat != s[i] {
return Err("uri.path.mismatch")
}
i = i + 1
}
Ok(out)
}
///|
fn segs(s : String) -> Array[String] {
let out : Array[String] = []
for part in s.split("/") {
let t = part.to_owned()
if t.length() > 0 {
out.push(t)
}
}
out
}
///|
pub fn is_absolute(uri : Uri) -> Bool {
uri.scheme.length() > 0
}
///|
pub fn origin(uri : Uri) -> String {
if !uri.has_authority {
return ""
}
let mut out = ""
if uri.scheme.length() > 0 {
out = out + uri.scheme + "://"
} else {
out = out + "//"
}
out = out + uri.host
if uri.port.length() > 0 {
out = out + ":" + uri.port
}
out
}
///|
pub fn query_has(input : String, key : String) -> Bool {
match query_get_all(input, key) {
Ok(values) => values.length() > 0
Err(_) => false
}
}
///|
pub fn encode_query(pairs : Array[(String, String)]) -> String {
let mut out = ""
let mut i = 0
while i < pairs.length() {
if i > 0 {
out = out + "&"
}
out = out +
encode_component(pairs[i].0) +
"=" +
encode_component(pairs[i].1)
i = i + 1
}
out
}
///|
pub fn with_query(uri : Uri, query : String) -> Uri {
{
scheme: uri.scheme,
userinfo: uri.userinfo,
host: uri.host,
port: uri.port,
path: uri.path,
query,
fragment: uri.fragment,
has_authority: uri.has_authority,
}
}
///|
pub fn with_path(uri : Uri, path : String) -> Uri {
{
scheme: uri.scheme,
userinfo: uri.userinfo,
host: uri.host,
port: uri.port,
path,
query: uri.query,
fragment: uri.fragment,
has_authority: uri.has_authority,
}
}
///|
pub fn with_fragment(uri : Uri, fragment : String) -> Uri {
{
scheme: uri.scheme,
userinfo: uri.userinfo,
host: uri.host,
port: uri.port,
path: uri.path,
query: uri.query,
fragment,
has_authority: uri.has_authority,
}
}
///|
pub fn same_origin(a : Uri, b : Uri) -> Bool {
origin(a) == origin(b) && origin(a).length() > 0
}
///|
pub fn strip_fragment(uri : Uri) -> Uri {
with_fragment(uri, "")
}
///|
pub fn query_drop(input : String, key : String) -> Result[String, String] {
match parse_query(input) {
Err(e) => Err(e)
Ok(pairs) => {
let kept : Array[(String, String)] = []
for pair in pairs {
if pair.0 != key {
kept.push(pair)
}
}
Ok(encode_query(kept))
}
}
}