///|
/// Host header parsing errors (RFC 9110 Section 7.2)
pub(all) enum HostError {
Empty
InvalidFormat
InvalidHost
InvalidPort
} derive(Show, Eq)
///|
/// Host header (RFC 9110 Section 7.2)
pub(all) struct Host {
host : String
port : Int?
} derive(Show, Eq)
///|
pub fn Host::parse(input : String) -> Result[Host, HostError] {
let s = host_trim_string(input)
if s.length() == 0 {
return Err(Empty)
}
// Check for whitespace
if host_has_whitespace(s) {
return Err(InvalidFormat)
}
// IPv6 address
if host_string_starts_with(s, "[") {
return host_parse_ipv6(s)
}
let split_result = host_split_host_port(s)
match split_result {
Err(e) => return Err(e)
Ok((host_part, port)) => {
if host_part.length() == 0 || host_contains(host_part, 64) { // @
return Err(InvalidHost)
}
// Check if it's a valid IPv4 address or reg-name
if !host_is_valid_ipv4_or_reg_name(host_part) {
return Err(InvalidHost)
}
Ok({ host: host_part, port })
}
}
}
///|
pub fn Host::host(self : Host) -> String {
self.host
}
///|
pub fn Host::port(self : Host) -> Int? {
self.port
}
///|
pub fn Host::is_ipv6(self : Host) -> Bool {
host_string_starts_with(self.host, "[")
}
///|
pub fn Host::to_string(self : Host) -> String {
match self.port {
Some(p) => self.host + ":" + p.to_string()
None => self.host
}
}
///|
/// Helper functions
///
///|
fn host_parse_ipv6(input : String) -> Result[Host, HostError] {
let end = host_find_char_in(input, 0, 93) // ]
match end {
None => Err(InvalidHost)
Some(e) => {
let host_inner = host_string_slice(input, 1, e)
let rest = host_string_slice_from(input, e + 1)
let port : Int? = if rest.length() == 0 {
None
} else if host_string_starts_with(rest, ":") {
let port_str = host_string_slice_from(rest, 1)
match host_parse_port(port_str) {
Some(p) => Some(p)
None => return Err(InvalidPort)
}
} else {
return Err(InvalidHost)
}
if host_inner.length() == 0 {
return Err(InvalidHost)
}
// Basic IPv6 validation (just check for hex digits and colons)
if !host_is_valid_ipv6_content(host_inner) {
return Err(InvalidHost)
}
Ok({ host: host_string_slice(input, 0, e + 1), port })
}
}
}
///|
fn host_split_host_port(input : String) -> Result[(String, Int?), HostError] {
let colon_pos = host_find_last_char_in(input, 58) // :
match colon_pos {
Some(pos) => {
let host_part = host_string_slice(input, 0, pos)
let port_str = host_string_slice_from(input, pos + 1)
// Check if host part contains multiple colons (IPv6 without brackets)
if host_contains(host_part, 58) {
return Err(InvalidHost)
}
if port_str.length() == 0 {
return Err(InvalidPort)
}
match host_parse_port(port_str) {
Some(p) => Ok((host_part, Some(p)))
None => Err(InvalidPort)
}
}
None => Ok((input, None))
}
}
///|
fn host_parse_port(input : String) -> Int? {
if input.length() == 0 {
return None
}
let mut result = 0
let mut idx = 0
let len = input.length()
while idx < len {
let ch = host_char_at(input, idx)
if !host_is_digit(ch) {
return None
}
result = result * 10 + (ch - 48)
if result > 65535 {
return None
}
idx = idx + 1
}
Some(result)
}
///|
fn host_is_valid_ipv4_or_reg_name(input : String) -> Bool {
// Try to parse as IPv4 first
if host_is_valid_ipv4(input) {
return true
}
// Check if it's a valid reg-name
host_is_valid_reg_name(input)
}
///|
fn host_is_valid_ipv4(input : String) -> Bool {
let parts = host_split_string(input, ".")
if parts.length() != 4 {
return false
}
let mut idx = 0
while idx < parts.length() {
let part = parts[idx]
if !host_is_valid_ipv4_part(part) {
return false
}
idx = idx + 1
}
true
}
///|
fn host_is_valid_ipv4_part(input : String) -> Bool {
if input.length() == 0 || input.length() > 3 {
return false
}
let mut idx = 0
while idx < input.length() {
let ch = host_char_at(input, idx)
if !host_is_digit(ch) {
return false
}
idx = idx + 1
}
// Check numeric value
let value = host_parse_int(input)
match value {
Some(v) => v >= 0 && v <= 255
None => false
}
}
///|
fn host_parse_int(input : String) -> Int? {
let mut result = 0
let mut idx = 0
let len = input.length()
while idx < len {
let ch = host_char_at(input, idx)
if !host_is_digit(ch) {
return None
}
result = result * 10 + (ch - 48)
idx = idx + 1
}
Some(result)
}
///|
fn host_is_valid_reg_name(input : String) -> Bool {
if input.length() == 0 {
return false
}
let mut idx = 0
while idx < input.length() {
let b = host_char_at(input, idx)
if host_is_unreserved(b) || host_is_sub_delim(b) {
idx = idx + 1
continue
}
if b == 37 { // %
if idx + 2 >= input.length() {
return false
}
let h1 = host_char_at(input, idx + 1)
let h2 = host_char_at(input, idx + 2)
if !host_is_hexdig(h1) || !host_is_hexdig(h2) {
return false
}
idx = idx + 3
continue
}
return false
}
true
}
///|
fn host_is_valid_ipv6_content(input : String) -> Bool {
if input.length() == 0 {
return false
}
// Check for IPvFuture (v or V prefix)
let first = host_char_at(input, 0)
if first == 118 || first == 86 { // v or V
return host_is_valid_ipvfuture(input)
}
// Basic validation: hex digits, colons, double colons
let mut idx = 0
while idx < input.length() {
let ch = host_char_at(input, idx)
if !host_is_hexdig(ch) && ch != 58 { // :
return false
}
idx = idx + 1
}
true
}
///|
fn host_is_valid_ipvfuture(input : String) -> Bool {
if input.length() < 3 {
return false
}
let first = host_char_at(input, 0)
if first != 118 && first != 86 { // v or V
return false
}
let mut idx = 1
let mut hex_len = 0
while idx < input.length() && host_is_hexdig(host_char_at(input, idx)) {
hex_len = hex_len + 1
idx = idx + 1
}
if hex_len == 0 || idx >= input.length() || host_char_at(input, idx) != 46 { // .
return false
}
idx = idx + 1
if idx >= input.length() {
return false
}
while idx < input.length() {
let b = host_char_at(input, idx)
if !host_is_ipvfuture_char(b) {
return false
}
idx = idx + 1
}
true
}
///|
fn host_is_ipvfuture_char(b : Int) -> Bool {
host_is_unreserved(b) || host_is_sub_delim(b) || b == 58 // :
}
///|
fn host_is_unreserved(b : Int) -> Bool {
host_is_alnum(b) || b == 45 || b == 46 || b == 95 || b == 126 // -, ., _, ~
}
///|
fn host_is_alnum(b : Int) -> Bool {
host_is_alpha(b) || host_is_digit(b)
}
///|
fn host_is_alpha(b : Int) -> Bool {
(b >= 65 && b <= 90) || (b >= 97 && b <= 122) // A-Z, a-z
}
///|
fn host_is_digit(b : Int) -> Bool {
b >= 48 && b <= 57 // 0-9
}
///|
fn host_is_hexdig(b : Int) -> Bool {
host_is_digit(b) || (b >= 65 && b <= 70) || (b >= 97 && b <= 102) // A-F, a-f
}
///|
fn host_is_sub_delim(b : Int) -> Bool {
b == 33 ||
b == 36 ||
b == 38 ||
b == 39 ||
b == 40 ||
b == 41 ||
b == 42 ||
b == 43 ||
b == 44 ||
b == 59 ||
b == 61 // !, $, &, ', (, ), *, +, ,, ;, =
}
///|
fn host_has_whitespace(s : String) -> Bool {
let mut idx = 0
while idx < s.length() {
let ch = host_char_at(s, idx)
if ch == 32 || ch == 9 || ch == 10 || ch == 13 { // space, tab, LF, CR
return true
}
idx = idx + 1
}
false
}
///|
fn host_string_starts_with(s : String, prefix : String) -> Bool {
if prefix.length() > s.length() {
return false
}
let mut idx = 0
while idx < prefix.length() {
if s[idx] != prefix[idx] {
return false
}
idx = idx + 1
}
true
}
///|
fn host_char_at(s : String, idx : Int) -> Int {
str_char_at(s, idx)
}
///|
fn host_find_char_in(s : String, start : Int, target : Int) -> Int? {
let mut idx = start
while idx < s.length() {
if host_char_at(s, idx) == target {
return Some(idx)
}
idx = idx + 1
}
None
}
///|
fn host_find_last_char_in(s : String, target : Int) -> Int? {
let mut idx = s.length() - 1
while idx >= 0 {
if host_char_at(s, idx) == target {
return Some(idx)
}
if idx == 0 {
break
}
idx = idx - 1
}
None
}
///|
fn host_contains(s : String, target : Int) -> Bool {
match host_find_char_in(s, 0, target) {
Some(_) => true
None => false
}
}
///|
fn host_string_slice(s : String, start : Int, end : Int) -> String {
str_string_slice(s, start, end)
}
///|
fn host_string_slice_from(s : String, start : Int) -> String {
str_string_slice_from(s, start)
}
///|
fn host_split_string(s : String, sep : String) -> Array[String] {
str_split_string(s, sep)
}
///|
fn host_trim_string(s : String) -> String {
str_trim_string(s)
}