///|
fn uri_alpha(c : UInt16) -> Bool {
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
///|
fn uri_digits(text : String) -> Bool {
text.iter().all(c => c >= '0' && c <= '9')
}
///|
fn uri_alphanumeric(c : Char) -> Bool {
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
}
///|
fn uri_unreserved(c : Char) -> Bool {
uri_alphanumeric(c) || "-._~".contains(c.to_string())
}
///|
fn uri_subdelim(c : Char) -> Bool {
"!$&'()*+,;=".contains(c.to_string())
}
///|
fn uri_component(text : String, extra : String) -> Bool {
let mut i = 0
while i < text.length() {
if text[i] == '%' {
if i + 2 >= text.length() ||
!text[i + 1:i + 3].iter().all(c => c.is_ascii_hexdigit()) {
return false
}
i = i + 3
} else {
let unit = text[i:i + 1].to_owned()
if !unit.iter().all(c => uri_unreserved(c) || uri_subdelim(c)) &&
!extra.contains(unit) {
return false
}
i = i + 1
}
}
true
}
///|
fn uri_ipv4(text : String) -> Bool {
let parts = text.split(".").map(p => p.to_owned()).collect()
if parts.length() != 4 {
return false
}
parts.all(p => {
if p == "" ||
p.length() > 3 ||
!uri_digits(p) ||
(p.length() > 1 && p[0] == '0') {
false
} else {
let value = @string.parse_int(p[:]) catch { _ => -1 }
value >= 0 && value <= 255
}
})
}
///|
fn uri_ipv6(text : String) -> Bool {
let halves = text.split("::").map(p => p.to_owned()).collect()
if halves.length() > 2 {
return false
}
let compressed = halves.length() == 2
let mut groups = 0
for h in 0.. p.to_owned()).collect()
for i in 0.. 4 ||
!part.iter().all(c => c.is_ascii_hexdigit()) {
return false
}
groups = groups + 1
}
}
}
if compressed {
groups < 8
} else {
groups == 8
}
}
///|
fn uri_ip_literal(text : String) -> Bool {
if text.has_prefix("v") || text.has_prefix("V") {
guard text.find(".") is Some(dot) && dot > 1 && dot + 1 < text.length() else {
return false
}
text[1:dot].iter().all(c => c.is_ascii_hexdigit()) &&
text[dot + 1:]
.iter()
.all(c => uri_unreserved(c) || uri_subdelim(c) || c == ':')
} else {
uri_ipv6(text)
}
}
///|
fn uri_authority(text : String) -> Bool {
let mut hostport = text
if text.find("@") is Some(at) {
if !uri_component(text[:at].to_owned(), ":") {
return false
}
hostport = text[at + 1:].to_owned()
}
if hostport.has_prefix("[") {
guard hostport.find("]") is Some(close) else { return false }
if !uri_ip_literal(hostport[1:close].to_owned()) {
return false
}
let rest = hostport[close + 1:].to_owned()
rest == "" || (rest.has_prefix(":") && uri_digits(rest[1:].to_owned()))
} else {
let colon = hostport.find(":")
let host = match colon {
Some(i) => {
if !uri_digits(hostport[i + 1:].to_owned()) {
return false
}
hostport[:i].to_owned()
}
None => hostport
}
// An invalid dotted quad can still be a reg-name under RFC 3986's ordered choice.
uri_component(host, "")
}
}
///|
/// RFC 3986 section 4.3 absolute-URI: unlike URI-reference, no fragment is allowed.
pub fn check_fetch_url(url : String) -> Unit raise FormatError {
guard url.find(":") is Some(colon) && colon > 0 else {
raise Invalid("fetch URL must be an absolute URI")
}
let scheme = url[:colon].to_owned()
guard uri_alpha(scheme[0]) &&
scheme
.iter()
.all(c => uri_alphanumeric(c) || c == '+' || c == '-' || c == '.') else {
raise Invalid("invalid URI scheme")
}
guard !url.contains("#") else {
raise Invalid("absolute-URI cannot contain a fragment")
}
let remainder = url[colon + 1:].to_owned()
let hier = match remainder.find("?") {
None => remainder
Some(query) => {
guard uri_component(remainder[query + 1:].to_owned(), ":@/?") else {
raise Invalid("invalid URI query")
}
remainder[:query].to_owned()
}
}
let path = if hier.has_prefix("//") {
let authority_path = hier[2:].to_owned()
let boundary = authority_path.find("/").unwrap_or(authority_path.length())
guard uri_authority(authority_path[:boundary].to_owned()) else {
raise Invalid("invalid URI authority, IP literal or port")
}
authority_path[boundary:].to_owned()
} else {
hier
}
guard uri_component(path, ":@/") else { raise Invalid("invalid URI path") }
}