1use crate::host::{self, with_host, JsObj, RegExpObj};
27use fancy_regex::{Captures, Regex};
28use fusevm::Value;
29use indexmap::IndexMap;
30
31const SURROGATE_LO: u32 = 0xD800;
37const SURROGATE_HI: u32 = 0xDFFF;
38const SURROGATE_PUA_BASE: u32 = 0xF_0000;
39
40fn remap_surrogate(cp: u32) -> u32 {
42 if (SURROGATE_LO..=SURROGATE_HI).contains(&cp) {
43 SURROGATE_PUA_BASE + (cp - SURROGATE_LO)
44 } else {
45 cp
46 }
47}
48
49pub fn build_regexp(pattern: &str, flags: &str) -> Result<Value, String> {
52 let mut seen = String::new();
54 for c in flags.chars() {
55 if !"gimsuyd".contains(c) || seen.contains(c) {
56 return Err(format!(
57 "SyntaxError: Invalid flags supplied to RegExp constructor '{flags}'"
58 ));
59 }
60 seen.push(c);
61 }
62 let global = flags.contains('g');
63 let ignore_case = flags.contains('i');
64 let multiline = flags.contains('m');
65 let dot_all = flags.contains('s');
66 let sticky = flags.contains('y');
67 let unicode = flags.contains('u');
68
69 let rust_pat = translate(pattern)?;
70 let mut prefixed = String::new();
72 if ignore_case || multiline || dot_all {
73 prefixed.push_str("(?");
74 if ignore_case {
75 prefixed.push('i');
76 }
77 if multiline {
78 prefixed.push('m');
79 }
80 if dot_all {
81 prefixed.push('s');
82 }
83 prefixed.push(')');
84 }
85 prefixed.push_str(&rust_pat);
86
87 let re = Regex::new(&prefixed).map_err(|e| {
88 let msg = e.to_string().lines().collect::<Vec<_>>().join(" ");
90 format!("SyntaxError: Invalid regular expression: /{pattern}/: {msg}")
91 })?;
92
93 let obj = RegExpObj {
94 re,
95 source: if pattern.is_empty() {
96 "(?:)".to_string()
97 } else {
98 pattern.to_string()
99 },
100 flags: flags.to_string(),
101 global,
102 ignore_case,
103 multiline,
104 dot_all,
105 sticky,
106 unicode,
107 last_index: 0,
108 };
109 Ok(with_host(|h| h.alloc(JsObj::RegExp(Box::new(obj)))))
110}
111
112fn translate(pat: &str) -> Result<String, String> {
119 let chars: Vec<char> = pat.chars().collect();
120 let mut out = String::new();
121 let mut i = 0;
122 let mut in_class = false;
126 let mut class_pos = 0usize;
127 while i < chars.len() {
128 let c = chars[i];
129 if c != '\\' {
132 if !in_class && c == '[' {
133 in_class = true;
134 class_pos = 0;
135 out.push('[');
136 i += 1;
137 if chars.get(i) == Some(&'^') {
139 out.push('^');
140 i += 1;
141 }
142 continue;
143 }
144 if in_class {
145 if c == ']' && class_pos > 0 {
148 in_class = false;
149 out.push(']');
150 i += 1;
151 continue;
152 }
153 if c == '[' {
154 out.push_str("\\[");
155 class_pos += 1;
156 i += 1;
157 continue;
158 }
159 }
160 }
161 match c {
162 '\\' => {
163 class_pos += 1;
164 match chars.get(i + 1).copied() {
165 Some('u') => {
167 i += 2;
168 let cp_hex: String;
169 if chars.get(i) == Some(&'{') {
170 i += 1;
171 let mut hex = String::new();
172 while i < chars.len() && chars[i] != '}' {
173 hex.push(chars[i]);
174 i += 1;
175 }
176 i += 1; cp_hex = hex;
178 } else {
179 cp_hex = chars[i..(i + 4).min(chars.len())].iter().collect();
181 i += 4;
182 }
183 match u32::from_str_radix(cp_hex.trim(), 16) {
184 Ok(cp) => out.push_str(&format!("\\x{{{:X}}}", remap_surrogate(cp))),
185 Err(_) => out.push_str(&format!("\\x{{{cp_hex}}}")),
188 }
189 continue;
190 }
191 Some('/') => {
193 out.push('/');
194 i += 2;
195 continue;
196 }
197 Some(other) => {
199 out.push('\\');
200 out.push(other);
201 i += 2;
202 continue;
203 }
204 None => {
205 out.push('\\');
206 i += 1;
207 }
208 }
209 }
210 _ => {
211 if in_class {
212 class_pos += 1;
213 }
214 out.push(c);
215 i += 1;
216 }
217 }
218 }
219 Ok(out)
220}
221
222pub fn regexp_property(r: &RegExpObj, name: &str) -> Option<Value> {
225 Some(match name {
226 "source" => with_host(|h| h.new_str(r.source.clone())),
227 "flags" => with_host(|h| h.new_str(r.flags.clone())),
228 "global" => Value::Bool(r.global),
229 "ignoreCase" => Value::Bool(r.ignore_case),
230 "multiline" => Value::Bool(r.multiline),
231 "dotAll" => Value::Bool(r.dot_all),
232 "sticky" => Value::Bool(r.sticky),
233 "unicode" => Value::Bool(r.unicode),
234 "lastIndex" => Value::Float(r.last_index as f64),
235 _ => return None,
236 })
237}
238
239pub fn is_regexp_method(name: &str) -> bool {
240 matches!(name, "test" | "exec" | "toString" | "compile")
241}
242
243pub fn regexp_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
245 match name {
246 "test" => {
247 let s = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
248 Ok(Value::Bool(regexp_test(recv, &s)))
249 }
250 "exec" => {
251 let s = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
252 regexp_exec(recv, &s)
253 }
254 "toString" => Ok(with_host(|h| {
255 let s = h.str_of(recv);
256 h.new_str(s)
257 })),
258 "compile" => Ok(recv.clone()),
260 _ => Err(host::type_error(&format!("{name} is not a function"))),
261 }
262}
263
264fn regexp_snapshot(recv: &Value) -> Option<(Regex, bool, bool, usize)> {
266 with_host(|h| match h.get(recv) {
267 Some(JsObj::RegExp(r)) => Some((r.re.clone(), r.global, r.sticky, r.last_index)),
268 _ => None,
269 })
270}
271
272fn set_last_index(recv: &Value, idx: usize) {
273 with_host(|h| {
274 if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
275 r.last_index = idx;
276 }
277 });
278}
279
280fn byte_of_char(s: &str, n: usize) -> usize {
282 s.char_indices().nth(n).map(|(b, _)| b).unwrap_or(s.len())
283}
284fn char_of_byte(s: &str, byte: usize) -> usize {
286 s[..byte.min(s.len())].chars().count()
287}
288
289pub fn regexp_test(recv: &Value, s: &str) -> bool {
291 let Some((re, global, sticky, last)) = regexp_snapshot(recv) else {
292 return false;
293 };
294 let start_char = if global || sticky { last } else { 0 };
295 if start_char > s.chars().count() {
296 if global || sticky {
297 set_last_index(recv, 0);
298 }
299 return false;
300 }
301 let start_byte = byte_of_char(s, start_char);
302 match re.find_from_pos(s, start_byte) {
305 Ok(Some(m)) if !sticky || m.start() == start_byte => {
306 if global || sticky {
307 set_last_index(recv, char_of_byte(s, m.end()));
308 }
309 true
310 }
311 _ => {
312 if global || sticky {
313 set_last_index(recv, 0);
314 }
315 false
316 }
317 }
318}
319
320pub fn regexp_exec(recv: &Value, s: &str) -> Result<Value, String> {
323 let Some((re, global, sticky, last)) = regexp_snapshot(recv) else {
324 return Ok(with_host(|h| h.null()));
325 };
326 let start_char = if global || sticky { last } else { 0 };
327 if start_char > s.chars().count() {
328 if global || sticky {
329 set_last_index(recv, 0);
330 }
331 return Ok(with_host(|h| h.null()));
332 }
333 let start_byte = byte_of_char(s, start_char);
334 let caps = re.captures_from_pos(s, start_byte).ok().flatten();
335 let caps = match caps {
336 Some(c) if !sticky || c.get(0).map(|m| m.start()) == Some(start_byte) => c,
337 _ => {
338 if global || sticky {
339 set_last_index(recv, 0);
340 }
341 return Ok(with_host(|h| h.null()));
342 }
343 };
344 let whole = caps.get(0).unwrap();
345 if global || sticky {
346 set_last_index(recv, char_of_byte(s, whole.end()));
347 }
348 Ok(build_match_array(&re, &caps, s))
349}
350
351fn build_match_array(re: &Regex, caps: &Captures, s: &str) -> Value {
354 let mut items: Vec<Value> = Vec::with_capacity(caps.len());
355 for i in 0..caps.len() {
356 items.push(match caps.get(i) {
357 Some(m) => with_host(|h| h.new_str(m.as_str().to_string())),
358 None => Value::Undef, });
360 }
361 let whole = caps.get(0).unwrap();
362 let arr = with_host(|h| h.new_array(items));
363 let index = char_of_byte(s, whole.start());
364 with_host(|h| {
365 let idx = Value::Float(index as f64);
366 h.set_fn_prop(&arr, "index", idx);
367 let input = h.new_str(s.to_string());
368 h.set_fn_prop(&arr, "input", input);
369 });
370 let names: Vec<&str> = re.capture_names().flatten().collect();
372 if names.is_empty() {
373 with_host(|h| h.set_fn_prop(&arr, "groups", Value::Undef));
374 } else {
375 let mut g: IndexMap<String, Value> = IndexMap::new();
376 for name in names {
377 let v = match caps.name(name) {
378 Some(m) => with_host(|h| h.new_str(m.as_str().to_string())),
379 None => Value::Undef,
380 };
381 g.insert(name.to_string(), v);
382 }
383 with_host(|h| {
384 let obj = h.new_object(g);
385 h.set_fn_prop(&arr, "groups", obj);
386 });
387 }
388 arr
389}
390
391pub fn str_match(s: &str, re_val: &Value) -> Result<Value, String> {
396 let Some((re, global, _, _)) = regexp_snapshot(re_val) else {
397 return Ok(with_host(|h| h.null()));
398 };
399 if !global {
400 set_last_index(re_val, 0);
402 return regexp_exec_from_zero(&re, s);
403 }
404 let matches: Vec<Value> = re
405 .find_iter(s)
406 .filter_map(|m| m.ok())
407 .map(|m| with_host(|h| h.new_str(m.as_str().to_string())))
408 .collect();
409 if matches.is_empty() {
410 Ok(with_host(|h| h.null()))
411 } else {
412 Ok(with_host(|h| h.new_array(matches)))
413 }
414}
415
416fn regexp_exec_from_zero(re: &Regex, s: &str) -> Result<Value, String> {
418 match re.captures(s).ok().flatten() {
419 Some(caps) => Ok(build_match_array(re, &caps, s)),
420 None => Ok(with_host(|h| h.null())),
421 }
422}
423
424pub fn str_match_all(s: &str, re_val: &Value) -> Result<Value, String> {
427 let Some((re, _, _, _)) = regexp_snapshot(re_val) else {
428 return Ok(with_host(|h| h.new_array(Vec::new())));
429 };
430 let mut items = Vec::new();
431 for caps in re.captures_iter(s).flatten() {
432 items.push(build_match_array(&re, &caps, s));
433 }
434 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
436}
437
438pub fn str_search(s: &str, re_val: &Value) -> Result<Value, String> {
440 let Some((re, _, _, _)) = regexp_snapshot(re_val) else {
441 return Ok(Value::Float(-1.0));
442 };
443 Ok(match re.find(s).ok().flatten() {
444 Some(m) => Value::Float(char_of_byte(s, m.start()) as f64),
445 None => Value::Float(-1.0),
446 })
447}
448
449pub fn str_split_regex(s: &str, re_val: &Value, limit: Option<usize>) -> Result<Value, String> {
452 let Some((re, _, _, _)) = regexp_snapshot(re_val) else {
453 return Ok(with_host(|h| h.new_array(Vec::new())));
454 };
455 let mut out: Vec<Value> = Vec::new();
456 let mut last_end = 0usize;
457 for caps in re.captures_iter(s).flatten() {
458 let m = caps.get(0).unwrap();
459 if m.start() == m.end() && m.start() == last_end && last_end == 0 {
461 continue;
462 }
463 out.push(with_host(|h| h.new_str(s[last_end..m.start()].to_string())));
464 for i in 1..caps.len() {
466 out.push(match caps.get(i) {
467 Some(g) => with_host(|h| h.new_str(g.as_str().to_string())),
468 None => Value::Undef,
469 });
470 }
471 last_end = m.end();
472 if let Some(l) = limit {
473 if out.len() >= l {
474 out.truncate(l);
475 return Ok(with_host(|h| h.new_array(out)));
476 }
477 }
478 }
479 out.push(with_host(|h| h.new_str(s[last_end..].to_string())));
480 if let Some(l) = limit {
481 out.truncate(l);
482 }
483 Ok(with_host(|h| h.new_array(out)))
484}
485
486pub fn str_replace_regex(
489 s: &str,
490 re_val: &Value,
491 repl: &Value,
492 all: bool,
493) -> Result<Value, String> {
494 let Some((re, global, _, _)) = regexp_snapshot(re_val) else {
495 return Ok(with_host(|h| h.new_str(s.to_string())));
496 };
497 let replace_all = all || global;
498 let is_fn = with_host(|h| host::is_callable(h, repl));
499
500 let mut out = String::new();
501 let mut last = 0usize;
502 let mut count = 0;
503 for caps in re.captures_iter(s).flatten() {
504 let m = caps.get(0).unwrap();
505 out.push_str(&s[last..m.start()]);
506 if is_fn {
507 let mut call_args: Vec<Value> = Vec::new();
509 for i in 0..caps.len() {
510 call_args.push(match caps.get(i) {
511 Some(g) => with_host(|h| h.new_str(g.as_str().to_string())),
512 None => Value::Undef,
513 });
514 }
515 call_args.push(Value::Float(char_of_byte(s, m.start()) as f64));
516 call_args.push(with_host(|h| h.new_str(s.to_string())));
517 let r = host::invoke(repl, call_args, None)?;
518 out.push_str(&with_host(|h| h.str_of(&r)));
519 } else {
520 let repl_str = with_host(|h| h.str_of(repl));
521 out.push_str(&expand_replacement(&repl_str, &caps, s));
522 }
523 last = m.end();
524 count += 1;
525 if !replace_all && count >= 1 {
526 break;
527 }
528 }
529 out.push_str(&s[last..]);
530 Ok(with_host(|h| h.new_str(out)))
531}
532
533fn expand_replacement(templ: &str, caps: &Captures, s: &str) -> String {
535 let chars: Vec<char> = templ.chars().collect();
536 let mut out = String::new();
537 let mut i = 0;
538 let whole = caps.get(0).unwrap();
539 while i < chars.len() {
540 if chars[i] == '$' && i + 1 < chars.len() {
541 let n = chars[i + 1];
542 match n {
543 '$' => {
544 out.push('$');
545 i += 2;
546 }
547 '&' => {
548 out.push_str(whole.as_str());
549 i += 2;
550 }
551 '`' => {
552 out.push_str(&s[..whole.start()]);
553 i += 2;
554 }
555 '\'' => {
556 out.push_str(&s[whole.end()..]);
557 i += 2;
558 }
559 '<' => {
560 let mut j = i + 2;
562 let mut name = String::new();
563 while j < chars.len() && chars[j] != '>' {
564 name.push(chars[j]);
565 j += 1;
566 }
567 if let Some(m) = caps.name(&name) {
568 out.push_str(m.as_str());
569 }
570 i = j + 1; }
572 d if d.is_ascii_digit() => {
573 let d2 = chars.get(i + 2).copied().filter(|c| c.is_ascii_digit());
575 let two = d2.and_then(|c2| format!("{d}{c2}").parse::<usize>().ok());
576 if let Some(gi) = two.filter(|gi| *gi < caps.len()) {
577 if let Some(g) = caps.get(gi) {
578 out.push_str(g.as_str());
579 }
580 i += 3;
581 } else {
582 let gi = d.to_digit(10).unwrap() as usize;
583 if gi >= 1 && gi < caps.len() {
584 if let Some(g) = caps.get(gi) {
585 out.push_str(g.as_str());
586 }
587 i += 2;
588 } else {
589 out.push('$');
590 i += 1;
591 }
592 }
593 }
594 _ => {
595 out.push('$');
596 i += 1;
597 }
598 }
599 } else {
600 out.push(chars[i]);
601 i += 1;
602 }
603 }
604 out
605}