// RFC 9309 path pattern normalization and matching.
///|
fn hex_value(ch : Char) -> Int {
let code = ch.to_int()
if code >= '0'.to_int() && code <= '9'.to_int() {
code - '0'.to_int()
} else if code >= 'A'.to_int() && code <= 'F'.to_int() {
code - 'A'.to_int() + 10
} else if code >= 'a'.to_int() && code <= 'f'.to_int() {
code - 'a'.to_int() + 10
} else {
-1
}
}
///|
fn hex_digit(value : Int) -> Char {
if value < 10 {
('0'.to_int() + value).unsafe_to_char()
} else {
('A'.to_int() + value - 10).unsafe_to_char()
}
}
///|
fn write_percent_encoded(out : StringBuilder, code : Int) -> Unit {
out.write_char('%')
out.write_char(hex_digit(code / 16))
out.write_char(hex_digit(code % 16))
}
///|
fn is_unreserved_ascii(code : Int) -> Bool {
(code >= 'A'.to_int() && code <= 'Z'.to_int()) ||
(code >= 'a'.to_int() && code <= 'z'.to_int()) ||
(code >= '0'.to_int() && code <= '9'.to_int()) ||
code == '-'.to_int() ||
code == '.'.to_int() ||
code == '_'.to_int() ||
code == '~'.to_int()
}
///|
fn normalize_percent_encoding(input : String) -> String {
let bytes = @utf8.encode(input)
let out = StringBuilder::new()
let mut i = 0
while i < bytes.length() {
let code = bytes[i].to_int()
if code == '%'.to_int() && i + 2 < bytes.length() {
let hi = hex_value(bytes[i + 1].to_int().unsafe_to_char())
let lo = hex_value(bytes[i + 2].to_int().unsafe_to_char())
if hi >= 0 && lo >= 0 {
let decoded = hi * 16 + lo
if is_unreserved_ascii(decoded) {
out.write_char(decoded.unsafe_to_char())
} else {
write_percent_encoded(out, decoded)
}
i = i + 3
continue
}
}
if code >= 128 {
write_percent_encoded(out, code)
} else {
out.write_char(code.unsafe_to_char())
}
i = i + 1
}
out.to_string()
}
///|
fn has_malformed_percent_encoding(input : String) -> Bool {
let chars = input.to_array()
let mut i = 0
while i < chars.length() {
if chars[i] == '%' {
if i + 2 >= chars.length() ||
hex_value(chars[i + 1]) < 0 ||
hex_value(chars[i + 2]) < 0 {
return true
}
i = i + 3
} else {
i = i + 1
}
}
false
}
///|
pub fn normalize_path_for_match(path : String) -> Result[String, RobotsError] {
if has_malformed_percent_encoding(path) {
Err(robots_error(Pattern, InvalidPercentEncoding, 0, 0, path))
} else {
let clean = if path.length() == 0 {
"/"
} else if path.has_prefix("/") {
path
} else {
"/\{path}"
}
Ok(normalize_percent_encoding(clean))
}
}
///|
fn normalize_rule_pattern_for_match(pattern : String) -> String {
normalize_percent_encoding(pattern)
}
///|
fn rule_match_length(pattern : String) -> Int {
let chars = pattern.to_array()
let mut n = 0
let mut i = 0
while i < chars.length() {
if chars[i] == '%' && i + 2 < chars.length() {
n = n + 1
i = i + 3
} else {
if chars[i] != '*' && chars[i] != '$' {
n = n + 1
}
i = i + 1
}
}
n
}
///|
fn pattern_exact_end(pattern : String) -> Bool {
pattern.has_suffix("$")
}
///|
fn pattern_body(pattern : String) -> String {
if pattern_exact_end(pattern) {
let chars = pattern.to_array()
String::from_array(chars[0:chars.length() - 1])
} else {
pattern
}
}
///|
fn wildcard_match(
pattern : Array[Char],
p_index : Int,
path : Array[Char],
s_index : Int,
exact_end : Bool,
) -> Bool {
if p_index == pattern.length() {
return if exact_end { s_index == path.length() } else { true }
}
if pattern[p_index] == '*' {
for next = s_index; next <= path.length(); next = next + 1 {
if wildcard_match(pattern, p_index + 1, path, next, exact_end) {
return true
}
}
false
} else if s_index < path.length() && pattern[p_index] == path[s_index] {
wildcard_match(pattern, p_index + 1, path, s_index + 1, exact_end)
} else {
false
}
}
///|
pub fn pattern_matches(pattern : String, path : String) -> Bool {
let normalized_pattern = normalize_rule_pattern_for_match(pattern)
let body = pattern_body(normalized_pattern)
wildcard_match(
body.to_array(),
0,
path.to_array(),
0,
pattern_exact_end(normalized_pattern),
)
}
///|
pub fn pattern_specificity(pattern : String) -> Int {
rule_match_length(normalize_rule_pattern_for_match(pattern))
}