1use crate::host::{self, with_host, JsObj, RegExpObj};
27use crate::utf16::{self, U16Index};
28use fancy_regex::{Captures, Regex};
29use fusevm::Value;
30use indexmap::IndexMap;
31use rustc_hash::FxHashMap;
32use std::cell::RefCell;
33use std::rc::Rc;
34
35const SURROGATE_LO: u32 = 0xD800;
41const SURROGATE_HI: u32 = 0xDFFF;
42const SURROGATE_PUA_BASE: u32 = 0xF_0000;
43
44fn remap_surrogate(cp: u32) -> u32 {
46 if (SURROGATE_LO..=SURROGATE_HI).contains(&cp) {
47 SURROGATE_PUA_BASE + (cp - SURROGATE_LO)
48 } else {
49 cp
50 }
51}
52
53pub fn build_regexp(pattern: &str, flags: &str) -> Result<Value, String> {
56 let mut seen = String::new();
58 for c in flags.chars() {
59 if !"gimsuyd".contains(c) || seen.contains(c) {
60 return Err(format!(
61 "SyntaxError: Invalid flags supplied to RegExp constructor '{flags}'"
62 ));
63 }
64 seen.push(c);
65 }
66 let global = flags.contains('g');
67 let ignore_case = flags.contains('i');
68 let multiline = flags.contains('m');
69 let dot_all = flags.contains('s');
70 let sticky = flags.contains('y');
71 let unicode = flags.contains('u');
72
73 let rust_pat = translate(pattern)?;
74 let mut prefixed = String::new();
76 if ignore_case || multiline || dot_all {
77 prefixed.push_str("(?");
78 if ignore_case {
79 prefixed.push('i');
80 }
81 if multiline {
82 prefixed.push('m');
83 }
84 if dot_all {
85 prefixed.push('s');
86 }
87 prefixed.push(')');
88 }
89 prefixed.push_str(&rust_pat);
90
91 let re = compiled(&prefixed).map_err(|e| {
92 let msg = e.lines().collect::<Vec<_>>().join(" ");
94 format!("SyntaxError: Invalid regular expression: /{pattern}/: {msg}")
95 })?;
96
97 let obj = RegExpObj {
100 re,
101 source: if pattern.is_empty() {
102 "(?:)".to_string()
103 } else {
104 pattern.to_string()
105 },
106 flags: flags.to_string(),
107 global,
108 ignore_case,
109 multiline,
110 dot_all,
111 sticky,
112 unicode,
113 last_index: U16Index::ZERO,
114 };
115 Ok(with_host(|h| h.alloc(JsObj::RegExp(Box::new(obj)))))
116}
117
118fn compiled(prefixed: &str) -> Result<Rc<Regex>, String> {
145 thread_local! {
146 static CACHE: RefCell<FxHashMap<String, Rc<Regex>>> =
147 RefCell::new(FxHashMap::default());
148 }
149 if let Some(hit) = CACHE.with(|c| c.borrow().get(prefixed).cloned()) {
150 return Ok(hit);
151 }
152 let re = Rc::new(Regex::new(prefixed).map_err(|e| e.to_string())?);
153 CACHE.with(|c| {
154 c.borrow_mut().insert(prefixed.to_string(), re.clone());
155 });
156 Ok(re)
157}
158
159fn translate(pat: &str) -> Result<String, String> {
166 let chars: Vec<char> = pat.chars().collect();
167 let mut out = String::new();
168 let mut i = 0;
169 let mut in_class = false;
173 let mut class_pos = 0usize;
174 while i < chars.len() {
175 let c = chars[i];
176 if c != '\\' {
179 if !in_class && c == '[' {
180 in_class = true;
181 class_pos = 0;
182 out.push('[');
183 i += 1;
184 if chars.get(i) == Some(&'^') {
186 out.push('^');
187 i += 1;
188 }
189 continue;
190 }
191 if in_class {
192 if c == ']' && class_pos > 0 {
195 in_class = false;
196 out.push(']');
197 i += 1;
198 continue;
199 }
200 if c == '[' {
201 out.push_str("\\[");
202 class_pos += 1;
203 i += 1;
204 continue;
205 }
206 }
207 }
208 match c {
209 '\\' => {
210 class_pos += 1;
211 match chars.get(i + 1).copied() {
212 Some('u') => {
214 i += 2;
215 let cp_hex: String;
216 if chars.get(i) == Some(&'{') {
217 i += 1;
218 let mut hex = String::new();
219 while i < chars.len() && chars[i] != '}' {
220 hex.push(chars[i]);
221 i += 1;
222 }
223 i += 1; cp_hex = hex;
225 } else {
226 cp_hex = chars[i..(i + 4).min(chars.len())].iter().collect();
228 i += 4;
229 }
230 match u32::from_str_radix(cp_hex.trim(), 16) {
231 Ok(cp) => out.push_str(&format!("\\x{{{:X}}}", remap_surrogate(cp))),
232 Err(_) => out.push_str(&format!("\\x{{{cp_hex}}}")),
235 }
236 continue;
237 }
238 Some('/') => {
240 out.push('/');
241 i += 2;
242 continue;
243 }
244 Some(other) => {
246 out.push('\\');
247 out.push(other);
248 i += 2;
249 continue;
250 }
251 None => {
252 out.push('\\');
253 i += 1;
254 }
255 }
256 }
257 _ => {
258 if in_class {
259 class_pos += 1;
260 }
261 out.push(c);
262 i += 1;
263 }
264 }
265 }
266 Ok(out)
267}
268
269fn canonical_flags(flags: &str) -> String {
273 "dgimsuvy"
274 .chars()
275 .filter(|c| flags.contains(*c))
276 .collect::<String>()
277}
278
279pub fn regexp_property(r: &RegExpObj, name: &str) -> Option<Value> {
282 Some(match name {
283 "source" => with_host(|h| h.new_str(r.source.clone())),
284 "flags" => with_host(|h| h.new_str(canonical_flags(&r.flags))),
290 "global" => Value::Bool(r.global),
291 "ignoreCase" => Value::Bool(r.ignore_case),
292 "multiline" => Value::Bool(r.multiline),
293 "dotAll" => Value::Bool(r.dot_all),
294 "sticky" => Value::Bool(r.sticky),
295 "unicode" => Value::Bool(r.unicode),
296 "hasIndices" => Value::Bool(r.flags.contains('d')),
301 "unicodeSets" => Value::Bool(r.flags.contains('v')),
302 "lastIndex" => Value::Float(r.last_index.get() as f64),
303 _ => return None,
304 })
305}
306
307pub fn is_regexp_method(name: &str) -> bool {
308 matches!(name, "test" | "exec" | "toString" | "compile")
309}
310
311pub fn regexp_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
313 match name {
314 "test" => {
315 let s = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
316 Ok(Value::Bool(regexp_test(recv, &s)))
317 }
318 "exec" => {
319 let s = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
320 regexp_exec(recv, &s)
321 }
322 "toString" => Ok(with_host(|h| {
323 let s = h.str_of(recv);
324 h.new_str(s)
325 })),
326 "compile" => Ok(recv.clone()),
328 _ => Err(host::type_error(&format!("{name} is not a function"))),
329 }
330}
331
332fn regexp_snapshot(recv: &Value) -> Option<(Rc<Regex>, bool, bool, U16Index)> {
334 with_host(|h| match h.get(recv) {
335 Some(JsObj::RegExp(r)) => Some((r.re.clone(), r.global, r.sticky, r.last_index)),
336 _ => None,
337 })
338}
339
340fn set_last_index(recv: &Value, idx: U16Index) {
341 with_host(|h| {
342 if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
343 r.last_index = idx;
344 }
345 });
346}
347
348fn byte_of_index(s: &str, n: U16Index) -> usize {
354 utf16::byte_of_index(s, n)
355}
356fn index_of_byte(s: &str, byte: usize) -> U16Index {
358 utf16::index_of_byte(s, byte)
359}
360
361pub fn regexp_test(recv: &Value, s: &str) -> bool {
363 let Some((re, global, sticky, last)) = regexp_snapshot(recv) else {
364 return false;
365 };
366 let start_idx = if global || sticky {
367 last
368 } else {
369 U16Index::ZERO
370 };
371 if start_idx.get() > utf16::len(s) {
372 if global || sticky {
373 set_last_index(recv, U16Index::ZERO);
374 }
375 return false;
376 }
377 let start_byte = byte_of_index(s, start_idx);
378 match re.find_from_pos(s, start_byte) {
381 Ok(Some(m)) if !sticky || m.start() == start_byte => {
382 if global || sticky {
383 set_last_index(recv, index_of_byte(s, m.end()));
384 }
385 true
386 }
387 _ => {
388 if global || sticky {
389 set_last_index(recv, U16Index::ZERO);
390 }
391 false
392 }
393 }
394}
395
396pub fn regexp_exec(recv: &Value, s: &str) -> Result<Value, String> {
399 let Some((re, global, sticky, last)) = regexp_snapshot(recv) else {
400 return Ok(with_host(|h| h.null()));
401 };
402 let start_idx = if global || sticky {
403 last
404 } else {
405 U16Index::ZERO
406 };
407 if start_idx.get() > utf16::len(s) {
408 if global || sticky {
409 set_last_index(recv, U16Index::ZERO);
410 }
411 return Ok(with_host(|h| h.null()));
412 }
413 let start_byte = byte_of_index(s, start_idx);
414 let caps = re.captures_from_pos(s, start_byte).ok().flatten();
415 let caps = match caps {
416 Some(c) if !sticky || c.get(0).map(|m| m.start()) == Some(start_byte) => c,
417 _ => {
418 if global || sticky {
419 set_last_index(recv, U16Index::ZERO);
420 }
421 return Ok(with_host(|h| h.null()));
422 }
423 };
424 let whole = caps.get(0).unwrap();
425 if global || sticky {
426 set_last_index(recv, index_of_byte(s, whole.end()));
427 }
428 Ok(build_match_array(&re, &caps, s))
429}
430
431fn build_match_array(re: &Regex, caps: &Captures, s: &str) -> Value {
434 let mut items: Vec<Value> = Vec::with_capacity(caps.len());
435 for i in 0..caps.len() {
436 items.push(match caps.get(i) {
437 Some(m) => with_host(|h| h.new_str(m.as_str().to_string())),
438 None => Value::Undef, });
440 }
441 let whole = caps.get(0).unwrap();
442 let arr = with_host(|h| h.new_array(items));
443 let index = index_of_byte(s, whole.start()).get();
444 with_host(|h| {
445 let idx = Value::Float(index as f64);
446 h.set_fn_prop(&arr, "index", idx);
447 let input = h.new_str(s.to_string());
448 h.set_fn_prop(&arr, "input", input);
449 });
450 let names: Vec<&str> = re.capture_names().flatten().collect();
452 if names.is_empty() {
453 with_host(|h| h.set_fn_prop(&arr, "groups", Value::Undef));
454 } else {
455 let mut g: IndexMap<String, Value> = IndexMap::new();
456 for name in names {
457 let v = match caps.name(name) {
458 Some(m) => with_host(|h| h.new_str(m.as_str().to_string())),
459 None => Value::Undef,
460 };
461 g.insert(name.to_string(), v);
462 }
463 with_host(|h| {
464 let obj = h.new_object(g);
465 let null = h.null();
468 h.set_proto(&obj, null);
469 h.set_fn_prop(&arr, "groups", obj);
470 });
471 }
472 arr
473}
474
475pub fn str_match(s: &str, re_val: &Value) -> Result<Value, String> {
480 let Some((re, global, _, _)) = regexp_snapshot(re_val) else {
481 return Ok(with_host(|h| h.null()));
482 };
483 if !global {
484 set_last_index(re_val, U16Index::ZERO);
486 return regexp_exec_from_zero(&re, s);
487 }
488 let matches: Vec<Value> = re
489 .find_iter(s)
490 .filter_map(|m| m.ok())
491 .map(|m| with_host(|h| h.new_str(m.as_str().to_string())))
492 .collect();
493 if matches.is_empty() {
494 Ok(with_host(|h| h.null()))
495 } else {
496 Ok(with_host(|h| h.new_array(matches)))
497 }
498}
499
500fn regexp_exec_from_zero(re: &Regex, s: &str) -> Result<Value, String> {
502 match re.captures(s).ok().flatten() {
503 Some(caps) => Ok(build_match_array(re, &caps, s)),
504 None => Ok(with_host(|h| h.null())),
505 }
506}
507
508pub fn str_match_all(s: &str, re_val: &Value) -> Result<Value, String> {
511 let Some((re, _, _, _)) = regexp_snapshot(re_val) else {
512 return Ok(with_host(|h| h.new_array(Vec::new())));
513 };
514 let mut items = Vec::new();
515 for caps in re.captures_iter(s).flatten() {
516 items.push(build_match_array(&re, &caps, s));
517 }
518 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
520}
521
522pub fn str_search(s: &str, re_val: &Value) -> Result<Value, String> {
524 let Some((re, _, _, _)) = regexp_snapshot(re_val) else {
525 return Ok(Value::Float(-1.0));
526 };
527 Ok(match re.find(s).ok().flatten() {
528 Some(m) => Value::Float(index_of_byte(s, m.start()).get() as f64),
529 None => Value::Float(-1.0),
530 })
531}
532
533pub fn str_split_regex(s: &str, re_val: &Value, limit: Option<usize>) -> Result<Value, String> {
536 let Some((re, _, _, _)) = regexp_snapshot(re_val) else {
537 return Ok(with_host(|h| h.new_array(Vec::new())));
538 };
539 let mut out: Vec<Value> = Vec::new();
540 let mut last_end = 0usize;
541 for caps in re.captures_iter(s).flatten() {
542 let m = caps.get(0).unwrap();
543 if m.start() == m.end() && m.start() == last_end && last_end == 0 {
545 continue;
546 }
547 out.push(with_host(|h| h.new_str(s[last_end..m.start()].to_string())));
548 for i in 1..caps.len() {
550 out.push(match caps.get(i) {
551 Some(g) => with_host(|h| h.new_str(g.as_str().to_string())),
552 None => Value::Undef,
553 });
554 }
555 last_end = m.end();
556 if let Some(l) = limit {
557 if out.len() >= l {
558 out.truncate(l);
559 return Ok(with_host(|h| h.new_array(out)));
560 }
561 }
562 }
563 out.push(with_host(|h| h.new_str(s[last_end..].to_string())));
564 if let Some(l) = limit {
565 out.truncate(l);
566 }
567 Ok(with_host(|h| h.new_array(out)))
568}
569
570pub fn str_replace_regex(
573 s: &str,
574 re_val: &Value,
575 repl: &Value,
576 all: bool,
577) -> Result<Value, String> {
578 let Some((re, global, _, _)) = regexp_snapshot(re_val) else {
579 return Ok(with_host(|h| h.new_str(s.to_string())));
580 };
581 let replace_all = all || global;
582 let is_fn = with_host(|h| host::is_callable(h, repl));
583
584 let mut out = String::new();
585 let mut last = 0usize;
586 let mut count = 0;
587 for caps in re.captures_iter(s).flatten() {
588 let m = caps.get(0).unwrap();
589 out.push_str(&s[last..m.start()]);
590 if is_fn {
591 let mut call_args: Vec<Value> = Vec::new();
593 for i in 0..caps.len() {
594 call_args.push(match caps.get(i) {
595 Some(g) => with_host(|h| h.new_str(g.as_str().to_string())),
596 None => Value::Undef,
597 });
598 }
599 call_args.push(Value::Float(index_of_byte(s, m.start()).get() as f64));
600 call_args.push(with_host(|h| h.new_str(s.to_string())));
601 let r = host::invoke(repl, call_args, None)?;
602 out.push_str(&with_host(|h| h.str_of(&r)));
603 } else {
604 let repl_str = with_host(|h| h.str_of(repl));
605 out.push_str(&expand_replacement(&repl_str, &caps, s));
606 }
607 last = m.end();
608 count += 1;
609 if !replace_all && count >= 1 {
610 break;
611 }
612 }
613 out.push_str(&s[last..]);
614 Ok(with_host(|h| h.new_str(out)))
615}
616
617fn expand_replacement(templ: &str, caps: &Captures, s: &str) -> String {
619 let chars: Vec<char> = templ.chars().collect();
620 let mut out = String::new();
621 let mut i = 0;
622 let whole = caps.get(0).unwrap();
623 while i < chars.len() {
624 if chars[i] == '$' && i + 1 < chars.len() {
625 let n = chars[i + 1];
626 match n {
627 '$' => {
628 out.push('$');
629 i += 2;
630 }
631 '&' => {
632 out.push_str(whole.as_str());
633 i += 2;
634 }
635 '`' => {
636 out.push_str(&s[..whole.start()]);
637 i += 2;
638 }
639 '\'' => {
640 out.push_str(&s[whole.end()..]);
641 i += 2;
642 }
643 '<' => {
644 let mut j = i + 2;
646 let mut name = String::new();
647 while j < chars.len() && chars[j] != '>' {
648 name.push(chars[j]);
649 j += 1;
650 }
651 if let Some(m) = caps.name(&name) {
652 out.push_str(m.as_str());
653 }
654 i = j + 1; }
656 d if d.is_ascii_digit() => {
657 let d2 = chars.get(i + 2).copied().filter(|c| c.is_ascii_digit());
659 let two = d2.and_then(|c2| format!("{d}{c2}").parse::<usize>().ok());
660 if let Some(gi) = two.filter(|gi| *gi < caps.len()) {
661 if let Some(g) = caps.get(gi) {
662 out.push_str(g.as_str());
663 }
664 i += 3;
665 } else {
666 let gi = d.to_digit(10).unwrap() as usize;
667 if gi >= 1 && gi < caps.len() {
668 if let Some(g) = caps.get(gi) {
669 out.push_str(g.as_str());
670 }
671 i += 2;
672 } else {
673 out.push('$');
674 i += 1;
675 }
676 }
677 }
678 _ => {
679 out.push('$');
680 i += 1;
681 }
682 }
683 } else {
684 out.push(chars[i]);
685 i += 1;
686 }
687 }
688 out
689}