use crate::host::{self, with_host, JsObj, RegExpObj};
use crate::utf16::{self, U16Index};
use fancy_regex::{Captures, Regex};
use fusevm::Value;
use indexmap::IndexMap;
use rustc_hash::FxHashMap;
use std::cell::RefCell;
use std::rc::Rc;
const SURROGATE_LO: u32 = 0xD800;
const SURROGATE_HI: u32 = 0xDFFF;
const SURROGATE_PUA_BASE: u32 = 0xF_0000;
fn remap_surrogate(cp: u32) -> u32 {
if (SURROGATE_LO..=SURROGATE_HI).contains(&cp) {
SURROGATE_PUA_BASE + (cp - SURROGATE_LO)
} else {
cp
}
}
pub fn build_regexp(pattern: &str, flags: &str) -> Result<Value, String> {
let mut seen = String::new();
for c in flags.chars() {
if !"gimsuyd".contains(c) || seen.contains(c) {
return Err(format!(
"SyntaxError: Invalid flags supplied to RegExp constructor '{flags}'"
));
}
seen.push(c);
}
let global = flags.contains('g');
let ignore_case = flags.contains('i');
let multiline = flags.contains('m');
let dot_all = flags.contains('s');
let sticky = flags.contains('y');
let unicode = flags.contains('u');
let rust_pat = translate(pattern)?;
let mut prefixed = String::new();
if ignore_case || multiline || dot_all {
prefixed.push_str("(?");
if ignore_case {
prefixed.push('i');
}
if multiline {
prefixed.push('m');
}
if dot_all {
prefixed.push('s');
}
prefixed.push(')');
}
prefixed.push_str(&rust_pat);
let re = compiled(&prefixed).map_err(|e| {
let msg = e.lines().collect::<Vec<_>>().join(" ");
format!("SyntaxError: Invalid regular expression: /{pattern}/: {msg}")
})?;
let obj = RegExpObj {
re,
source: if pattern.is_empty() {
"(?:)".to_string()
} else {
pattern.to_string()
},
flags: flags.to_string(),
global,
ignore_case,
multiline,
dot_all,
sticky,
unicode,
last_index: U16Index::ZERO,
};
Ok(with_host(|h| h.alloc(JsObj::RegExp(Box::new(obj)))))
}
fn compiled(prefixed: &str) -> Result<Rc<Regex>, String> {
thread_local! {
static CACHE: RefCell<FxHashMap<String, Rc<Regex>>> =
RefCell::new(FxHashMap::default());
}
if let Some(hit) = CACHE.with(|c| c.borrow().get(prefixed).cloned()) {
return Ok(hit);
}
let re = Rc::new(Regex::new(prefixed).map_err(|e| e.to_string())?);
CACHE.with(|c| {
c.borrow_mut().insert(prefixed.to_string(), re.clone());
});
Ok(re)
}
fn translate(pat: &str) -> Result<String, String> {
let chars: Vec<char> = pat.chars().collect();
let mut out = String::new();
let mut i = 0;
let mut in_class = false;
let mut class_pos = 0usize;
while i < chars.len() {
let c = chars[i];
if c != '\\' {
if !in_class && c == '[' {
in_class = true;
class_pos = 0;
out.push('[');
i += 1;
if chars.get(i) == Some(&'^') {
out.push('^');
i += 1;
}
continue;
}
if in_class {
if c == ']' && class_pos > 0 {
in_class = false;
out.push(']');
i += 1;
continue;
}
if c == '[' {
out.push_str("\\[");
class_pos += 1;
i += 1;
continue;
}
}
}
match c {
'\\' => {
class_pos += 1;
match chars.get(i + 1).copied() {
Some('u') => {
i += 2;
let cp_hex: String;
if chars.get(i) == Some(&'{') {
i += 1;
let mut hex = String::new();
while i < chars.len() && chars[i] != '}' {
hex.push(chars[i]);
i += 1;
}
i += 1; cp_hex = hex;
} else {
cp_hex = chars[i..(i + 4).min(chars.len())].iter().collect();
i += 4;
}
match u32::from_str_radix(cp_hex.trim(), 16) {
Ok(cp) => out.push_str(&format!("\\x{{{:X}}}", remap_surrogate(cp))),
Err(_) => out.push_str(&format!("\\x{{{cp_hex}}}")),
}
continue;
}
Some('/') => {
out.push('/');
i += 2;
continue;
}
Some(other) => {
out.push('\\');
out.push(other);
i += 2;
continue;
}
None => {
out.push('\\');
i += 1;
}
}
}
_ => {
if in_class {
class_pos += 1;
}
out.push(c);
i += 1;
}
}
}
Ok(out)
}
fn canonical_flags(flags: &str) -> String {
"dgimsuvy"
.chars()
.filter(|c| flags.contains(*c))
.collect::<String>()
}
pub fn regexp_property(r: &RegExpObj, name: &str) -> Option<Value> {
Some(match name {
"source" => with_host(|h| h.new_str(r.source.clone())),
"flags" => with_host(|h| h.new_str(canonical_flags(&r.flags))),
"global" => Value::Bool(r.global),
"ignoreCase" => Value::Bool(r.ignore_case),
"multiline" => Value::Bool(r.multiline),
"dotAll" => Value::Bool(r.dot_all),
"sticky" => Value::Bool(r.sticky),
"unicode" => Value::Bool(r.unicode),
"hasIndices" => Value::Bool(r.flags.contains('d')),
"unicodeSets" => Value::Bool(r.flags.contains('v')),
"lastIndex" => Value::Float(r.last_index.get() as f64),
_ => return None,
})
}
pub fn is_regexp_method(name: &str) -> bool {
matches!(name, "test" | "exec" | "toString" | "compile")
}
pub fn regexp_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
match name {
"test" => {
let s = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
Ok(Value::Bool(regexp_test(recv, &s)))
}
"exec" => {
let s = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
regexp_exec(recv, &s)
}
"toString" => Ok(with_host(|h| {
let s = h.str_of(recv);
h.new_str(s)
})),
"compile" => Ok(recv.clone()),
_ => Err(host::type_error(&format!("{name} is not a function"))),
}
}
fn regexp_snapshot(recv: &Value) -> Option<(Rc<Regex>, bool, bool, U16Index)> {
with_host(|h| match h.get(recv) {
Some(JsObj::RegExp(r)) => Some((r.re.clone(), r.global, r.sticky, r.last_index)),
_ => None,
})
}
fn set_last_index(recv: &Value, idx: U16Index) {
with_host(|h| {
if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
r.last_index = idx;
}
});
}
fn byte_of_index(s: &str, n: U16Index) -> usize {
utf16::byte_of_index(s, n)
}
fn index_of_byte(s: &str, byte: usize) -> U16Index {
utf16::index_of_byte(s, byte)
}
pub fn regexp_test(recv: &Value, s: &str) -> bool {
let Some((re, global, sticky, last)) = regexp_snapshot(recv) else {
return false;
};
let start_idx = if global || sticky {
last
} else {
U16Index::ZERO
};
if start_idx.get() > utf16::len(s) {
if global || sticky {
set_last_index(recv, U16Index::ZERO);
}
return false;
}
let start_byte = byte_of_index(s, start_idx);
match re.find_from_pos(s, start_byte) {
Ok(Some(m)) if !sticky || m.start() == start_byte => {
if global || sticky {
set_last_index(recv, index_of_byte(s, m.end()));
}
true
}
_ => {
if global || sticky {
set_last_index(recv, U16Index::ZERO);
}
false
}
}
}
pub fn regexp_exec(recv: &Value, s: &str) -> Result<Value, String> {
let Some((re, global, sticky, last)) = regexp_snapshot(recv) else {
return Ok(with_host(|h| h.null()));
};
let start_idx = if global || sticky {
last
} else {
U16Index::ZERO
};
if start_idx.get() > utf16::len(s) {
if global || sticky {
set_last_index(recv, U16Index::ZERO);
}
return Ok(with_host(|h| h.null()));
}
let start_byte = byte_of_index(s, start_idx);
let caps = re.captures_from_pos(s, start_byte).ok().flatten();
let caps = match caps {
Some(c) if !sticky || c.get(0).map(|m| m.start()) == Some(start_byte) => c,
_ => {
if global || sticky {
set_last_index(recv, U16Index::ZERO);
}
return Ok(with_host(|h| h.null()));
}
};
let whole = caps.get(0).unwrap();
if global || sticky {
set_last_index(recv, index_of_byte(s, whole.end()));
}
Ok(build_match_array(&re, &caps, s))
}
fn build_match_array(re: &Regex, caps: &Captures, s: &str) -> Value {
let mut items: Vec<Value> = Vec::with_capacity(caps.len());
for i in 0..caps.len() {
items.push(match caps.get(i) {
Some(m) => with_host(|h| h.new_str(m.as_str().to_string())),
None => Value::Undef, });
}
let whole = caps.get(0).unwrap();
let arr = with_host(|h| h.new_array(items));
let index = index_of_byte(s, whole.start()).get();
with_host(|h| {
let idx = Value::Float(index as f64);
h.set_fn_prop(&arr, "index", idx);
let input = h.new_str(s.to_string());
h.set_fn_prop(&arr, "input", input);
});
let names: Vec<&str> = re.capture_names().flatten().collect();
if names.is_empty() {
with_host(|h| h.set_fn_prop(&arr, "groups", Value::Undef));
} else {
let mut g: IndexMap<String, Value> = IndexMap::new();
for name in names {
let v = match caps.name(name) {
Some(m) => with_host(|h| h.new_str(m.as_str().to_string())),
None => Value::Undef,
};
g.insert(name.to_string(), v);
}
with_host(|h| {
let obj = h.new_object(g);
let null = h.null();
h.set_proto(&obj, null);
h.set_fn_prop(&arr, "groups", obj);
});
}
arr
}
pub fn str_match(s: &str, re_val: &Value) -> Result<Value, String> {
let Some((re, global, _, _)) = regexp_snapshot(re_val) else {
return Ok(with_host(|h| h.null()));
};
if !global {
set_last_index(re_val, U16Index::ZERO);
return regexp_exec_from_zero(&re, s);
}
let matches: Vec<Value> = re
.find_iter(s)
.filter_map(|m| m.ok())
.map(|m| with_host(|h| h.new_str(m.as_str().to_string())))
.collect();
if matches.is_empty() {
Ok(with_host(|h| h.null()))
} else {
Ok(with_host(|h| h.new_array(matches)))
}
}
fn regexp_exec_from_zero(re: &Regex, s: &str) -> Result<Value, String> {
match re.captures(s).ok().flatten() {
Some(caps) => Ok(build_match_array(re, &caps, s)),
None => Ok(with_host(|h| h.null())),
}
}
pub fn str_match_all(s: &str, re_val: &Value) -> Result<Value, String> {
let Some((re, _, _, _)) = regexp_snapshot(re_val) else {
return Ok(with_host(|h| h.new_array(Vec::new())));
};
let mut items = Vec::new();
for caps in re.captures_iter(s).flatten() {
items.push(build_match_array(&re, &caps, s));
}
Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
}
pub fn str_search(s: &str, re_val: &Value) -> Result<Value, String> {
let Some((re, _, _, _)) = regexp_snapshot(re_val) else {
return Ok(Value::Float(-1.0));
};
Ok(match re.find(s).ok().flatten() {
Some(m) => Value::Float(index_of_byte(s, m.start()).get() as f64),
None => Value::Float(-1.0),
})
}
pub fn str_split_regex(s: &str, re_val: &Value, limit: Option<usize>) -> Result<Value, String> {
let Some((re, _, _, _)) = regexp_snapshot(re_val) else {
return Ok(with_host(|h| h.new_array(Vec::new())));
};
let mut out: Vec<Value> = Vec::new();
let mut last_end = 0usize;
for caps in re.captures_iter(s).flatten() {
let m = caps.get(0).unwrap();
if m.start() == m.end() && m.start() == last_end && last_end == 0 {
continue;
}
out.push(with_host(|h| h.new_str(s[last_end..m.start()].to_string())));
for i in 1..caps.len() {
out.push(match caps.get(i) {
Some(g) => with_host(|h| h.new_str(g.as_str().to_string())),
None => Value::Undef,
});
}
last_end = m.end();
if let Some(l) = limit {
if out.len() >= l {
out.truncate(l);
return Ok(with_host(|h| h.new_array(out)));
}
}
}
out.push(with_host(|h| h.new_str(s[last_end..].to_string())));
if let Some(l) = limit {
out.truncate(l);
}
Ok(with_host(|h| h.new_array(out)))
}
pub fn str_replace_regex(
s: &str,
re_val: &Value,
repl: &Value,
all: bool,
) -> Result<Value, String> {
let Some((re, global, _, _)) = regexp_snapshot(re_val) else {
return Ok(with_host(|h| h.new_str(s.to_string())));
};
let replace_all = all || global;
let is_fn = with_host(|h| host::is_callable(h, repl));
let mut out = String::new();
let mut last = 0usize;
let mut count = 0;
for caps in re.captures_iter(s).flatten() {
let m = caps.get(0).unwrap();
out.push_str(&s[last..m.start()]);
if is_fn {
let mut call_args: Vec<Value> = Vec::new();
for i in 0..caps.len() {
call_args.push(match caps.get(i) {
Some(g) => with_host(|h| h.new_str(g.as_str().to_string())),
None => Value::Undef,
});
}
call_args.push(Value::Float(index_of_byte(s, m.start()).get() as f64));
call_args.push(with_host(|h| h.new_str(s.to_string())));
let r = host::invoke(repl, call_args, None)?;
out.push_str(&with_host(|h| h.str_of(&r)));
} else {
let repl_str = with_host(|h| h.str_of(repl));
out.push_str(&expand_replacement(&repl_str, &caps, s));
}
last = m.end();
count += 1;
if !replace_all && count >= 1 {
break;
}
}
out.push_str(&s[last..]);
Ok(with_host(|h| h.new_str(out)))
}
fn expand_replacement(templ: &str, caps: &Captures, s: &str) -> String {
let chars: Vec<char> = templ.chars().collect();
let mut out = String::new();
let mut i = 0;
let whole = caps.get(0).unwrap();
while i < chars.len() {
if chars[i] == '$' && i + 1 < chars.len() {
let n = chars[i + 1];
match n {
'$' => {
out.push('$');
i += 2;
}
'&' => {
out.push_str(whole.as_str());
i += 2;
}
'`' => {
out.push_str(&s[..whole.start()]);
i += 2;
}
'\'' => {
out.push_str(&s[whole.end()..]);
i += 2;
}
'<' => {
let mut j = i + 2;
let mut name = String::new();
while j < chars.len() && chars[j] != '>' {
name.push(chars[j]);
j += 1;
}
if let Some(m) = caps.name(&name) {
out.push_str(m.as_str());
}
i = j + 1; }
d if d.is_ascii_digit() => {
let d2 = chars.get(i + 2).copied().filter(|c| c.is_ascii_digit());
let two = d2.and_then(|c2| format!("{d}{c2}").parse::<usize>().ok());
if let Some(gi) = two.filter(|gi| *gi < caps.len()) {
if let Some(g) = caps.get(gi) {
out.push_str(g.as_str());
}
i += 3;
} else {
let gi = d.to_digit(10).unwrap() as usize;
if gi >= 1 && gi < caps.len() {
if let Some(g) = caps.get(gi) {
out.push_str(g.as_str());
}
i += 2;
} else {
out.push('$');
i += 1;
}
}
}
_ => {
out.push('$');
i += 1;
}
}
} else {
out.push(chars[i]);
i += 1;
}
}
out
}