///|
/// Punycode encoding/decoding (RFC 3492)
/// Used for Internationalized Domain Names in Applications (IDNA)
///|
/// Error type for Punycode operations
pub(all) suberror PunycodeError {
Overflow
InvalidInput
BadInput
}
///|
pub extend PunycodeError with Show::{to_string, output}
///|
pub impl Show for PunycodeError with fn output(self, logger) {
match self {
Overflow => logger.write_string("Overflow")
InvalidInput => logger.write_string("InvalidInput")
BadInput => logger.write_string("BadInput")
}
}
// RFC 3492 constants (Section 4)
///|
/// Base for variable-length integer encoding. Punycode uses base-36
/// (letters a-z represent 0-25, digits 0-9 represent 26-35).
const Base : Int32 = 36
///|
/// Minimum threshold for variable-length integer encoding.
/// Ensures at least one digit is always output.
const Tmin : Int32 = 1
///|
/// Maximum threshold for variable-length integer encoding.
/// Equals the number of lowercase letters (a-z).
const Tmax : Int32 = 26
///|
/// Skew parameter for bias adaptation function.
/// Controls how quickly the bias adjusts to character frequencies.
const Skew : Int32 = 38
///|
/// Damping factor applied only to the first delta value.
/// Prevents the bias from changing too rapidly at the start.
const Damp : Int32 = 700
///|
/// Initial bias value for the adaptation algorithm.
/// Chosen to work well for typical internationalized domain names.
const InitialBias : Int32 = 72
///|
/// Initial value of n, the threshold between basic ASCII (0-127)
/// and non-basic code points that require encoding.
const InitialN : Int32 = 0x80
///|
/// Delimiter character separating the basic ASCII portion
/// from the Punycode-encoded portion in the output.
const Delimiter : Char = '-'
///|
/// Largest positive value representable by MoonBit's signed Int type.
const MaxInt : Int32 = 0x7FFFFFFF
///|
/// Bias adaptation function (RFC 3492 Section 3.4)
fn adapt(delta : Int32, numpoints : Int32, firsttime : Bool) -> Int32 {
let mut delta = delta
if firsttime {
delta = delta / Damp
} else {
delta = delta / 2
}
delta = delta + delta / numpoints
let mut k = 0
while delta > (Base - Tmin) * Tmax / 2 {
delta = delta / (Base - Tmin)
k = k + Base
}
k + (Base - Tmin + 1) * delta / (delta + Skew)
}
///|
/// Encode a digit to a basic code point (a-z for 0-25, 0-9 for 26-35)
fn encode_digit(d : Int32) -> Char {
if d < 26 {
(d + 'a'.to_int()).unsafe_to_char()
} else {
(d - 26 + '0'.to_int()).unsafe_to_char()
}
}
///|
/// Decode a basic code point to a digit
fn decode_digit(cp : Int32) -> Int32 {
if cp >= 'a'.to_int() && cp <= 'z'.to_int() {
cp - 'a'.to_int()
} else if cp >= 'A'.to_int() && cp <= 'Z'.to_int() {
cp - 'A'.to_int()
} else if cp >= '0'.to_int() && cp <= '9'.to_int() {
cp - '0'.to_int() + 26
} else {
Base // Invalid
}
}
///|
/// Encode a Unicode string to Punycode
pub fn encode(input : String) -> String raise PunycodeError {
// The UTF-16 length bounds the number of code points.
let code_points : FixedArray[Int32] = FixedArray::make(input.length(), 0)
let mut count = 0
let mut basic_count = 0
for c in input {
let cp = c.to_int()
code_points[count] = cp
count = count + 1
if cp < InitialN {
basic_count = basic_count + 1
}
}
if basic_count == count {
// Only basic code points: they are copied, followed by the delimiter.
return if basic_count == 0 { "" } else { input + "-" }
}
// Reserve the basic code points, the delimiter and a few digits for each
// other code point; the size hint counts bytes, two per UTF-16 code unit.
let output = StringBuilder(
size_hint=2 * (basic_count + 1 + 4 * (count - basic_count)),
)
// Copy all basic code points to output
for i in 0.. 0 {
output.write_char(Delimiter)
}
let mut n = InitialN
let mut delta = 0
let mut bias = InitialBias
let mut handled = basic_count
while handled < count {
// Find the minimum code point >= n
let mut m = 0x10FFFF + 1
for i in 0..= n && cp < m {
m = cp
}
}
// Increase delta enough to advance the decoder's state to
let code_point_step = m - n
let point_count = handled + 1
if code_point_step > (MaxInt - delta) / point_count {
raise Overflow
}
delta = delta + code_point_step * point_count
n = m
for i in 0..= bias + Tmax {
Tmax
} else {
k - bias
}
if q < t {
break
}
output.write_char(encode_digit(t + (q - t) % (Base - t)))
q = (q - t) / (Base - t)
k = k + Base
}
output.write_char(encode_digit(q))
bias = adapt(delta, handled + 1, handled == basic_count)
delta = 0
handled = handled + 1
}
}
if delta == MaxInt {
raise Overflow
}
delta = delta + 1
n = n + 1
}
output.to_string()
}
///|
/// Decode a Punycode string to Unicode
pub fn decode(input : String) -> String raise PunycodeError {
// Valid input is ASCII, so it is read as UTF-16 code units; any other
// unit is rejected where it is reached.
let length = input.length()
// Find the last delimiter
let mut basic_end = -1
for i = length - 1; i >= 0; i = i - 1 {
if input[i].to_int() == Delimiter.to_int() {
basic_end = i
break
}
}
// Code points of the output. Each one consumes at least one code unit of
// input, so the input length bounds their number.
let output : FixedArray[Int32] = FixedArray::make(length, 0)
let mut output_length = 0
// Copy basic code points before the last delimiter
for i in 0..= InitialN {
raise BadInput
}
output[i] = unit
output_length = i + 1
}
// Main decoding loop
let mut n = InitialN
let mut i_val = 0
let mut bias = InitialBias
let mut in_pos = if basic_end >= 0 { basic_end + 1 } else { 0 }
// UTF-16 length of the output
let mut output_units = output_length
while in_pos < length {
let oldi = i_val
let mut w = 1
let mut k = Base
while true {
if in_pos >= length {
raise BadInput
}
let digit = decode_digit(input[in_pos].to_int())
in_pos = in_pos + 1
if digit >= Base {
raise BadInput
}
if digit > (MaxInt - i_val) / w {
raise Overflow
}
i_val = i_val + digit * w
let t = if k <= bias {
Tmin
} else if k >= bias + Tmax {
Tmax
} else {
k - bias
}
if digit < t {
break
}
if w > MaxInt / (Base - t) {
raise Overflow
}
w = w * (Base - t)
k = k + Base
}
let out_len = output_length + 1
bias = adapt(i_val - oldi, out_len, oldi == 0)
if i_val / out_len > MaxInt - n {
raise Overflow
}
n = n + i_val / out_len
i_val = i_val % out_len
// Reject values that are not valid Unicode scalar values before building
// the output string. Int::to_char also rejects surrogate code points.
guard n.to_char() is Some(_) else { raise BadInput }
// Insert n at position i_val.
output.blit_to(
output,
len=output_length - i_val,
src_offset=i_val,
dst_offset=i_val + 1,
)
output[i_val] = n
output_length = out_len
output_units += if n > 0xFFFF { 2 } else { 1 }
i_val = i_val + 1
}
// The size hint counts bytes, so the builder is filled exactly.
let result = StringBuilder(size_hint=2 * output_units)
for i in 0..