//! `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. fn push_held(interp: &mut Interp, held: &mut HeldBytes, list: &mut Vec, item: T) -> JsResult<()> { if list.len() == list.capacity() { let more = list.capacity().max(4); held.add(interp, more.saturating_mul(size_of::()))?; 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. pub(crate) fn flat_charged(interp: &mut Interp, s: &Str) -> JsResult> { if s.is_rope() { 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 { 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(interp: &mut Interp, f: impl FnOnce(&mut Interp) -> JsResult) -> JsResult { 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 { 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 { 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. fn find(interp: &mut Interp, hay: &[u16], needle: &[u16], from: usize) -> JsResult> { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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> { 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| { 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 { 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 { 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 { 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 { 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 { 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 { 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, 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 { 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 { 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> { 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 { 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::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::().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 { 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> = 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)>> { 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)) }) }