Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions core/engine/src/builtins/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
Context, JsArgs, JsExpect, JsResult, JsString, JsValue,
builtins::{Array, BuiltInObject, Number, RegExp},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
error::JsNativeError,
error::{JsNativeError, RuntimeLimitError},
js_string,
object::{JsObject, internal_methods::get_prototype_from_constructor},
property::{Attribute, PropertyDescriptor},
Expand Down Expand Up @@ -733,15 +733,23 @@ impl String {

let n = n as usize;

// Charge each repetition against the VM loop-iteration limit.
let mut result = Vec::with_capacity(n);
for _ in 0..n {
crate::vm::opcode::IncrementLoopIteration::operation((), context)?;
result.push(string.as_str());
// Charge all repetitions against the VM loop-iteration limit in one step
// so a huge `n` cannot allocate a `Vec` of `n` views before the limit fires.
let max = context.vm.runtime_limits.loop_iteration_limit();
let frame = context.vm.frame_mut();
let remaining = max.saturating_sub(frame.loop_iteration_count);
if n as u64 > remaining {
return Err(RuntimeLimitError::LoopIteration.into());
}
frame.loop_iteration_count = frame.loop_iteration_count.wrapping_add(n as u64);

// 6. Return the String value that is made from n copies of S appended together.
Ok(JsString::concat_array(&result).into())
let Some(repeated) = JsString::repeat(string.as_str(), n) else {
return Err(JsNativeError::range()
.with_message("String.prototype.repeat: cannot allocate the result string")
.into());
};
Ok(repeated.into())
}

/// `String.prototype.slice( beginIndex [, endIndex] )`
Expand Down
22 changes: 22 additions & 0 deletions core/engine/src/builtins/string/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,28 @@ fn repeat_throws_when_count_overflows_max_length() {
)]);
}

#[test]
fn repeat_loop_limit_fails_before_building_result() {
run_test_actions([
TestAction::inspect_context(|context| {
context.runtime_limits_mut().set_loop_iteration_limit(0);
}),
TestAction::assert_runtime_limit_error(
"'x'.repeat(100)",
crate::error::RuntimeLimitError::LoopIteration,
),
]);
}

#[test]
fn repeat_moderate_count_without_loop_limit() {
run_test_actions([
TestAction::assert_eq("'ab'.repeat(1000).length", 2000),
TestAction::assert_eq("'ab'.repeat(1000).slice(0, 4)", js_str!("abab")),
TestAction::assert_eq("'ab'.repeat(1000).slice(-4)", js_str!("abab")),
]);
}

#[test]
fn repeat_generic() {
run_test_actions([
Expand Down
55 changes: 54 additions & 1 deletion core/string/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ mod tests;
use self::iter::Windows;
use crate::display::{JsStrDisplayEscaped, JsStrDisplayLossy, JsStringDebugInfo};
use crate::iter::CodePointsIter;
use crate::r#type::{Latin1, Utf16};
use crate::r#type::{InternalStringType, Latin1, Utf16};
pub use crate::vtable::StaticString;
use crate::vtable::{SequenceString, SliceString};
#[doc(inline)]
Expand Down Expand Up @@ -705,6 +705,59 @@ impl JsString {
StaticJsStrings::get_string(&string.as_str()).unwrap_or(string)
}

/// Creates a new [`JsString`] from `n` copies of `s`.
///
/// Returns [`None`] if `n * s.len()` overflows or the result cannot be allocated.
/// This fills a single buffer of length `n * s.len()` instead of building an
/// intermediate list of `n` string views.
#[must_use]
pub fn repeat(s: JsStr<'_>, n: usize) -> Option<Self> {
if n == 0 || s.is_empty() {
return Some(Self::default());
}
if n == 1 {
return Some(Self::from(s));
}
let total = s.len().checked_mul(n)?;
let string = match s.variant() {
JsStrVariant::Latin1(bytes) => Self::repeat_bytes::<Latin1>(bytes, total)?,
JsStrVariant::Utf16(units) => Self::repeat_bytes::<Utf16>(units, total)?,
};
Some(StaticJsStrings::get_string(&string.as_str()).unwrap_or(string))
}

/// Allocates `total` units and fills them by doubling copies of `src`.
///
/// `total` must be a positive multiple of `src.len()`.
fn repeat_bytes<T: InternalStringType>(src: &[T::Byte], total: usize) -> Option<Self> {
debug_assert!(!src.is_empty());
debug_assert!(total >= src.len());
debug_assert_eq!(total % src.len(), 0);

let ptr = SequenceString::<T>::try_allocate(total).ok()?;

// SAFETY:
// - `try_allocate` returns a valid `SequenceString<T>` with room for `total` units
// of `T::Byte`, and writes the header (including `len = total`).
// - The first `copy_nonoverlapping` reads `src.len()` units from a valid slice and
// writes them to the start of the allocation; `src` and the new allocation do not overlap.
// - Each doubling copy moves `to_copy` units from `[0, filled)` to `[filled, filled + to_copy)`.
// Those ranges are adjacent and non-overlapping, and `filled + to_copy <= total`, so
// both source and destination stay in-bounds.
// - Alignment of `data` is guaranteed by `try_allocate`.
unsafe {
let data = (&raw mut (*ptr.as_ptr()).data).cast::<T::Byte>();
ptr::copy_nonoverlapping(src.as_ptr(), data, src.len());
let mut filled = src.len();
while filled < total {
let to_copy = filled.min(total - filled);
ptr::copy_nonoverlapping(data, data.add(filled), to_copy);
filled += to_copy;
}
Some(Self { ptr: ptr.cast() })
}
}

/// Creates a new [`JsString`] from `data`, without checking if the string is in the interner.
fn from_slice_skip_interning(string: JsStr<'_>) -> Self {
let count = string.len();
Expand Down
33 changes: 33 additions & 0 deletions core/string/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,39 @@ fn concat() {
assert_eq!(xyzw.refcount(), Some(1));
}

#[test]
fn repeat() {
assert_eq!(
JsString::repeat(JsStr::latin1(b"ab"), 0).unwrap(),
JsString::default()
);
assert_eq!(
JsString::repeat(JsStr::latin1(b""), 8).unwrap(),
JsString::default()
);
assert_eq!(
JsString::repeat(JsStr::latin1(b"ab"), 1).unwrap().as_str(),
JsStr::latin1(b"ab")
);
assert_eq!(
JsString::repeat(JsStr::latin1(b"ab"), 8).unwrap().as_str(),
JsStr::latin1(b"abababababababab")
);

let utf16 = JsString::from(&[0xD83D, 0xDE00][..]);
let repeated = JsString::repeat(utf16.as_str(), 3).unwrap();
assert_eq!(repeated.len(), 6);
assert_eq!(&repeated, &[0xD83D, 0xDE00, 0xD83D, 0xDE00, 0xD83D, 0xDE00]);

let long = JsString::repeat(JsStr::latin1(b"xy"), 1024).unwrap();
assert_eq!(long.len(), 2048);
assert!(
long.as_str()
.as_latin1()
.is_some_and(|s| s.starts_with(b"xy") && s.ends_with(b"xy"))
);
}

#[test]
fn trim_start_non_ascii_to_ascii() {
let s = "\u{2029}abc";
Expand Down
Loading