diff --git a/Jint/Native/RegExp/RegExpPrototype.cs b/Jint/Native/RegExp/RegExpPrototype.cs index 50a647556..0ba4936a3 100644 --- a/Jint/Native/RegExp/RegExpPrototype.cs +++ b/Jint/Native/RegExp/RegExpPrototype.cs @@ -259,8 +259,6 @@ private JsValue Replace(JsValue thisObject, JsValue stringArg, JsValue replaceVa string Evaluator(Match match) { var actualGroupCount = GetActualRegexGroupCount(rei, match); - var replacerArgs = new List(actualGroupCount + 2); - replacerArgs.Add(match.Value); ObjectInstance? groups = null; // Pre-initialize groups with unique names in order @@ -277,10 +275,16 @@ string Evaluator(Match match) } } + // matched + captures + position + string + optional groups object; the array is freshly + // allocated and exactly JsValue[], so writes can skip the covariant store type check. + var replacerArgs = new JsValue[actualGroupCount + 2 + (groups is not null ? 1 : 0)]; + var argIndex = 0; + Arguments.WriteNoTypeCheck(replacerArgs, argIndex++, match.Value); + for (var i = 1; i < actualGroupCount; i++) { var capture = match.Groups[i]; - replacerArgs.Add(capture.Success ? capture.Value : Undefined); + Arguments.WriteNoTypeCheck(replacerArgs, argIndex++, capture.Success ? capture.Value : Undefined); var groupName = GetRegexGroupName(rei, i); if (!string.IsNullOrWhiteSpace(groupName) && capture.Success) @@ -289,11 +293,11 @@ string Evaluator(Match match) } } - replacerArgs.Add(match.Index); - replacerArgs.Add(s); + Arguments.WriteNoTypeCheck(replacerArgs, argIndex++, match.Index); + Arguments.WriteNoTypeCheck(replacerArgs, argIndex++, s); if (groups is not null) { - replacerArgs.Add(groups); + Arguments.WriteNoTypeCheck(replacerArgs, argIndex, groups); } return CallFunctionalReplace(replaceValue, replacerArgs); @@ -374,18 +378,22 @@ string Evaluator(Match match) string replacement; if (functionalReplace) { - var replacerArgs = new List(); - replacerArgs.Add(matched); + // matched + captures + position + string + optional named captures; the array is freshly + // allocated and exactly JsValue[], so writes can skip the covariant store type check. + var hasNamedCaptures = !namedCaptures.IsUndefined(); + var replacerArgs = new JsValue[captures.Count + 3 + (hasNamedCaptures ? 1 : 0)]; + var argIndex = 0; + Arguments.WriteNoTypeCheck(replacerArgs, argIndex++, matched); foreach (var capture in captures) { - replacerArgs.Add(capture); + Arguments.WriteNoTypeCheck(replacerArgs, argIndex++, capture); } - replacerArgs.Add(position); - replacerArgs.Add(s); - if (!namedCaptures.IsUndefined()) + Arguments.WriteNoTypeCheck(replacerArgs, argIndex++, position); + Arguments.WriteNoTypeCheck(replacerArgs, argIndex++, s); + if (hasNamedCaptures) { - replacerArgs.Add(namedCaptures); + Arguments.WriteNoTypeCheck(replacerArgs, argIndex, namedCaptures); } replacement = CallFunctionalReplace(replaceValue, replacerArgs); @@ -422,9 +430,9 @@ string Evaluator(Match match) #pragma warning restore CA1845 } - private static string CallFunctionalReplace(JsValue replacer, List replacerArgs) + private static string CallFunctionalReplace(JsValue replacer, JsCallArguments replacerArgs) { - var result = ((ICallable) replacer).Call(Undefined, replacerArgs.ToArray()); + var result = ((ICallable) replacer).Call(Undefined, replacerArgs); return TypeConverter.ToString(result); } diff --git a/Jint/Runtime/Arguments.cs b/Jint/Runtime/Arguments.cs index 5a7435182..bbf6efb04 100644 --- a/Jint/Runtime/Arguments.cs +++ b/Jint/Runtime/Arguments.cs @@ -1,4 +1,8 @@ +using System.Diagnostics; using System.Runtime.CompilerServices; +#if NET8_0_OR_GREATER +using System.Runtime.InteropServices; +#endif using Jint.Native; namespace Jint.Runtime; @@ -31,6 +35,36 @@ public static JsValue At(this JsValue[] args, int index) return At(args, index, JsValue.Undefined); } + /// + /// Writes to without the covariant array store + /// type check (stelemref). Because is not sealed, a plain array[index] = value + /// must verify at runtime that the array's actual element type accepts the value; that check is pure + /// overhead when the array is known to be exactly []. Bounds are checked explicitly. + /// + /// + /// Callers MUST guarantee that is exactly of type [] — + /// freshly allocated via new JsValue[n] or rented from + /// (whose factories only ever create such arrays). Writing through this helper into an array whose + /// element type is a subtype of would break array type safety. In particular, + /// never use it on caller-supplied whose provenance is unknown. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void WriteNoTypeCheck(JsValue[] array, int index, JsValue value) + { + Debug.Assert(array.GetType() == typeof(JsValue[]), "array must be exactly JsValue[]"); + + if ((uint) index >= (uint) array.Length) + { + Throw.ArgumentOutOfRangeException(nameof(index), "Index was outside the bounds of the array."); + } + +#if NET8_0_OR_GREATER + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index) = value; +#else + array[index] = value; +#endif + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static JsCallArguments Skip(this JsValue[] args, int count) { diff --git a/Jint/Runtime/Interpreter/Expressions/ExpressionCache.cs b/Jint/Runtime/Interpreter/Expressions/ExpressionCache.cs index aa309f8e9..26a3618b5 100644 --- a/Jint/Runtime/Interpreter/Expressions/ExpressionCache.cs +++ b/Jint/Runtime/Interpreter/Expressions/ExpressionCache.cs @@ -113,13 +113,18 @@ public JsValue[] ArgumentListEvaluation(EvaluationContext context, object key, o return arguments; } + /// + /// Evaluates argument expressions into , which callers must have + /// allocated via new JsValue[n] or rented from the engine's + /// so that it is exactly [] (see ). + /// internal int BuildArguments(EvaluationContext context, JsValue[] targetArray, int startIndex = 0) { var expressions = _expressions; int i = startIndex; for (; (uint) i < (uint) expressions.Length; i++) { - targetArray[i] = GetValue(context, expressions[i])!; + Arguments.WriteNoTypeCheck(targetArray, i, GetValue(context, expressions[i])!); // Check for generator suspension after each expression evaluation // This is needed because yield expressions return normally instead of throwing