From e510d706fc5596b0f598cc6605a557d122af2b63 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 31 Dec 2025 11:46:49 +0200 Subject: [PATCH] Implement Immutable ArrayBuffers --- .../Test262Harness.settings.json | 1 - .../ArrayBuffer/ArrayBufferPrototype.cs | 137 +++++++++++++++++- Jint/Native/DataView/DataViewPrototype.cs | 11 +- Jint/Native/JsArrayBuffer.cs | 17 +++ Jint/Native/JsTypedArray.cs | 4 + README.md | 1 + 6 files changed, 167 insertions(+), 4 deletions(-) diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json index b13a7cb000..dd489b9338 100644 --- a/Jint.Tests.Test262/Test262Harness.settings.json +++ b/Jint.Tests.Test262/Test262Harness.settings.json @@ -9,7 +9,6 @@ "async-iteration", "Atomics", "decorators", - "immutable-arraybuffer", "import-defer", "joint-iteration", "json-parse-with-source", diff --git a/Jint/Native/ArrayBuffer/ArrayBufferPrototype.cs b/Jint/Native/ArrayBuffer/ArrayBufferPrototype.cs index bcc8186f6a..6111820936 100644 --- a/Jint/Native/ArrayBuffer/ArrayBufferPrototype.cs +++ b/Jint/Native/ArrayBuffer/ArrayBufferPrototype.cs @@ -32,12 +32,15 @@ protected override void Initialize() ["byteLength"] = new GetSetPropertyDescriptor(new ClrFunction(_engine, "get byteLength", ByteLength, 0, lengthFlags), Undefined, PropertyFlag.Configurable), [KnownKeys.Constructor] = new PropertyDescriptor(_constructor, PropertyFlag.NonEnumerable), ["detached"] = new GetSetPropertyDescriptor(new ClrFunction(_engine, "get detached", Detached, 0, lengthFlags), Undefined, PropertyFlag.Configurable), + ["immutable"] = new GetSetPropertyDescriptor(new ClrFunction(_engine, "get immutable", Immutable, 0, lengthFlags), Undefined, PropertyFlag.Configurable), ["maxByteLength"] = new GetSetPropertyDescriptor(new ClrFunction(_engine, "get maxByteLength", MaxByteLength, 0, lengthFlags), Undefined, PropertyFlag.Configurable), ["resizable"] = new GetSetPropertyDescriptor(new ClrFunction(_engine, "get resizable", Resizable, 0, lengthFlags), Undefined, PropertyFlag.Configurable), ["resize"] = new PropertyDescriptor(new ClrFunction(_engine, "resize", Resize, 1, lengthFlags), PropertyFlag.NonEnumerable), ["slice"] = new PropertyDescriptor(new ClrFunction(_engine, "slice", Slice, 2, lengthFlags), PropertyFlag.NonEnumerable), + ["sliceToImmutable"] = new PropertyDescriptor(new ClrFunction(_engine, "sliceToImmutable", SliceToImmutable, 2, lengthFlags), PropertyFlag.NonEnumerable), ["transfer"] = new PropertyDescriptor(new ClrFunction(_engine, "transfer", Transfer, 0, lengthFlags), PropertyFlag.NonEnumerable), ["transferToFixedLength"] = new PropertyDescriptor(new ClrFunction(_engine, "transferToFixedLength", TransferToFixedLength, 0, lengthFlags), PropertyFlag.NonEnumerable), + ["transferToImmutable"] = new PropertyDescriptor(new ClrFunction(_engine, "transferToImmutable", TransferToImmutable, 0, lengthFlags), PropertyFlag.NonEnumerable), }; SetProperties(properties); @@ -56,6 +59,20 @@ private JsValue Detached(JsValue thisObject, JsCallArguments arguments) return o.IsDetachedBuffer; } + /// + /// https://tc39.es/proposal-immutable-arraybuffer/#sec-get-arraybuffer.prototype.immutable + /// + private JsValue Immutable(JsValue thisObject, JsCallArguments arguments) + { + var o = thisObject as JsArrayBuffer; + if (o is null || o.IsSharedArrayBuffer) + { + Throw.TypeError(_realm, "Method ArrayBuffer.prototype.immutable called on incompatible receiver " + thisObject); + } + + return o.IsImmutableBuffer; + } + /// /// https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.maxbytelength /// @@ -95,6 +112,7 @@ private JsValue Resizable(JsValue thisObject, JsCallArguments arguments) /// /// https://tc39.es/ecma262/#sec-arraybuffer.prototype.resize + /// https://tc39.es/proposal-immutable-arraybuffer/#sec-arraybuffer.prototype.resize /// private JsValue Resize(JsValue thisObject, JsCallArguments arguments) { @@ -104,6 +122,13 @@ private JsValue Resize(JsValue thisObject, JsCallArguments arguments) Throw.TypeError(_realm, "Method ArrayBuffer.prototype.resize called on incompatible receiver " + thisObject); } + // Step 2: Perform ? RequireInternalSlot(O, [[ArrayBufferMaxByteLength]]). + // This check must happen before reading newLength + if (o.IsFixedLengthArrayBuffer) + { + Throw.TypeError(_realm, "Cannot resize a fixed-length ArrayBuffer"); + } + var newLength = arguments.At(0); var newByteLength = TypeConverter.ToIndex(_realm, newLength); @@ -204,9 +229,16 @@ private JsValue Slice(JsValue thisObject, JsCallArguments arguments) Throw.TypeError(_realm); } + // https://tc39.es/proposal-immutable-arraybuffer/#sec-arraybuffer.prototype.slice + // If IsImmutableBuffer(new) is true, throw a TypeError exception. + if (bufferInstance.IsImmutableBuffer) + { + Throw.TypeError(_realm, "Cannot use an immutable ArrayBuffer as species constructor result"); + } + // NOTE: Side-effects of the above steps may have detached O. - if (bufferInstance.IsDetachedBuffer) + if (o.IsDetachedBuffer) { Throw.TypeError(_realm); } @@ -233,6 +265,96 @@ private JsValue TransferToFixedLength(JsValue thisObject, JsCallArguments argume return ArrayBufferCopyAndDetach(thisObject, arguments.At(0), PreserveResizability.FixedLength); } + /// + /// https://tc39.es/proposal-immutable-arraybuffer/#sec-arraybuffer.prototype.transfertoimmutable + /// + private JsValue TransferToImmutable(JsValue thisObject, JsCallArguments arguments) + { + // 1. Let O be the this value. + // 2. Return ? ArrayBufferCopyAndDetach(O, newLength, immutable). + return ArrayBufferCopyAndDetach(thisObject, arguments.At(0), PreserveResizability.Immutable); + } + + /// + /// https://tc39.es/proposal-immutable-arraybuffer/#sec-arraybuffer.prototype.slicetoimmutable + /// + private JsValue SliceToImmutable(JsValue thisObject, JsCallArguments arguments) + { + // 1. Let O be the this value. + var o = thisObject as JsArrayBuffer; + if (o is null || o.IsSharedArrayBuffer) + { + Throw.TypeError(_realm, "Method ArrayBuffer.prototype.sliceToImmutable called on incompatible receiver " + thisObject); + } + + // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]). + // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception. + // (already checked above) + + // 4. If IsDetachedBuffer(O) is true, throw a TypeError exception. + o.AssertNotDetached(); + + var start = arguments.At(0); + var end = arguments.At(1); + + // 5. Let len be O.[[ArrayBufferByteLength]]. + var len = o.ArrayBufferByteLength; + + // 6. Let relativeStart be ? ToIntegerOrInfinity(start). + var relativeStart = TypeConverter.ToIntegerOrInfinity(start); + + // 7-8. Set first based on relativeStart + var first = relativeStart switch + { + double.NegativeInfinity => 0, + < 0 => (int) System.Math.Max(len + relativeStart, 0), + _ => (int) System.Math.Min(relativeStart, len) + }; + + // 9-10. Set relativeEnd based on end + double relativeEnd; + if (end.IsUndefined()) + { + relativeEnd = len; + } + else + { + relativeEnd = TypeConverter.ToIntegerOrInfinity(end); + } + + // 11-12. Set final based on relativeEnd + var final = relativeEnd switch + { + double.NegativeInfinity => 0, + < 0 => (int) System.Math.Max(len + relativeEnd, 0), + _ => (int) System.Math.Min(relativeEnd, len) + }; + + // 13. Let newLen be max(final - first, 0). + var newLen = (uint) System.Math.Max(final - first, 0); + + // 14. Let new be ? AllocateArrayBuffer(%ArrayBuffer%, newLen). + var newBuffer = _engine.Realm.Intrinsics.ArrayBuffer.AllocateArrayBuffer(_engine.Realm.Intrinsics.ArrayBuffer, newLen); + + // 15. If IsDetachedBuffer(O) is true, throw a TypeError exception. + o.AssertNotDetached(); + + // 16. Let fromBuf be O.[[ArrayBufferData]]. + var fromBuf = o.ArrayBufferData!; + + // 17. Let toBuf be new.[[ArrayBufferData]]. + var toBuf = newBuffer.ArrayBufferData!; + + // 18. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, newLen). + System.Array.Copy(fromBuf, first, toBuf, 0, newLen); + + // 19. Set new.[[ArrayBufferImmutable]] to true. + newBuffer._isImmutable = true; + + // 20. Return new. + return newBuffer; + } + private JsValue ArrayBufferCopyAndDetach(JsValue o, JsValue newLength, PreserveResizability preserveResizability) { if (o is not JsArrayBuffer arrayBuffer || arrayBuffer.IsSharedArrayBuffer) @@ -253,6 +375,10 @@ private JsValue ArrayBufferCopyAndDetach(JsValue o, JsValue newLength, PreserveR arrayBuffer.AssertNotDetached(); + // https://tc39.es/proposal-immutable-arraybuffer/#sec-arraybuffercopyanddetach + // If IsImmutableBuffer(arrayBuffer) is true, throw a TypeError exception. + arrayBuffer.AssertNotImmutable(); + uint? newMaxByteLength = null; if (preserveResizability == PreserveResizability.PreserveResizability && arrayBuffer._arrayBufferMaxByteLength != null) { @@ -273,6 +399,12 @@ private JsValue ArrayBufferCopyAndDetach(JsValue o, JsValue newLength, PreserveR // NOTE: Neither creation of the new Data Block nor copying from the old Data Block are observable. Implementations may implement this method as a zero-copy move or a realloc. + // If preserveResizability is immutable, set new buffer's immutable flag + if (preserveResizability == PreserveResizability.Immutable) + { + newBuffer._isImmutable = true; + } + arrayBuffer.DetachArrayBuffer(); return newBuffer; @@ -281,6 +413,7 @@ private JsValue ArrayBufferCopyAndDetach(JsValue o, JsValue newLength, PreserveR private enum PreserveResizability { PreserveResizability, - FixedLength + FixedLength, + Immutable } } diff --git a/Jint/Native/DataView/DataViewPrototype.cs b/Jint/Native/DataView/DataViewPrototype.cs index aa172d5e8f..1fdc997279 100644 --- a/Jint/Native/DataView/DataViewPrototype.cs +++ b/Jint/Native/DataView/DataViewPrototype.cs @@ -351,6 +351,7 @@ private static DataViewWithBufferWitnessRecord MakeDataViewWithBufferWitnessReco /// /// https://tc39.es/ecma262/#sec-setviewvalue + /// https://tc39.es/proposal-immutable-arraybuffer/#sec-setviewvalue /// private JsValue SetViewValue( JsValue view, @@ -365,6 +366,15 @@ private JsValue SetViewValue( Throw.TypeError(_realm, "Method called on incompatible receiver " + view); } + var buffer = dataView._viewedArrayBuffer!; + + // https://tc39.es/proposal-immutable-arraybuffer/#sec-setviewvalue + // Check immutability BEFORE processing arguments + if (buffer.IsImmutableBuffer) + { + Throw.TypeError(_realm, "Cannot modify an immutable ArrayBuffer"); + } + var getIndex = TypeConverter.ToIndex(_realm, requestIndex); TypedArrayValue numberValue; @@ -378,7 +388,6 @@ private JsValue SetViewValue( } var isLittleEndianBoolean = TypeConverter.ToBoolean(isLittleEndian); - var buffer = dataView._viewedArrayBuffer!; buffer.AssertNotDetached(); var viewOffset = dataView._byteOffset; diff --git a/Jint/Native/JsArrayBuffer.cs b/Jint/Native/JsArrayBuffer.cs index 58ba80fd3a..ea03560203 100644 --- a/Jint/Native/JsArrayBuffer.cs +++ b/Jint/Native/JsArrayBuffer.cs @@ -15,6 +15,7 @@ public class JsArrayBuffer : ObjectInstance internal byte[]? _arrayBufferData; internal readonly int? _arrayBufferMaxByteLength; + internal bool _isImmutable; internal readonly JsValue _arrayBufferDetachKey = Undefined; @@ -52,6 +53,11 @@ internal static byte[] CreateByteDataBlock(Realm realm, ulong byteLength) internal virtual bool IsSharedArrayBuffer => false; + /// + /// https://tc39.es/proposal-immutable-arraybuffer/#sec-isimmutablebuffer + /// + internal bool IsImmutableBuffer => _isImmutable; + /// /// https://tc39.es/ecma262/#sec-detacharraybuffer /// @@ -355,4 +361,15 @@ internal void AssertNotDetached() Throw.TypeError(_engine.Realm, "ArrayBuffer has been detached"); } } + + /// + /// https://tc39.es/proposal-immutable-arraybuffer/#sec-isimmutablebuffer + /// + internal void AssertNotImmutable() + { + if (IsImmutableBuffer) + { + Throw.TypeError(_engine.Realm, "Cannot modify an immutable ArrayBuffer"); + } + } } diff --git a/Jint/Native/JsTypedArray.cs b/Jint/Native/JsTypedArray.cs index 3671f475f2..3be0551385 100644 --- a/Jint/Native/JsTypedArray.cs +++ b/Jint/Native/JsTypedArray.cs @@ -369,6 +369,10 @@ internal void DoIntegerIndexedElementSet(int index, TypedArrayValue numValue) var elementType = _arrayElementType; var elementSize = elementType.GetElementSize(); var indexedPosition = index * elementSize + offset; + + // https://tc39.es/proposal-immutable-arraybuffer/#sec-integerindexedelementset + _viewedArrayBuffer.AssertNotImmutable(); + _viewedArrayBuffer.SetValueInBuffer(indexedPosition, elementType, numValue, true, ArrayBufferOrder.Unordered); } diff --git a/README.md b/README.md index 6c4a36269d..2936f6c0d8 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ and many more. - ✔ `Error.isError` - ✔ Explicit Resource Management (`using` and `await using`) +- ✔ Immutable Arraybuffers - ✔ Iterator Sequencing - ✔ `Math.sumPrecise` - ✔ `ShadowRealm`