Files
vectorcraft/crates/js/src/string_builtins.rs
T
BFlat 8448e5c18f Merge pull request #6788 from dotCooCoo/feat/e4x-xml-xpath
JS: XML and XMLList xpath evaluates XPath 1.0 expressions
2026-11-12 16:01:37 +07:00

790 lines
32 KiB
Rust

//! `String` (ECMA-262 5.1 section 15.5): the constructor, `String.fromCharCode` and the methods of
//! `String.prototype`, with `substr` from Annex B.
//!
//! The methods read a string's code units once (flattening a rope, whose flat copy counts as a new
//! string), take a unit of fuel for every [`UNITS_PER_FUEL`] code units they read or write, and
//! reserve each new string's bytes on the heap before building it, so a method that would make a
//! string past the heap limit ends the run instead of allocating it.
use std::rc::Rc;
use crate::LimitKind;
use crate::builtins::{arg, constructor, method, native};
use crate::heap::{Class, NativeKind, OPEN, ObjId, Object, PROP_BYTES, Prop, string_bytes};
use crate::interp::{Abrupt, HeldBytes, Interp, JsResult};
use crate::regexp_builtins::{exec_raw, new_regexp_from, regexp_of};
use crate::regexp_vm::{Matcher, NONE, use_fuel_n};
use crate::value::{Str, Value, index_key, is_str_space, to_uint32};
/// Code units a unit of fuel pays for when a built-in reads or writes text.
pub(crate) const UNITS_PER_FUEL: usize = 16;
pub(crate) fn install(interp: &mut Interp) -> JsResult<()> {
let proto = interp.intrinsics.string_proto;
let ctor = native(interp, "String", string_ctor, 1, NativeKind::Plain, true)?;
constructor(interp, "String", ctor, proto)?;
method(interp, ctor, "fromCharCode", from_char_code, 1)?;
let methods: [(&'static str, crate::heap::NativeFn, u32); 20] = [
("toString", to_string, 0),
("valueOf", to_string, 0),
("charAt", char_at, 1),
("charCodeAt", char_code_at, 1),
("concat", concat, 1),
("indexOf", index_of, 1),
("lastIndexOf", last_index_of, 1),
("localeCompare", locale_compare, 1),
("match", match_, 1),
("replace", replace, 2),
("search", search, 1),
("slice", slice, 2),
("split", split, 2),
("substring", substring, 2),
("substr", substr, 2),
("toLowerCase", to_lower_case, 0),
("toLocaleLowerCase", to_lower_case, 0),
("toUpperCase", to_upper_case, 0),
("toLocaleUpperCase", to_upper_case, 0),
("trim", trim, 0),
];
for (name, f, len) in methods {
method(interp, proto, name, f, len)?;
}
Ok(())
}
// ---- helpers the other text built-ins share ----
/// ToInteger (section 9.4) applied to a number.
pub(crate) fn to_integer(n: f64) -> f64 {
if n.is_nan() {
0.0
} else if n.is_infinite() {
n
} else {
n.trunc() + 0.0
}
}
/// Takes the fuel for reading or writing `n` code units.
pub(crate) fn charge_units(interp: &mut Interp, n: usize) -> JsResult<()> {
use_fuel_n(interp, (n / UNITS_PER_FUEL) as u64 + 1)
}
/// Reserves the heap bytes of a new string of `len` code units before it is built, ending the
/// run with the heap limit when it would not fit. Everything the caller still needs must be
/// reachable from the roots.
pub(crate) fn reserve_units(interp: &mut Interp, len: usize) -> JsResult<()> {
reserve_string(interp, len)?;
charge_units(interp, len)
}
/// [`reserve_units`] without the fuel, for a caller that took it as it read the units.
fn reserve_string(interp: &mut Interp, len: usize) -> JsResult<()> {
check_string_room(interp, len)?;
let bytes = string_bytes(len);
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
Ok(())
}
/// Ends the run with the heap limit when a new string of `len` code units would not fit, after
/// a collection if one is needed. Everything the caller still needs must be reachable from the
/// roots.
fn check_string_room(interp: &mut Interp, len: usize) -> JsResult<()> {
if len > interp.limits.heap_bytes / 4 {
return Err(Abrupt::Limit(LimitKind::Heap));
}
interp.reserve_heap(string_bytes(len))
}
/// Pushes `item` onto `list`, first holding the bytes of the list's new capacity when it must
/// grow.
pub(crate) fn push_held<T>(interp: &mut Interp, held: &mut HeldBytes, list: &mut Vec<T>, item: T) -> JsResult<()> {
if list.len() == list.capacity() {
let more = list.capacity().max(4);
held.add(interp, more.saturating_mul(size_of::<T>()))?;
list.reserve_exact(more);
}
list.push(item);
Ok(())
}
/// The code units of `s`. Flattening a rope makes a copy of its units, so for a rope that copy's
/// bytes are reserved and added to the heap, and its units charged as fuel, first. The rope stays
/// rooted while the copy is reserved, so what it holds is still counted.
pub(crate) fn flat_charged(interp: &mut Interp, s: &Str) -> JsResult<Rc<[u16]>> {
if s.is_rope() {
interp.rooted(&[Value::String(s.clone())], |interp| reserve_units(interp, s.len()))?;
}
Ok(s.flat())
}
/// A new string holding `units[start..end]`.
pub(crate) fn substring_value(interp: &mut Interp, units: &[u16], start: usize, end: usize) -> JsResult<Value> {
let part = units.get(start..end.max(start)).unwrap_or_default();
reserve_units(interp, part.len())?;
Ok(Value::String(Str::from_units(part.to_vec())))
}
/// Runs `f`, then drops the temporary roots it added.
pub(crate) fn rooted<T>(interp: &mut Interp, f: impl FnOnce(&mut Interp) -> JsResult<T>) -> JsResult<T> {
let mark = interp.temp_roots.len();
let r = f(interp);
interp.temp_roots.truncate(mark);
r
}
/// A new, empty array, kept in the temporary roots: call it inside [`rooted`].
pub(crate) fn new_array(interp: &mut Interp) -> ObjId {
let proto = interp.intrinsics.array_proto;
let a = interp.heap.alloc_object(Object::new(Class::Array { length: 0, length_writable: true }, Some(proto)));
interp.temp_roots.push(Value::Object(a));
a
}
/// Sets element `i` of an array made by [`new_array`], reserving its bytes first.
pub(crate) fn set_element(interp: &mut Interp, a: ObjId, i: usize, v: Value) -> JsResult<()> {
let i = u32::try_from(i).map_err(|_| Abrupt::Limit(LimitKind::Heap))?;
interp.temp_roots.push(v.clone());
let reserved = interp.reserve_heap(PROP_BYTES + string_bytes(10));
interp.temp_roots.pop();
reserved?;
interp.use_fuel()?;
interp.define(a, index_key(i), Prop::Data { value: v, flags: OPEN })
}
/// CheckObjectCoercible on `this`, then ToString.
pub(crate) fn this_str(interp: &mut Interp, this: &Value, method: &str) -> JsResult<Str> {
if this.is_nullish() {
let message = format!("String.prototype.{method} was called on null or undefined");
return Err(interp.type_error(&message));
}
interp.to_str(this)
}
/// ToInteger of an argument.
fn int_arg(interp: &mut Interp, args: &[Value], i: usize) -> JsResult<f64> {
let n = interp.to_number(&arg(args, i))?;
Ok(to_integer(n))
}
/// Clamps an integer to `0..=len`.
fn clamp(n: f64, len: usize) -> usize {
if n <= 0.0 {
0
} else if n >= len as f64 {
len
} else {
n as usize
}
}
/// The first index at `from` or after where `needle` occurs in `hay`. Every position tried takes
/// fuel.
pub(crate) fn find(interp: &mut Interp, hay: &[u16], needle: &[u16], from: usize) -> JsResult<Option<usize>> {
if needle.len() > hay.len() {
return Ok(None);
}
let last = hay.len() - needle.len();
let per = (needle.len() / UNITS_PER_FUEL) as u64 + 1;
let mut steps = 0u64;
let mut i = from;
let mut found = None;
while i <= last {
steps += per;
if steps >= 1024 {
use_fuel_n(interp, std::mem::take(&mut steps))?;
}
if hay.get(i..i + needle.len()) == Some(needle) {
found = Some(i);
break;
}
i += 1;
}
use_fuel_n(interp, steps)?;
Ok(found)
}
// ---- the constructor ----
/// `String(value)` and `new String(value)` (sections 15.5.1 and 15.5.2).
fn string_ctor(interp: &mut Interp, _: Value, args: &[Value], construct: bool) -> JsResult<Value> {
let s = match args.first() {
Some(v) => interp.to_str(v)?,
None => Str::from(""),
};
if !construct {
return Ok(Value::String(s));
}
let proto = interp.intrinsics.string_proto;
Ok(Value::Object(interp.heap.alloc_object(Object::new(Class::String(s), Some(proto)))))
}
/// `String.fromCharCode(…)` (section 15.5.3.2).
fn from_char_code(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
let mut units = Vec::with_capacity(args.len());
for a in args {
let n = interp.to_number(a)?;
units.push(to_uint32(n) as u16);
}
reserve_units(interp, units.len())?;
Ok(Value::String(Str::from_units(units)))
}
// ---- String.prototype ----
/// `toString` and `valueOf` (sections 15.5.4.2 and 15.5.4.3).
fn to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
match &this {
Value::String(_) => Ok(this),
Value::Object(o) => match interp.heap.object(*o).map(|o| &o.class) {
Some(Class::String(s)) => Ok(Value::String(s.clone())),
_ => Err(interp.type_error("String.prototype.toString was called on a value that is not a string")),
},
_ => Err(interp.type_error("String.prototype.toString was called on a value that is not a string")),
}
}
fn char_at(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let s = this_str(interp, &this, "charAt")?;
let pos = int_arg(interp, args, 0)?;
if pos < 0.0 || pos >= s.len() as f64 {
return Ok(Value::str(""));
}
let unit = flat_charged(interp, &s)?.get(pos as usize).copied().unwrap_or(0);
Ok(Value::String(Str::from_units(vec![unit])))
}
fn char_code_at(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let s = this_str(interp, &this, "charCodeAt")?;
let pos = int_arg(interp, args, 0)?;
if pos < 0.0 || pos >= s.len() as f64 {
return Ok(Value::Number(f64::NAN));
}
Ok(Value::Number(f64::from(flat_charged(interp, &s)?.get(pos as usize).copied().unwrap_or(0))))
}
fn concat(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let mut s = this_str(interp, &this, "concat")?;
for a in args {
let r = interp.to_str(a)?;
s = interp.concat(&s, &r)?;
}
Ok(Value::String(s))
}
fn index_of(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let s = this_str(interp, &this, "indexOf")?;
let search = interp.to_str(&arg(args, 0))?;
let pos = int_arg(interp, args, 1)?;
let (hay, needle) = (flat_charged(interp, &s)?, flat_charged(interp, &search)?);
let start = clamp(pos, hay.len());
let found = find(interp, &hay, &needle, start)?;
Ok(Value::Number(found.map_or(-1.0, |i| i as f64)))
}
fn last_index_of(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let s = this_str(interp, &this, "lastIndexOf")?;
let search = interp.to_str(&arg(args, 0))?;
let n = interp.to_number(&arg(args, 1))?;
let pos = if n.is_nan() { f64::INFINITY } else { to_integer(n) };
let (hay, needle) = (flat_charged(interp, &s)?, flat_charged(interp, &search)?);
let start = clamp(pos, hay.len());
if needle.len() > hay.len() {
return Ok(Value::Number(-1.0));
}
let mut k = start.min(hay.len() - needle.len());
let per = (needle.len() / UNITS_PER_FUEL) as u64 + 1;
loop {
use_fuel_n(interp, per)?;
if hay.get(k..k + needle.len()) == Some(&needle[..]) {
return Ok(Value::Number(k as f64));
}
if k == 0 {
return Ok(Value::Number(-1.0));
}
k -= 1;
}
}
/// `localeCompare` (section 15.5.4.9): code unit order.
fn locale_compare(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let s = this_str(interp, &this, "localeCompare")?;
let that = interp.to_str(&arg(args, 0))?;
let (a, b) = (flat_charged(interp, &s)?, flat_charged(interp, &that)?);
charge_units(interp, a.len().min(b.len()))?;
Ok(Value::Number(match a.cmp(&b) {
std::cmp::Ordering::Less => -1.0,
std::cmp::Ordering::Equal => 0.0,
std::cmp::Ordering::Greater => 1.0,
}))
}
fn slice(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let s = this_str(interp, &this, "slice")?;
let len = s.len() as f64;
let start = int_arg(interp, args, 0)?;
let end = if matches!(arg(args, 1), Value::Undefined) { len } else { int_arg(interp, args, 1)? };
let from = if start < 0.0 { (len + start).max(0.0) } else { start.min(len) };
let to = if end < 0.0 { (len + end).max(0.0) } else { end.min(len) };
if to <= from {
return Ok(Value::str(""));
}
let units = flat_charged(interp, &s)?;
substring_value(interp, &units, from as usize, to as usize)
}
fn substring(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let s = this_str(interp, &this, "substring")?;
let len = s.len();
let start = int_arg(interp, args, 0)?;
let end = if matches!(arg(args, 1), Value::Undefined) { len as f64 } else { int_arg(interp, args, 1)? };
let (a, b) = (clamp(start, len), clamp(end, len));
let units = flat_charged(interp, &s)?;
substring_value(interp, &units, a.min(b), a.max(b))
}
/// `substr` (Annex B.2.3). Its first step applies ToString to `this` with no object-coercible
/// check, so `null` and `undefined` give substrings of `"null"` and `"undefined"`.
fn substr(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let s = interp.to_str(&this)?;
let len = s.len() as f64;
let start = int_arg(interp, args, 0)?;
let length = if matches!(arg(args, 1), Value::Undefined) { f64::INFINITY } else { int_arg(interp, args, 1)? };
let start = if start >= 0.0 { start } else { (len + start).max(0.0) };
let count = length.max(0.0).min(len - start);
if count <= 0.0 {
return Ok(Value::str(""));
}
let units = flat_charged(interp, &s)?;
substring_value(interp, &units, start as usize, (start + count) as usize)
}
/// The UTF-16 length of `c` mapped to lower or upper case.
fn mapped_len(c: char, upper: bool) -> usize {
if upper { c.to_uppercase().map(char::len_utf16).sum() } else { c.to_lowercase().map(char::len_utf16).sum() }
}
/// Maps the characters of `units` to lower or upper case (Unicode's full mappings, final sigma
/// included), passing lone surrogates through. The result's length is reserved before it is built.
fn map_case(interp: &mut Interp, units: &[u16], upper: bool) -> JsResult<Vec<u16>> {
let len: usize = char::decode_utf16(units.iter().copied()).map(|r| r.map_or(1, |c| mapped_len(c, upper))).sum();
reserve_units(interp, len)?;
let mut out = Vec::with_capacity(len);
let mut run = String::new();
let flush = |run: &mut String, out: &mut Vec<u16>| {
let mapped = if upper { run.to_uppercase() } else { run.to_lowercase() };
out.extend(mapped.encode_utf16());
run.clear();
};
for r in char::decode_utf16(units.iter().copied()) {
match r {
Ok(c) => run.push(c),
Err(e) => {
flush(&mut run, &mut out);
out.push(e.unpaired_surrogate());
}
}
}
flush(&mut run, &mut out);
Ok(out)
}
fn to_lower_case(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
let s = this_str(interp, &this, "toLowerCase")?;
let units = flat_charged(interp, &s)?;
let out = map_case(interp, &units, false)?;
Ok(Value::String(Str::from_units(out)))
}
fn to_upper_case(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
let s = this_str(interp, &this, "toUpperCase")?;
let units = flat_charged(interp, &s)?;
let out = map_case(interp, &units, true)?;
Ok(Value::String(Str::from_units(out)))
}
fn trim(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
let s = this_str(interp, &this, "trim")?;
let units = flat_charged(interp, &s)?;
// The scan reads every unit once at most.
charge_units(interp, units.len())?;
let start = units.iter().position(|&u| !is_str_space(u)).unwrap_or(units.len());
let end = units.iter().rposition(|&u| !is_str_space(u)).map_or(start, |i| i + 1);
if start == 0 && end == units.len() {
return Ok(Value::String(s));
}
substring_value(interp, &units, start, end)
}
// ---- the methods that take a regular expression ----
/// The regular expression argument of `match` and `search`: a RegExp object, or a new one made
/// from the value as `new RegExp(value)` would.
fn regexp_arg(interp: &mut Interp, v: &Value) -> JsResult<ObjId> {
if let Some((o, _)) = regexp_of(interp, v) {
return Ok(o);
}
let o = new_regexp_from(interp, v, &Value::Undefined)?;
interp.temp_roots.push(Value::Object(o));
Ok(o)
}
/// `match` (section 15.5.4.10).
fn match_(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
rooted(interp, |interp| {
let s = this_str(interp, &this, "match")?;
let rx = regexp_arg(interp, &arg(args, 0))?;
let global = interp.get_value(&Value::Object(rx), &crate::heap::key("global"))?.truthy();
if !global {
return crate::regexp_builtins::exec_value(interp, rx, &s);
}
let last_index = crate::heap::key("lastIndex");
interp.put(rx, last_index.clone(), Value::Number(0.0), true)?;
let a = new_array(interp);
let mut n = 0usize;
let mut previous = 0.0;
let units = flat_charged(interp, &s)?;
while let Some(caps) = exec_raw(interp, rx, &s)? {
let this_index = interp.get_value(&Value::Object(rx), &last_index)?;
let this_index = interp.to_number(&this_index)?;
if this_index == previous {
previous = this_index + 1.0;
interp.put(rx, last_index.clone(), Value::Number(previous), true)?;
} else {
previous = this_index;
}
let m = substring_value(interp, &units, caps.first().copied().unwrap_or(0), caps.get(1).copied().unwrap_or(0))?;
set_element(interp, a, n, m)?;
n += 1;
}
Ok(if n == 0 { Value::Null } else { Value::Object(a) })
})
}
/// `search` (section 15.5.4.12): the index of the first match from the start, ignoring
/// `lastIndex` and `global`.
fn search(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
rooted(interp, |interp| {
let s = this_str(interp, &this, "search")?;
let rx = regexp_arg(interp, &arg(args, 0))?;
let Some((_, re)) = regexp_of(interp, &Value::Object(rx)) else { return Err(interp.type_error("not a regular expression")) };
let units = flat_charged(interp, &s)?;
let mut m = Matcher::new(&re, interp)?;
let found = m.search(interp, &units, 0)?;
Ok(Value::Number(if found { m.caps.first().copied().unwrap_or(0) as f64 } else { -1.0 }))
})
}
/// A piece of a result string: a range of the input, a range of the replacement pattern, or the
/// units of a string a replacement function returned.
enum Piece {
Range(usize, usize),
Template(usize, usize),
Units(Rc<[u16]>),
}
impl Piece {
fn len(&self) -> usize {
match self {
Piece::Range(a, b) | Piece::Template(a, b) => b.saturating_sub(*a),
Piece::Units(u) => u.len(),
}
}
}
/// The pieces of a result string and the length they add up to.
#[derive(Default)]
struct Pieces {
list: Vec<Piece>,
len: usize,
}
impl Pieces {
/// Adds `piece`, holding its bytes and taking fuel for its units, and ends the run with the
/// heap limit once the result would not fit.
fn add(&mut self, interp: &mut Interp, held: &mut HeldBytes, piece: Piece) -> JsResult<()> {
let n = piece.len();
if n == 0 {
return Ok(());
}
push_held(interp, held, &mut self.list, piece)?;
self.len = self.len.saturating_add(n);
charge_units(interp, n)?;
check_string_room(interp, self.len)
}
/// Joins the pieces into a new string, reserving its length first. `units` is the input and
/// `template` the replacement pattern.
fn build(&self, interp: &mut Interp, units: &[u16], template: &[u16]) -> JsResult<Value> {
reserve_string(interp, self.len)?;
let mut out = Vec::with_capacity(self.len);
for p in &self.list {
match p {
Piece::Range(a, b) => out.extend_from_slice(units.get(*a..*b).unwrap_or_default()),
Piece::Template(a, b) => out.extend_from_slice(template.get(*a..*b).unwrap_or_default()),
Piece::Units(u) => out.extend_from_slice(u),
}
}
Ok(Value::String(Str::from_units(out)))
}
}
/// A part of a replacement pattern (section 15.5.4.11, Table 22).
#[derive(Clone, Copy)]
enum Part {
/// The pattern's own text in a range.
Text(usize, usize),
/// `$&`
Matched,
/// `` $` ``
Before,
/// `$'`
After,
/// `$n` or `$nn`
Group(usize),
}
impl Part {
/// The piece this part gives for one match: `caps` holds the match and its groups as pairs of
/// positions in an input of `len` units.
fn piece(self, caps: &[usize], len: usize) -> Option<Piece> {
let (start, end) = (caps.first().copied().unwrap_or(0), caps.get(1).copied().unwrap_or(0));
match self {
Part::Text(a, b) => Some(Piece::Template(a, b)),
Part::Matched => Some(Piece::Range(start, end)),
Part::Before => Some(Piece::Range(0, start)),
Part::After => Some(Piece::Range(end, len)),
Part::Group(n) => {
let (s, e) = (caps.get(2 * n).copied().unwrap_or(NONE), caps.get(2 * n + 1).copied().unwrap_or(NONE));
(s != NONE && e != NONE).then_some(Piece::Range(s, e))
}
}
}
}
/// Reads a replacement pattern once into its parts, for a regular expression with `groups`
/// groups. The parts are held against the heap limit, and the units read take fuel.
fn parse_template(interp: &mut Interp, held: &mut HeldBytes, template: &[u16], groups: usize) -> JsResult<Vec<Part>> {
charge_units(interp, template.len())?;
let digit = |i: usize| template.get(i).filter(|&&u| (0x30..=0x39).contains(&u)).map(|&u| usize::from(u - 0x30));
let mut parts = Vec::new();
let mut lit = 0;
let mut i = 0;
while let Some(&u) = template.get(i) {
if u != 0x24 {
i += 1;
continue;
}
let (part, taken) = match template.get(i + 1) {
// `$$` gives one `$`: the text up to and including the first.
Some(0x24) => {
push_held(interp, held, &mut parts, Part::Text(lit, i + 1))?;
lit = i + 2;
i += 2;
continue;
}
Some(0x26) => (Part::Matched, 2),
Some(0x60) => (Part::Before, 2),
Some(0x27) => (Part::After, 2),
_ => match (digit(i + 1), digit(i + 2)) {
(Some(a), Some(b)) if (1..=groups).contains(&(a * 10 + b)) => (Part::Group(a * 10 + b), 3),
(Some(a), _) if (1..=groups).contains(&a) => (Part::Group(a), 2),
_ => {
i += 1;
continue;
}
},
};
if i > lit {
push_held(interp, held, &mut parts, Part::Text(lit, i))?;
}
push_held(interp, held, &mut parts, part)?;
i += taken;
lit = i;
}
if template.len() > lit {
push_held(interp, held, &mut parts, Part::Text(lit, template.len()))?;
}
Ok(parts)
}
/// `replace` (section 15.5.4.11). The matches, the parsed replacement pattern and the pieces of
/// the result are held against the heap limit while the result is put together, and the
/// result's length is checked against the heap as each piece is added.
fn replace(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
rooted(interp, |interp| {
let s = this_str(interp, &this, "replace")?;
let search_value = arg(args, 0);
let replace_value = arg(args, 1);
let units = flat_charged(interp, &s)?;
let mut held = HeldBytes::new(interp);
// Each match as the positions of the match and its groups.
let mut matches: Vec<Vec<usize>> = Vec::new();
if let Some((rx, _)) = regexp_of(interp, &search_value) {
let global = interp.get_value(&search_value, &crate::heap::key("global"))?.truthy();
if global {
let last_index = crate::heap::key("lastIndex");
interp.put(rx, last_index.clone(), Value::Number(0.0), true)?;
let mut previous = 0.0;
while let Some(caps) = exec_raw(interp, rx, &s)? {
held.add(interp, size_of::<usize>().saturating_mul(caps.capacity()))?;
push_held(interp, &mut held, &mut matches, caps)?;
let this_index = interp.get_value(&search_value, &last_index)?;
let this_index = interp.to_number(&this_index)?;
if this_index == previous {
previous = this_index + 1.0;
interp.put(rx, last_index.clone(), Value::Number(previous), true)?;
} else {
previous = this_index;
}
}
} else if let Some(caps) = exec_raw(interp, rx, &s)? {
matches.push(caps);
}
} else {
let search = interp.to_str(&search_value)?;
let needle = flat_charged(interp, &search)?;
if let Some(i) = find(interp, &units, &needle, 0)? {
matches.push(vec![i, i + needle.len()]);
}
}
let replacer = interp.is_callable(&replace_value).then(|| replace_value.clone());
let template = match &replacer {
Some(_) => None,
None => {
let t = interp.to_str(&replace_value)?;
Some(flat_charged(interp, &t)?)
}
};
if matches.is_empty() {
return Ok(Value::String(s));
}
let template: &[u16] = template.as_deref().unwrap_or_default();
// Every match has the same number of groups.
let groups = matches.first().map_or(0, |caps| (caps.len() / 2).saturating_sub(1));
let parts = if replacer.is_none() { parse_template(interp, &mut held, template, groups)? } else { Vec::new() };
let mut pieces = Pieces::default();
let mut last = 0usize;
for caps in &matches {
let (start, end) = (caps.first().copied().unwrap_or(0), caps.get(1).copied().unwrap_or(0));
pieces.add(interp, &mut held, Piece::Range(last, start))?;
match &replacer {
Some(f) => {
let mut call_args = Vec::with_capacity(caps.len() / 2 + 2);
for pair in caps.chunks(2) {
let v = match pair {
[a, b] if *a != NONE && *b != NONE => substring_value(interp, &units, *a, *b)?,
_ => Value::Undefined,
};
call_args.push(v);
}
call_args.push(Value::Number(start as f64));
call_args.push(Value::String(s.clone()));
let r = interp.call(f, &Value::Undefined, &call_args)?;
let text = interp.to_str(&r)?;
let text = flat_charged(interp, &text)?;
pieces.add(interp, &mut held, Piece::Units(text))?;
}
None => {
// Reading the parts takes fuel even when they give empty pieces.
use_fuel_n(interp, (parts.len() / UNITS_PER_FUEL) as u64)?;
for part in &parts {
if let Some(p) = part.piece(caps, units.len()) {
pieces.add(interp, &mut held, p)?;
}
}
}
}
use_fuel_n(interp, 1)?;
last = end;
}
pieces.add(interp, &mut held, Piece::Range(last, units.len()))?;
pieces.build(interp, &units, template)
})
}
/// `split` (section 15.5.4.14).
fn split(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
rooted(interp, |interp| {
let s = this_str(interp, &this, "split")?;
let separator = arg(args, 0);
let limit = arg(args, 1);
let a = new_array(interp);
let lim = if matches!(limit, Value::Undefined) { u32::MAX } else { to_uint32(interp.to_number(&limit)?) };
let rx = regexp_of(interp, &separator);
let sep = match &rx {
Some(_) => None,
None => {
let sep = interp.to_str(&separator)?;
Some(flat_charged(interp, &sep)?)
}
};
if lim == 0 {
return Ok(Value::Object(a));
}
if matches!(separator, Value::Undefined) {
set_element(interp, a, 0, Value::String(s))?;
return Ok(Value::Object(a));
}
let units = flat_charged(interp, &s)?;
let size = units.len();
let re: Option<Rc<crate::regexp_vm::Regex>> = rx.map(|(_, re)| re);
let mut matcher = re.as_deref().map(|re| Matcher::new(re, interp)).transpose()?;
// SplitMatch (section 15.5.4.14): the end of a separator match at `q` and the groups.
let mut split_match = |interp: &mut Interp, q: usize| -> JsResult<Option<(usize, Vec<usize>)>> {
match (&mut matcher, &sep) {
(Some(m), _) => {
if m.match_at(interp, &units, q)? {
Ok(Some((m.caps.get(1).copied().unwrap_or(q), m.caps.get(2..).map(<[usize]>::to_vec).unwrap_or_default())))
} else {
Ok(None)
}
}
(None, Some(r)) => {
use_fuel_n(interp, (r.len() / UNITS_PER_FUEL) as u64 + 1)?;
if units.get(q..q + r.len()) == Some(&r[..]) { Ok(Some((q + r.len(), Vec::new()))) } else { Ok(None) }
}
(None, None) => Ok(None),
}
};
if size == 0 {
if split_match(interp, 0)?.is_none() {
set_element(interp, a, 0, Value::String(s.clone()))?;
}
return Ok(Value::Object(a));
}
let mut n = 0usize;
let (mut p, mut q) = (0usize, 0usize);
while q != size {
match split_match(interp, q)? {
None => q += 1,
Some((e, _)) if e == p => q += 1,
Some((e, caps)) => {
let t = substring_value(interp, &units, p, q)?;
set_element(interp, a, n, t)?;
n += 1;
if n as u64 == u64::from(lim) {
return Ok(Value::Object(a));
}
p = e;
for pair in caps.chunks(2) {
let v = match pair {
[x, y] if *x != NONE && *y != NONE => substring_value(interp, &units, *x, *y)?,
_ => Value::Undefined,
};
set_element(interp, a, n, v)?;
n += 1;
if n as u64 == u64::from(lim) {
return Ok(Value::Object(a));
}
}
q = p;
}
}
}
let t = substring_value(interp, &units, p, size)?;
set_element(interp, a, n, t)?;
Ok(Value::Object(a))
})
}