primitive ParseURIAuthority
"""
Parse a standalone authority string (`[userinfo@]host[:port]`).
Use this for HTTP CONNECT request-targets (authority-form) where the
request-target is `host:port` without a scheme or `//` prefix.
`ParseURI` would misinterpret `host:port` as `scheme:path`.
The `@` delimiter is identified by scanning for the last `@` in the
authority string, following the WHATWG URL Standard convention. RFC 3986
grammar makes the first `@` the delimiter (userinfo cannot contain
literal `@`, only `%40`), but using the last `@` is more robust against
real-world URIs with unencoded `@` in userinfo.
"""
fun apply(raw: String val): (URIAuthority val | URIParseError val) =>
_parse(raw, 0, raw.size())
fun _parse(raw: String val, start: USize, end_pos: USize)
: (URIAuthority val | URIParseError val)
=>
// Find @ for userinfo
var userinfo: (String | None) = None
var host_start = start
// Check for IP-literal first — if the host starts with '[', @ before
// the closing ']' is part of the IP-literal, not a userinfo delimiter.
var at_pos: USize = end_pos // sentinel: no @ found
try
if (host_start < end_pos) and (raw(host_start)? == '[') then
// IP-literal: find closing ']' first, then look for @ before '['
// Actually, in authority "[::1]:8080", there's no userinfo.
// But in "user@[::1]:8080", the @ is before the '['.
// We need to find the LAST @ before any '[' or just the last @.
// RFC 3986: userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
// The @ delimiter separates userinfo from host.
// Scan for @ only before the '['.
None // no @ possible when first char is '['
else
// Scan for last @ (RFC 3986 says first @ delimits, but in practice
// the last @ is used since userinfo can contain encoded @)
var i = start
while i < end_pos do
if raw(i)? == '@' then
at_pos = i
end
i = i + 1
end
end
else
_Unreachable()
end
if at_pos < end_pos then
let ui: String val = raw.substring(start.isize(), at_pos.isize())
userinfo = ui
host_start = at_pos + 1
end
// Check for IP-literal (starts with '[')
try
if (host_start < end_pos) and (raw(host_start)? == '[') then
// Find closing ']'
var bracket_end: USize = host_start + 1
var found = false
while bracket_end < end_pos do
if raw(bracket_end)? == ']' then
found = true
break
end
bracket_end = bracket_end + 1
end
if not found then
return InvalidHost
end
// Validate IP-literal content
let literal_content: String val =
raw.substring((host_start + 1).isize(), bracket_end.isize())
if not _valid_ip_literal(literal_content) then
return InvalidHost
end
let host_str: String val =
raw.substring(host_start.isize(), (bracket_end + 1).isize())
// After ']', optional ':port'
let after_bracket = bracket_end + 1
if after_bracket < end_pos then
try
if raw(after_bracket)? == ':' then
let port_str: String val =
raw.substring((after_bracket + 1).isize(), end_pos.isize())
match _parse_port(port_str)
| let p: U16 =>
return URIAuthority(userinfo, host_str, p)
| None =>
return URIAuthority(userinfo, host_str, None)
| let e: URIParseError val =>
return e
end
else
return InvalidHost
end
else
_Unreachable()
end
end
return URIAuthority(userinfo, host_str, None)
end
else
_Unreachable()
end
// Regular host (reg-name or IPv4) — find ':' for port, scanning from end
// to handle IPv6 addresses (though those should be in brackets)
var colon_pos: USize = end_pos // sentinel
try
var i = end_pos
while i > host_start do
i = i - 1
if raw(i)? == ':' then
colon_pos = i
break
end
end
else
_Unreachable()
end
if colon_pos < end_pos then
let host_str: String val =
raw.substring(host_start.isize(), colon_pos.isize())
let port_str: String val =
raw.substring((colon_pos + 1).isize(), end_pos.isize())
match \exhaustive\ _parse_port(port_str)
| let p: U16 =>
URIAuthority(userinfo, host_str, p)
| None =>
URIAuthority(userinfo, host_str, None)
| let e: URIParseError val =>
e
end
else
let host_val: String val =
raw.substring(host_start.isize(), end_pos.isize())
URIAuthority(userinfo, host_val, None)
end
fun _parse_port(port_str: String val): (U16 | None | URIParseError val) =>
if port_str.size() == 0 then
// Empty port (e.g., "host:") — RFC 3986 allows empty port
return None
end
// All characters must be digits
for c in port_str.values() do
if (c < '0') or (c > '9') then
return InvalidPort
end
end
// Parse as U64 first to detect overflow
try
let value = port_str.u64()?
if value > 65535 then
return InvalidPort
end
value.u16()
else
InvalidPort
end
fun _valid_ip_literal(content: String val): Bool =>
if content.size() == 0 then
return false
end
// IPvFuture: starts with 'v' (case-insensitive)
try
let first = content(0)?
if (first == 'v') or (first == 'V') then
return _valid_ipvfuture(content)
end
else
_Unreachable() // content.size() > 0 guaranteed above
end
// Otherwise must be IPv6
_valid_ipv6(content)
fun _valid_ipv6(content: String val): Bool =>
// Basic validation: only hex digits, ':', and '.' (for IPv4-mapped)
for c in content.values() do
if not (
((c >= '0') and (c <= '9')) or
((c >= 'A') and (c <= 'F')) or
((c >= 'a') and (c <= 'f')) or
(c == ':') or (c == '.'))
then
return false
end
end
true
fun _valid_ipvfuture(content: String val): Bool =>
// IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
// Must have at least "v" + hexdig + "." + char = 4 chars
if content.size() < 4 then
return false
end
// Find the '.' separator
var dot_pos: USize = 0
var found_dot = false
try
var i: USize = 1 // skip 'v'
while i < content.size() do
if content(i)? == '.' then
dot_pos = i
found_dot = true
break
end
i = i + 1
end
else
_Unreachable()
end
if not found_dot then
return false
end
// Must have at least one hex digit between 'v' and '.'
if dot_pos < 2 then
return false
end
// Validate hex digits between 'v' and '.'
try
var i: USize = 1
while i < dot_pos do
let c = content(i)?
if not (
((c >= '0') and (c <= '9')) or
((c >= 'A') and (c <= 'F')) or
((c >= 'a') and (c <= 'f')))
then
return false
end
i = i + 1
end
else
_Unreachable()
end
// Must have at least one char after '.'
if (dot_pos + 1) >= content.size() then
return false
end
// Validate chars after '.': unreserved / sub-delims / ":"
try
var i: USize = dot_pos + 1
while i < content.size() do
let c = content(i)?
if not (_is_unreserved(c) or _is_sub_delim(c) or (c == ':')) then
return false
end
i = i + 1
end
else
_Unreachable()
end
true
fun _is_unreserved(c: U8): Bool =>
((c >= 'A') and (c <= 'Z')) or
((c >= 'a') and (c <= 'z')) or
((c >= '0') and (c <= '9')) or
(c == '-') or (c == '.') or (c == '_') or (c == '~')
fun _is_sub_delim(c: U8): Bool =>
(c == '!') or (c == '$') or (c == '&') or (c == '\'') or
(c == '(') or (c == ')') or (c == '*') or (c == '+') or
(c == ',') or (c == ';') or (c == '=')