/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "vm/TypedArrayObject-inl.h" #include "vm/TypedArrayObject.h" #include "mozilla/FloatingPoint.h" #include "mozilla/IntegerTypeTraits.h" #include "mozilla/PodOperations.h" #include "mozilla/TextUtils.h" #include #include #include #include #include #include #if !defined(XP_WIN) && !defined(__wasi__) # include #endif #include #include "jsnum.h" #include "jstypes.h" #include "builtin/Array.h" #include "builtin/DataViewObject.h" #include "gc/Barrier.h" #include "gc/MaybeRooted.h" #include "jit/InlinableNatives.h" #include "js/Conversions.h" #include "js/experimental/TypedData.h" // JS_GetArrayBufferViewType, JS_GetTypedArray{Length,ByteOffset,ByteLength}, JS_IsTypedArrayObject #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/PropertySpec.h" #include "js/ScalarType.h" // JS::Scalar::Type #include "js/UniquePtr.h" #include "js/Wrapper.h" #include "util/DifferentialTesting.h" #include "util/Text.h" #include "util/WindowsWrapper.h" #include "vm/ArrayBufferObject.h" #include "vm/FunctionFlags.h" // js::FunctionFlags #include "vm/GlobalObject.h" #include "vm/JSContext.h" #include "vm/JSObject.h" #include "vm/PIC.h" #include "vm/SelfHosting.h" #include "vm/SharedMem.h" #include "vm/Uint8Clamped.h" #include "vm/WrapperObject.h" #include "gc/Nursery-inl.h" #include "vm/ArrayBufferObject-inl.h" #include "vm/Compartment-inl.h" #include "vm/GeckoProfiler-inl.h" #include "vm/NativeObject-inl.h" using namespace js; using JS::CanonicalizeNaN; using JS::ToInt32; using JS::ToUint32; using mozilla::IsAsciiDigit; /* * TypedArrayObject * * The non-templated base class for the specific typed implementations. * This class holds all the member variables that are used by * the subclasses. */ bool TypedArrayObject::convertForSideEffect(JSContext* cx, HandleValue v) const { switch (type()) { case Scalar::BigInt64: case Scalar::BigUint64: { return ToBigInt(cx, v) != nullptr; } case Scalar::Int8: case Scalar::Uint8: case Scalar::Int16: case Scalar::Uint16: case Scalar::Int32: case Scalar::Uint32: case Scalar::Float32: case Scalar::Float64: case Scalar::Uint8Clamped: { double ignore; return ToNumber(cx, v, &ignore); } case Scalar::MaxTypedArrayViewType: case Scalar::Int64: case Scalar::Simd128: MOZ_CRASH("Unsupported TypedArray type"); } MOZ_ASSERT_UNREACHABLE("Invalid scalar type"); return false; } /* static */ bool TypedArrayObject::is(HandleValue v) { return v.isObject() && v.toObject().is(); } /* static */ bool TypedArrayObject::ensureHasBuffer(JSContext* cx, Handle tarray) { if (tarray->hasBuffer()) { return true; } size_t byteLength = tarray->byteLength(); AutoRealm ar(cx, tarray); Rooted buffer( cx, ArrayBufferObject::createZeroed(cx, tarray->byteLength())); if (!buffer) { return false; } // Attaching the first view to an array buffer is infallible. MOZ_ALWAYS_TRUE(buffer->addView(cx, tarray)); // tarray is not shared, because if it were it would have a buffer. memcpy(buffer->dataPointer(), tarray->dataPointerUnshared(), byteLength); // If the object is in the nursery, the buffer will be freed by the next // nursery GC. Free the data slot pointer if the object has no inline data. size_t nbytes = RoundUp(byteLength, sizeof(Value)); Nursery& nursery = cx->nursery(); if (tarray->isTenured() && !tarray->hasInlineElements() && !nursery.isInside(tarray->elements())) { js_free(tarray->elements()); RemoveCellMemory(tarray, nbytes, MemoryUse::TypedArrayElements); } tarray->setFixedSlot(TypedArrayObject::DATA_SLOT, PrivateValue(buffer->dataPointer())); tarray->setFixedSlot(TypedArrayObject::BUFFER_SLOT, ObjectValue(*buffer)); return true; } #ifdef DEBUG void TypedArrayObject::assertZeroLengthArrayData() const { if (length() == 0 && !hasBuffer()) { uint8_t* end = fixedData(TypedArrayObject::FIXED_DATA_START); MOZ_ASSERT(end[0] == ZeroLengthArrayData); } } #endif void TypedArrayObject::finalize(JS::GCContext* gcx, JSObject* obj) { MOZ_ASSERT(!IsInsideNursery(obj)); TypedArrayObject* curObj = &obj->as(); // Template objects or discarded objects (which didn't have enough room // for inner elements) don't have anything to free. if (!curObj->elementsRaw()) { return; } curObj->assertZeroLengthArrayData(); // Typed arrays with a buffer object do not need to be free'd if (curObj->hasBuffer()) { return; } // Free the data slot pointer if it does not point into the old JSObject. if (!curObj->hasInlineElements()) { size_t nbytes = RoundUp(curObj->byteLength(), sizeof(Value)); gcx->free_(obj, curObj->elements(), nbytes, MemoryUse::TypedArrayElements); } } /* static */ size_t TypedArrayObject::objectMoved(JSObject* obj, JSObject* old) { TypedArrayObject* newObj = &obj->as(); const TypedArrayObject* oldObj = &old->as(); MOZ_ASSERT(newObj->elementsRaw() == oldObj->elementsRaw()); MOZ_ASSERT(obj->isTenured()); // Typed arrays with a buffer object do not need an update. if (oldObj->hasBuffer()) { return 0; } if (!IsInsideNursery(old)) { // Update the data slot pointer if it points to the old JSObject. if (oldObj->hasInlineElements()) { newObj->setInlineElements(); } return 0; } void* buf = oldObj->elements(); // Discarded objects (which didn't have enough room for inner elements) don't // have any data to move. if (!buf) { return 0; } Nursery& nursery = obj->runtimeFromMainThread()->gc.nursery(); if (!nursery.isInside(buf)) { nursery.removeMallocedBufferDuringMinorGC(buf); size_t nbytes = RoundUp(newObj->byteLength(), sizeof(Value)); AddCellMemory(newObj, nbytes, MemoryUse::TypedArrayElements); return 0; } // Determine if we can use inline data for the target array. If this is // possible, the nursery will have picked an allocation size that is large // enough. size_t nbytes = oldObj->byteLength(); MOZ_ASSERT(nbytes <= Nursery::MaxNurseryBufferSize); constexpr size_t headerSize = dataOffset() + sizeof(HeapSlot); // See AllocKindForLazyBuffer. gc::AllocKind newAllocKind = obj->asTenured().getAllocKind(); MOZ_ASSERT_IF(nbytes == 0, headerSize + sizeof(uint8_t) <= GetGCKindBytes(newAllocKind)); if (headerSize + nbytes <= GetGCKindBytes(newAllocKind)) { MOZ_ASSERT(oldObj->hasInlineElements()); #ifdef DEBUG if (nbytes == 0) { uint8_t* output = newObj->fixedData(TypedArrayObject::FIXED_DATA_START); output[0] = ZeroLengthArrayData; } #endif newObj->setInlineElements(); } else { MOZ_ASSERT(!oldObj->hasInlineElements()); AutoEnterOOMUnsafeRegion oomUnsafe; nbytes = RoundUp(nbytes, sizeof(Value)); void* data = newObj->zone()->pod_arena_malloc( js::ArrayBufferContentsArena, nbytes); if (!data) { oomUnsafe.crash( "Failed to allocate typed array elements while tenuring."); } MOZ_ASSERT(!nursery.isInside(data)); newObj->setReservedSlot(DATA_SLOT, PrivateValue(data)); AddCellMemory(newObj, nbytes, MemoryUse::TypedArrayElements); } mozilla::PodCopy(newObj->elements(), oldObj->elements(), nbytes); // Set a forwarding pointer for the element buffers in case they were // preserved on the stack by Ion. nursery.setForwardingPointerWhileTenuring( oldObj->elements(), newObj->elements(), /* direct = */ nbytes >= sizeof(uintptr_t)); return newObj->hasInlineElements() ? 0 : nbytes; } bool TypedArrayObject::hasInlineElements() const { return elements() == this->fixedData(TypedArrayObject::FIXED_DATA_START) && byteLength() <= TypedArrayObject::INLINE_BUFFER_LIMIT; } void TypedArrayObject::setInlineElements() { char* dataSlot = reinterpret_cast(this) + dataOffset(); *reinterpret_cast(dataSlot) = this->fixedData(TypedArrayObject::FIXED_DATA_START); } /* Helper clamped uint8_t type */ uint32_t js::ClampDoubleToUint8(const double x) { // Not < so that NaN coerces to 0 if (!(x >= 0)) { return 0; } if (x > 255) { return 255; } double toTruncate = x + 0.5; uint8_t y = uint8_t(toTruncate); /* * now val is rounded to nearest, ties rounded up. We want * rounded to nearest ties to even, so check whether we had a * tie. */ if (y == toTruncate) { /* * It was a tie (since adding 0.5 gave us the exact integer * we want). Since we rounded up, we either already have an * even number or we have an odd number but the number we * want is one less. So just unconditionally masking out the * ones bit should do the trick to get us the value we * want. */ return y & ~1; } return y; } namespace { static TypedArrayObject* NewTypedArrayObject(JSContext* cx, const JSClass* clasp, HandleObject proto, gc::AllocKind allocKind, gc::Heap heap) { MOZ_ASSERT(proto); MOZ_ASSERT(CanChangeToBackgroundAllocKind(allocKind, clasp)); allocKind = ForegroundToBackgroundAllocKind(allocKind); // Typed arrays can store data inline so we only use fixed slots to cover the // reserved slots, ignoring the AllocKind. MOZ_ASSERT(ClassCanHaveFixedData(clasp)); constexpr size_t nfixed = TypedArrayObject::RESERVED_SLOTS; static_assert(nfixed <= NativeObject::MAX_FIXED_SLOTS); static_assert(nfixed == TypedArrayObject::FIXED_DATA_START); Rooted shape( cx, SharedShape::getInitialShape(cx, clasp, cx->realm(), AsTaggedProto(proto), nfixed, ObjectFlags())); if (!shape) { return nullptr; } NativeObject* obj = NativeObject::create(cx, allocKind, heap, shape); if (!obj) { return nullptr; } return &obj->as(); } template class TypedArrayObjectTemplate : public TypedArrayObject { friend class TypedArrayObject; public: static constexpr Scalar::Type ArrayTypeID() { return TypeIDOfType::id; } static constexpr JSProtoKey protoKey() { return TypeIDOfType::protoKey; } static constexpr bool ArrayTypeIsUnsigned() { return TypeIsUnsigned(); } static constexpr bool ArrayTypeIsFloatingPoint() { return TypeIsFloatingPoint(); } static constexpr size_t BYTES_PER_ELEMENT = sizeof(NativeType); static JSObject* createPrototype(JSContext* cx, JSProtoKey key) { Handle global = cx->global(); RootedObject typedArrayProto( cx, GlobalObject::getOrCreateTypedArrayPrototype(cx, global)); if (!typedArrayProto) { return nullptr; } const JSClass* clasp = TypedArrayObject::protoClassForType(ArrayTypeID()); return GlobalObject::createBlankPrototypeInheriting(cx, clasp, typedArrayProto); } static JSObject* createConstructor(JSContext* cx, JSProtoKey key) { Handle global = cx->global(); RootedFunction ctorProto( cx, GlobalObject::getOrCreateTypedArrayConstructor(cx, global)); if (!ctorProto) { return nullptr; } JSFunction* fun = NewFunctionWithProto( cx, class_constructor, 3, FunctionFlags::NATIVE_CTOR, nullptr, ClassName(key, cx), ctorProto, gc::AllocKind::FUNCTION, TenuredObject); if (fun) { fun->setJitInfo(&jit::JitInfo_TypedArrayConstructor); } return fun; } static inline const JSClass* instanceClass() { return TypedArrayObject::classForType(ArrayTypeID()); } static bool is(HandleValue v) { return v.isObject() && v.toObject().hasClass(instanceClass()); } static bool convertValue(JSContext* cx, HandleValue v, NativeType* result); static TypedArrayObject* newBuiltinClassInstance(JSContext* cx, gc::AllocKind allocKind, gc::Heap heap) { RootedObject proto(cx, GlobalObject::getOrCreatePrototype(cx, protoKey())); if (!proto) { return nullptr; } return NewTypedArrayObject(cx, instanceClass(), proto, allocKind, heap); } static TypedArrayObject* makeProtoInstance(JSContext* cx, HandleObject proto, gc::AllocKind allocKind) { MOZ_ASSERT(proto); return NewTypedArrayObject(cx, instanceClass(), proto, allocKind, gc::Heap::Default); } static TypedArrayObject* makeInstance( JSContext* cx, Handle buffer, size_t byteOffset, size_t len, HandleObject proto, gc::Heap heap = gc::Heap::Default) { MOZ_ASSERT(len <= MaxByteLength / BYTES_PER_ELEMENT); gc::AllocKind allocKind = buffer ? gc::GetGCObjectKind(instanceClass()) : AllocKindForLazyBuffer(len * BYTES_PER_ELEMENT); AutoSetNewObjectMetadata metadata(cx); Rooted obj(cx); if (proto) { obj = makeProtoInstance(cx, proto, allocKind); } else { obj = newBuiltinClassInstance(cx, allocKind, heap); } if (!obj || !obj->init(cx, buffer, byteOffset, len, BYTES_PER_ELEMENT)) { return nullptr; } return obj; } static TypedArrayObject* makeTemplateObject(JSContext* cx, int32_t len) { MOZ_ASSERT(len >= 0); size_t nbytes; MOZ_ALWAYS_TRUE(CalculateAllocSize(len, &nbytes)); bool fitsInline = nbytes <= INLINE_BUFFER_LIMIT; gc::AllocKind allocKind = !fitsInline ? gc::GetGCObjectKind(instanceClass()) : AllocKindForLazyBuffer(nbytes); MOZ_ASSERT(allocKind >= gc::GetGCObjectKind(instanceClass())); AutoSetNewObjectMetadata metadata(cx); Rooted tarray( cx, newBuiltinClassInstance(cx, allocKind, gc::Heap::Tenured)); if (!tarray) { return nullptr; } initTypedArraySlots(tarray, len); // Template objects don't need memory for their elements, since there // won't be any elements to store. MOZ_ASSERT(tarray->getReservedSlot(DATA_SLOT).isUndefined()); return tarray; } static void initTypedArraySlots(TypedArrayObject* tarray, int32_t len) { MOZ_ASSERT(len >= 0); tarray->initFixedSlot(TypedArrayObject::BUFFER_SLOT, NullValue()); tarray->initFixedSlot(TypedArrayObject::LENGTH_SLOT, PrivateValue(len)); tarray->initFixedSlot(TypedArrayObject::BYTEOFFSET_SLOT, PrivateValue(size_t(0))); #ifdef DEBUG if (len == 0) { uint8_t* output = tarray->fixedData(TypedArrayObject::FIXED_DATA_START); output[0] = TypedArrayObject::ZeroLengthArrayData; } #endif } static void initTypedArrayData(TypedArrayObject* tarray, void* buf, size_t nbytes, gc::AllocKind allocKind) { if (buf) { InitReservedSlot(tarray, TypedArrayObject::DATA_SLOT, buf, nbytes, MemoryUse::TypedArrayElements); } else { #ifdef DEBUG constexpr size_t dataOffset = ArrayBufferViewObject::dataOffset(); constexpr size_t offset = dataOffset + sizeof(HeapSlot); MOZ_ASSERT(offset + nbytes <= GetGCKindBytes(allocKind)); #endif void* data = tarray->fixedData(FIXED_DATA_START); tarray->initReservedSlot(DATA_SLOT, PrivateValue(data)); memset(data, 0, nbytes); } } static TypedArrayObject* makeTypedArrayWithTemplate( JSContext* cx, TypedArrayObject* templateObj, int32_t len) { if (len < 0 || size_t(len) > MaxByteLength / BYTES_PER_ELEMENT) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); return nullptr; } size_t nbytes = size_t(len) * BYTES_PER_ELEMENT; MOZ_ASSERT(nbytes <= MaxByteLength); bool fitsInline = nbytes <= INLINE_BUFFER_LIMIT; AutoSetNewObjectMetadata metadata(cx); gc::AllocKind allocKind = !fitsInline ? gc::GetGCObjectKind(instanceClass()) : AllocKindForLazyBuffer(nbytes); MOZ_ASSERT(templateObj->getClass() == instanceClass()); RootedObject proto(cx, templateObj->staticPrototype()); TypedArrayObject* obj = makeProtoInstance(cx, proto, allocKind); if (!obj) { return nullptr; } initTypedArraySlots(obj, len); void* buf = nullptr; if (!fitsInline) { MOZ_ASSERT(len > 0); nbytes = RoundUp(nbytes, sizeof(Value)); buf = cx->nursery().allocateZeroedBuffer(obj, nbytes, js::ArrayBufferContentsArena); if (!buf) { ReportOutOfMemory(cx); return nullptr; } } initTypedArrayData(obj, buf, nbytes, allocKind); return obj; } static TypedArrayObject* makeTypedArrayWithTemplate( JSContext* cx, TypedArrayObject* templateObj, HandleObject array) { MOZ_ASSERT(!IsWrapper(array)); MOZ_ASSERT(!array->is()); return fromArray(cx, array); } static TypedArrayObject* makeTypedArrayWithTemplate( JSContext* cx, TypedArrayObject* templateObj, HandleObject arrayBuffer, HandleValue byteOffsetValue, HandleValue lengthValue) { MOZ_ASSERT(!IsWrapper(arrayBuffer)); MOZ_ASSERT(arrayBuffer->is()); uint64_t byteOffset, length; if (!byteOffsetAndLength(cx, byteOffsetValue, lengthValue, &byteOffset, &length)) { return nullptr; } return fromBufferSameCompartment( cx, arrayBuffer.as(), byteOffset, length, nullptr); } // ES2023 draft rev cf86f1cdc28e809170733d74ea64fd0f3dd79f78 // 23.2.5.1 TypedArray ( ...args ) static bool class_constructor(JSContext* cx, unsigned argc, Value* vp) { AutoJSConstructorProfilerEntry pseudoFrame(cx, "[TypedArray]"); CallArgs args = CallArgsFromVp(argc, vp); // Step 1. if (!ThrowIfNotConstructing(cx, args, "typed array")) { return false; } // Steps 2-6. JSObject* obj = create(cx, args); if (!obj) { return false; } args.rval().setObject(*obj); return true; } private: static JSObject* create(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(args.isConstructing()); // Steps 5 and 6.c. if (args.length() == 0 || !args[0].isObject()) { // Step 6.c.ii. uint64_t len; if (!ToIndex(cx, args.get(0), JSMSG_BAD_ARRAY_LENGTH, &len)) { return nullptr; } // Steps 5.a and 6.c.iii. RootedObject proto(cx); if (!GetPrototypeFromBuiltinConstructor(cx, args, protoKey(), &proto)) { return nullptr; } return fromLength(cx, len, proto); } RootedObject dataObj(cx, &args[0].toObject()); // Step 6.b.i. // 23.2.5.1.1 AllocateTypedArray, step 1. RootedObject proto(cx); if (!GetPrototypeFromBuiltinConstructor(cx, args, protoKey(), &proto)) { return nullptr; } // Steps 6.b.ii and 6.b.iv. if (!UncheckedUnwrap(dataObj)->is()) { return fromArray(cx, dataObj, proto); } // Steps 6.b.iii.1-2. // 23.2.5.1.3 InitializeTypedArrayFromArrayBuffer, steps 2 and 4. uint64_t byteOffset, length; if (!byteOffsetAndLength(cx, args.get(1), args.get(2), &byteOffset, &length)) { return nullptr; } // Step 6.b.iii.3. if (dataObj->is()) { HandleArrayBufferObjectMaybeShared buffer = dataObj.as(); return fromBufferSameCompartment(cx, buffer, byteOffset, length, proto); } return fromBufferWrapped(cx, dataObj, byteOffset, length, proto); } // ES2023 draft rev cf86f1cdc28e809170733d74ea64fd0f3dd79f78 // 23.2.5.1.3 InitializeTypedArrayFromArrayBuffer ( O, buffer, byteOffset, // length ) Steps 2 and 4. static bool byteOffsetAndLength(JSContext* cx, HandleValue byteOffsetValue, HandleValue lengthValue, uint64_t* byteOffset, uint64_t* length) { // Step 2. *byteOffset = 0; if (!byteOffsetValue.isUndefined()) { if (!ToIndex(cx, byteOffsetValue, byteOffset)) { return false; } // Step 7. if (*byteOffset % BYTES_PER_ELEMENT != 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_CONSTRUCT_OFFSET_BOUNDS, Scalar::name(ArrayTypeID()), Scalar::byteSizeString(ArrayTypeID())); return false; } } // Step 4. *length = UINT64_MAX; if (!lengthValue.isUndefined()) { if (!ToIndex(cx, lengthValue, length)) { return false; } } return true; } // ES2023 draft rev cf86f1cdc28e809170733d74ea64fd0f3dd79f78 // 23.2.5.1.3 InitializeTypedArrayFromArrayBuffer ( O, buffer, byteOffset, // length ) Steps 5-8. static bool computeAndCheckLength( JSContext* cx, HandleArrayBufferObjectMaybeShared bufferMaybeUnwrapped, uint64_t byteOffset, uint64_t lengthIndex, size_t* length) { MOZ_ASSERT(byteOffset % BYTES_PER_ELEMENT == 0); MOZ_ASSERT(byteOffset < uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)); MOZ_ASSERT_IF(lengthIndex != UINT64_MAX, lengthIndex < uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)); // Step 5. if (bufferMaybeUnwrapped->isDetached()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } // Step 6. size_t bufferByteLength = bufferMaybeUnwrapped->byteLength(); size_t len; if (lengthIndex == UINT64_MAX) { // Steps 7.a and 7.c. if (bufferByteLength % BYTES_PER_ELEMENT != 0) { // The given byte array doesn't map exactly to // |BYTES_PER_ELEMENT * N| JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_CONSTRUCT_OFFSET_MISALIGNED, Scalar::name(ArrayTypeID()), Scalar::byteSizeString(ArrayTypeID())); return false; } if (byteOffset > bufferByteLength) { // |byteOffset| is invalid. JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_CONSTRUCT_OFFSET_LENGTH_BOUNDS, Scalar::name(ArrayTypeID())); return false; } // Step 7.b. size_t newByteLength = bufferByteLength - size_t(byteOffset); len = newByteLength / BYTES_PER_ELEMENT; } else { // Step 8.a. uint64_t newByteLength = lengthIndex * BYTES_PER_ELEMENT; // Step 8.b. if (byteOffset + newByteLength > bufferByteLength) { // |byteOffset + newByteLength| is too big for the arraybuffer JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_CONSTRUCT_ARRAY_LENGTH_BOUNDS, Scalar::name(ArrayTypeID())); return false; } len = size_t(lengthIndex); } if (len > MaxByteLength / BYTES_PER_ELEMENT) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_CONSTRUCT_TOO_LARGE, Scalar::name(ArrayTypeID())); return false; } MOZ_ASSERT(len < SIZE_MAX); *length = len; return true; } // ES2023 draft rev cf86f1cdc28e809170733d74ea64fd0f3dd79f78 // 23.2.5.1.3 InitializeTypedArrayFromArrayBuffer ( O, buffer, byteOffset, // length ) Steps 5-13. static TypedArrayObject* fromBufferSameCompartment( JSContext* cx, HandleArrayBufferObjectMaybeShared buffer, uint64_t byteOffset, uint64_t lengthIndex, HandleObject proto) { // Steps 5-8. size_t length = 0; if (!computeAndCheckLength(cx, buffer, byteOffset, lengthIndex, &length)) { return nullptr; } // Steps 9-13. return makeInstance(cx, buffer, byteOffset, length, proto); } // Create a TypedArray object in another compartment. // // ES6 supports creating a TypedArray in global A (using global A's // TypedArray constructor) backed by an ArrayBuffer created in global B. // // Our TypedArrayObject implementation doesn't support a TypedArray in // compartment A backed by an ArrayBuffer in compartment B. So in this // case, we create the TypedArray in B (!) and return a cross-compartment // wrapper. // // Extra twist: the spec says the new TypedArray's [[Prototype]] must be // A's TypedArray.prototype. So even though we're creating the TypedArray // in B, its [[Prototype]] must be (a cross-compartment wrapper for) the // TypedArray.prototype in A. static JSObject* fromBufferWrapped(JSContext* cx, HandleObject bufobj, uint64_t byteOffset, uint64_t lengthIndex, HandleObject proto) { JSObject* unwrapped = CheckedUnwrapStatic(bufobj); if (!unwrapped) { ReportAccessDenied(cx); return nullptr; } if (!unwrapped->is()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_BAD_ARGS); return nullptr; } RootedArrayBufferObjectMaybeShared unwrappedBuffer(cx); unwrappedBuffer = &unwrapped->as(); size_t length = 0; if (!computeAndCheckLength(cx, unwrappedBuffer, byteOffset, lengthIndex, &length)) { return nullptr; } // Make sure to get the [[Prototype]] for the created typed array from // this compartment. RootedObject protoRoot(cx, proto); if (!protoRoot) { protoRoot = GlobalObject::getOrCreatePrototype(cx, protoKey()); if (!protoRoot) { return nullptr; } } RootedObject typedArray(cx); { JSAutoRealm ar(cx, unwrappedBuffer); RootedObject wrappedProto(cx, protoRoot); if (!cx->compartment()->wrap(cx, &wrappedProto)) { return nullptr; } typedArray = makeInstance(cx, unwrappedBuffer, byteOffset, length, wrappedProto); if (!typedArray) { return nullptr; } } if (!cx->compartment()->wrap(cx, &typedArray)) { return nullptr; } return typedArray; } public: static JSObject* fromBuffer(JSContext* cx, HandleObject bufobj, size_t byteOffset, int64_t lengthInt) { if (byteOffset % BYTES_PER_ELEMENT != 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_CONSTRUCT_OFFSET_BOUNDS, Scalar::name(ArrayTypeID()), Scalar::byteSizeString(ArrayTypeID())); return nullptr; // invalid byteOffset } uint64_t lengthIndex = lengthInt >= 0 ? uint64_t(lengthInt) : UINT64_MAX; if (bufobj->is()) { HandleArrayBufferObjectMaybeShared buffer = bufobj.as(); return fromBufferSameCompartment(cx, buffer, byteOffset, lengthIndex, nullptr); } return fromBufferWrapped(cx, bufobj, byteOffset, lengthIndex, nullptr); } static bool maybeCreateArrayBuffer(JSContext* cx, uint64_t count, MutableHandle buffer) { if (count > MaxByteLength / BYTES_PER_ELEMENT) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); return false; } size_t byteLength = count * BYTES_PER_ELEMENT; MOZ_ASSERT(byteLength <= MaxByteLength); static_assert(INLINE_BUFFER_LIMIT % BYTES_PER_ELEMENT == 0, "ArrayBuffer inline storage shouldn't waste any space"); if (byteLength <= INLINE_BUFFER_LIMIT) { // The array's data can be inline, and the buffer created lazily. return true; } ArrayBufferObject* buf = ArrayBufferObject::createZeroed(cx, byteLength); if (!buf) { return false; } buffer.set(buf); return true; } // ES2023 draft rev cf86f1cdc28e809170733d74ea64fd0f3dd79f78 // 23.2.5.1.1 AllocateTypedArray ( constructorName, newTarget, defaultProto [ // , length ] ) static TypedArrayObject* fromLength(JSContext* cx, uint64_t nelements, HandleObject proto = nullptr, gc::Heap heap = gc::Heap::Default) { Rooted buffer(cx); if (!maybeCreateArrayBuffer(cx, nelements, &buffer)) { return nullptr; } return makeInstance(cx, buffer, 0, nelements, proto, heap); } static TypedArrayObject* fromArray(JSContext* cx, HandleObject other, HandleObject proto = nullptr); static TypedArrayObject* fromTypedArray(JSContext* cx, HandleObject other, bool isWrapped, HandleObject proto); static TypedArrayObject* fromObject(JSContext* cx, HandleObject other, HandleObject proto); static const NativeType getIndex(TypedArrayObject* tarray, size_t index) { MOZ_ASSERT(index < tarray->length()); return jit::AtomicOperations::loadSafeWhenRacy( tarray->dataPointerEither().cast() + index); } static void setIndex(TypedArrayObject& tarray, size_t index, NativeType val) { MOZ_ASSERT(index < tarray.length()); jit::AtomicOperations::storeSafeWhenRacy( tarray.dataPointerEither().cast() + index, val); } static bool getElement(JSContext* cx, TypedArrayObject* tarray, size_t index, MutableHandleValue val); static bool getElementPure(TypedArrayObject* tarray, size_t index, Value* vp); static bool setElement(JSContext* cx, Handle obj, uint64_t index, HandleValue v, ObjectOpResult& result); }; template bool TypedArrayObjectTemplate::convertValue(JSContext* cx, HandleValue v, NativeType* result) { double d; if (!ToNumber(cx, v, &d)) { return false; } if (js::SupportDifferentialTesting()) { // See the comment in ElementSpecific::doubleToNative. d = JS::CanonicalizeNaN(d); } // Assign based on characteristics of the destination type if constexpr (ArrayTypeIsFloatingPoint()) { *result = NativeType(d); } else if constexpr (ArrayTypeIsUnsigned()) { static_assert(sizeof(NativeType) <= 4); uint32_t n = ToUint32(d); *result = NativeType(n); } else if constexpr (ArrayTypeID() == Scalar::Uint8Clamped) { // The uint8_clamped type has a special rounding converter // for doubles. *result = NativeType(d); } else { static_assert(sizeof(NativeType) <= 4); int32_t n = ToInt32(d); *result = NativeType(n); } return true; } template <> bool TypedArrayObjectTemplate::convertValue(JSContext* cx, HandleValue v, int64_t* result) { JS_TRY_VAR_OR_RETURN_FALSE(cx, *result, ToBigInt64(cx, v)); return true; } template <> bool TypedArrayObjectTemplate::convertValue(JSContext* cx, HandleValue v, uint64_t* result) { JS_TRY_VAR_OR_RETURN_FALSE(cx, *result, ToBigUint64(cx, v)); return true; } // https://tc39.github.io/proposal-bigint/#sec-integerindexedelementset // 9.4.5.11 IntegerIndexedElementSet ( O, index, value ) template /* static */ bool TypedArrayObjectTemplate::setElement( JSContext* cx, Handle obj, uint64_t index, HandleValue v, ObjectOpResult& result) { MOZ_ASSERT(!obj->hasDetachedBuffer()); MOZ_ASSERT(index < obj->length()); // Step 1 is enforced by the caller. // Steps 2-3. NativeType nativeValue; if (!convertValue(cx, v, &nativeValue)) { return false; } // Step 4. if (index < obj->length()) { MOZ_ASSERT(!obj->hasDetachedBuffer(), "detaching an array buffer sets the length to zero"); TypedArrayObjectTemplate::setIndex(*obj, index, nativeValue); } // Step 5. return result.succeed(); } #define CREATE_TYPE_FOR_TYPED_ARRAY(_, T, N) \ typedef TypedArrayObjectTemplate N##Array; JS_FOR_EACH_TYPED_ARRAY(CREATE_TYPE_FOR_TYPED_ARRAY) #undef CREATE_TYPE_FOR_TYPED_ARRAY } /* anonymous namespace */ TypedArrayObject* js::NewTypedArrayWithTemplateAndLength( JSContext* cx, HandleObject templateObj, int32_t len) { MOZ_ASSERT(templateObj->is()); TypedArrayObject* tobj = &templateObj->as(); switch (tobj->type()) { #define CREATE_TYPED_ARRAY(_, T, N) \ case Scalar::N: \ return TypedArrayObjectTemplate::makeTypedArrayWithTemplate(cx, tobj, \ len); JS_FOR_EACH_TYPED_ARRAY(CREATE_TYPED_ARRAY) #undef CREATE_TYPED_ARRAY default: MOZ_CRASH("Unsupported TypedArray type"); } } TypedArrayObject* js::NewTypedArrayWithTemplateAndArray( JSContext* cx, HandleObject templateObj, HandleObject array) { MOZ_ASSERT(templateObj->is()); TypedArrayObject* tobj = &templateObj->as(); switch (tobj->type()) { #define CREATE_TYPED_ARRAY(_, T, N) \ case Scalar::N: \ return TypedArrayObjectTemplate::makeTypedArrayWithTemplate(cx, tobj, \ array); JS_FOR_EACH_TYPED_ARRAY(CREATE_TYPED_ARRAY) #undef CREATE_TYPED_ARRAY default: MOZ_CRASH("Unsupported TypedArray type"); } } TypedArrayObject* js::NewTypedArrayWithTemplateAndBuffer( JSContext* cx, HandleObject templateObj, HandleObject arrayBuffer, HandleValue byteOffset, HandleValue length) { MOZ_ASSERT(templateObj->is()); TypedArrayObject* tobj = &templateObj->as(); switch (tobj->type()) { #define CREATE_TYPED_ARRAY(_, T, N) \ case Scalar::N: \ return TypedArrayObjectTemplate::makeTypedArrayWithTemplate( \ cx, tobj, arrayBuffer, byteOffset, length); JS_FOR_EACH_TYPED_ARRAY(CREATE_TYPED_ARRAY) #undef CREATE_TYPED_ARRAY default: MOZ_CRASH("Unsupported TypedArray type"); } } TypedArrayObject* js::NewUint8ArrayWithLength(JSContext* cx, int32_t len, gc::Heap heap) { return TypedArrayObjectTemplate::fromLength(cx, len, nullptr, heap); } template /* static */ TypedArrayObject* TypedArrayObjectTemplate::fromArray( JSContext* cx, HandleObject other, HandleObject proto /* = nullptr */) { // Allow nullptr proto for FriendAPI methods, which don't care about // subclassing. if (other->is()) { return fromTypedArray(cx, other, /* wrapped= */ false, proto); } if (other->is() && UncheckedUnwrap(other)->is()) { return fromTypedArray(cx, other, /* wrapped= */ true, proto); } return fromObject(cx, other, proto); } // ES2023 draft rev cf86f1cdc28e809170733d74ea64fd0f3dd79f78 // 23.2.5.1 TypedArray ( ...args ) // 23.2.5.1.2 InitializeTypedArrayFromTypedArray ( O, srcArray ) template /* static */ TypedArrayObject* TypedArrayObjectTemplate::fromTypedArray( JSContext* cx, HandleObject other, bool isWrapped, HandleObject proto) { MOZ_ASSERT_IF(!isWrapped, other->is()); MOZ_ASSERT_IF(isWrapped, other->is() && UncheckedUnwrap(other)->is()); Rooted srcArray(cx); if (!isWrapped) { srcArray = &other->as(); } else { srcArray = other->maybeUnwrapAs(); if (!srcArray) { ReportAccessDenied(cx); return nullptr; } } // InitializeTypedArrayFromTypedArray, step 1. (Skipped) // InitializeTypedArrayFromTypedArray, step 2. if (srcArray->hasDetachedBuffer()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return nullptr; } // InitializeTypedArrayFromTypedArray, steps 3-7. (Skipped) // InitializeTypedArrayFromTypedArray, step 8. size_t elementLength = srcArray->length(); // InitializeTypedArrayFromTypedArray, step 9. (Skipped) // InitializeTypedArrayFromTypedArray, step 10.a. (Partial) // InitializeTypedArrayFromTypedArray, step 11.a. Rooted buffer(cx); if (!maybeCreateArrayBuffer(cx, elementLength, &buffer)) { return nullptr; } // InitializeTypedArrayFromTypedArray, step 11.b. if (Scalar::isBigIntType(ArrayTypeID()) != Scalar::isBigIntType(srcArray->type())) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_NOT_COMPATIBLE, srcArray->getClass()->name, TypedArrayObject::classes[ArrayTypeID()].name); return nullptr; } // Step 6.b.i. // InitializeTypedArrayFromTypedArray, steps 12-15. Rooted obj( cx, makeInstance(cx, buffer, 0, elementLength, proto)); if (!obj) { return nullptr; } MOZ_RELEASE_ASSERT(!srcArray->hasDetachedBuffer()); // InitializeTypedArrayFromTypedArray, steps 10.a. (Remaining parts) // InitializeTypedArrayFromTypedArray, steps 11.c-f. MOZ_ASSERT(!obj->isSharedMemory()); if (srcArray->isSharedMemory()) { if (!ElementSpecific::setFromTypedArray(obj, srcArray, 0)) { return nullptr; } } else { if (!ElementSpecific::setFromTypedArray(obj, srcArray, 0)) { return nullptr; } } // Step 6.b.v. return obj; } static MOZ_ALWAYS_INLINE bool IsOptimizableInit(JSContext* cx, HandleObject iterable, bool* optimized) { MOZ_ASSERT(!*optimized); if (!IsPackedArray(iterable)) { return true; } ForOfPIC::Chain* stubChain = ForOfPIC::getOrCreate(cx); if (!stubChain) { return false; } return stubChain->tryOptimizeArray(cx, iterable.as(), optimized); } // ES2023 draft rev cf86f1cdc28e809170733d74ea64fd0f3dd79f78 // 23.2.5.1 TypedArray ( ...args ) // 23.2.5.1.4 InitializeTypedArrayFromList ( O, values ) // 23.2.5.1.5 InitializeTypedArrayFromArrayLike ( O, arrayLike ) template /* static */ TypedArrayObject* TypedArrayObjectTemplate::fromObject( JSContext* cx, HandleObject other, HandleObject proto) { // Steps 1-4 and 6.a (Already performed in caller). // Steps 6.b.i (Allocation deferred until later). // Steps 6.b.ii-iii. (Not applicable) // Step 6.b.iv. bool optimized = false; if (!IsOptimizableInit(cx, other, &optimized)) { return nullptr; } // Fast path when iterable is a packed array using the default iterator. if (optimized) { // Steps 6.b.iv.2-3. (We don't need to call IterableToList for the fast // path). Handle array = other.as(); // InitializeTypedArrayFromList, step 1. size_t len = array->getDenseInitializedLength(); // InitializeTypedArrayFromList, step 2. Rooted buffer(cx); if (!maybeCreateArrayBuffer(cx, len, &buffer)) { return nullptr; } // Steps 6.b.i. Rooted obj(cx, makeInstance(cx, buffer, 0, len, proto)); if (!obj) { return nullptr; } // InitializeTypedArrayFromList, steps 3-4. MOZ_ASSERT(!obj->isSharedMemory()); if (!ElementSpecific::initFromIterablePackedArray(cx, obj, array)) { return nullptr; } // InitializeTypedArrayFromList, step 5. (The assertion isn't applicable for // the fast path). // Step 6.b.v. return obj; } // Step 6.b.iv.1 (Assertion; implicit in our implementation). // Step 6.b.iv.2. RootedValue callee(cx); RootedId iteratorId(cx, PropertyKey::Symbol(cx->wellKnownSymbols().iterator)); if (!GetProperty(cx, other, other, iteratorId, &callee)) { return nullptr; } // Steps 6.b.iv.3-4. RootedObject arrayLike(cx); if (!callee.isNullOrUndefined()) { // Throw if other[Symbol.iterator] isn't callable. if (!callee.isObject() || !callee.toObject().isCallable()) { RootedValue otherVal(cx, ObjectValue(*other)); UniqueChars bytes = DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, otherVal, nullptr); if (!bytes) { return nullptr; } JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_NOT_ITERABLE, bytes.get()); return nullptr; } FixedInvokeArgs<2> args2(cx); args2[0].setObject(*other); args2[1].set(callee); // Step 6.b.iv.3.a. RootedValue rval(cx); if (!CallSelfHostedFunction(cx, cx->names().IterableToList, UndefinedHandleValue, args2, &rval)) { return nullptr; } // Step 6.b.iv.3.b (Implemented below). arrayLike = &rval.toObject(); } else { // Step 4.a is an assertion: object is not an Iterator. Testing this is // literally the very last thing we did, so we don't assert here. // Step 4.b (Implemented below). arrayLike = other; } // We implement InitializeTypedArrayFromList in terms of // InitializeTypedArrayFromArrayLike. // InitializeTypedArrayFromArrayLike, step 1. uint64_t len; if (!GetLengthProperty(cx, arrayLike, &len)) { return nullptr; } // InitializeTypedArrayFromArrayLike, step 2. Rooted buffer(cx); if (!maybeCreateArrayBuffer(cx, len, &buffer)) { return nullptr; } MOZ_ASSERT(len <= MaxByteLength / BYTES_PER_ELEMENT); // Steps 6.b.i. Rooted obj(cx, makeInstance(cx, buffer, 0, len, proto)); if (!obj) { return nullptr; } // InitializeTypedArrayFromArrayLike, steps 3-4. MOZ_ASSERT(!obj->isSharedMemory()); if (!ElementSpecific::setFromNonTypedArray(cx, obj, arrayLike, len)) { return nullptr; } // Step 6.b.v. return obj; } bool TypedArrayConstructor(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_CALL_OR_CONSTRUCT, args.isConstructing() ? "construct" : "call"); return false; } template static bool GetTemplateObjectForNative(JSContext* cx, const JS::HandleValueArray args, MutableHandleObject res) { if (args.length() == 0) { return true; } HandleValue arg = args[0]; if (arg.isInt32()) { uint32_t len = 0; if (arg.toInt32() >= 0) { len = arg.toInt32(); } size_t nbytes; if (!js::CalculateAllocSize(len, &nbytes) || nbytes > TypedArrayObject::MaxByteLength) { return true; } res.set(TypedArrayObjectTemplate::makeTemplateObject(cx, len)); return !!res; } // We don't support wrappers, because of the complicated interaction between // wrapped ArrayBuffers and TypedArrays, see |fromBufferWrapped()|. if (arg.isObject() && !IsWrapper(&arg.toObject())) { // We don't use the template's length in the object case, so we can create // the template typed array with an initial length of zero. uint32_t len = 0; res.set(TypedArrayObjectTemplate::makeTemplateObject(cx, len)); return !!res; } return true; } /* static */ bool TypedArrayObject::GetTemplateObjectForNative( JSContext* cx, Native native, const JS::HandleValueArray args, MutableHandleObject res) { MOZ_ASSERT(!res); #define CHECK_TYPED_ARRAY_CONSTRUCTOR(_, T, N) \ if (native == &TypedArrayObjectTemplate::class_constructor) { \ return ::GetTemplateObjectForNative(cx, args, res); \ } JS_FOR_EACH_TYPED_ARRAY(CHECK_TYPED_ARRAY_CONSTRUCTOR) #undef CHECK_TYPED_ARRAY_CONSTRUCTOR return true; } static bool LengthGetterImpl(JSContext* cx, const CallArgs& args) { auto* tarr = &args.thisv().toObject().as(); args.rval().set(tarr->lengthValue()); return true; } static bool TypedArray_lengthGetter(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } static bool ByteOffsetGetterImpl(JSContext* cx, const CallArgs& args) { auto* tarr = &args.thisv().toObject().as(); args.rval().set(tarr->byteOffsetValue()); return true; } static bool TypedArray_byteOffsetGetter(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } static bool ByteLengthGetterImpl(JSContext* cx, const CallArgs& args) { auto* tarr = &args.thisv().toObject().as(); args.rval().set(tarr->byteLengthValue()); return true; } static bool TypedArray_byteLengthGetter(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } static bool BufferGetterImpl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(TypedArrayObject::is(args.thisv())); Rooted tarray( cx, &args.thisv().toObject().as()); if (!TypedArrayObject::ensureHasBuffer(cx, tarray)) { return false; } args.rval().set(tarray->bufferValue()); return true; } static bool TypedArray_bufferGetter(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } // ES2019 draft rev fc9ecdcd74294d0ca3146d4b274e2a8e79565dc3 // 22.2.3.32 get %TypedArray%.prototype [ @@toStringTag ] static bool TypedArray_toStringTagGetter(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Steps 1-2. if (!args.thisv().isObject()) { args.rval().setUndefined(); return true; } JSObject* obj = CheckedUnwrapStatic(&args.thisv().toObject()); if (!obj) { ReportAccessDenied(cx); return false; } // Step 3. if (!obj->is()) { args.rval().setUndefined(); return true; } // Steps 4-6. JSProtoKey protoKey = StandardProtoKeyOrNull(obj); MOZ_ASSERT(protoKey); args.rval().setString(ClassName(protoKey, cx)); return true; } /* static */ const JSPropertySpec TypedArrayObject::protoAccessors[] = { JS_PSG("length", TypedArray_lengthGetter, 0), JS_PSG("buffer", TypedArray_bufferGetter, 0), JS_PSG("byteLength", TypedArray_byteLengthGetter, 0), JS_PSG("byteOffset", TypedArray_byteOffsetGetter, 0), JS_SYM_GET(toStringTag, TypedArray_toStringTagGetter, 0), JS_PS_END}; template static inline bool SetFromTypedArray(Handle target, Handle source, size_t offset) { // WARNING: |source| may be an unwrapped typed array from a different // compartment. Proceed with caution! if (target->isSharedMemory() || source->isSharedMemory()) { return ElementSpecific::setFromTypedArray(target, source, offset); } return ElementSpecific::setFromTypedArray(target, source, offset); } template static inline bool SetFromNonTypedArray(JSContext* cx, Handle target, HandleObject source, size_t len, size_t offset) { MOZ_ASSERT(!source->is(), "use SetFromTypedArray"); if (target->isSharedMemory()) { return ElementSpecific::setFromNonTypedArray( cx, target, source, len, offset); } return ElementSpecific::setFromNonTypedArray( cx, target, source, len, offset); } // ES2023 draft rev 22cc56ab08fcab92a865978c0aa5c6f1d8ce250f // 23.2.3.24.1 SetTypedArrayFromTypedArray ( target, targetOffset, source ) static bool SetTypedArrayFromTypedArray(JSContext* cx, Handle target, double targetOffset, Handle source) { // WARNING: |source| may be an unwrapped typed array from a different // compartment. Proceed with caution! MOZ_ASSERT(targetOffset >= 0); // Steps 1-2. (Performed in caller.) MOZ_ASSERT(!target->hasDetachedBuffer()); // Steps 4-5. if (source->hasDetachedBuffer()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } // Step 3 (Reordered). size_t targetLength = target->length(); // Steps 13-14 (Split into two checks to provide better error messages). if (targetOffset > targetLength) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX); return false; } // Step 14 (Cont'd). size_t offset = size_t(targetOffset); if (source->length() > targetLength - offset) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SOURCE_ARRAY_TOO_LONG); return false; } // Step 15. if (Scalar::isBigIntType(target->type()) != Scalar::isBigIntType(source->type())) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_NOT_COMPATIBLE, source->getClass()->name, target->getClass()->name); return false; } // Steps 6-12, 16-24. switch (target->type()) { #define SET_FROM_TYPED_ARRAY(_, T, N) \ case Scalar::N: \ if (!SetFromTypedArray(target, source, offset)) return false; \ break; JS_FOR_EACH_TYPED_ARRAY(SET_FROM_TYPED_ARRAY) #undef SET_FROM_TYPED_ARRAY default: MOZ_CRASH("Unsupported TypedArray type"); } return true; } // ES2023 draft rev 22cc56ab08fcab92a865978c0aa5c6f1d8ce250f // 23.2.3.24.1 SetTypedArrayFromArrayLike ( target, targetOffset, source ) static bool SetTypedArrayFromArrayLike(JSContext* cx, Handle target, double targetOffset, HandleObject src) { MOZ_ASSERT(targetOffset >= 0); // Steps 1-2. (Performed in caller.) MOZ_ASSERT(!target->hasDetachedBuffer()); // Step 3. // We can't reorder this step because side-effects in step 5 can detach the // underlying array buffer from the typed array. size_t targetLength = target->length(); // Step 4. (Performed in caller.) // Step 5. uint64_t srcLength; if (!GetLengthProperty(cx, src, &srcLength)) { return false; } // Steps 6-7 (Split into two checks to provide better error messages). if (targetOffset > targetLength) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX); return false; } // Step 7 (Cont'd). size_t offset = size_t(targetOffset); if (srcLength > targetLength - offset) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SOURCE_ARRAY_TOO_LONG); return false; } MOZ_ASSERT(srcLength <= targetLength); // Steps 8-9. if (srcLength > 0) { switch (target->type()) { #define SET_FROM_NON_TYPED_ARRAY(_, T, N) \ case Scalar::N: \ if (!SetFromNonTypedArray(cx, target, src, srcLength, offset)) \ return false; \ break; JS_FOR_EACH_TYPED_ARRAY(SET_FROM_NON_TYPED_ARRAY) #undef SET_FROM_NON_TYPED_ARRAY default: MOZ_CRASH("Unsupported TypedArray type"); } } // Step 10. return true; } // ES2023 draft rev 22cc56ab08fcab92a865978c0aa5c6f1d8ce250f // 23.2.3.24 %TypedArray%.prototype.set ( source [ , offset ] ) // 23.2.3.24.1 SetTypedArrayFromTypedArray ( target, targetOffset, source ) // 23.2.3.24.2 SetTypedArrayFromArrayLike ( target, targetOffset, source ) /* static */ bool TypedArrayObject::set_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(TypedArrayObject::is(args.thisv())); // Steps 1-3 (Validation performed as part of CallNonGenericMethod). Rooted target( cx, &args.thisv().toObject().as()); // Steps 4-5. double targetOffset = 0; if (args.length() > 1) { // Step 4. if (!ToInteger(cx, args[1], &targetOffset)) { return false; } // Step 5. if (targetOffset < 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX); return false; } } // 23.2.3.24.1, steps 1-2. // 23.2.3.24.2, steps 1-2. if (target->hasDetachedBuffer()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } // 23.2.3.24.2, step 4. (23.2.3.24.1 only applies if args[0] is a typed // array, so it doesn't make a difference there to apply ToObject here.) RootedObject src(cx, ToObject(cx, args.get(0))); if (!src) { return false; } Rooted srcTypedArray(cx); { JSObject* obj = CheckedUnwrapStatic(src); if (!obj) { ReportAccessDenied(cx); return false; } if (obj->is()) { srcTypedArray = &obj->as(); } } // Steps 6-7. if (srcTypedArray) { if (!SetTypedArrayFromTypedArray(cx, target, targetOffset, srcTypedArray)) { return false; } } else { if (!SetTypedArrayFromArrayLike(cx, target, targetOffset, src)) { return false; } } // Step 8. args.rval().setUndefined(); return true; } /* static */ bool TypedArrayObject::set(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod( cx, args); } // ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 // 22.2.3.5 %TypedArray%.prototype.copyWithin ( target, start [ , end ] ) /* static */ bool TypedArrayObject::copyWithin_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(TypedArrayObject::is(args.thisv())); // Steps 1-2. Rooted tarray( cx, &args.thisv().toObject().as()); if (tarray->hasDetachedBuffer()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } // Step 3. size_t len = tarray->length(); // Step 4. double relativeTarget; if (!ToInteger(cx, args.get(0), &relativeTarget)) { return false; } // Step 5. uint64_t to; if (relativeTarget < 0) { to = std::max(len + relativeTarget, 0.0); } else { to = std::min(relativeTarget, double(len)); } // Step 6. double relativeStart; if (!ToInteger(cx, args.get(1), &relativeStart)) { return false; } // Step 7. uint64_t from; if (relativeStart < 0) { from = std::max(len + relativeStart, 0.0); } else { from = std::min(relativeStart, double(len)); } // Step 8. double relativeEnd; if (!args.hasDefined(2)) { relativeEnd = len; } else { if (!ToInteger(cx, args[2], &relativeEnd)) { return false; } } // Step 9. uint64_t final_; if (relativeEnd < 0) { final_ = std::max(len + relativeEnd, 0.0); } else { final_ = std::min(relativeEnd, double(len)); } // Step 10. MOZ_ASSERT(to <= len); uint64_t count; if (from <= final_) { count = std::min(final_ - from, len - to); } else { count = 0; } // Step 11. // // Note that getting or setting a typed array element must throw if the // underlying buffer is detached, so the code below checks for detachment. // This happens *only* if a get/set occurs, i.e. when |count > 0|. // // Also note that this copies elements effectively by memmove, *not* in // step 11's specified order. This is unobservable, even when the underlying // buffer is a SharedArrayBuffer instance, because the access is unordered and // therefore is allowed to have data races. if (count == 0) { args.rval().setObject(*tarray); return true; } if (tarray->hasDetachedBuffer()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } // Don't multiply by |tarray->bytesPerElement()| in case the compiler can't // strength-reduce multiplication by 1/2/4/8 into the equivalent shift. const size_t ElementShift = TypedArrayShift(tarray->type()); MOZ_ASSERT((SIZE_MAX >> ElementShift) > to); size_t byteDest = to << ElementShift; MOZ_ASSERT((SIZE_MAX >> ElementShift) > from); size_t byteSrc = from << ElementShift; MOZ_ASSERT((SIZE_MAX >> ElementShift) >= count); size_t byteSize = count << ElementShift; #ifdef DEBUG { size_t viewByteLength = tarray->byteLength(); MOZ_ASSERT(byteSize <= viewByteLength); MOZ_ASSERT(byteDest < viewByteLength); MOZ_ASSERT(byteSrc < viewByteLength); MOZ_ASSERT(byteDest <= viewByteLength - byteSize); MOZ_ASSERT(byteSrc <= viewByteLength - byteSize); } #endif SharedMem data = tarray->dataPointerEither().cast(); if (tarray->isSharedMemory()) { jit::AtomicOperations::memmoveSafeWhenRacy(data + byteDest, data + byteSrc, byteSize); } else { memmove(data.unwrapUnshared() + byteDest, data.unwrapUnshared() + byteSrc, byteSize); } args.rval().setObject(*tarray); return true; } /* static */ bool TypedArrayObject::copyWithin(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "[TypedArray].prototype", "copyWithin"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } /* static */ const JSFunctionSpec TypedArrayObject::protoFunctions[] = { JS_SELF_HOSTED_FN("subarray", "TypedArraySubarray", 2, 0), JS_FN("set", TypedArrayObject::set, 1, 0), JS_FN("copyWithin", TypedArrayObject::copyWithin, 2, 0), JS_SELF_HOSTED_FN("every", "TypedArrayEvery", 1, 0), JS_SELF_HOSTED_FN("fill", "TypedArrayFill", 3, 0), JS_SELF_HOSTED_FN("filter", "TypedArrayFilter", 1, 0), JS_SELF_HOSTED_FN("find", "TypedArrayFind", 1, 0), JS_SELF_HOSTED_FN("findIndex", "TypedArrayFindIndex", 1, 0), JS_SELF_HOSTED_FN("findLast", "TypedArrayFindLast", 1, 0), JS_SELF_HOSTED_FN("findLastIndex", "TypedArrayFindLastIndex", 1, 0), JS_SELF_HOSTED_FN("forEach", "TypedArrayForEach", 1, 0), JS_SELF_HOSTED_FN("indexOf", "TypedArrayIndexOf", 2, 0), JS_SELF_HOSTED_FN("join", "TypedArrayJoin", 1, 0), JS_SELF_HOSTED_FN("lastIndexOf", "TypedArrayLastIndexOf", 1, 0), JS_SELF_HOSTED_FN("map", "TypedArrayMap", 1, 0), JS_SELF_HOSTED_FN("reduce", "TypedArrayReduce", 1, 0), JS_SELF_HOSTED_FN("reduceRight", "TypedArrayReduceRight", 1, 0), JS_SELF_HOSTED_FN("reverse", "TypedArrayReverse", 0, 0), JS_SELF_HOSTED_FN("slice", "TypedArraySlice", 2, 0), JS_SELF_HOSTED_FN("some", "TypedArraySome", 1, 0), JS_SELF_HOSTED_FN("sort", "TypedArraySort", 1, 0), JS_SELF_HOSTED_FN("entries", "TypedArrayEntries", 0, 0), JS_SELF_HOSTED_FN("keys", "TypedArrayKeys", 0, 0), JS_SELF_HOSTED_FN("values", "$TypedArrayValues", 0, 0), JS_SELF_HOSTED_SYM_FN(iterator, "$TypedArrayValues", 0, 0), JS_SELF_HOSTED_FN("includes", "TypedArrayIncludes", 2, 0), JS_SELF_HOSTED_FN("toString", "ArrayToString", 0, 0), JS_SELF_HOSTED_FN("toLocaleString", "TypedArrayToLocaleString", 2, 0), JS_SELF_HOSTED_FN("at", "TypedArrayAt", 1, 0), JS_SELF_HOSTED_FN("toReversed", "TypedArrayToReversed", 0, 0), JS_SELF_HOSTED_FN("toSorted", "TypedArrayToSorted", 1, 0), JS_SELF_HOSTED_FN("with", "TypedArrayWith", 2, 0), JS_FS_END, }; /* static */ const JSFunctionSpec TypedArrayObject::staticFunctions[] = { JS_SELF_HOSTED_FN("from", "TypedArrayStaticFrom", 3, 0), JS_SELF_HOSTED_FN("of", "TypedArrayStaticOf", 0, 0), JS_FS_END, }; /* static */ const JSPropertySpec TypedArrayObject::staticProperties[] = { JS_SELF_HOSTED_SYM_GET(species, "$TypedArraySpecies", 0), JS_PS_END, }; static JSObject* CreateSharedTypedArrayPrototype(JSContext* cx, JSProtoKey key) { return GlobalObject::createBlankPrototype( cx, cx->global(), &TypedArrayObject::sharedTypedArrayPrototypeClass); } static const ClassSpec TypedArrayObjectSharedTypedArrayPrototypeClassSpec = { GenericCreateConstructor, CreateSharedTypedArrayPrototype, TypedArrayObject::staticFunctions, TypedArrayObject::staticProperties, TypedArrayObject::protoFunctions, TypedArrayObject::protoAccessors, nullptr, ClassSpec::DontDefineConstructor, }; /* static */ const JSClass TypedArrayObject::sharedTypedArrayPrototypeClass = { "TypedArrayPrototype", JSCLASS_HAS_CACHED_PROTO(JSProto_TypedArray), JS_NULL_CLASS_OPS, &TypedArrayObjectSharedTypedArrayPrototypeClassSpec, }; namespace { // This default implementation is only valid for integer types less // than 32-bits in size. template bool TypedArrayObjectTemplate::getElementPure( TypedArrayObject* tarray, size_t index, Value* vp) { static_assert(sizeof(NativeType) < 4, "this method must only handle NativeType values that are " "always exact int32_t values"); *vp = Int32Value(getIndex(tarray, index)); return true; } // We need to specialize for floats and other integer types. template <> bool TypedArrayObjectTemplate::getElementPure(TypedArrayObject* tarray, size_t index, Value* vp) { *vp = Int32Value(getIndex(tarray, index)); return true; } template <> bool TypedArrayObjectTemplate::getElementPure( TypedArrayObject* tarray, size_t index, Value* vp) { uint32_t val = getIndex(tarray, index); *vp = NumberValue(val); return true; } template <> bool TypedArrayObjectTemplate::getElementPure(TypedArrayObject* tarray, size_t index, Value* vp) { float val = getIndex(tarray, index); double dval = val; /* * Doubles in typed arrays could be typed-punned arrays of integers. This * could allow user code to break the engine-wide invariant that only * canonical nans are stored into jsvals, which means user code could * confuse the engine into interpreting a double-typed jsval as an * object-typed jsval. * * This could be removed for platforms/compilers known to convert a 32-bit * non-canonical nan to a 64-bit canonical nan. */ *vp = JS::CanonicalizedDoubleValue(dval); return true; } template <> bool TypedArrayObjectTemplate::getElementPure(TypedArrayObject* tarray, size_t index, Value* vp) { double val = getIndex(tarray, index); /* * Doubles in typed arrays could be typed-punned arrays of integers. This * could allow user code to break the engine-wide invariant that only * canonical nans are stored into jsvals, which means user code could * confuse the engine into interpreting a double-typed jsval as an * object-typed jsval. */ *vp = JS::CanonicalizedDoubleValue(val); return true; } template <> bool TypedArrayObjectTemplate::getElementPure(TypedArrayObject* tarray, size_t index, Value* vp) { return false; } template <> bool TypedArrayObjectTemplate::getElementPure( TypedArrayObject* tarray, size_t index, Value* vp) { return false; } } /* anonymous namespace */ namespace { template bool TypedArrayObjectTemplate::getElement(JSContext* cx, TypedArrayObject* tarray, size_t index, MutableHandleValue val) { MOZ_ALWAYS_TRUE(getElementPure(tarray, index, val.address())); return true; } template <> bool TypedArrayObjectTemplate::getElement(JSContext* cx, TypedArrayObject* tarray, size_t index, MutableHandleValue val) { int64_t n = getIndex(tarray, index); BigInt* res = BigInt::createFromInt64(cx, n); if (!res) { return false; } val.setBigInt(res); return true; } template <> bool TypedArrayObjectTemplate::getElement(JSContext* cx, TypedArrayObject* tarray, size_t index, MutableHandleValue val) { uint64_t n = getIndex(tarray, index); BigInt* res = BigInt::createFromUint64(cx, n); if (!res) { return false; } val.setBigInt(res); return true; } } /* anonymous namespace */ namespace js { template <> bool TypedArrayObject::getElement(JSContext* cx, size_t index, MutableHandleValue val) { switch (type()) { #define GET_ELEMENT(_, T, N) \ case Scalar::N: \ return N##Array::getElement(cx, this, index, val); JS_FOR_EACH_TYPED_ARRAY(GET_ELEMENT) #undef GET_ELEMENT case Scalar::MaxTypedArrayViewType: case Scalar::Int64: case Scalar::Simd128: break; } MOZ_CRASH("Unknown TypedArray type"); } template <> bool TypedArrayObject::getElement( JSContext* cx, size_t index, typename MaybeRooted::MutableHandleType vp) { return getElementPure(index, vp.address()); } } // namespace js bool TypedArrayObject::getElementPure(size_t index, Value* vp) { switch (type()) { #define GET_ELEMENT_PURE(_, T, N) \ case Scalar::N: \ return N##Array::getElementPure(this, index, vp); JS_FOR_EACH_TYPED_ARRAY(GET_ELEMENT_PURE) #undef GET_ELEMENT case Scalar::MaxTypedArrayViewType: case Scalar::Int64: case Scalar::Simd128: break; } MOZ_CRASH("Unknown TypedArray type"); } /* static */ bool TypedArrayObject::getElements(JSContext* cx, Handle tarray, Value* vp) { size_t length = tarray->length(); MOZ_ASSERT_IF(length > 0, !tarray->hasDetachedBuffer()); switch (tarray->type()) { #define GET_ELEMENTS(_, T, N) \ case Scalar::N: \ for (size_t i = 0; i < length; ++i, ++vp) { \ if (!N##Array::getElement(cx, tarray, i, \ MutableHandleValue::fromMarkedLocation(vp))) { \ return false; \ } \ } \ return true; JS_FOR_EACH_TYPED_ARRAY(GET_ELEMENTS) #undef GET_ELEMENTS case Scalar::MaxTypedArrayViewType: case Scalar::Int64: case Scalar::Simd128: break; } MOZ_CRASH("Unknown TypedArray type"); } /*** *** JS impl ***/ /* * TypedArrayObject boilerplate */ static const JSClassOps TypedArrayClassOps = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve TypedArrayObject::finalize, // finalize nullptr, // call nullptr, // construct ArrayBufferViewObject::trace, // trace }; static const ClassExtension TypedArrayClassExtension = { TypedArrayObject::objectMoved, // objectMovedOp }; static const JSPropertySpec static_prototype_properties[Scalar::MaxTypedArrayViewType][2] = { #define IMPL_TYPED_ARRAY_PROPERTIES(ExternalType, NativeType, Name) \ {JS_INT32_PS("BYTES_PER_ELEMENT", Name##Array::BYTES_PER_ELEMENT, \ JSPROP_READONLY | JSPROP_PERMANENT), \ JS_PS_END}, JS_FOR_EACH_TYPED_ARRAY(IMPL_TYPED_ARRAY_PROPERTIES) #undef IMPL_TYPED_ARRAY_PROPERTIES }; static const ClassSpec TypedArrayObjectClassSpecs[Scalar::MaxTypedArrayViewType] = { #define IMPL_TYPED_ARRAY_CLASS_SPEC(ExternalType, NativeType, Name) \ {Name##Array::createConstructor, \ Name##Array::createPrototype, \ nullptr, \ static_prototype_properties[Scalar::Type::Name], \ nullptr, \ static_prototype_properties[Scalar::Type::Name], \ nullptr, \ JSProto_TypedArray}, JS_FOR_EACH_TYPED_ARRAY(IMPL_TYPED_ARRAY_CLASS_SPEC) #undef IMPL_TYPED_ARRAY_CLASS_SPEC }; const JSClass TypedArrayObject::classes[Scalar::MaxTypedArrayViewType] = { #define IMPL_TYPED_ARRAY_CLASS(ExternalType, NativeType, Name) \ {#Name "Array", \ JSCLASS_HAS_RESERVED_SLOTS(TypedArrayObject::RESERVED_SLOTS) | \ JSCLASS_HAS_CACHED_PROTO(JSProto_##Name##Array) | \ JSCLASS_DELAY_METADATA_BUILDER | JSCLASS_SKIP_NURSERY_FINALIZE | \ JSCLASS_BACKGROUND_FINALIZE, \ &TypedArrayClassOps, &TypedArrayObjectClassSpecs[Scalar::Type::Name], \ &TypedArrayClassExtension}, JS_FOR_EACH_TYPED_ARRAY(IMPL_TYPED_ARRAY_CLASS) #undef IMPL_TYPED_ARRAY_CLASS }; // The various typed array prototypes are supposed to 1) be normal objects, // 2) stringify to "[object ]", and 3) (Gecko-specific) // be xrayable. The first and second requirements mandate (in the absence of // @@toStringTag) a custom class. The third requirement mandates that each // prototype's class have the relevant typed array's cached JSProtoKey in them. // Thus we need one class with cached prototype per kind of typed array, with a // delegated ClassSpec. // // Actually ({}).toString.call(Uint8Array.prototype) should throw, because // Uint8Array.prototype lacks the the typed array internal slots. (Same as // with %TypedArray%.prototype.) It's not clear this is desirable (see // above), but it's what we've always done, so keep doing it till we // implement @@toStringTag or ES6 changes. const JSClass TypedArrayObject::protoClasses[Scalar::MaxTypedArrayViewType] = { #define IMPL_TYPED_ARRAY_PROTO_CLASS(ExternalType, NativeType, Name) \ {#Name "Array.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_##Name##Array), \ JS_NULL_CLASS_OPS, &TypedArrayObjectClassSpecs[Scalar::Type::Name]}, JS_FOR_EACH_TYPED_ARRAY(IMPL_TYPED_ARRAY_PROTO_CLASS) #undef IMPL_TYPED_ARRAY_PROTO_CLASS }; /* static */ bool TypedArrayObject::isOriginalLengthGetter(Native native) { return native == TypedArray_lengthGetter; } /* static */ bool TypedArrayObject::isOriginalByteOffsetGetter(Native native) { return native == TypedArray_byteOffsetGetter; } /* static */ bool TypedArrayObject::isOriginalByteLengthGetter(Native native) { return native == TypedArray_byteLengthGetter; } bool js::IsTypedArrayConstructor(const JSObject* obj) { #define CHECK_TYPED_ARRAY_CONSTRUCTOR(_, T, N) \ if (IsNativeFunction(obj, N##Array::class_constructor)) { \ return true; \ } JS_FOR_EACH_TYPED_ARRAY(CHECK_TYPED_ARRAY_CONSTRUCTOR) #undef CHECK_TYPED_ARRAY_CONSTRUCTOR return false; } bool js::IsTypedArrayConstructor(HandleValue v, Scalar::Type type) { return IsNativeFunction(v, TypedArrayConstructorNative(type)); } JSNative js::TypedArrayConstructorNative(Scalar::Type type) { #define TYPED_ARRAY_CONSTRUCTOR_NATIVE(_, T, N) \ if (type == Scalar::N) { \ return N##Array::class_constructor; \ } JS_FOR_EACH_TYPED_ARRAY(TYPED_ARRAY_CONSTRUCTOR_NATIVE) #undef TYPED_ARRAY_CONSTRUCTOR_NATIVE MOZ_CRASH("unexpected typed array type"); } bool js::IsBufferSource(JSObject* object, SharedMem* dataPointer, size_t* byteLength) { if (object->is()) { TypedArrayObject& view = object->as(); *dataPointer = view.dataPointerEither().cast(); *byteLength = view.byteLength(); return true; } if (object->is()) { DataViewObject& view = object->as(); *dataPointer = view.dataPointerEither().cast(); *byteLength = view.byteLength(); return true; } if (object->is()) { ArrayBufferObject& buffer = object->as(); *dataPointer = buffer.dataPointerShared(); *byteLength = buffer.byteLength(); return true; } if (object->is()) { SharedArrayBufferObject& buffer = object->as(); *dataPointer = buffer.dataPointerShared(); *byteLength = buffer.byteLength(); return true; } return false; } template static inline bool StringIsInfinity(mozilla::Range s) { static constexpr std::string_view Infinity = "Infinity"; // Compilers optimize this to one |cmp| instruction on x64 resp. two for x86, // when the input is a Latin-1 string, because the string "Infinity" is // exactly eight characters long, so it can be represented as a single uint64 // value. return s.length() == Infinity.length() && EqualChars(s.begin().get(), Infinity.data(), Infinity.length()); } template static inline bool StringIsNaN(mozilla::Range s) { static constexpr std::string_view NaN = "NaN"; // "NaN" is not as nicely optimizable as "Infinity", but oh well. return s.length() == NaN.length() && EqualChars(s.begin().get(), NaN.data(), NaN.length()); } template static mozilla::Maybe StringToTypedArrayIndexSlow( mozilla::Range s) { const mozilla::RangedPtr start = s.begin(); const mozilla::RangedPtr end = s.end(); const CharT* actualEnd; double result = js_strtod(start.get(), end.get(), &actualEnd); // The complete string must have been parsed. if (actualEnd != end.get()) { return mozilla::Nothing(); } // Now convert it back to a string. ToCStringBuf cbuf; size_t cstrlen; const char* cstr = js::NumberToCString(&cbuf, result, &cstrlen); MOZ_ASSERT(cstr); // Both strings must be equal for a canonical numeric index string. if (s.length() != cstrlen || !EqualChars(start.get(), cstr, cstrlen)) { return mozilla::Nothing(); } // Directly perform IsInteger() check and encode negative and non-integer // indices as OOB. // See 9.4.5.2 [[HasProperty]], steps 3.b.iii and 3.b.v. // See 9.4.5.3 [[DefineOwnProperty]], steps 3.b.i and 3.b.iii. // See 9.4.5.8 IntegerIndexedElementGet, steps 5 and 8. // See 9.4.5.9 IntegerIndexedElementSet, steps 6 and 9. if (result < 0 || !IsInteger(result)) { return mozilla::Some(UINT64_MAX); } // Anything equals-or-larger than 2^53 is definitely OOB, encode it // accordingly so that the cast to uint64_t is well defined. if (result >= DOUBLE_INTEGRAL_PRECISION_LIMIT) { return mozilla::Some(UINT64_MAX); } // The string is an actual canonical numeric index. return mozilla::Some(result); } template mozilla::Maybe js::StringToTypedArrayIndex( mozilla::Range s) { mozilla::RangedPtr cp = s.begin(); const mozilla::RangedPtr end = s.end(); MOZ_ASSERT(cp < end, "caller must check for empty strings"); bool negative = false; if (*cp == '-') { negative = true; if (++cp == end) { return mozilla::Nothing(); } } if (!IsAsciiDigit(*cp)) { // Check for "NaN", "Infinity", or "-Infinity". if ((!negative && StringIsNaN({cp, end})) || StringIsInfinity({cp, end})) { return mozilla::Some(UINT64_MAX); } return mozilla::Nothing(); } uint32_t digit = AsciiDigitToNumber(*cp++); // Don't allow leading zeros. if (digit == 0 && cp != end) { // The string may be of the form "0.xyz". The exponent form isn't possible // when the string starts with "0". if (*cp == '.') { return StringToTypedArrayIndexSlow(s); } return mozilla::Nothing(); } uint64_t index = digit; for (; cp < end; cp++) { if (!IsAsciiDigit(*cp)) { // Take the slow path when the string has fractional parts or an exponent. if (*cp == '.' || *cp == 'e') { return StringToTypedArrayIndexSlow(s); } return mozilla::Nothing(); } digit = AsciiDigitToNumber(*cp); static_assert( uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT) < (UINT64_MAX - 10) / 10, "2^53 is way below UINT64_MAX, so |10 * index + digit| can't overflow"); index = 10 * index + digit; // Also take the slow path when the string is larger-or-equals 2^53. if (index >= uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)) { return StringToTypedArrayIndexSlow(s); } } if (negative) { return mozilla::Some(UINT64_MAX); } return mozilla::Some(index); } template mozilla::Maybe js::StringToTypedArrayIndex( mozilla::Range s); template mozilla::Maybe js::StringToTypedArrayIndex( mozilla::Range s); bool js::SetTypedArrayElement(JSContext* cx, Handle obj, uint64_t index, HandleValue v, ObjectOpResult& result) { TypedArrayObject* tobj = &obj->as(); switch (tobj->type()) { #define SET_TYPED_ARRAY_ELEMENT(_, T, N) \ case Scalar::N: \ return TypedArrayObjectTemplate::setElement(cx, obj, index, v, result); JS_FOR_EACH_TYPED_ARRAY(SET_TYPED_ARRAY_ELEMENT) #undef SET_TYPED_ARRAY_ELEMENT case Scalar::MaxTypedArrayViewType: case Scalar::Int64: case Scalar::Simd128: break; } MOZ_CRASH("Unsupported TypedArray type"); } // ES2021 draft rev b3f9b5089bcc3ddd8486379015cd11eb1427a5eb // 9.4.5.3 [[DefineOwnProperty]], step 3.b. bool js::DefineTypedArrayElement(JSContext* cx, Handle obj, uint64_t index, Handle desc, ObjectOpResult& result) { // These are all substeps of 3.b. // Step i. if (index >= obj->length()) { if (obj->hasDetachedBuffer()) { return result.fail(JSMSG_TYPED_ARRAY_DETACHED); } return result.fail(JSMSG_DEFINE_BAD_INDEX); } // Step ii. if (desc.isAccessorDescriptor()) { return result.fail(JSMSG_CANT_REDEFINE_PROP); } // Step iii. if (desc.hasConfigurable() && !desc.configurable()) { return result.fail(JSMSG_CANT_REDEFINE_PROP); } // Step iv. if (desc.hasEnumerable() && !desc.enumerable()) { return result.fail(JSMSG_CANT_REDEFINE_PROP); } // Step v. if (desc.hasWritable() && !desc.writable()) { return result.fail(JSMSG_CANT_REDEFINE_PROP); } // Step vi. if (desc.hasValue()) { switch (obj->type()) { #define DEFINE_TYPED_ARRAY_ELEMENT(_, T, N) \ case Scalar::N: \ return TypedArrayObjectTemplate::setElement(cx, obj, index, \ desc.value(), result); JS_FOR_EACH_TYPED_ARRAY(DEFINE_TYPED_ARRAY_ELEMENT) #undef DEFINE_TYPED_ARRAY_ELEMENT case Scalar::MaxTypedArrayViewType: case Scalar::Int64: case Scalar::Simd128: break; } MOZ_CRASH("Unsupported TypedArray type"); } // Step vii. return result.succeed(); } template static constexpr typename std::enable_if_t, U> UnsignedSortValue(U val) { return val; } template static constexpr typename std::enable_if_t && std::is_signed_v, U> UnsignedSortValue(U val) { // Flip sign bit. return val ^ static_cast(std::numeric_limits::min()); } template static constexpr typename std::enable_if_t, UnsignedT> UnsignedSortValue(UnsignedT val) { // Flip sign bit for positive numbers; flip all bits for negative numbers, // except negative NaNs. using FloatingPoint = mozilla::FloatingPoint; static_assert(std::is_same_v, "FloatingPoint::Bits matches the unsigned int representation"); // FF80'0000 is negative infinity, (FF80'0000, FFFF'FFFF] are all NaNs with // the sign-bit set (and the equivalent holds for double values). So any value // larger than negative infinity is a negative NaN. constexpr UnsignedT NegativeInfinity = FloatingPoint::kSignBit | FloatingPoint::kExponentBits; if (val > NegativeInfinity) { return val; } if (val & FloatingPoint::kSignBit) { return ~val; } return val ^ FloatingPoint::kSignBit; } template static typename std::enable_if_t || std::is_same_v> TypedArrayStdSort(SharedMem data, size_t length) { T* unwrapped = data.cast().unwrapUnshared(); std::sort(unwrapped, unwrapped + length); } template static typename std::enable_if_t> TypedArrayStdSort( SharedMem data, size_t length) { // Sort on the unsigned representation for performance reasons. using UnsignedT = typename mozilla::UnsignedStdintTypeForSize::Type; UnsignedT* unwrapped = data.cast().unwrapUnshared(); std::sort(unwrapped, unwrapped + length, [](UnsignedT x, UnsignedT y) { constexpr auto SortValue = UnsignedSortValue; return SortValue(x) < SortValue(y); }); } template static typename std::enable_if_t, bool> TypedArrayStdSort(JSContext* cx, TypedArrayObject* typedArray) { TypedArrayStdSort(typedArray->dataPointerEither(), typedArray->length()); return true; } template static typename std::enable_if_t, bool> TypedArrayStdSort(JSContext* cx, TypedArrayObject* typedArray) { // Always create a copy when sorting shared memory backed typed arrays to // ensure concurrent write accesses doesn't lead to UB when calling std::sort. size_t length = typedArray->length(); auto ptr = cx->make_pod_array(length); if (!ptr) { return false; } SharedMem unshared = SharedMem::unshared(ptr.get()); SharedMem data = typedArray->dataPointerShared().cast(); Ops::podCopy(unshared, data, length); TypedArrayStdSort(unshared.template cast(), length); Ops::podCopy(data, unshared, length); return true; } template static bool TypedArrayCountingSort(JSContext* cx, TypedArrayObject* typedArray) { static_assert(std::is_integral_v || std::is_same_v, "Counting sort expects integral array elements"); size_t length = typedArray->length(); // Determined by performance testing. if (length <= 64) { return TypedArrayStdSort(cx, typedArray); } // Map signed values onto the unsigned range when storing in buffer. using UnsignedT = typename mozilla::UnsignedStdintTypeForSize::Type; constexpr T min = std::numeric_limits::min(); constexpr size_t InlineStorage = sizeof(T) == 1 ? 256 : 0; Vector buffer(cx); if (!buffer.resize(size_t(std::numeric_limits::max()) + 1)) { return false; } SharedMem data = typedArray->dataPointerEither().cast(); // Populate the buffer. for (size_t i = 0; i < length; i++) { T val = Ops::load(data + i); buffer[UnsignedT(val - min)]++; } // Traverse the buffer in order and write back elements to array. UnsignedT val = UnsignedT(-1); // intentional overflow on first increment for (size_t i = 0; i < length;) { // Invariant: sum(buffer[val:]) == length-i size_t j; do { j = buffer[++val]; } while (j == 0); for (; j > 0; j--) { Ops::store(data + i++, T(val + min)); } } return true; } template static void SortByColumn(SharedMem data, size_t length, SharedMem aux, uint8_t col) { static_assert(std::is_unsigned_v, "SortByColumn sorts on unsigned values"); static_assert(std::is_same_v, "SortByColumn only works on unshared data"); // |counts| is used to compute the starting index position for each key. // Letting counts[0] always be 0, simplifies the transform step below. // Example: // // Computing frequency counts for the input [1 2 1] gives: // 0 1 2 3 ... (keys) // 0 0 2 1 (frequencies) // // Transforming frequencies to indexes gives: // 0 1 2 3 ... (keys) // 0 0 2 3 (indexes) constexpr size_t R = 256; // Initialize all entries to zero. size_t counts[R + 1] = {}; const auto ByteAtCol = [col](U x) { U y = UnsignedSortValue(x); return static_cast(y >> (col * 8)); }; // Compute frequency counts. for (size_t i = 0; i < length; i++) { U val = Ops::load(data + i); uint8_t b = ByteAtCol(val); counts[b + 1]++; } // Transform counts to indices. std::partial_sum(std::begin(counts), std::end(counts), std::begin(counts)); // Distribute for (size_t i = 0; i < length; i++) { U val = Ops::load(data + i); uint8_t b = ByteAtCol(val); size_t j = counts[b]++; MOZ_ASSERT(j < length, "index is in bounds when |data| can't be modified concurrently"); UnsharedOps::store(aux + j, val); } // Copy back Ops::podCopy(data, aux, length); } template static bool TypedArrayRadixSort(JSContext* cx, TypedArrayObject* typedArray) { size_t length = typedArray->length(); // Determined by performance testing. constexpr size_t StdSortMinCutoff = sizeof(T) == 2 ? 64 : 256; // Radix sort uses O(n) additional space, limit this space to 64 MB. constexpr size_t StdSortMaxCutoff = (64 * 1024 * 1024) / sizeof(T); if (length <= StdSortMinCutoff || length >= StdSortMaxCutoff) { return TypedArrayStdSort(cx, typedArray); } if constexpr (sizeof(T) == 2) { // Radix sort uses O(n) additional space, so when |n| reaches 2^16, switch // over to counting sort to limit the additional space needed to 2^16. constexpr size_t CountingSortMaxCutoff = 65536; if (length >= CountingSortMaxCutoff) { return TypedArrayCountingSort(cx, typedArray); } } using UnsignedT = typename mozilla::UnsignedStdintTypeForSize::Type; auto ptr = cx->make_zeroed_pod_array(length); if (!ptr) { return false; } SharedMem aux = SharedMem::unshared(ptr.get()); SharedMem data = typedArray->dataPointerEither().cast(); // Always create a copy when sorting shared memory backed typed arrays to // ensure concurrent write accesses don't lead to computing bad indices. SharedMem unshared; SharedMem shared; UniquePtr ptrUnshared; if constexpr (std::is_same_v) { ptrUnshared = cx->make_pod_array(length); if (!ptrUnshared) { return false; } unshared = SharedMem::unshared(ptrUnshared.get()); shared = data; Ops::podCopy(unshared, shared, length); data = unshared; } for (uint8_t col = 0; col < sizeof(UnsignedT); col++) { SortByColumn(data, length, aux, col); } if constexpr (std::is_same_v) { Ops::podCopy(shared, unshared, length); } return true; } using TypedArraySortFn = bool (*)(JSContext*, TypedArrayObject*); template static constexpr typename std::enable_if_t TypedArraySort() { return TypedArrayCountingSort; } template static constexpr typename std::enable_if_t TypedArraySort() { return TypedArrayRadixSort; } template static constexpr typename std::enable_if_t TypedArraySort() { return TypedArrayStdSort; } bool js::intrinsic_TypedArrayNativeSort(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 1); TypedArrayObject* typedArray = UnwrapAndDowncastValue(cx, args[0]); if (!typedArray) { return false; } MOZ_RELEASE_ASSERT(!typedArray->hasDetachedBuffer()); bool isShared = typedArray->isSharedMemory(); switch (typedArray->type()) { #define SORT(_, T, N) \ case Scalar::N: \ if (isShared) { \ if (!TypedArraySort()(cx, typedArray)) { \ return false; \ } \ } else { \ if (!TypedArraySort()(cx, typedArray)) { \ return false; \ } \ } \ break; JS_FOR_EACH_TYPED_ARRAY(SORT) #undef SORT default: MOZ_CRASH("Unsupported TypedArray type"); } args.rval().set(args[0]); return true; } /* JS Public API */ #define IMPL_TYPED_ARRAY_JSAPI_CONSTRUCTORS(ExternalType, NativeType, Name) \ JS_PUBLIC_API JSObject* JS_New##Name##Array(JSContext* cx, \ size_t nelements) { \ return TypedArrayObjectTemplate::fromLength(cx, nelements); \ } \ \ JS_PUBLIC_API JSObject* JS_New##Name##ArrayFromArray(JSContext* cx, \ HandleObject other) { \ return TypedArrayObjectTemplate::fromArray(cx, other); \ } \ \ JS_PUBLIC_API JSObject* JS_New##Name##ArrayWithBuffer( \ JSContext* cx, HandleObject arrayBuffer, size_t byteOffset, \ int64_t length) { \ return TypedArrayObjectTemplate::fromBuffer( \ cx, arrayBuffer, byteOffset, length); \ } \ \ JS_PUBLIC_API JSObject* js::Unwrap##Name##Array(JSObject* obj) { \ obj = obj->maybeUnwrapIf(); \ if (!obj) { \ return nullptr; \ } \ const JSClass* clasp = obj->getClass(); \ if (clasp != TypedArrayObjectTemplate::instanceClass()) { \ return nullptr; \ } \ return obj; \ } \ \ JS_PUBLIC_API ExternalType* JS_Get##Name##ArrayLengthAndData( \ JSObject* obj, size_t* length, bool* isSharedMemory, \ const JS::AutoRequireNoGC& nogc) { \ TypedArrayObject* tarr = obj->maybeUnwrapAs(); \ if (!tarr) { \ return nullptr; \ } \ return JS::TypedArray::fromObject(tarr) \ .getLengthAndData(length, isSharedMemory, nogc); \ } \ \ JS_PUBLIC_API ExternalType* JS_Get##Name##ArrayData( \ JSObject* obj, bool* isSharedMemory, const JS::AutoRequireNoGC& nogc) { \ size_t length; \ return JS_Get##Name##ArrayLengthAndData(obj, &length, isSharedMemory, \ nogc); \ } \ JS_PUBLIC_API JSObject* JS_GetObjectAs##Name##Array( \ JSObject* obj, size_t* length, bool* isShared, ExternalType** data) { \ obj = js::Unwrap##Name##Array(obj); \ if (!obj) { \ return nullptr; \ } \ TypedArrayObject* tarr = &obj->as(); \ *length = tarr->length(); \ *isShared = tarr->isSharedMemory(); \ *data = static_cast(tarr->dataPointerEither().unwrap( \ /*safe - caller sees isShared flag*/)); \ return obj; \ } JS_FOR_EACH_TYPED_ARRAY(IMPL_TYPED_ARRAY_JSAPI_CONSTRUCTORS) #undef IMPL_TYPED_ARRAY_JSAPI_CONSTRUCTORS JS_PUBLIC_API bool JS_IsTypedArrayObject(JSObject* obj) { return obj->canUnwrapAs(); } JS_PUBLIC_API size_t JS_GetTypedArrayLength(JSObject* obj) { TypedArrayObject* tarr = obj->maybeUnwrapAs(); if (!tarr) { return 0; } return tarr->length(); } JS_PUBLIC_API size_t JS_GetTypedArrayByteOffset(JSObject* obj) { TypedArrayObject* tarr = obj->maybeUnwrapAs(); if (!tarr) { return 0; } return tarr->byteOffset(); } JS_PUBLIC_API size_t JS_GetTypedArrayByteLength(JSObject* obj) { TypedArrayObject* tarr = obj->maybeUnwrapAs(); if (!tarr) { return 0; } return tarr->byteLength(); } JS_PUBLIC_API bool JS_GetTypedArraySharedness(JSObject* obj) { TypedArrayObject* tarr = obj->maybeUnwrapAs(); if (!tarr) { return false; } return tarr->isSharedMemory(); } JS_PUBLIC_API JS::Scalar::Type JS_GetArrayBufferViewType(JSObject* obj) { ArrayBufferViewObject* view = obj->maybeUnwrapAs(); if (!view) { return Scalar::MaxTypedArrayViewType; } if (view->is()) { return view->as().type(); } if (view->is()) { return Scalar::MaxTypedArrayViewType; } MOZ_CRASH("invalid ArrayBufferView type"); } JS_PUBLIC_API size_t JS_MaxMovableTypedArraySize() { return TypedArrayObject::INLINE_BUFFER_LIMIT; } namespace JS { const JSClass* const TypedArray_base::classes = TypedArrayObject::classes; #define INSTANTIATE(ExternalType, NativeType, Name) \ template class TypedArray; JS_FOR_EACH_TYPED_ARRAY(INSTANTIATE) #undef INSTANTIATE JS::ArrayBufferOrView JS::ArrayBufferOrView::unwrap(JSObject* maybeWrapped) { if (!maybeWrapped) { return JS::ArrayBufferOrView(nullptr); } auto* ab = maybeWrapped->maybeUnwrapIf(); if (ab) { return ArrayBufferOrView::fromObject(ab); } return ArrayBufferView::unwrap(maybeWrapped); } bool JS::ArrayBufferOrView::isDetached() const { MOZ_ASSERT(obj); if (obj->is()) { return obj->as().isDetached(); } else { return obj->as().hasDetachedBuffer(); } } JS::TypedArray_base JS::TypedArray_base::fromObject(JSObject* unwrapped) { if (unwrapped && unwrapped->is()) { return TypedArray_base(unwrapped); } return TypedArray_base(nullptr); } // Template getLengthAndData function for TypedArrays, implemented here because // it requires internal APIs. template typename TypedArray::DataType* TypedArray::getLengthAndData( size_t* length, bool* isSharedMemory, const AutoRequireNoGC&) { using ExternalType = TypedArray::DataType; if (!obj) { return nullptr; } TypedArrayObject* tarr = &obj->as(); MOZ_ASSERT(tarr); *length = tarr->length(); *isSharedMemory = tarr->isSharedMemory(); return static_cast( tarr->dataPointerEither().unwrap(/*safe - caller sees isShared*/)); }; // Force the method defined above to actually be instantianted in this // compilation unit and emitted into the object file, since otherwise a binary // could include the header file and emit an undefined symbol that would not be // satisfied by the linker. (This happens with opt gtest, at least. In a DEBUG // build, the header contains a call to this function so it will always be // emitted.) #define INSTANTIATE_GET_DATA(a, b, Name) \ template typename TypedArray::DataType* \ TypedArray::getLengthAndData( \ size_t* length, bool* isSharedMemory, const AutoRequireNoGC&); JS_FOR_EACH_TYPED_ARRAY(INSTANTIATE_GET_DATA) #undef INSTANTIATE_GET_DATA } /* namespace JS */