// URI parser implementation following RFC 3986
///|
pub enum Host {
IPv6Address(@bytes.View)
RegName(@bytes.View)
} derive(Eq, Show)
///|
pub struct Authority {
mut userinfo : @bytes.View?
mut host : Host
mut port : Int?
} derive(Eq, Show)
///|
pub struct Uri {
mut scheme : @bytes.View?
mut authority : Authority?
path : Array[@bytes.View]
mut query : @bytes.View?
mut fragment : @bytes.View?
} derive(Eq, Show)
///|
pub suberror ParseError {
InvalidScheme(@bytes.View)
InvalidHeirPart(@bytes.View)
InvalidPercentEncoding(@bytes.View)
MissingColon
MissingScheme
InvalidSegment(@bytes.View)
InvalidSchemeOrSegment(@bytes.View)
Invalid(@bytes.View)
}
///|
fn @bytes.View::slice_until(
self : @bytes.View,
other : @bytes.View,
) -> @bytes.View {
self.data()[self.start_offset():other.start_offset()]
}
// RFC 3986 Character Classification Functions
///|
fn is_alpha_byte(b : Byte) -> Bool {
(b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
}
///|
fn is_digit_byte(b : Byte) -> Bool {
b >= '0' && b <= '9'
}
///|
fn is_hexdig_byte(b : Byte) -> Bool {
is_digit_byte(b) || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')
}
///|
fn is_unreserved_byte(b : Byte) -> Bool {
is_alpha_byte(b) ||
is_digit_byte(b) ||
b == '-' ||
b == '.' ||
b == '_' ||
b == '~'
}
///|
fn is_gen_delim_byte(b : Byte) -> Bool {
b == ':' ||
b == '/' ||
b == '?' ||
b == '#' ||
b == '[' ||
b == ']' ||
b == '@'
}
///|
fn is_sub_delim_byte(b : Byte) -> Bool {
b == '!' ||
b == '$' ||
b == '&' ||
b == '\'' ||
b == '(' ||
b == ')' ||
b == '*' ||
b == '+' ||
b == ',' ||
b == ';' ||
b == '='
}
///|
fn is_pchar_byte(b : Byte) -> Bool {
is_unreserved_byte(b) || is_sub_delim_byte(b) || b == ':' || b == '@'
}
// RFC 3986 Parsing Functions
// URI-reference = scheme ":" "//" authority path-abempty [ "?" query ] [ "#" fragment ]
// / scheme ":" "/" [ segment-nz *( "/" segment ) ] [ "?" query ] [ "#" fragment ]
// / scheme ":" segment-nz *( "/" segment ) [ "?" query ] [ "#" fragment ]
// / scheme ":" [ "?" query ] [ "#" fragment ]
// / "//" authority path-abempty [ "?" query ] [ "#" fragment ]
// / "/" [ segment-nz *( "/" segment ) ] [ "?" query ] [ "#" fragment ]
// / segment-nz-nc *( "/" segment ) [ "?" query ] [ "#" fragment ]
// / [ "?" query ] [ "#" fragment ]
///|
fn parse_scheme_or_segment_nz_nc(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
let s = source
let segment = s
loop s {
[':', .. k] as s => {
uri.scheme = Some(source.slice_until(s))
parse_hier_part_then_query(k, uri)
}
['/', .. k] as s => {
uri.path.push(segment.slice_until(s))
parse_path_abempty_then_query(k, uri)
}
['?', .. k] as s => {
uri.path.push(segment.slice_until(s))
parse_query_then_fragment(k, uri)
}
['#', .. k] as s => {
uri.path.push(segment.slice_until(s))
parse_query_then_fragment(k, uri)
}
[b, .. k] if is_unreserved_byte(b) || is_sub_delim_byte(b) || b == '@' =>
continue k
_ => raise ParseError::InvalidSchemeOrSegment(source)
}
}
///|
fn parse_uri_reference(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
match source {
[.. "//", .. k] => {
// relative-ref > relative-part > "//" authority path-abempty
let k = parse_authority(k, uri)
parse_path_abempty_then_query(k, uri)
}
['/', .. k] =>
// relative-ref > relative-part > "/" [ segment-nz *( "/" segment ) ] [ "?" query ] [ "#" fragment ]
parse_path_abempty_then_query(k, uri)
['?', .. k] =>
// relative-ref > relative-part > "?" query
parse_query_then_fragment(k, uri)
['#', .. k] =>
// relative-ref > relative-part > "#" fragment
parse_fragment(k, uri)
[..] as s => parse_scheme_or_segment_nz_nc(s, uri)
}
}
///|
pub fn Uri::parse(source : @bytes.View) -> Uri raise ParseError {
let uri = Uri::{
scheme: None,
authority: None,
path: [],
query: None,
fragment: None,
}
let source = parse_uri_reference(source, uri)
if !(source is []) {
raise ParseError::Invalid(source)
}
uri
}
///|
fn parse_scheme(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
match source {
[b, .. k] if is_alpha_byte(b) =>
loop k {
[b, .. s] if is_alpha_byte(b) ||
is_digit_byte(b) ||
b == '+' ||
b == '-' ||
b == '.' => continue s
[':', .. k] as s => {
uri.scheme = Some(source.slice_until(s))
return k
}
[_, ..] as s => raise ParseError::InvalidScheme(s)
[] => raise ParseError::MissingColon
}
[_, ..] as s => raise ParseError::InvalidScheme(s)
[] => raise ParseError::MissingScheme
}
}
///|
fn parse_percent_encoding(source : @bytes.View) -> @bytes.View raise ParseError {
match source {
[
'0'..='9'
| 'A'..='Z'
| 'a'..='z',
'0'..='9'
| 'A'..='Z'
| 'a'..='z',
.. source,
] => source
[..] => raise ParseError::InvalidPercentEncoding(source)
}
}
///|
fn parse_userinfo(
source : @bytes.View,
authority : Authority,
) -> @bytes.View raise ParseError {
let start = source
loop source {
['@', .. k] as s => {
authority.userinfo = Some(start.slice_until(s))
return k
}
['%', .. k] => continue parse_percent_encoding(k)
[b, .. k] if is_unreserved_byte(b) || is_sub_delim_byte(b) || b == ':' =>
continue k
[_, ..] as s => raise ParseError::InvalidHeirPart(s)
[] as s => return s
}
}
///|
fn parse_dec_octet(source : @bytes.View) -> (@bytes.View, Int) raise ParseError {
match source {
['2', '5', d, .. k] if d >= '0' && d <= '5' => (k, 250 + (d.to_int() - '0'))
['2', d1, d2, .. k] if d1 >= '0' && d1 <= '4' && is_digit_byte(d2) =>
(k, 200 + (d1.to_int() - '0') * 10 + (d2.to_int() - '0'))
['1', d1, d2, .. k] if is_digit_byte(d1) && is_digit_byte(d2) =>
(k, 100 + (d1.to_int() - '0') * 10 + (d2.to_int() - '0'))
[d1, d2, .. k] if d1 >= '1' && d1 <= '9' && is_digit_byte(d2) =>
(k, (d1.to_int() - '0') * 10 + (d2.to_int() - '0'))
[d, .. k] if is_digit_byte(d) => (k, d.to_int() - '0')
_ => raise ParseError::InvalidHeirPart(source)
}
}
///|
fn parse_ipv4_address(
source : @bytes.View,
) -> (@bytes.View, @bytes.View) raise ParseError {
let start = source
let (s1, _) = parse_dec_octet(source)
match s1 {
['.', .. s2] => {
let (s3, _) = parse_dec_octet(s2)
match s3 {
['.', .. s4] => {
let (s5, _) = parse_dec_octet(s4)
match s5 {
['.', .. s6] => {
let (s7, _) = parse_dec_octet(s6)
(s7, start.slice_until(s7))
}
_ => raise ParseError::InvalidHeirPart(source)
}
}
_ => raise ParseError::InvalidHeirPart(source)
}
}
_ => raise ParseError::InvalidHeirPart(source)
}
}
///|
fn parse_reg_name(
source : @bytes.View,
authority : Authority,
) -> @bytes.View raise ParseError {
let s = source
loop s {
['%', ..] as s => continue parse_percent_encoding(s)
[b, .. k] if is_unreserved_byte(b) || is_sub_delim_byte(b) => continue k
_ as s => {
authority.host = Host::RegName(source.slice_until(s))
s
}
}
}
///|
fn parse_ipv6_address(
source : @bytes.View,
authority : Authority,
) -> @bytes.View raise ParseError {
// Simplified IPv6 parsing - match until ']'
let s = source
loop s {
[']', .. k] as s => {
authority.host = IPv6Address(source.slice_until(s))
k
}
[b, .. s] if is_hexdig_byte(b) || b == ':' || b == '.' => continue s
_ as s => raise ParseError::InvalidHeirPart(s)
}
}
///|
fn parse_host(
source : @bytes.View,
authority : Authority,
) -> @bytes.View raise ParseError {
// Try IP literal first
match source {
['[', .. k] => parse_ipv6_address(k, authority)
_ =>
// Try IPv4 address, fallback to reg-name
parse_reg_name(source, authority)
}
}
///|
fn parse_port(source : @bytes.View, authority : Authority) -> @bytes.View {
match source {
[':', .. k] => {
let mut port_val = 0
loop k {
[d, .. s] if is_digit_byte(d) => {
port_val = port_val * 10 + (d.to_int() - '0')
continue s
}
_ as s => {
authority.port = Some(port_val)
return s
}
}
}
_ => source
}
}
///|
fn parse_authority(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
let authority = Authority::{ userinfo: None, host: RegName(""), port: None }
uri.authority = Some(authority)
// Try to parse userinfo first
let remaining = parse_userinfo(source, authority) catch {
_ => source // No userinfo, continue with host
}
let remaining = parse_host(remaining, authority)
parse_port(remaining, authority)
}
///|
fn parse_segment(
source : @bytes.View,
) -> (@bytes.View, @bytes.View) raise ParseError {
let start = source
loop source {
['%', .. k] => continue parse_percent_encoding(k)
[b, .. k] if is_pchar_byte(b) => continue k
_ as s => (s, start.slice_until(s))
}
}
///| Continuation-based completion functions
fn parse_done(source : @bytes.View, uri : Uri) -> @bytes.View {
source
}
///|
fn parse_fragment(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
let s = source
loop s {
['%', ..] as s => continue parse_percent_encoding(s)
[b, .. k] if is_pchar_byte(b) || b == '/' || b == '?' => continue k
[..] as s => {
uri.fragment = Some(source.slice_until(s))
return s
}
}
}
///|
fn parse_query_then_fragment(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
let start = source
loop source {
['#', .. k] as s => {
uri.query = Some(start.slice_until(s))
parse_fragment(k, uri)
}
['%', ..] as s => continue parse_percent_encoding(s)
[b, .. k] if is_pchar_byte(b) || b == '/' || b == '?' => continue k
[..] as s => {
uri.query = Some(start.slice_until(s))
s
}
}
}
///|
fn parse_path_abempty_then_query(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
let s = source
loop s {
['%', .. k] => continue parse_percent_encoding(k)
['/', .. k] as s => {
uri.path.push(source.slice_until(s))
parse_path_abempty_then_query(k, uri)
}
['?', .. k] as s => {
uri.path.push(source.slice_until(s))
parse_query_then_fragment(k, uri)
}
['#', .. k] as s => {
uri.path.push(source.slice_until(s))
parse_fragment(k, uri)
}
[b, .. k] if is_pchar_byte(b) => continue k
[] as s => {
uri.path.push(source.slice_until(s))
s
}
[..] as s => raise ParseError::InvalidSegment(s)
}
}
///|
fn parse_hier_part_then_query(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
match source {
[.. "//", .. k] =>
match parse_authority(k, uri) {
['/', .. k] => parse_path_abempty_then_query(k, uri)
['?', .. k] => parse_query_then_fragment(k, uri)
['#', ..] => parse_fragment(k, uri)
[] => parse_done(k, uri)
[..] as s => raise ParseError::Invalid(s)
}
_ => parse_path_abempty_then_query(source, uri)
}
}
///|
fn parse_path_noscheme_then_query(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
let (remaining, segment_val) = parse_segment_nz_nc(source)
uri.path.push(segment_val)
parse_path_abempty_then_query(remaining, uri)
}
///|
fn parse_segment_nz_nc(
source : @bytes.View,
) -> (@bytes.View, @bytes.View) raise ParseError {
let start = source
let mut has_content = false
let s = source
loop s {
['%', .. k] => {
let k = parse_percent_encoding(k)
has_content = true
continue k
}
[b, .. k] if is_unreserved_byte(b) || is_sub_delim_byte(b) || b == '@' => {
has_content = true
continue k
}
_ as s => if has_content { (s, start.slice_until(s)) } else { (source, "") }
}
}
///|
fn parse_relative_part_then_query(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
match source {
[.. "//", .. k] => {
let remaining = parse_authority(k, uri)
parse_path_abempty_then_query(remaining, uri)
}
['/', .. k] => parse_path_abempty_then_query(k, uri)
['?', .. k] => parse_query_then_fragment(k, uri)
['#', ..] => parse_fragment(source, uri)
[] => parse_done(source, uri)
_ => parse_path_noscheme_then_query(source, uri)
}
}
///|
fn parse_relative_ref(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
uri.scheme = None
parse_relative_part_then_query(source, uri)
}
///|
fn parse_absolute_uri(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
let remaining = parse_scheme(source, uri) // already consumes the ':'
parse_hier_part_then_query_no_fragment(remaining, uri)
}
///|
fn parse_hier_part_then_query_no_fragment(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
match source {
[.. "//", .. k] => {
let remaining = parse_authority(k, uri)
parse_path_abempty_then_query_no_fragment(remaining, uri)
}
_ => parse_path_abempty_then_query_no_fragment(source, uri)
}
}
///|
fn parse_path_abempty_then_query_no_fragment(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
let s = source
loop s {
['/', .. k] => {
let (remaining, segment_val) = parse_segment(k)
uri.path.push(segment_val)
continue remaining
}
_ as s => parse_query_no_fragment_or_done(s, uri)
}
}
///|
fn parse_query_no_fragment_or_done(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
match source {
['?', .. k] => parse_query_no_fragment_then_done(k, uri)
_ => parse_done(source, uri)
}
}
///|
fn parse_query_no_fragment_then_done(
source : @bytes.View,
uri : Uri,
) -> @bytes.View raise ParseError {
let start = source
loop source {
['%', .. k] => continue parse_percent_encoding(k)
[b, .. k] if is_pchar_byte(b) || b == '/' || b == '?' => continue k
_ as s => {
uri.query = Some(start.slice_until(s))
parse_done(s, uri)
}
}
}
///|
fn remove_dot_segments(path : Array[@bytes.View]) -> Array[@bytes.View] {
let output = Array::new()
for segment in path {
if segment == "." {
continue // Remove single dot segments
} else if segment == ".." {
if output.length() > 0 {
output.pop() |> ignore() // Remove last segment and the .. segment
}
// If output is empty, ignore the .. segment
} else {
output.push(segment)
}
}
output
}
///|
test "RFC 3986 URI Reference Parsing" {
let uri = Uri::{
scheme: None,
authority: None,
path: [],
query: None,
fragment: None,
}
assert_eq(
parse_uri_reference(
"http://example.com/path/to/resource?query=parameter#fragment", uri,
),
"",
)
assert_eq(uri.scheme, Some("http"))
assert_eq(
uri.authority,
Some(Authority::{ userinfo: None, host: RegName("example.com"), port: None }),
)
assert_eq(uri.path, ["path", "to", "resource"])
assert_eq(uri.query, Some("query=parameter"))
assert_eq(uri.fragment, Some("fragment"))
}
///|
test "RFC 3986 Authority with Userinfo and Port" {
let uri = Uri::{
scheme: None,
authority: None,
path: [],
query: None,
fragment: None,
}
assert_eq(
parse_uri_reference("ftp://user:pass@ftp.example.com:21/path", uri),
"",
)
assert_eq(uri.scheme, Some("ftp"))
assert_eq(
uri.authority,
Some(Authority::{
userinfo: Some("user:pass"),
host: RegName("ftp.example.com"),
port: Some(21),
}),
)
assert_eq(uri.path, ["path"])
}
///|
test "RFC 3986 IPv6 Literal" {
let uri = Uri::{
scheme: None,
authority: None,
path: [],
query: None,
fragment: None,
}
assert_eq(parse_uri_reference("http://[2001:db8::1]:8080/", uri), "")
assert_eq(uri.scheme, Some("http"))
assert_eq(
uri.authority,
Some(Authority::{
userinfo: None,
host: IPv6Address("2001:db8::1"),
port: Some(8080),
}),
)
assert_eq(uri.path, [])
}
///|
test "RFC 3986 Percent Encoding Preservation" {
let uri = Uri::{
scheme: None,
authority: None,
path: [],
query: None,
fragment: None,
}
assert_eq(parse_uri_reference("http://example.com/hello%20world", uri), "")
assert_eq(uri.scheme, Some("http"))
assert_eq(uri.path, ["hello%20world"]) // preserved, not decoded
}
///|
test "RFC 3986 Relative Reference" {
let uri = Uri::{
scheme: None,
authority: None,
path: [],
query: None,
fragment: None,
}
assert_eq(parse_uri_reference("../path/file.txt", uri), "")
assert_eq(uri.scheme, None)
assert_eq(uri.authority, None)
assert_eq(uri.path, ["..", "path", "file.txt"])
}
///|
test "RFC 3986 Absolute Path" {
let uri = Uri::{
scheme: None,
authority: None,
path: [],
query: None,
fragment: None,
}
assert_eq(parse_uri_reference("/absolute/path", uri), "")
assert_eq(uri.scheme, None)
assert_eq(uri.authority, None)
assert_eq(uri.path, ["absolute", "path"])
}
///|
test "RFC 3986 Query Only" {
let uri = Uri::{
scheme: None,
authority: None,
path: [],
query: None,
fragment: None,
}
assert_eq(parse_uri_reference("?key=value&foo=bar", uri), "")
assert_eq(uri.scheme, None)
assert_eq(uri.authority, None)
assert_eq(uri.path, [])
assert_eq(uri.query, Some("key=value&foo=bar"))
}
///|
test "RFC 3986 Fragment Only" {
let uri = Uri::{
scheme: None,
authority: None,
path: [],
query: None,
fragment: None,
}
assert_eq(parse_uri_reference("#section1", uri), "")
assert_eq(uri.scheme, None)
assert_eq(uri.authority, None)
assert_eq(uri.path, [])
assert_eq(uri.fragment, Some("section1"))
}
///|
test "RFC 3986 Character Classification" {
// Test unreserved characters
assert_eq(is_unreserved_byte('A'), true)
assert_eq(is_unreserved_byte('z'), true)
assert_eq(is_unreserved_byte('0'), true)
assert_eq(is_unreserved_byte('-'), true)
assert_eq(is_unreserved_byte('.'), true)
assert_eq(is_unreserved_byte('_'), true)
assert_eq(is_unreserved_byte('~'), true)
assert_eq(is_unreserved_byte('!'), false)
// Test gen-delims
assert_eq(is_gen_delim_byte(':'), true)
assert_eq(is_gen_delim_byte('/'), true)
assert_eq(is_gen_delim_byte('?'), true)
assert_eq(is_gen_delim_byte('#'), true)
assert_eq(is_gen_delim_byte('['), true)
assert_eq(is_gen_delim_byte(']'), true)
assert_eq(is_gen_delim_byte('@'), true)
// Test sub-delims
assert_eq(is_sub_delim_byte('!'), true)
assert_eq(is_sub_delim_byte('$'), true)
assert_eq(is_sub_delim_byte('&'), true)
assert_eq(is_sub_delim_byte('\''), true)
assert_eq(is_sub_delim_byte('('), true)
assert_eq(is_sub_delim_byte(')'), true)
assert_eq(is_sub_delim_byte('*'), true)
assert_eq(is_sub_delim_byte('+'), true)
assert_eq(is_sub_delim_byte(','), true)
assert_eq(is_sub_delim_byte(';'), true)
assert_eq(is_sub_delim_byte('='), true)
}
///|
test "RFC 3986 Dot Segment Removal" {
let path1 : Array[@bytes.View] = ["a", "b", "c", ".", "..", "..", "g"]
let result1 = remove_dot_segments(path1)
assert_eq(result1, ["a", "g"])
let path2 : Array[@bytes.View] = ["..", "..", "a", "b"]
let result2 = remove_dot_segments(path2)
assert_eq(result2, ["a", "b"])
let path3 : Array[@bytes.View] = [".", "a", ".", "b", "."]
let result3 = remove_dot_segments(path3)
assert_eq(result3, ["a", "b"])
}
///|
test "RFC 3986 Absolute URI Parsing" {
let uri = Uri::{
scheme: None,
authority: None,
path: [],
query: None,
fragment: None,
}
inspect(
parse_absolute_uri(
"http://example.com/path/to/resource?query=parameter", uri,
),
content="",
)
assert_eq(uri.scheme, Some("http"))
assert_eq(
uri.authority,
Some(Authority::{ userinfo: None, host: RegName("example.com"), port: None }),
)
assert_eq(uri.path, ["path", "to", "resource"])
assert_eq(uri.query, Some("query=parameter"))
assert_eq(uri.fragment, None)
}