use crate::host::{with_host, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
const BASE: u32 = 36;
const TMIN: u32 = 1;
const TMAX: u32 = 26;
const SKEW: u32 = 38;
const DAMP: u32 = 700;
const INITIAL_BIAS: u32 = 72;
const INITIAL_N: u32 = 128;
const DOTS: &[char] = &['\u{2E}', '\u{3002}', '\u{FF0E}', '\u{FF61}'];
pub const METHODS: &[&str] = &[
"encode",
"decode",
"toASCII",
"toUnicode",
"ucs2Decode",
"ucs2Encode",
];
pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
let input = super::arg_str(args, 0);
Some(match method {
"encode" => encode_js(&input),
"decode" => decode_js(&input),
"toASCII" => Ok(with_host(|h| h.new_str(to_ascii(&input)))),
"toUnicode" => Ok(with_host(|h| h.new_str(to_unicode(&input)))),
"ucs2Decode" => Ok(ucs2_decode_val(&input)),
"ucs2Encode" => Ok(ucs2_encode_val(args.first())),
_ => return None,
})
}
pub fn constant(name: &str) -> Option<Value> {
match name {
"ucs2" => Some(with_host(|h| {
let mut m = IndexMap::new();
m.insert(
"decode".into(),
h.alloc(JsObj::Builtin("punycode.ucs2Decode".into())),
);
m.insert(
"encode".into(),
h.alloc(JsObj::Builtin("punycode.ucs2Encode".into())),
);
h.new_object(m)
})),
"version" => Some(with_host(|h| h.new_str("2.3.1"))),
_ => None,
}
}
fn encode_js(s: &str) -> Result<Value, String> {
let cps: Vec<u32> = s.chars().map(|c| c as u32).collect();
match encode(&cps) {
Ok(out) => Ok(with_host(|h| h.new_str(out))),
Err(e) => Err(crate::host::range_error(&e)),
}
}
fn decode_js(s: &str) -> Result<Value, String> {
match decode(s) {
Ok(cps) => {
let out: String = cps.iter().filter_map(|&c| char::from_u32(c)).collect();
Ok(with_host(|h| h.new_str(out)))
}
Err(e) => Err(crate::host::range_error(&e)),
}
}
fn ucs2_decode_val(s: &str) -> Value {
with_host(|h| {
let items: Vec<Value> = s.chars().map(|c| Value::Float(c as u32 as f64)).collect();
h.new_array(items)
})
}
fn ucs2_encode_val(arg: Option<&Value>) -> Value {
let cps: Vec<u32> = match arg {
Some(v) => with_host(|h| match h.get(v) {
Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u32).collect(),
_ => Vec::new(),
}),
None => Vec::new(),
};
let out: String = cps.iter().filter_map(|&c| char::from_u32(c)).collect();
with_host(|h| h.new_str(out))
}
fn map_labels(domain: &str, f: impl Fn(&str) -> String) -> String {
domain
.split(|c| DOTS.contains(&c))
.map(f)
.collect::<Vec<_>>()
.join(".")
}
fn to_ascii(domain: &str) -> String {
map_labels(domain, |label| {
if label.chars().any(|c| (c as u32) >= 0x80) {
let cps: Vec<u32> = label.chars().map(|c| c as u32).collect();
match encode(&cps) {
Ok(enc) => format!("xn--{enc}"),
Err(_) => label.to_string(),
}
} else {
label.to_string()
}
})
}
fn to_unicode(domain: &str) -> String {
map_labels(domain, |label| {
let lower = label.to_lowercase();
match lower.strip_prefix("xn--") {
Some(rest) => match decode(rest) {
Ok(cps) => cps.iter().filter_map(|&c| char::from_u32(c)).collect(),
Err(_) => label.to_string(),
},
None => label.to_string(),
}
})
}
fn adapt(mut delta: u32, num_points: u32, first_time: bool) -> u32 {
delta = if first_time { delta / DAMP } else { delta / 2 };
delta += delta / num_points;
let mut k = 0;
while delta > ((BASE - TMIN) * TMAX) / 2 {
delta /= BASE - TMIN;
k += BASE;
}
k + (((BASE - TMIN + 1) * delta) / (delta + SKEW))
}
fn digit_to_basic(d: u32) -> char {
if d < 26 {
(b'a' + d as u8) as char
} else {
(b'0' + (d - 26) as u8) as char
}
}
fn basic_to_digit(c: char) -> Result<u32, String> {
match c {
'a'..='z' => Ok(c as u32 - 'a' as u32),
'A'..='Z' => Ok(c as u32 - 'A' as u32),
'0'..='9' => Ok(c as u32 - '0' as u32 + 26),
_ => Err(format!("Invalid input: {c}")),
}
}
fn encode(input: &[u32]) -> Result<String, String> {
let mut output = String::new();
let mut b: u32 = 0;
for &c in input {
if c < 0x80 {
output.push(c as u8 as char);
b += 1;
}
}
let mut h = b;
if b > 0 {
output.push('-');
}
let input_len = input.len() as u32;
let mut n = INITIAL_N;
let mut delta: u32 = 0;
let mut bias = INITIAL_BIAS;
while h < input_len {
let mut m = u32::MAX;
for &c in input {
if c >= n && c < m {
m = c;
}
}
delta = delta
.checked_add((m - n).checked_mul(h + 1).ok_or("overflow")?)
.ok_or("overflow")?;
n = m;
for &c in input {
if c < n {
delta = delta.checked_add(1).ok_or("overflow")?;
}
if c == n {
let mut q = delta;
let mut k = BASE;
loop {
let t = threshold(k, bias);
if q < t {
break;
}
let digit = t + ((q - t) % (BASE - t));
output.push(digit_to_basic(digit));
q = (q - t) / (BASE - t);
k += BASE;
}
output.push(digit_to_basic(q));
bias = adapt(delta, h + 1, h == b);
delta = 0;
h += 1;
}
}
delta += 1;
n += 1;
}
Ok(output)
}
fn decode(input: &str) -> Result<Vec<u32>, String> {
let chars: Vec<char> = input.chars().collect();
let mut output: Vec<u32> = Vec::new();
let mut idx = match input.rfind('-') {
Some(pos) => {
for &c in &chars[..pos] {
if (c as u32) >= 0x80 {
return Err("Illegal basic code point".into());
}
output.push(c as u32);
}
pos + 1
}
None => 0,
};
let mut n = INITIAL_N;
let mut i: u32 = 0;
let mut bias = INITIAL_BIAS;
let len = chars.len();
while idx < len {
let oldi = i;
let mut w: u32 = 1;
let mut k = BASE;
loop {
if idx >= len {
return Err("Invalid input".into());
}
let digit = basic_to_digit(chars[idx])?;
idx += 1;
i = i
.checked_add(digit.checked_mul(w).ok_or("overflow")?)
.ok_or("overflow")?;
let t = threshold(k, bias);
if digit < t {
break;
}
w = w.checked_mul(BASE - t).ok_or("overflow")?;
k += BASE;
}
let out_len = output.len() as u32 + 1;
bias = adapt(i - oldi, out_len, oldi == 0);
n = n.checked_add(i / out_len).ok_or("overflow")?;
i %= out_len;
output.insert(i as usize, n);
i += 1;
}
Ok(output)
}
fn threshold(k: u32, bias: u32) -> u32 {
if k <= bias {
TMIN
} else if k >= bias + TMAX {
TMAX
} else {
k - bias
}
}