/// Percent-encoding and decoding utilities based on RFC 3986
///|
/// Check if a string contains valid percent-encoding
/// pct-encoded = "%" HEXDIG HEXDIG
pub fn is_valid_percent_encoding(s : String) -> Bool {
let chars = s.to_array()
let len = chars.length()
fn check_from(i : Int) -> Bool {
if i >= len {
true
} else if chars[i] == '%' {
if i + 2 < len && is_hexdig(chars[i + 1]) && is_hexdig(chars[i + 2]) {
check_from(i + 3)
} else {
false
}
} else {
check_from(i + 1)
}
}
check_from(0)
}
///|
/// Convert a single byte to its uppercase percent-encoded form (e.g. 0xA9 -> "%A9")
fn byte_to_percent(b : Byte) -> String {
fn hex_digit(n : Int) -> String {
(if n < 10 {
(Char::to_int('0') + n).unsafe_to_char()
} else {
(Char::to_int('A') + n - 10).unsafe_to_char()
}).to_string()
}
let v = Byte::to_int(b)
"%" + hex_digit(v / 16) + hex_digit(v % 16)
}
///|
/// Encode a string with percent-encoding for characters that need it
/// The predicate function determines which characters should be encoded
/// Characters are encoded as their UTF-8 byte sequences, per RFC 3986
/// A '%' that begins a valid pct-encoded triplet ("%HH") is preserved
/// as-is so already-encoded strings pass through unchanged; any other
/// '%' is encoded as "%25" (RFC 3986 section 2.4)
pub fn percent_encode(s : String, should_encode : (Char) -> Bool) -> String {
let chars = s.to_array()
let len = chars.length()
let buf = StringBuilder()
for i = 0; i < len; i = i + 1 {
let c = chars[i]
if c == '%' &&
i + 2 < len &&
is_hexdig(chars[i + 1]) &&
is_hexdig(chars[i + 2]) {
buf.write_char(c) // already-encoded triplet: do not double-encode
} else if should_encode(c) {
@encoding.to_utf8_bytes(c)
.iter()
.each(fn(b) { buf.write_string(byte_to_percent(b)) })
} else {
buf.write_char(c)
}
}
buf.to_string()
}
///|
/// Decode a percent-encoded string
/// Percent-escape sequences are collected as raw bytes; literal characters
/// contribute their UTF-8 bytes. The byte sequence is then decoded as UTF-8,
/// so multi-byte sequences produced by `percent_encode` round-trip correctly.
pub fn percent_decode(s : String) -> URIResult[String] {
let chars = s.to_array()
let len = chars.length()
let bytes : Array[Byte] = []
fn collect_from(i : Int) -> URIResult[Unit] {
if i >= len {
Ok(())
} else if chars[i] == '%' {
if i + 2 < len {
match (hex_to_int(chars[i + 1]), hex_to_int(chars[i + 2])) {
(Some(h1), Some(h2)) => {
bytes.push((h1 * 16 + h2).to_byte())
collect_from(i + 3)
}
_ =>
Err(InvalidPercentEncoding("Invalid hex digits at position \{i}"))
}
} else {
Err(InvalidPercentEncoding("Incomplete percent encoding at end"))
}
} else {
@encoding.to_utf8_bytes(chars[i]).iter().each(fn(b) { bytes.push(b) })
collect_from(i + 1)
}
}
match collect_from(0) {
Ok(_) =>
Ok(decode_utf8(Bytes::from_array(bytes.view()))) catch {
_ => Err(InvalidPercentEncoding("Invalid UTF-8 byte sequence"))
}
Err(e) => Err(e)
}
}
///|
/// Decode a UTF-8 byte sequence, raising on malformed input
fn decode_utf8(bytes : Bytes) -> String raise {
@encoding.decoder(@encoding.Encoding::UTF8).decode(bytes)
}
///|
/// Encode a string for use in URI path segments
pub fn encode_path_segment(s : String) -> String {
percent_encode(s, fn(c) { !is_pchar(c) })
}
///|
/// Encode a string for use in URI query
pub fn encode_query(s : String) -> String {
percent_encode(s, fn(c) { !is_query_char(c) })
}
///|
/// Encode a string for use in URI fragment
pub fn encode_fragment(s : String) -> String {
percent_encode(s, fn(c) { !is_fragment_char(c) })
}
///|
/// Encode a string for use in URI userinfo
pub fn encode_userinfo(s : String) -> String {
percent_encode(s, fn(c) { !is_userinfo_char(c) })
}
///|
/// Encode a string for use in URI reg-name (domain names)
pub fn encode_reg_name(s : String) -> String {
percent_encode(s, fn(c) { !is_reg_name_char(c) })
}
///|
/// Check if a string is already properly percent-encoded and contains only valid characters
pub fn is_valid_encoded_string(
s : String,
char_validator : (Char) -> Bool,
) -> Bool {
let chars = s.to_array()
let len = chars.length()
fn check_from(i : Int) -> Bool {
if i >= len {
true
} else if chars[i] == '%' {
if i + 2 < len && is_hexdig(chars[i + 1]) && is_hexdig(chars[i + 2]) {
check_from(i + 3)
} else {
false
}
} else if char_validator(chars[i]) {
check_from(i + 1)
} else {
false
}
}
check_from(0)
}