// Copyright (c) 2026 colmugx
// SPDX-License-Identifier: Apache-2.0
///|
/// Component of a URI/IRI reference that constrains the character set.
priv enum FormatPart {
UserInfo
Host
Path
Query
Fragment
}
///|
/// `uri` (RFC 3986 `URI`): an absolute URI, so a scheme is mandatory.
fn format_uri(value : String) -> Bool {
format_uri_like(value, false, true)
}
///|
/// `uri-reference` (RFC 3986 `URI-reference`, including the empty reference).
fn format_uri_reference(value : String) -> Bool {
format_uri_like(value, false, false)
}
///|
/// `iri` (RFC 3987 `IRI`): an absolute IRI, so a scheme is mandatory.
fn format_iri(value : String) -> Bool {
format_uri_like(value, true, true)
}
///|
/// `iri-reference` (RFC 3987 `IRI-reference`, including the empty reference).
fn format_iri_reference(value : String) -> Bool {
format_uri_like(value, true, false)
}
///|
/// RFC 3986 section 3 splitting and RFC 3986/3987 component productions. The
/// package's `Uri::parse` owns scheme/hier-part/query/fragment splitting, so
/// this only has to decide whether every component matches its ABNF
/// alternative.
fn format_uri_like(value : String, iri : Bool, absolute : Bool) -> Bool {
let parsed = Uri::parse(value)
if absolute && parsed.scheme is None {
return false
}
let authority_present = parsed.authority is Some(_)
if parsed.authority is Some(authority) && !format_authority(authority, iri) {
return false
}
if !format_part_valid(parsed.path[:], Path, iri) {
return false
}
if parsed.query is Some(query) && !format_part_valid(query[:], Query, iri) {
return false
}
if parsed.fragment is Some(fragment) &&
!format_part_valid(fragment[:], Fragment, iri) {
return false
}
// RFC 3986 3.3: with no authority the path is `path-absolute`,
// `path-rootless` or `path-empty`; a relative reference additionally uses
// `path-noscheme`, whose first segment may not contain a colon. `Uri::parse`
// already routes a leading `//` to the authority, so no further split checks
// are needed for `path-abempty`.
if parsed.scheme is None && !authority_present {
let path = parsed.path
if path != "" &&
!path.has_prefix("/") &&
!format_first_segment_colon_free(path) {
return false
}
}
true
}
///|
/// `path-noscheme`: the first segment of a relative-path reference may not
/// contain `:` (`./this:that` is fine because of the leading dot segment).
fn format_first_segment_colon_free(path : String) -> Bool {
let mut index = 0
while index < path.length() {
match path.get_char(index) {
Some('/') => return true
Some(':') => return false
Some(ch) => index = index + char_width(ch)
None => return false
}
}
true
}
///|
/// `authority = [ userinfo "@" ] host [ ":" port ]` with at most one `@`.
fn format_authority(authority : String, iri : Bool) -> Bool {
let (userinfo, hostport) = match split_at_first(authority, '@') {
(info, Some(tail)) => (Some(info), tail)
(rest, None) => (None, rest)
}
if hostport.contains_char('@') {
return false
}
if userinfo is Some(info) && !format_part_valid(info[:], UserInfo, iri) {
return false
}
let (host, port) = split_host_port(hostport)
if port is Some(digits) && !digits.is_empty() {
if !format_all_digits(digits[:]) {
return false
}
}
if host.is_empty() {
return true
}
if host.has_prefix("[") {
return valid_ip_literal(host)
}
format_part_valid(host[:], Host, iri)
}
///|
fn format_part_valid(text : StringView, part : FormatPart, iri : Bool) -> Bool {
let mut index = 0
while index < text.length() {
let next = text.get_char(index)
if next is Some('%') {
guard index + 2 < text.length() &&
text.get_char(index + 1) is Some(high) &&
format_is_hex(high) &&
text.get_char(index + 2) is Some(low) &&
format_is_hex(low) else {
return false
}
index = index + 3
} else if next is Some(ch) {
if !format_component_char(ch, part, iri) {
return false
}
index = index + char_width(ch)
} else {
return false
}
}
true
}
///|
fn format_component_char(ch : Char, part : FormatPart, iri : Bool) -> Bool {
if is_unreserved_byte(ch.to_int()) || uri_subdelimiter(ch) {
return true
}
if ch.to_int() > 0x7F {
if !iri {
return false
}
return match part {
Query => format_is_ucschar(ch) || format_is_iprivate(ch)
_ => format_is_ucschar(ch)
}
}
match part {
UserInfo => ch == ':'
Path => ch == ':' || ch == '@' || ch == '/'
Query | Fragment => ch == ':' || ch == '@' || ch == '/' || ch == '?'
Host => false
}
}
///|
/// RFC 3987 `ucschar`: the non-ASCII-visible part of an IRI.
fn format_is_ucschar(ch : Char) -> Bool {
let code = ch.to_int()
if code < 0xA0 {
false
} else if code <= 0xD7FF {
true
} else if code >= 0xF900 && code <= 0xFDCF {
true
} else if code >= 0xFDF0 && code <= 0xFFEF {
true
} else if code >= 0x10000 && code <= 0xEFFFD {
let low = code & 0xFFFF
if code >= 0xE0000 {
low >= 0x1000
} else {
low <= 0xFFFD
}
} else {
false
}
}
///|
/// RFC 3987 `iprivate`: private-use code points, which appear in queries but
/// not in hosts, paths or fragments.
fn format_is_iprivate(ch : Char) -> Bool {
let code = ch.to_int()
(code >= 0xE000 && code <= 0xF8FF) ||
(code >= 0xF0000 && code <= 0xFFFFD) ||
(code >= 0x100000 && code <= 0x10FFFD)
}