diff --git a/dom/base/StructuredCloneHolder.cpp b/dom/base/StructuredCloneHolder.cpp index a7afd310cb..7b7dbdf67d 100644 --- a/dom/base/StructuredCloneHolder.cpp +++ b/dom/base/StructuredCloneHolder.cpp @@ -138,6 +138,207 @@ StructuredCloneCallbacksError(JSContext* aCx, NS_WARNING("Failed to clone data."); } +struct SameThreadStreamTransferData +{ + SameThreadStreamTransferData(JSContext* aCx, JS::HandleObject aRecord) + : mRecord(aCx, aRecord) + {} + + ~SameThreadStreamTransferData() + { + mRecord.reset(); + } + + JS::PersistentRootedObject mRecord; +}; + +bool +CallStreamTransferHelper(JSContext* aCx, + const char* aHelperName, + JS::HandleValue aArgument, + JS::MutableHandleValue aResult) +{ + JS::Rooted global(aCx, JS::CurrentGlobalOrNull(aCx)); + if (!global) { + return false; + } + + // Resolve the stream extras lazily before looking up the internal helper. + JS::Rooted ignored(aCx); + if (!JS_GetProperty(aCx, global, "WritableStream", &ignored)) { + return false; + } + + JS::Rooted helper(aCx); + if (!JS_GetProperty(aCx, global, aHelperName, &helper)) { + return false; + } + + if (!helper.isObject() || !JS::IsCallable(&helper.toObject())) { + aResult.setUndefined(); + return true; + } + + JS::Rooted argument(aCx, aArgument); + if (!JS_WrapValue(aCx, &argument)) { + return false; + } + + return JS::Call(aCx, JS::UndefinedHandleValue, helper, + JS::HandleValueArray(argument), aResult); +} + +bool +TryWriteSameThreadStreamTransfer(JSContext* aCx, + JS::Handle aObj, + const char* aHelperName, + uint32_t aTransferTag, + uint32_t* aTag, + JS::TransferableOwnership* aOwnership, + void** aContent, + uint64_t* aExtraData, + bool* aHandled) +{ + *aHandled = false; + + JS::Rooted argument(aCx, JS::ObjectValue(*aObj)); + JS::Rooted transferRecord(aCx); + if (!CallStreamTransferHelper(aCx, aHelperName, argument, &transferRecord)) { + return false; + } + + if (transferRecord.isUndefined()) { + return true; + } + + if (!transferRecord.isObject()) { + return false; + } + + JS::Rooted record(aCx, &transferRecord.toObject()); + SameThreadStreamTransferData* data = + new SameThreadStreamTransferData(aCx, record); + + *aTag = aTransferTag; + *aOwnership = JS::SCTAG_TMO_CUSTOM; + *aContent = data; + *aExtraData = 0; + *aHandled = true; + return true; +} + +bool +TryWriteReadableStreamPortTransfer(JSContext* aCx, + JS::Handle aObj, + nsTArray& aPortIdentifiers, + uint32_t* aTag, + JS::TransferableOwnership* aOwnership, + void** aContent, + uint64_t* aExtraData, + bool* aHandled) +{ + *aHandled = false; + + JS::Rooted argument(aCx, JS::ObjectValue(*aObj)); + JS::Rooted transferPortValue(aCx); + if (!CallStreamTransferHelper(aCx, "__uxpTransferReadableStreamPort", + argument, &transferPortValue)) { + return false; + } + + if (transferPortValue.isUndefined()) { + return true; + } + + if (!transferPortValue.isObject()) { + return false; + } + + JS::Rooted transferPortObj(aCx, &transferPortValue.toObject()); + MessagePort* port = nullptr; + nsresult rv = UNWRAP_OBJECT(MessagePort, &transferPortObj, port); + if (NS_FAILED(rv) || !port) { + return false; + } + + *aExtraData = aPortIdentifiers.Length(); + MessagePortIdentifier* identifier = aPortIdentifiers.AppendElement(); + port->CloneAndDisentangle(*identifier); + + *aTag = SCTAG_DOM_TRANSFERRED_READABLESTREAM; + *aOwnership = JS::SCTAG_TMO_CUSTOM; + *aContent = nullptr; + *aHandled = true; + return true; +} + +bool +ReadSameThreadStreamTransfer(JSContext* aCx, + void* aContent, + const char* aHelperName, + JS::MutableHandleObject aReturnObject) +{ + MOZ_ASSERT(aContent); + SameThreadStreamTransferData* data = + static_cast(aContent); + + JS::Rooted record(aCx, JS::ObjectValue(*data->mRecord)); + JS::Rooted result(aCx); + if (!CallStreamTransferHelper(aCx, aHelperName, record, &result)) { + return false; + } + + if (!result.isObject()) { + return false; + } + + aReturnObject.set(&result.toObject()); + delete data; + return true; +} + +bool +ReadReadableStreamPortTransfer(JSContext* aCx, + nsISupports* aParent, + nsTArray& aPortIdentifiers, + uint64_t aExtraData, + JS::MutableHandleObject aReturnObject) +{ + if (aExtraData >= aPortIdentifiers.Length()) { + return false; + } + + nsCOMPtr global = do_QueryInterface(aParent); + + ErrorResult rv; + RefPtr port = + MessagePort::Create(global, aPortIdentifiers[aExtraData], rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return false; + } + + JS::Rooted portValue(aCx); + if (!GetOrCreateDOMReflector(aCx, port, &portValue)) { + JS_ClearPendingException(aCx); + return false; + } + + JS::Rooted result(aCx); + if (!CallStreamTransferHelper(aCx, + "__uxpReceiveReadableStreamTransferFromPort", + portValue, &result)) { + return false; + } + + if (!result.isObject()) { + return false; + } + + aReturnObject.set(&result.toObject()); + return true; +} + } // anonymous namespace const JSStructuredCloneCallbacks StructuredCloneHolder::sCallbacks = { @@ -1375,6 +1576,35 @@ StructuredCloneHolder::CustomReadTransferHandler(JSContext* aCx, return true; } + if (mStructuredCloneScope == StructuredCloneScope::SameProcessSameThread) { + if (aTag == SCTAG_DOM_TRANSFERRED_WRITABLESTREAM) { + return ReadSameThreadStreamTransfer(aCx, aContent, + "__uxpReceiveWritableStreamTransfer", + aReturnObject); + } + + if (aTag == SCTAG_DOM_TRANSFERRED_READABLESTREAM) { + return ReadSameThreadStreamTransfer(aCx, aContent, + "__uxpReceiveReadableStreamTransfer", + aReturnObject); + } + + if (aTag == SCTAG_DOM_TRANSFERRED_TRANSFORMSTREAM) { + return ReadSameThreadStreamTransfer(aCx, aContent, + "__uxpReceiveTransformStreamTransfer", + aReturnObject); + } + } + + if ((mStructuredCloneScope == StructuredCloneScope::SameProcessDifferentThread || + mStructuredCloneScope == StructuredCloneScope::DifferentProcess) && + aTag == SCTAG_DOM_TRANSFERRED_READABLESTREAM) { + MOZ_ASSERT(!aContent); + return ReadReadableStreamPortTransfer(aCx, mParent, mPortIdentifiers, + aExtraData, + aReturnObject); + } + return false; } @@ -1443,6 +1673,55 @@ StructuredCloneHolder::CustomWriteTransferHandler(JSContext* aCx, } } + if (mStructuredCloneScope == StructuredCloneScope::SameProcessSameThread) { + bool handled = false; + if (!TryWriteSameThreadStreamTransfer(aCx, obj, + "__uxpTransferWritableStream", + SCTAG_DOM_TRANSFERRED_WRITABLESTREAM, + aTag, aOwnership, aContent, + aExtraData, &handled)) { + return false; + } + if (handled) { + return true; + } + + if (!TryWriteSameThreadStreamTransfer(aCx, obj, + "__uxpTransferReadableStream", + SCTAG_DOM_TRANSFERRED_READABLESTREAM, + aTag, aOwnership, aContent, + aExtraData, &handled)) { + return false; + } + if (handled) { + return true; + } + + if (!TryWriteSameThreadStreamTransfer(aCx, obj, + "__uxpTransferTransformStream", + SCTAG_DOM_TRANSFERRED_TRANSFORMSTREAM, + aTag, aOwnership, aContent, + aExtraData, &handled)) { + return false; + } + if (handled) { + return true; + } + } + + if (mStructuredCloneScope == StructuredCloneScope::SameProcessDifferentThread || + mStructuredCloneScope == StructuredCloneScope::DifferentProcess) { + bool handled = false; + if (!TryWriteReadableStreamPortTransfer(aCx, obj, mPortIdentifiers, + aTag, aOwnership, aContent, + aExtraData, &handled)) { + return false; + } + if (handled) { + return true; + } + } + return false; } @@ -1480,6 +1759,26 @@ StructuredCloneHolder::CustomFreeTransferHandler(uint32_t aTag, delete data; return; } + + if ((aTag == SCTAG_DOM_TRANSFERRED_WRITABLESTREAM || + aTag == SCTAG_DOM_TRANSFERRED_READABLESTREAM || + aTag == SCTAG_DOM_TRANSFERRED_TRANSFORMSTREAM) && + mStructuredCloneScope == StructuredCloneScope::SameProcessSameThread) { + MOZ_ASSERT(aContent); + SameThreadStreamTransferData* data = + static_cast(aContent); + delete data; + return; + } + + if (aTag == SCTAG_DOM_TRANSFERRED_READABLESTREAM && + (mStructuredCloneScope == StructuredCloneScope::SameProcessDifferentThread || + mStructuredCloneScope == StructuredCloneScope::DifferentProcess)) { + MOZ_ASSERT(!aContent); + MOZ_ASSERT(aExtraData < mPortIdentifiers.Length()); + MessagePort::ForceClose(mPortIdentifiers[aExtraData]); + return; + } } bool diff --git a/dom/base/StructuredCloneTags.h b/dom/base/StructuredCloneTags.h index 09b91f7afb..15518104f9 100644 --- a/dom/base/StructuredCloneTags.h +++ b/dom/base/StructuredCloneTags.h @@ -68,6 +68,11 @@ enum StructuredCloneTags { // This tag is used by both main thread and workers. SCTAG_DOM_URLSEARCHPARAMS, + // Same-thread transferable stream records. These are not supported by IDB. + SCTAG_DOM_TRANSFERRED_READABLESTREAM, + SCTAG_DOM_TRANSFERRED_WRITABLESTREAM, + SCTAG_DOM_TRANSFERRED_TRANSFORMSTREAM, + // When adding a new tag for IDB, please don't add it to the end of the list! // Tags that are supported by IDB must not ever change. See the static assert // in IDBObjectStore.cpp, method CommonStructuredCloneReadCallback. diff --git a/dom/base/nsFrameMessageManager.cpp b/dom/base/nsFrameMessageManager.cpp index ffb5709e46..a80d9164b9 100644 --- a/dom/base/nsFrameMessageManager.cpp +++ b/dom/base/nsFrameMessageManager.cpp @@ -1929,6 +1929,9 @@ public: { ProcessGlobal* global = ProcessGlobal::Get(); MOZ_ASSERT(!aRunInGlobalScope); + if (NS_WARN_IF(!global)) { + return false; + } global->LoadScript(aURL); return true; } diff --git a/dom/script/ScriptLoader.cpp b/dom/script/ScriptLoader.cpp index c0b970e58a..acf5027949 100644 --- a/dom/script/ScriptLoader.cpp +++ b/dom/script/ScriptLoader.cpp @@ -2334,8 +2334,7 @@ ScriptLoader::EvaluateScript(ScriptLoadRequest* aRequest) } if (aRequest->IsModuleRequest()) { - // For modules, currentScript is set to null. - AutoCurrentScriptUpdater scriptUpdater(this, nullptr); + AutoCurrentScriptUpdater scriptUpdater(this, aRequest->Element()); ModuleLoadRequest* request = aRequest->AsModuleRequest(); MOZ_ASSERT(request->mModuleScript); diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp index f63b1de981..65f778b5f9 100644 --- a/dom/workers/RuntimeService.cpp +++ b/dom/workers/RuntimeService.cpp @@ -301,6 +301,7 @@ LoadContextOptions(const char* aPrefName, void* /* aClosure */) .setAsyncStack(GetWorkerPref(NS_LITERAL_CSTRING("asyncstack"))) .setWerror(GetWorkerPref(NS_LITERAL_CSTRING("werror"))) .setStreams(GetWorkerPref(NS_LITERAL_CSTRING("streams"))) + .setWeakRefs(GetWorkerPref(NS_LITERAL_CSTRING("weakrefs"))) .setExtraWarnings(GetWorkerPref(NS_LITERAL_CSTRING("strict"))) .setArrayProtoValues(GetWorkerPref( NS_LITERAL_CSTRING("array_prototype_values"))); diff --git a/dom/workers/WorkerPrefs.h b/dom/workers/WorkerPrefs.h index 87806a6fbc..d870ac8e46 100644 --- a/dom/workers/WorkerPrefs.h +++ b/dom/workers/WorkerPrefs.h @@ -35,6 +35,7 @@ WORKER_SIMPLE_PREF("dom.serviceWorkers.openWindow.enabled", OpenWindowEnabled, O WORKER_SIMPLE_PREF("dom.storageManager.enabled", StorageManagerEnabled, STORAGEMANAGER_ENABLED) WORKER_SIMPLE_PREF("dom.push.enabled", PushEnabled, PUSH_ENABLED) WORKER_SIMPLE_PREF("dom.streams.enabled", StreamsEnabled, STREAMS_ENABLED) +WORKER_SIMPLE_PREF("javascript.options.weakrefs", WeakRefsEnabled, WEAKREFS_ENABLED) WORKER_SIMPLE_PREF("dom.requestcontext.enabled", RequestContextEnabled, REQUESTCONTEXT_ENABLED) WORKER_SIMPLE_PREF("gfx.offscreencanvas.enabled", OffscreenCanvasEnabled, OFFSCREENCANVAS_ENABLED) WORKER_SIMPLE_PREF("dom.webkitBlink.dirPicker.enabled", WebkitBlinkDirectoryPickerEnabled, DOM_WEBKITBLINK_DIRPICKER_WEBKITBLINK) diff --git a/js/src/builtin/AtomicsObject.cpp b/js/src/builtin/AtomicsObject.cpp index a4ae0736d4..f917db9b21 100644 --- a/js/src/builtin/AtomicsObject.cpp +++ b/js/src/builtin/AtomicsObject.cpp @@ -47,10 +47,12 @@ #include "builtin/AtomicsObject.h" #include "mozilla/Atomics.h" +#include "mozilla/Casting.h" #include "mozilla/FloatingPoint.h" #include "mozilla/Maybe.h" #include "mozilla/Unused.h" +#include "builtin/Promise.h" #include "jsapi.h" #include "jsfriendapi.h" #include "jsnum.h" @@ -58,7 +60,9 @@ #include "jit/AtomicOperations.h" #include "jit/InlinableNatives.h" #include "js/Class.h" +#include "vm/BigIntType.h" #include "vm/GlobalObject.h" +#include "vm/HelperThreads.h" #include "vm/Time.h" #include "vm/TypedArrayObject.h" #include "wasm/WasmInstance.h" @@ -121,6 +125,68 @@ GetTypedArrayIndex(JSContext* cx, HandleValue v, Handle view, return true; } +static bool +IsWaitableTypedArray(Scalar::Type viewType) +{ + return viewType == Scalar::Int32 || viewType == Scalar::BigInt64; +} + +static uint32_t +GetWaiterByteOffset(Handle view, uint32_t offset) +{ + return view->byteOffset() + offset * TypedArrayElemSize(view->type()); +} + +static uint64_t +BigIntToRawBits(Scalar::Type viewType, BigInt* value) +{ + if (viewType == Scalar::BigInt64) + return uint64_t(BigInt::toInt64(value)); + MOZ_ASSERT(viewType == Scalar::BigUint64); + return BigInt::toUint64(value); +} + +static bool +BigIntToRawBits(JSContext* cx, Scalar::Type viewType, HandleValue value, uint64_t* bits) +{ + MOZ_ASSERT(Scalar::isBigIntType(viewType)); + + RootedBigInt bigint(cx, ToBigInt(cx, value)); + if (!bigint) + return false; + + *bits = BigIntToRawBits(viewType, bigint); + return true; +} + +static bool +SetBigIntResult(JSContext* cx, Scalar::Type viewType, uint64_t bits, MutableHandleValue result) +{ + MOZ_ASSERT(Scalar::isBigIntType(viewType)); + + BigInt* bigint = viewType == Scalar::BigInt64 + ? BigInt::createFromInt64(cx, mozilla::BitwiseCast(bits)) + : BigInt::createFromUint64(cx, bits); + if (!bigint) + return false; + + result.setBigInt(bigint); + return true; +} + +static bool +WaitValueMatches(Handle view, uint32_t offset, uint64_t expected) +{ + SharedMem viewData = view->viewDataShared(); + if (view->type() == Scalar::BigInt64) { + return jit::AtomicOperations::loadSafeWhenRacy(viewData.cast() + offset) == + expected; + } + + return uint32_t(jit::AtomicOperations::loadSafeWhenRacy(viewData.cast() + offset)) == + uint32_t(expected); +} + static int32_t CompareExchange(Scalar::Type viewType, int32_t oldCandidate, int32_t newCandidate, SharedMem viewData, uint32_t offset, bool* badArrayType = nullptr) @@ -191,6 +257,20 @@ js::atomics_compareExchange(JSContext* cx, unsigned argc, Value* vp) uint32_t offset; if (!GetTypedArrayIndex(cx, idxv, view, &offset)) return false; + + if (Scalar::isBigIntType(view->type())) { + uint64_t oldCandidate; + if (!BigIntToRawBits(cx, view->type(), oldv, &oldCandidate)) + return false; + uint64_t newCandidate; + if (!BigIntToRawBits(cx, view->type(), newv, &newCandidate)) + return false; + + uint64_t result = jit::AtomicOperations::compareExchangeSeqCst( + view->viewDataShared().cast() + offset, oldCandidate, newCandidate); + return SetBigIntResult(cx, view->type(), result, r); + } + int32_t oldCandidate; if (!ToInt32(cx, oldv, &oldCandidate)) return false; @@ -259,6 +339,22 @@ js::atomics_load(JSContext* cx, unsigned argc, Value* vp) r.setNumber(v); return true; } + case Scalar::BigInt64: { + int64_t v = jit::AtomicOperations::loadSeqCst(viewData.cast() + offset); + BigInt* bigint = BigInt::createFromInt64(cx, v); + if (!bigint) + return false; + r.setBigInt(bigint); + return true; + } + case Scalar::BigUint64: { + uint64_t v = jit::AtomicOperations::loadSeqCst(viewData.cast() + offset); + BigInt* bigint = BigInt::createFromUint64(cx, v); + if (!bigint) + return false; + r.setBigInt(bigint); + return true; + } default: return ReportBadArrayType(cx); } @@ -337,6 +433,25 @@ ExchangeOrStore(JSContext* cx, unsigned argc, Value* vp) uint32_t offset; if (!GetTypedArrayIndex(cx, idxv, view, &offset)) return false; + + if (Scalar::isBigIntType(view->type())) { + RootedBigInt bigint(cx, ToBigInt(cx, valv)); + if (!bigint) + return false; + + uint64_t value = BigIntToRawBits(view->type(), bigint); + if (op == DoStore) { + jit::AtomicOperations::storeSeqCst(view->viewDataShared().cast() + offset, + value); + r.setBigInt(bigint); + return true; + } + + uint64_t result = jit::AtomicOperations::exchangeSeqCst( + view->viewDataShared().cast() + offset, value); + return SetBigIntResult(cx, view->type(), result, r); + } + double integerValue; if (!ToInteger(cx, valv, &integerValue)) return false; @@ -380,6 +495,24 @@ AtomicsBinop(JSContext* cx, HandleValue objv, HandleValue idxv, HandleValue valv uint32_t offset; if (!GetTypedArrayIndex(cx, idxv, view, &offset)) return false; + + if (Scalar::isBigIntType(view->type())) { + uint64_t value; + if (!BigIntToRawBits(cx, view->type(), valv, &value)) + return false; + + SharedMem addr = view->viewDataShared().cast() + offset; + uint64_t old = jit::AtomicOperations::loadSeqCst(addr); + for (;;) { + uint64_t replacement = T::operate64(old, value); + uint64_t observed = jit::AtomicOperations::compareExchangeSeqCst(addr, old, + replacement); + if (observed == old) + return SetBigIntResult(cx, view->type(), old, r); + old = observed; + } + } + int32_t numberValue; if (!ToInt32(cx, valv, &numberValue)) return false; @@ -434,6 +567,7 @@ class PerformAdd public: INTEGRAL_TYPES_FOR_EACH(jit::AtomicOperations::fetchAddSeqCst) static int32_t perform(int32_t x, int32_t y) { return x + y; } + static uint64_t operate64(uint64_t x, uint64_t y) { return x + y; } }; bool @@ -448,6 +582,7 @@ class PerformSub public: INTEGRAL_TYPES_FOR_EACH(jit::AtomicOperations::fetchSubSeqCst) static int32_t perform(int32_t x, int32_t y) { return x - y; } + static uint64_t operate64(uint64_t x, uint64_t y) { return x - y; } }; bool @@ -462,6 +597,7 @@ class PerformAnd public: INTEGRAL_TYPES_FOR_EACH(jit::AtomicOperations::fetchAndSeqCst) static int32_t perform(int32_t x, int32_t y) { return x & y; } + static uint64_t operate64(uint64_t x, uint64_t y) { return x & y; } }; bool @@ -476,6 +612,7 @@ class PerformOr public: INTEGRAL_TYPES_FOR_EACH(jit::AtomicOperations::fetchOrSeqCst) static int32_t perform(int32_t x, int32_t y) { return x | y; } + static uint64_t operate64(uint64_t x, uint64_t y) { return x | y; } }; bool @@ -490,6 +627,7 @@ class PerformXor public: INTEGRAL_TYPES_FOR_EACH(jit::AtomicOperations::fetchXorSeqCst) static int32_t perform(int32_t x, int32_t y) { return x ^ y; } + static uint64_t operate64(uint64_t x, uint64_t y) { return x ^ y; } }; bool @@ -693,11 +831,13 @@ js::atomics_cmpxchg_asm_callout(wasm::Instance* instance, int32_t vt, int32_t of namespace js { +class AtomicsWaitAsyncTask; + // Represents one waiting worker. // // The type is declared opaque in SharedArrayObject.h. Instances of -// js::FutexWaiter are stack-allocated and linked onto a list across a -// call to FutexRuntime::wait(). +// js::FutexWaiter are linked onto a list across a call to FutexRuntime::wait() +// or an async wait task. // // The 'waiters' field of the SharedArrayRawBuffer points to the highest // priority waiter in the list, and lower priority nodes are linked through @@ -711,14 +851,34 @@ class FutexWaiter public: FutexWaiter(uint32_t offset, JSRuntime* rt) : offset(offset), + kind(Sync), rt(rt), + asyncTask(nullptr), lower_pri(nullptr), back(nullptr) { } - uint32_t offset; // int32 element index within the SharedArrayBuffer + FutexWaiter(uint32_t offset, AtomicsWaitAsyncTask* asyncTask) + : offset(offset), + kind(Async), + rt(nullptr), + asyncTask(asyncTask), + lower_pri(nullptr), + back(nullptr) + { + } + + bool isWaiting() const; + void notify(); + + uint32_t offset; // byte offset within the SharedArrayBuffer + enum WaiterKind { + Sync, + Async + } kind; JSRuntime* rt; // The runtime of the waiter + AtomicsWaitAsyncTask* asyncTask; // The async waiter task, if any FutexWaiter* lower_pri; // Lower priority nodes in circular doubly-linked list of waiters FutexWaiter* back; // Other direction }; @@ -742,8 +902,195 @@ class AutoLockFutexAPI js::UniqueLock& unique() { return *unique_; } }; +static void +AddWaiter(SharedArrayRawBuffer* sarb, FutexWaiter* waiter) +{ + if (FutexWaiter* waiters = sarb->waiters()) { + waiter->lower_pri = waiters; + waiter->back = waiters->back; + waiters->back->lower_pri = waiter; + waiters->back = waiter; + } else { + waiter->lower_pri = waiter->back = waiter; + sarb->setWaiters(waiter); + } +} + +static void +RemoveWaiter(SharedArrayRawBuffer* sarb, FutexWaiter* waiter) +{ + if (waiter->lower_pri == waiter) { + sarb->setWaiters(nullptr); + } else { + waiter->lower_pri->back = waiter->back; + waiter->back->lower_pri = waiter->lower_pri; + if (sarb->waiters() == waiter) + sarb->setWaiters(waiter->lower_pri); + } + waiter->lower_pri = nullptr; + waiter->back = nullptr; +} + +class AtomicsWaitAsyncTask : public PromiseTask +{ + enum class State { + Waiting, + Notified, + TimedOut + }; + + SharedArrayRawBuffer* sarb_; + FutexWaiter waiter_; + mozilla::Maybe timeout_; + ConditionVariable cond_; + State state_; + bool isInWaiterList_; + + public: + AtomicsWaitAsyncTask(JSContext* cx, Handle promise, + SharedArrayRawBuffer* sarb, uint32_t offset, + mozilla::Maybe& timeout) + : PromiseTask(cx, promise), + sarb_(sarb), + waiter_(offset, this), + timeout_(timeout), + state_(State::Waiting), + isInWaiterList_(false) + { + } + + ~AtomicsWaitAsyncTask() { + if (isInWaiterList_) { + AutoLockFutexAPI lock; + if (isInWaiterList_) + removeFromWaiterList(); + } + sarb_->dropReference(); + } + + FutexWaiter* waiter() { + return &waiter_; + } + + void setInWaiterList() { + isInWaiterList_ = true; + } + + bool isWaiting() const { + return state_ == State::Waiting; + } + + void notify() { + MOZ_ASSERT(isWaiting()); + state_ = State::Notified; + cond_.notify_all(); + } + + void execute() override { + AutoLockFutexAPI lock; + + const bool isTimed = timeout_.isSome(); + auto finalEnd = timeout_.map([](mozilla::TimeDuration& timeout) { + return mozilla::TimeStamp::Now() + timeout; + }); + auto maxSlice = mozilla::TimeDuration::FromSeconds(4000.0); + + while (state_ == State::Waiting) { + if (isTimed) { + auto sliceEnd = finalEnd.map([&](mozilla::TimeStamp& finalEnd) { + auto sliceEnd = mozilla::TimeStamp::Now() + maxSlice; + if (finalEnd < sliceEnd) + sliceEnd = finalEnd; + return sliceEnd; + }); + + mozilla::Unused << cond_.wait_until(lock.unique(), *sliceEnd); + if (state_ == State::Waiting && mozilla::TimeStamp::Now() >= *finalEnd) { + state_ = State::TimedOut; + break; + } + } else { + cond_.wait(lock.unique()); + } + } + + if (isInWaiterList_) + removeFromWaiterList(); + } + + private: + void removeFromWaiterList() { + MOZ_ASSERT(isInWaiterList_); + RemoveWaiter(sarb_, &waiter_); + isInWaiterList_ = false; + } + + bool finishPromise(JSContext* cx, Handle promise) override { + RootedValue result(cx); + if (state_ == State::TimedOut) + result.setString(cx->names().futexTimedOut); + else + result.setString(cx->names().futexOK); + return PromiseObject::resolve(cx, promise, result); + } +}; + +bool +FutexWaiter::isWaiting() const +{ + if (kind == Sync) + return rt->fx.isWaiting(); + return asyncTask->isWaiting(); +} + +void +FutexWaiter::notify() +{ + if (kind == Sync) { + rt->fx.notify(FutexRuntime::NotifyExplicit); + return; + } + asyncTask->notify(); +} + } // namespace js +static bool +GetWaitTimeout(JSContext* cx, HandleValue timeoutv, + mozilla::Maybe* timeout) +{ + timeout->reset(); + if (!timeoutv.isUndefined()) { + double timeout_ms; + if (!ToNumber(cx, timeoutv, &timeout_ms)) + return false; + if (!mozilla::IsNaN(timeout_ms)) { + if (timeout_ms < 0) + *timeout = mozilla::Some(mozilla::TimeDuration::FromSeconds(0.0)); + else if (!mozilla::IsInfinite(timeout_ms)) + *timeout = mozilla::Some(mozilla::TimeDuration::FromMilliseconds(timeout_ms)); + } + } + return true; +} + +static bool +CreateWaitAsyncResult(JSContext* cx, bool isAsync, HandleValue value, MutableHandleValue rval) +{ + RootedPlainObject obj(cx, NewBuiltinClassInstance(cx)); + if (!obj) + return false; + + RootedValue asyncValue(cx, BooleanValue(isAsync)); + if (!NativeDefineDataProperty(cx, obj, cx->names().async, asyncValue, JSPROP_ENUMERATE)) + return false; + if (!NativeDefineDataProperty(cx, obj, cx->names().value, value, JSPROP_ENUMERATE)) + return false; + + rval.setObject(*obj); + return true; +} + bool js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) { @@ -759,26 +1106,24 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) Rooted view(cx, nullptr); if (!GetSharedTypedArray(cx, objv, &view)) return false; - if (view->type() != Scalar::Int32) + if (!IsWaitableTypedArray(view->type())) return ReportBadArrayType(cx); uint32_t offset; if (!GetTypedArrayIndex(cx, idxv, view, &offset)) return false; - int32_t value; - if (!ToInt32(cx, valv, &value)) - return false; - mozilla::Maybe timeout; - if (!timeoutv.isUndefined()) { - double timeout_ms; - if (!ToNumber(cx, timeoutv, &timeout_ms)) + uint64_t value = 0; + if (view->type() == Scalar::BigInt64) { + if (!BigIntToRawBits(cx, view->type(), valv, &value)) return false; - if (!mozilla::IsNaN(timeout_ms)) { - if (timeout_ms < 0) - timeout = mozilla::Some(mozilla::TimeDuration::FromSeconds(0.0)); - else if (!mozilla::IsInfinite(timeout_ms)) - timeout = mozilla::Some(mozilla::TimeDuration::FromMilliseconds(timeout_ms)); - } + } else { + int32_t int32Value; + if (!ToInt32(cx, valv, &int32Value)) + return false; + value = uint32_t(int32Value); } + mozilla::Maybe timeout; + if (!GetWaitTimeout(cx, timeoutv, &timeout)) + return false; if (!rt->fx.canWait()) return ReportCannotWait(cx); @@ -787,8 +1132,7 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) // and it provides the necessary memory fence. AutoLockFutexAPI lock; - SharedMem addr = view->viewDataShared().cast() + offset; - if (jit::AtomicOperations::loadSafeWhenRacy(addr) != value) { + if (!WaitValueMatches(view, offset, value)) { r.setString(cx->names().futexNotEqual); return true; } @@ -796,16 +1140,8 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) Rooted sab(cx, view->bufferShared()); SharedArrayRawBuffer* sarb = sab->rawBufferObject(); - FutexWaiter w(offset, rt); - if (FutexWaiter* waiters = sarb->waiters()) { - w.lower_pri = waiters; - w.back = waiters->back; - waiters->back->lower_pri = &w; - waiters->back = &w; - } else { - w.lower_pri = w.back = &w; - sarb->setWaiters(&w); - } + FutexWaiter w(GetWaiterByteOffset(view, offset), rt); + AddWaiter(sarb, &w); FutexRuntime::WaitResult result = FutexRuntime::FutexOK; bool retval = rt->fx.wait(cx, lock.unique(), timeout, &result); @@ -820,17 +1156,98 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) } } - if (w.lower_pri == &w) { - sarb->setWaiters(nullptr); - } else { - w.lower_pri->back = w.back; - w.back->lower_pri = w.lower_pri; - if (sarb->waiters() == &w) - sarb->setWaiters(w.lower_pri); - } + RemoveWaiter(sarb, &w); return retval; } +bool +js::atomics_waitAsync(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue objv = args.get(0); + HandleValue idxv = args.get(1); + HandleValue valv = args.get(2); + HandleValue timeoutv = args.get(3); + MutableHandleValue r = args.rval(); + + Rooted view(cx, nullptr); + if (!GetSharedTypedArray(cx, objv, &view)) + return false; + if (!IsWaitableTypedArray(view->type())) + return ReportBadArrayType(cx); + uint32_t offset; + if (!GetTypedArrayIndex(cx, idxv, view, &offset)) + return false; + uint64_t value = 0; + if (view->type() == Scalar::BigInt64) { + if (!BigIntToRawBits(cx, view->type(), valv, &value)) + return false; + } else { + int32_t int32Value; + if (!ToInt32(cx, valv, &int32Value)) + return false; + value = uint32_t(int32Value); + } + mozilla::Maybe timeout; + if (!GetWaitTimeout(cx, timeoutv, &timeout)) + return false; + + Rooted promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) + return false; + + RootedValue promiseValue(cx, ObjectValue(*promise)); + RootedValue result(cx); + if (!CreateWaitAsyncResult(cx, true, promiseValue, &result)) + return false; + + Rooted sab(cx, view->bufferShared()); + SharedArrayRawBuffer* sarb = sab->rawBufferObject(); + if (!sarb->addReference()) { + JS_ReportErrorASCII(cx, "Reference count overflow on SharedArrayBuffer"); + return false; + } + + auto task = cx->make_unique(cx, promise, sarb, offset, timeout); + if (!task) { + sarb->dropReference(); + return false; + } + + bool isAsync = false; + RootedValue immediateResult(cx); + { + AutoLockFutexAPI lock; + + if (!WaitValueMatches(view, offset, value)) { + immediateResult.setString(cx->names().futexNotEqual); + } else if (timeout.isSome() && timeout->ToMilliseconds() == 0.0) { + immediateResult.setString(cx->names().futexTimedOut); + } else { + if (!cx->startAsyncTaskCallback || !cx->finishAsyncTaskCallback || + !CanUseExtraThreads()) + { + JS_ReportErrorASCII(cx, "Atomics.waitAsync not supported in this runtime."); + return false; + } + + task->waiter()->offset = GetWaiterByteOffset(view, offset); + AddWaiter(sarb, task->waiter()); + task->setInWaiterList(); + isAsync = true; + } + } + + if (!isAsync) + return CreateWaitAsyncResult(cx, false, immediateResult, r); + + if (!StartPromiseTask(cx, Move(task))) + return false; + + r.set(result); + return true; +} + bool js::atomics_notify(JSContext* cx, unsigned argc, Value* vp) { @@ -843,11 +1260,12 @@ js::atomics_notify(JSContext* cx, unsigned argc, Value* vp) Rooted view(cx, nullptr); if (!GetSharedTypedArray(cx, objv, &view)) return false; - if (view->type() != Scalar::Int32) + if (!IsWaitableTypedArray(view->type())) return ReportBadArrayType(cx); uint32_t offset; if (!GetTypedArrayIndex(cx, idxv, view, &offset)) return false; + offset = GetWaiterByteOffset(view, offset); double count; if (countv.isUndefined()) { count = mozilla::PositiveInfinity(); @@ -870,9 +1288,9 @@ js::atomics_notify(JSContext* cx, unsigned argc, Value* vp) do { FutexWaiter* c = iter; iter = iter->lower_pri; - if (c->offset != offset || !c->rt->fx.isWaiting()) + if (c->offset != offset || !c->isWaiting()) continue; - c->rt->fx.notify(FutexRuntime::NotifyExplicit); + c->notify(); ++woken; --count; } while (count > 0 && iter != waiters); @@ -1106,6 +1524,7 @@ const JSFunctionSpec AtomicsMethods[] = { JS_INLINABLE_FN("xor", atomics_xor, 3,0, AtomicsXor), JS_INLINABLE_FN("isLockFree", atomics_isLockFree, 1,0, AtomicsIsLockFree), JS_FN("wait", atomics_wait, 4,0), + JS_FN("waitAsync", atomics_waitAsync, 4,0), JS_FN("notify", atomics_notify, 3,0), JS_FN("wake", atomics_notify, 3,0), //Legacy name JS_FS_END diff --git a/js/src/builtin/AtomicsObject.h b/js/src/builtin/AtomicsObject.h index 18f00dad16..309c97ae91 100644 --- a/js/src/builtin/AtomicsObject.h +++ b/js/src/builtin/AtomicsObject.h @@ -24,18 +24,19 @@ class AtomicsObject : public JSObject static MOZ_MUST_USE bool toString(JSContext* cx, unsigned int argc, Value* vp); }; -MOZ_MUST_USE bool atomics_compareExchange(JSContext* cx, unsigned argc, Value* vp); -MOZ_MUST_USE bool atomics_exchange(JSContext* cx, unsigned argc, Value* vp); -MOZ_MUST_USE bool atomics_load(JSContext* cx, unsigned argc, Value* vp); -MOZ_MUST_USE bool atomics_store(JSContext* cx, unsigned argc, Value* vp); -MOZ_MUST_USE bool atomics_add(JSContext* cx, unsigned argc, Value* vp); -MOZ_MUST_USE bool atomics_sub(JSContext* cx, unsigned argc, Value* vp); -MOZ_MUST_USE bool atomics_and(JSContext* cx, unsigned argc, Value* vp); -MOZ_MUST_USE bool atomics_or(JSContext* cx, unsigned argc, Value* vp); -MOZ_MUST_USE bool atomics_xor(JSContext* cx, unsigned argc, Value* vp); -MOZ_MUST_USE bool atomics_isLockFree(JSContext* cx, unsigned argc, Value* vp); -MOZ_MUST_USE bool atomics_wait(JSContext* cx, unsigned argc, Value* vp); -MOZ_MUST_USE bool atomics_notify(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_compareExchange(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_exchange(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_load(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_store(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_add(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_sub(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_and(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_or(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_xor(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_isLockFree(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_wait(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_waitAsync(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_notify(JSContext* cx, unsigned argc, Value* vp); /* asm.js callouts */ namespace wasm { class Instance; } diff --git a/js/src/builtin/Map.js b/js/src/builtin/Map.js index 434cd6529b..bcf0ded02e 100644 --- a/js/src/builtin/Map.js +++ b/js/src/builtin/Map.js @@ -55,6 +55,49 @@ function MapForEach(callbackfn, thisArg = undefined) { } } +// ES2024 +// Map.groupBy ( items, callbackfn ) +function MapGroupBy(items, callbackfn) { + // Step 1. + RequireObjectCoercible(items); + + // Step 2. + if (!IsCallable(callbackfn)) + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(1, callbackfn)); + + // Step 3. + var groups = std_Map_create(); + + // Step 4. + var k = 0; + + // Steps 5-8. + for (var value of allowContentIter(items)) { + // Step 6.a. + if (k >= MAX_NUMERIC_INDEX) + ThrowTypeError(JSMSG_TOO_LONG_ARRAY); + + // Step 6.b. + var key = callContentFunction(callbackfn, undefined, value, k); + + // Steps 6.c-d. + var elements; + if (callFunction(std_Map_has, groups, key)) { + elements = callFunction(std_Map_get, groups, key); + callFunction(std_Array_push, elements, value); + } else { + elements = [value]; + callFunction(std_Map_set, groups, key, elements); + } + + // Step 6.e. + k++; + } + + // Step 9. + return groups; +} + var iteratorTemp = { mapIterationResultPair : null }; function MapIteratorNext() { diff --git a/js/src/builtin/MapObject.cpp b/js/src/builtin/MapObject.cpp index fe748a6bde..daeb930c86 100644 --- a/js/src/builtin/MapObject.cpp +++ b/js/src/builtin/MapObject.cpp @@ -332,9 +332,15 @@ const JSPropertySpec MapObject::staticProperties[] = { JS_PS_END }; +const JSFunctionSpec MapObject::staticMethods[] = { + JS_SELF_HOSTED_FN("groupBy", "MapGroupBy", 2, 0), + JS_FS_END +}; + static JSObject* InitClass(JSContext* cx, Handle global, const Class* clasp, JSProtoKey key, Native construct, const JSPropertySpec* properties, const JSFunctionSpec* methods, + const JSFunctionSpec* staticMethods, const JSPropertySpec* staticProperties) { RootedPlainObject proto(cx, NewBuiltinClassInstance(cx)); @@ -343,8 +349,13 @@ InitClass(JSContext* cx, Handle global, const Class* clasp, JSPro Rooted ctor(cx, global->createConstructor(cx, construct, ClassName(key, cx), 0)); if (!ctor || - !JS_DefineProperties(cx, ctor, staticProperties) || - !LinkConstructorAndPrototype(cx, ctor, proto) || + !JS_DefineProperties(cx, ctor, staticProperties)) + { + return nullptr; + } + if (staticMethods && !JS_DefineFunctions(cx, ctor, staticMethods)) + return nullptr; + if (!LinkConstructorAndPrototype(cx, ctor, proto) || !DefinePropertiesAndFunctions(cx, proto, properties, methods) || !GlobalObject::initBuiltinConstructor(cx, global, key, ctor, proto)) { @@ -359,7 +370,7 @@ MapObject::initClass(JSContext* cx, JSObject* obj) Rooted global(cx, &obj->as()); RootedObject proto(cx, InitClass(cx, global, &class_, JSProto_Map, construct, properties, methods, - staticProperties)); + staticMethods, staticProperties)); if (proto) { // Define the "entries" method. JSFunction* fun = JS_DefineFunction(cx, proto, "entries", entries, 0, 0); @@ -1084,7 +1095,7 @@ SetObject::initClass(JSContext* cx, JSObject* obj) Rooted global(cx, &obj->as()); RootedObject proto(cx, InitClass(cx, global, &class_, JSProto_Set, construct, properties, methods, - staticProperties)); + nullptr, staticProperties)); if (proto) { // Define the "values" method. JSFunction* fun = JS_DefineFunction(cx, proto, "values", values, 0, 0); diff --git a/js/src/builtin/MapObject.h b/js/src/builtin/MapObject.h index a9f685ea00..2b46fec2c9 100644 --- a/js/src/builtin/MapObject.h +++ b/js/src/builtin/MapObject.h @@ -109,8 +109,10 @@ class MapObject : public NativeObject { static MOZ_MUST_USE bool getKeysAndValuesInterleaved(JSContext* cx, HandleObject obj, JS::MutableHandle> entries); - static MOZ_MUST_USE bool entries(JSContext* cx, unsigned argc, Value* vp); - static MOZ_MUST_USE bool has(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool entries(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool get(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool has(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool set(JSContext* cx, unsigned argc, Value* vp); static MapObject* create(JSContext* cx, HandleObject proto = nullptr); // Publicly exposed Map calls for JSAPI access (webidl maplike/setlike @@ -137,6 +139,7 @@ class MapObject : public NativeObject { static const JSPropertySpec properties[]; static const JSFunctionSpec methods[]; + static const JSFunctionSpec staticMethods[]; static const JSPropertySpec staticProperties[]; ValueMap* getData() { return static_cast(getPrivate()); } static ValueMap& extract(HandleObject o); @@ -150,22 +153,20 @@ class MapObject : public NativeObject { static MOZ_MUST_USE bool iterator_impl(JSContext* cx, const CallArgs& args, IteratorKind kind); - static MOZ_MUST_USE bool size_impl(JSContext* cx, const CallArgs& args); - static MOZ_MUST_USE bool size(JSContext* cx, unsigned argc, Value* vp); - static MOZ_MUST_USE bool get_impl(JSContext* cx, const CallArgs& args); - static MOZ_MUST_USE bool get(JSContext* cx, unsigned argc, Value* vp); - static MOZ_MUST_USE bool has_impl(JSContext* cx, const CallArgs& args); - static MOZ_MUST_USE bool set_impl(JSContext* cx, const CallArgs& args); - static MOZ_MUST_USE bool set(JSContext* cx, unsigned argc, Value* vp); - static MOZ_MUST_USE bool delete_impl(JSContext* cx, const CallArgs& args); - static MOZ_MUST_USE bool delete_(JSContext* cx, unsigned argc, Value* vp); - static MOZ_MUST_USE bool keys_impl(JSContext* cx, const CallArgs& args); - static MOZ_MUST_USE bool keys(JSContext* cx, unsigned argc, Value* vp); - static MOZ_MUST_USE bool values_impl(JSContext* cx, const CallArgs& args); - static MOZ_MUST_USE bool values(JSContext* cx, unsigned argc, Value* vp); - static MOZ_MUST_USE bool entries_impl(JSContext* cx, const CallArgs& args); - static MOZ_MUST_USE bool clear_impl(JSContext* cx, const CallArgs& args); - static MOZ_MUST_USE bool clear(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool size_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool size(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool get_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool has_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool set_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool delete_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool delete_(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool keys_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool keys(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool values_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool values(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool entries_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool clear_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool clear(JSContext* cx, unsigned argc, Value* vp); }; class MapIteratorObject : public NativeObject diff --git a/js/src/builtin/Object.js b/js/src/builtin/Object.js index 6627aea593..01442144cc 100644 --- a/js/src/builtin/Object.js +++ b/js/src/builtin/Object.js @@ -240,12 +240,16 @@ function ObjectGroupBy(items, callbackfn) { // Steps 5-8. for (var value of allowContentIter(items)) { // Step 6.a. - var key = callContentFunction(callbackfn, undefined, value, k); + if (k >= MAX_NUMERIC_INDEX) + ThrowTypeError(JSMSG_TOO_LONG_ARRAY); // Step 6.b. + var key = callContentFunction(callbackfn, undefined, value, k); + + // Step 6.c. key = ToPropertyKey(key); - // Steps 6.c-d. + // Steps 6.d-e. var elements = groups[key]; if (elements === undefined) { _DefineDataProperty(groups, key, [value]); @@ -253,7 +257,7 @@ function ObjectGroupBy(items, callbackfn) { callFunction(std_Array_push, elements, value); } - // Step 6.e. + // Step 6.f. k++; } diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp index 9660a758a1..56803b5b91 100644 --- a/js/src/builtin/Promise.cpp +++ b/js/src/builtin/Promise.cpp @@ -3673,6 +3673,48 @@ Promise_static_resolve(JSContext* cx, unsigned argc, Value* vp) return true; } +// ES2024 +// Promise.withResolvers ( ) +static bool +Promise_static_withResolvers(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!args.thisv().isObject()) { + ReportValueError(cx, JSMSG_NOT_CONSTRUCTOR, -1, args.thisv(), nullptr); + return false; + } + RootedObject C(cx, &args.thisv().toObject()); + + // Step 2. + Rooted capability(cx); + if (!NewPromiseCapability(cx, C, &capability, false)) + return false; + + // Step 3. + RootedPlainObject obj(cx, NewBuiltinClassInstance(cx)); + if (!obj) + return false; + + // Steps 4-7. + RootedValue promise(cx, ObjectValue(*capability.promise())); + if (!::JS_DefineProperty(cx, obj, "promise", promise, JSPROP_ENUMERATE)) + return false; + + RootedValue resolve(cx, ObjectValue(*capability.resolve())); + if (!::JS_DefineProperty(cx, obj, "resolve", resolve, JSPROP_ENUMERATE)) + return false; + + RootedValue reject(cx, ObjectValue(*capability.reject())); + if (!::JS_DefineProperty(cx, obj, "reject", reject, JSPROP_ENUMERATE)) + return false; + + // Step 8. + args.rval().setObject(*obj); + return true; +} + /** * Unforgeable version of ES2016, 25.4.4.5, Promise.resolve. */ @@ -5180,6 +5222,7 @@ static const JSFunctionSpec promise_static_methods[] = { JS_FN("race", Promise_static_race, 1, 0), JS_FN("reject", Promise_reject, 1, 0), JS_FN("resolve", Promise_static_resolve, 1, 0), + JS_FN("withResolvers", Promise_static_withResolvers, 0, 0), JS_FS_END }; diff --git a/js/src/builtin/Stream.cpp b/js/src/builtin/Stream.cpp index 26457709d8..0d5d0b2701 100644 --- a/js/src/builtin/Stream.cpp +++ b/js/src/builtin/Stream.cpp @@ -5,6 +5,7 @@ #include "builtin/Stream.h" +#include "js/CompilationAndEvaluation.h" #include "js/Stream.h" #include "jscntxt.h" @@ -16,6 +17,10 @@ #include "vm/NativeObject-inl.h" +#include "zlib.h" + +#include + using namespace js; enum StreamSlots { @@ -96,14 +101,72 @@ Is(const HandleValue v) return v.isObject() && v.toObject().is(); } -#ifdef DEBUG +template +MOZ_ALWAYS_INLINE bool +IsMaybeWrapped(const HandleValue v) +{ + if (!v.isObject()) + return false; + + JSObject* obj = &v.toObject(); + if (obj->is()) + return true; + + JSObject* unwrapped = CheckedUnwrap(obj); + return unwrapped && unwrapped->is(); +} + +template +MOZ_ALWAYS_INLINE T* +UnwrapInternalSlotObject(JSObject* obj) +{ + JSObject* unwrapped = UncheckedUnwrap(obj); + MOZ_ASSERT(unwrapped); + MOZ_ASSERT(unwrapped->is()); + return &unwrapped->as(); +} + +template +MOZ_ALWAYS_INLINE T* +UnwrapMaybeWrappedArg(const HandleValue v) +{ + if (!v.isObject()) + return nullptr; + + JSObject* obj = &v.toObject(); + if (obj->is()) + return &obj->as(); + + JSObject* unwrapped = CheckedUnwrap(obj); + if (!unwrapped || !unwrapped->is()) + return nullptr; + + return &unwrapped->as(); +} + static bool IsReadableStreamController(const JSObject* controller) { return controller->is() || controller->is(); } -#endif // DEBUG + +[[nodiscard]] static NativeObject* +UnwrapMaybeWrappedReadableStreamController(const HandleValue v) +{ + if (!v.isObject()) + return nullptr; + + JSObject* obj = &v.toObject(); + if (IsReadableStreamController(obj)) + return &obj->as(); + + JSObject* unwrapped = CheckedUnwrap(obj); + if (!unwrapped || !IsReadableStreamController(unwrapped)) + return nullptr; + + return &unwrapped->as(); +} static inline uint32_t ControllerFlags(const NativeObject* controller) @@ -183,7 +246,8 @@ inline static MOZ_MUST_USE ReadableStream* StreamFromController(const NativeObject* controller) { MOZ_ASSERT(IsReadableStreamController(controller)); - return &controller->getFixedSlot(ControllerSlot_Stream).toObject().as(); + return UnwrapInternalSlotObject( + &controller->getFixedSlot(ControllerSlot_Stream).toObject()); } inline static MOZ_MUST_USE NativeObject* @@ -215,15 +279,17 @@ inline static MOZ_MUST_USE ReadableStream* StreamFromReader(const NativeObject* reader) { MOZ_ASSERT(ReaderHasStream(reader)); - return &reader->getFixedSlot(ReaderSlot_Stream).toObject().as(); + return UnwrapInternalSlotObject( + &reader->getFixedSlot(ReaderSlot_Stream).toObject()); } inline static MOZ_MUST_USE NativeObject* ReaderFromStream(const NativeObject* stream) { Value readerVal = stream->getFixedSlot(StreamSlot_Reader); - MOZ_ASSERT(JS::IsReadableStreamReader(&readerVal.toObject())); - return &readerVal.toObject().as(); + NativeObject* reader = UnwrapInternalSlotObject(&readerVal.toObject()); + MOZ_ASSERT(JS::IsReadableStreamReader(reader)); + return reader; } inline static bool @@ -305,26 +371,55 @@ RejectNonGenericMethod(JSContext* cx, const CallArgs& args, return ReturnPromiseRejectedWithPendingError(cx, args); } -inline static MOZ_MUST_USE NativeObject* +[[nodiscard]] static bool +ResolveMaybeWrappedPromise(JSContext* cx, HandleObject promise, HandleValue value) +{ + RootedObject wrappedPromise(cx, promise); + RootedValue wrappedValue(cx, value); + if (!JS_WrapObject(cx, &wrappedPromise) || !JS_WrapValue(cx, &wrappedValue)) + return false; + return ResolvePromise(cx, wrappedPromise, wrappedValue); +} + +[[nodiscard]] static bool +RejectMaybeWrappedPromise(JSContext* cx, HandleObject promise, HandleValue reason) +{ + RootedObject wrappedPromise(cx, promise); + RootedValue wrappedReason(cx, reason); + if (!JS_WrapObject(cx, &wrappedPromise) || !JS_WrapValue(cx, &wrappedReason)) + return false; + return RejectPromise(cx, wrappedPromise, wrappedReason); +} + +[[nodiscard]] inline static NativeObject* SetNewList(JSContext* cx, HandleNativeObject container, uint32_t slot) { - NativeObject* list = NewObjectWithNullTaggedProto(cx); - if (!list) - return nullptr; - container->setFixedSlot(slot, ObjectValue(*list)); + RootedNativeObject list(cx); + { + JSAutoCompartment ac(cx, container); + list = NewObjectWithNullTaggedProto(cx); + if (!list) + return nullptr; + container->setFixedSlot(slot, ObjectValue(*list)); + } return list; } inline static MOZ_MUST_USE bool AppendToList(JSContext* cx, HandleNativeObject list, HandleValue value) { + JSAutoCompartment ac(cx, list); + RootedValue wrappedValue(cx, value); + if (!JS_WrapValue(cx, &wrappedValue)) + return false; + uint32_t length = list->getDenseInitializedLength(); if (!list->ensureElements(cx, length + 1)) return false; list->ensureDenseInitializedLength(cx, length, 1); - list->setDenseElement(length, value); + list->setDenseElement(length, wrappedValue); return true; } @@ -341,6 +436,7 @@ template inline static MOZ_MUST_USE T* ShiftFromList(JSContext* cx, HandleNativeObject list) { + JSAutoCompartment ac(cx, list); uint32_t length = list->getDenseInitializedLength(); MOZ_ASSERT(length > 0); @@ -915,30 +1011,79 @@ ReadableStream_getReader(JSContext* cx, unsigned argc, Value* vp) return CallNonGenericMethod, ReadableStream_getReader_impl>(cx, args); } +[[nodiscard]] static bool +CallStreamExtra(JSContext* cx, const char* name, HandleValue streamVal, + HandleValue arg0, HandleValue arg1, MutableHandleValue rval) +{ + RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + if (!global) + return false; + + RootedValue fun(cx); + if (!JS_GetProperty(cx, global, name, &fun)) + return false; + + if (!fun.isObject() || !IsCallable(fun)) { + if (!InitStreamExtras(cx, global)) + return false; + if (!JS_GetProperty(cx, global, name, &fun)) + return false; + } + + if (!fun.isObject() || !IsCallable(fun)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, name); + return false; + } + + FixedInvokeArgs<3> invokeArgs(cx); + invokeArgs[0].set(streamVal); + invokeArgs[1].set(arg0); + invokeArgs[2].set(arg1); + return Call(cx, fun, UndefinedHandleValue, invokeArgs, rval); +} + // Streams spec, 3.2.4.4. pipeThrough({ writable, readable }, options) -static MOZ_MUST_USE bool +[[nodiscard]] static bool ReadableStream_pipeThrough(JSContext* cx, unsigned argc, Value* vp) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, "pipeThrough"); - return false; - // // Step 1: Perform ? Invoke(this, "pipeTo", « writable, options »). + CallArgs args = CallArgsFromVp(argc, vp); + if (!Is(args.thisv())) { + ReportValueError3(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(), + nullptr, "pipeThrough", ""); + return false; + } - // // Step 2: Return readable. - // return readable; + RootedValue rval(cx); + if (!CallStreamExtra(cx, "__uxpStreamsPipeThrough", args.thisv(), + args.get(0), args.get(1), &rval)) + return false; + + args.rval().set(rval); + return true; } // Streams spec, 3.2.4.5. pipeTo(dest, { preventClose, preventAbort, preventCancel } = {}) -// TODO: Unimplemented since spec is not complete yet. -static MOZ_MUST_USE bool +[[nodiscard]] static bool ReadableStream_pipeTo(JSContext* cx, unsigned argc, Value* vp) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, "pipeTo"); - return false; + CallArgs args = CallArgsFromVp(argc, vp); + if (!Is(args.thisv())) { + ReportValueError3(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(), + nullptr, "pipeTo", ""); + return false; + } + + RootedValue rval(cx); + if (!CallStreamExtra(cx, "__uxpStreamsPipeTo", args.thisv(), + args.get(0), args.get(1), &rval)) + return false; + + args.rval().set(rval); + return true; } -static MOZ_MUST_USE bool +[[nodiscard]] static bool ReadableStreamTee(JSContext* cx, Handle stream, bool cloneForBranch2, MutableHandle branch1, MutableHandle branch2); @@ -1313,7 +1458,8 @@ ReadableStreamAddReadIntoRequest(JSContext* cx, Handle stream) { // Step 1: MOZ_ASSERT: ! IsReadableStreamBYOBReader(stream.[[reader]]) is true. RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader)); - RootedNativeObject reader(cx, &val.toObject().as()); + RootedNativeObject reader(cx, ReaderFromStream(stream)); + MOZ_ASSERT(reader->is()); // Step 2: MOZ_ASSERT: stream.[[state]] is "readable" or "closed". MOZ_ASSERT(stream->readable() || stream->closed()); @@ -1326,7 +1472,8 @@ ReadableStreamAddReadIntoRequest(JSContext* cx, Handle stream) // Step 4: Let readIntoRequest be Record {[[promise]]: promise}. // Step 5: Append readIntoRequest as the last element of stream.[[reader]].[[readIntoRequests]]. val = reader->getFixedSlot(ReaderSlot_Requests); - RootedNativeObject readIntoRequests(cx, &val.toObject().as()); + RootedNativeObject readIntoRequests(cx); + readIntoRequests = UnwrapInternalSlotObject(&val.toObject()); // Since [[promise]] is the Record's only field, we store it directly. val = ObjectValue(*promise); if (!AppendToList(cx, readIntoRequests, val)) @@ -1356,7 +1503,8 @@ ReadableStreamAddReadRequest(JSContext* cx, Handle stream) // Step 4: Let readRequest be Record {[[promise]]: promise}. // Step 5: Append readRequest as the last element of stream.[[reader]].[[readRequests]]. RootedValue val(cx, reader->getFixedSlot(ReaderSlot_Requests)); - RootedNativeObject readRequests(cx, &val.toObject().as()); + RootedNativeObject readRequests(cx); + readRequests = UnwrapInternalSlotObject(&val.toObject()); // Since [[promise]] is the Record's only field, we store it directly. val = ObjectValue(*promise); @@ -1400,6 +1548,8 @@ ReadableStream::cancel(JSContext* cx, Handle stream, HandleValu // with stream.[[storedError]]. if (stream->errored()) { RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError)); + if (!JS_WrapValue(cx, &storedError)) + return nullptr; return PromiseObject::unforgeableReject(cx, storedError); } @@ -1443,13 +1593,14 @@ ReadableStreamCloseInternal(JSContext* cx, Handle stream) return true; // Step 5: If ! IsReadableStreamDefaultReader(reader) is true, - RootedNativeObject reader(cx, &val.toObject().as()); + RootedNativeObject reader(cx, ReaderFromStream(stream)); if (reader->is()) { // Step a: Repeat for each readRequest that is an element of // reader.[[readRequests]], val = reader->getFixedSlot(ReaderSlot_Requests); if (!val.isUndefined()) { - RootedNativeObject readRequests(cx, &val.toObject().as()); + RootedNativeObject readRequests(cx); + readRequests = UnwrapInternalSlotObject(&val.toObject()); uint32_t len = readRequests->getDenseInitializedLength(); RootedObject readRequest(cx); RootedObject resultObj(cx); @@ -1462,7 +1613,7 @@ ReadableStreamCloseInternal(JSContext* cx, Handle stream) if (!resultObj) return false; resultVal = ObjectValue(*resultObj); - if (!ResolvePromise(cx, readRequest, resultVal)) + if (!ResolveMaybeWrappedPromise(cx, readRequest, resultVal)) return false; } @@ -1474,7 +1625,7 @@ ReadableStreamCloseInternal(JSContext* cx, Handle stream) // Step 6: Resolve reader.[[closedPromise]] with undefined. // Step 7: Return (implicit). RootedObject closedPromise(cx, &reader->getFixedSlot(ReaderSlot_ClosedPromise).toObject()); - if (!ResolvePromise(cx, closedPromise, UndefinedHandleValue)) + if (!ResolveMaybeWrappedPromise(cx, closedPromise, UndefinedHandleValue)) return false; if (stream->mode() == JS::ReadableStreamMode::ExternalSource && @@ -1502,7 +1653,13 @@ ReadableStreamErrorInternal(JSContext* cx, Handle stream, Handl SetStreamState(stream, (state & ReadableStream::Disturbed) | ReadableStream::Errored); // Step 4: Set stream.[[storedError]] to e. - stream->setFixedSlot(StreamSlot_StoredError, e); + RootedValue storedError(cx, e); + { + JSAutoCompartment ac(cx, stream); + if (!JS_WrapValue(cx, &storedError)) + return false; + stream->setFixedSlot(StreamSlot_StoredError, storedError); + } // Step 5: Let reader be stream.[[reader]]. RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader)); @@ -1510,20 +1667,21 @@ ReadableStreamErrorInternal(JSContext* cx, Handle stream, Handl // Step 6: If reader is undefined, return. if (val.isUndefined()) return true; - RootedNativeObject reader(cx, &val.toObject().as()); + RootedNativeObject reader(cx, ReaderFromStream(stream)); // Steps 7,8: (Identical in our implementation.) // Step a: Repeat for each readRequest that is an element of // reader.[[readRequests]], val = reader->getFixedSlot(ReaderSlot_Requests); - RootedNativeObject readRequests(cx, &val.toObject().as()); - Rooted readRequest(cx); + RootedNativeObject readRequests(cx); + readRequests = UnwrapInternalSlotObject(&val.toObject()); + RootedObject readRequest(cx); uint32_t len = readRequests->getDenseInitializedLength(); for (uint32_t i = 0; i < len; i++) { // Step i: Reject readRequest.[[promise]] with e. val = readRequests->getDenseElement(i); - readRequest = &val.toObject().as(); - if (!PromiseObject::reject(cx, readRequest, e)) + readRequest = &val.toObject(); + if (!RejectMaybeWrappedPromise(cx, readRequest, e)) return false; } @@ -1533,8 +1691,8 @@ ReadableStreamErrorInternal(JSContext* cx, Handle stream, Handl // Step 9: Reject reader.[[closedPromise]] with e. val = reader->getFixedSlot(ReaderSlot_ClosedPromise); - Rooted closedPromise(cx, &val.toObject().as()); - if (!PromiseObject::reject(cx, closedPromise, e)) + RootedObject closedPromise(cx, &val.toObject()); + if (!RejectMaybeWrappedPromise(cx, closedPromise, e)) return false; if (stream->mode() == JS::ReadableStreamMode::ExternalSource && @@ -1558,7 +1716,7 @@ ReadableStreamFulfillReadOrReadIntoRequest(JSContext* cx, HandlegetFixedSlot(StreamSlot_Reader)); - RootedNativeObject reader(cx, &val.toObject().as()); + RootedNativeObject reader(cx, ReaderFromStream(stream)); // Step 2: Let readIntoRequest be the first element of // reader.[[readIntoRequests]]. @@ -1566,7 +1724,8 @@ ReadableStreamFulfillReadOrReadIntoRequest(JSContext* cx, HandlegetFixedSlot(ReaderSlot_Requests); - RootedNativeObject readIntoRequests(cx, &val.toObject().as()); + RootedNativeObject readIntoRequests(cx); + readIntoRequests = UnwrapInternalSlotObject(&val.toObject()); Rooted readIntoRequest(cx); readIntoRequest = ShiftFromList(cx, readIntoRequests); MOZ_ASSERT(readIntoRequest); @@ -1577,7 +1736,8 @@ ReadableStreamFulfillReadOrReadIntoRequest(JSContext* cx, HandlegetFixedSlot(ReaderSlot_Requests); - return readRequests.toObject().as().getDenseInitializedLength(); + NativeObject* requests = UnwrapInternalSlotObject(&readRequests.toObject()); + return requests->getDenseInitializedLength(); } // Stream spec 3.4.10. ReadableStreamHasBYOBReader ( stream ) @@ -1604,7 +1765,10 @@ ReadableStreamHasBYOBReader(ReadableStream* stream) // Step 3: If ! IsReadableStreamBYOBReader(reader) is false, return false. // Step 4: Return true. Value reader = stream->getFixedSlot(StreamSlot_Reader); - return reader.isObject() && reader.toObject().is(); + if (!reader.isObject()) + return false; + JSObject* unwrapped = UncheckedUnwrap(&reader.toObject()); + return unwrapped && unwrapped->is(); } // Streap spec 3.4.11. ReadableStreamHasDefaultReader ( stream ) @@ -1616,7 +1780,10 @@ ReadableStreamHasDefaultReader(ReadableStream* stream) // Step 3: If ! ReadableStreamDefaultReader(reader) is false, return false. // Step 4: Return true. Value reader = stream->getFixedSlot(StreamSlot_Reader); - return reader.isObject() && reader.toObject().is(); + if (!reader.isObject()) + return false; + JSObject* unwrapped = UncheckedUnwrap(&reader.toObject()); + return unwrapped && unwrapped->is(); } static MOZ_MUST_USE bool @@ -1663,14 +1830,13 @@ ReadableStreamDefaultReader::constructor(JSContext* cx, unsigned argc, Value* vp return false; // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception. - if (!Is(args.get(0))) { + Rooted stream(cx, UnwrapMaybeWrappedArg(args.get(0))); + if (!stream) { ReportArgTypeError(cx, "ReadableStreamDefaultReader", "ReadableStream", args.get(0)); return false; } - Rooted stream(cx, &args.get(0).toObject().as()); - RootedObject reader(cx, CreateReadableStreamDefaultReader(cx, stream)); if (!reader) return false; @@ -1680,36 +1846,40 @@ ReadableStreamDefaultReader::constructor(JSContext* cx, unsigned argc, Value* vp } // Streams spec, 3.5.4.1 get closed -static MOZ_MUST_USE bool -ReadableStreamDefaultReader_closed(JSContext* cx, unsigned argc, Value* vp) +[[nodiscard]] static bool +ReadableStreamDefaultReader_closed_impl(JSContext* cx, const CallArgs& args) { - CallArgs args = CallArgsFromVp(argc, vp); - - // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise - // rejected with a TypeError exception. - if (!Is(args.thisv())) - return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "get closed"); - // Step 2: Return this.[[closedPromise]]. NativeObject* reader = &args.thisv().toObject().as(); args.rval().set(reader->getFixedSlot(ReaderSlot_ClosedPromise)); return true; } -static MOZ_MUST_USE JSObject* -ReadableStreamReaderGenericCancel(JSContext* cx, HandleNativeObject reader, HandleValue reason); - -// Streams spec, 3.5.4.2. cancel ( reason ) -static MOZ_MUST_USE bool -ReadableStreamDefaultReader_cancel(JSContext* cx, unsigned argc, Value* vp) +static bool +ReadableStreamDefaultReader_closed(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise // rejected with a TypeError exception. - if (!Is(args.thisv())) - return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "cancel"); + if (!Is(args.thisv())) { + if (IsMaybeWrapped(args.thisv())) { + return CallNonGenericMethod, + ReadableStreamDefaultReader_closed_impl>(cx, args); + } + return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "get closed"); + } + return ReadableStreamDefaultReader_closed_impl(cx, args); +} + +static MOZ_MUST_USE JSObject* +ReadableStreamReaderGenericCancel(JSContext* cx, HandleNativeObject reader, HandleValue reason); + +// Streams spec, 3.5.4.2. cancel ( reason ) +[[nodiscard]] static bool +ReadableStreamDefaultReader_cancel_impl(JSContext* cx, const CallArgs& args) +{ // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise // rejected with a TypeError exception. RootedNativeObject reader(cx, &args.thisv().toObject().as()); @@ -1727,17 +1897,28 @@ ReadableStreamDefaultReader_cancel(JSContext* cx, unsigned argc, Value* vp) return true; } -// Streams spec, 3.5.4.3 read ( ) -static MOZ_MUST_USE bool -ReadableStreamDefaultReader_read(JSContext* cx, unsigned argc, Value* vp) +static bool +ReadableStreamDefaultReader_cancel(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise // rejected with a TypeError exception. - if (!Is(args.thisv())) - return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "read"); + if (!Is(args.thisv())) { + if (IsMaybeWrapped(args.thisv())) { + return CallNonGenericMethod, + ReadableStreamDefaultReader_cancel_impl>(cx, args); + } + return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "cancel"); + } + return ReadableStreamDefaultReader_cancel_impl(cx, args); +} + +// Streams spec, 3.5.4.3 read ( ) +[[nodiscard]] static bool +ReadableStreamDefaultReader_read_impl(JSContext* cx, const CallArgs& args) +{ // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise // rejected with a TypeError exception. Rooted reader(cx); @@ -1756,7 +1937,25 @@ ReadableStreamDefaultReader_read(JSContext* cx, unsigned argc, Value* vp) return true; } -static MOZ_MUST_USE bool +static bool +ReadableStreamDefaultReader_read(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise + // rejected with a TypeError exception. + if (!Is(args.thisv())) { + if (IsMaybeWrapped(args.thisv())) { + return CallNonGenericMethod, + ReadableStreamDefaultReader_read_impl>(cx, args); + } + return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "read"); + } + + return ReadableStreamDefaultReader_read_impl(cx, args); +} + +[[nodiscard]] static bool ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader); // Streams spec, 3.5.4.4. releaseLock ( ) @@ -1811,8 +2010,7 @@ static const JSPropertySpec ReadableStreamDefaultReader_properties[] = { JS_PS_END }; -CLASS_SPEC(ReadableStreamDefaultReader, 1, ReaderSlotCount, ClassSpec::DontDefineConstructor, 0, - JS_NULL_CLASS_OPS); +CLASS_SPEC(ReadableStreamDefaultReader, 1, ReaderSlotCount, 0, 0, JS_NULL_CLASS_OPS); // Streams spec, 3.6.3 new ReadableStreamBYOBReader ( stream ) @@ -1862,12 +2060,12 @@ ReadableStreamBYOBReader::constructor(JSContext* cx, unsigned argc, Value* vp) return false; // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception. - if (!Is(args.get(0))) { + Rooted stream(cx, UnwrapMaybeWrappedArg(args.get(0))); + if (!stream) { ReportArgTypeError(cx, "ReadableStreamBYOBReader", "ReadableStream", args.get(0)); return false; } - Rooted stream(cx, &args.get(0).toObject().as()); RootedObject reader(cx, CreateReadableStreamBYOBReader(cx, stream)); if (!reader) return false; @@ -2026,7 +2224,7 @@ static const JSFunctionSpec ReadableStreamBYOBReader_methods[] = { JS_FS_END }; -CLASS_SPEC(ReadableStreamBYOBReader, 1, 3, ClassSpec::DontDefineConstructor, 0, JS_NULL_CLASS_OPS); +CLASS_SPEC(ReadableStreamBYOBReader, 1, 3, 0, 0, JS_NULL_CLASS_OPS); inline static MOZ_MUST_USE bool ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject controller); @@ -2047,7 +2245,21 @@ ReadableStreamReaderGenericCancel(JSContext* cx, HandleNativeObject reader, Hand // Step 2: Assert: stream is not undefined (implicit). // Step 3: Return ! ReadableStreamCancel(stream, reason). - return &ReadableStreamCancel(cx, stream, reason)->as(); + RootedObject cancelPromise(cx); + { + JSAutoCompartment ac(cx, stream); + RootedValue wrappedReason(cx, reason); + if (!JS_WrapValue(cx, &wrappedReason)) + return nullptr; + + cancelPromise = ReadableStreamCancel(cx, stream, wrappedReason); + if (!cancelPromise) + return nullptr; + } + + if (!JS_WrapObject(cx, &cancelPromise)) + return nullptr; + return cancelPromise; } // Streams spec, 3.7.4. ReadableStreamReaderGenericInitialize ( reader, stream ) @@ -2056,10 +2268,19 @@ ReadableStreamReaderGenericInitialize(JSContext* cx, HandleNativeObject reader, Handle stream) { // Step 1: Set reader.[[ownerReadableStream]] to stream. - reader->setFixedSlot(ReaderSlot_Stream, ObjectValue(*stream)); + RootedValue streamVal(cx, ObjectValue(*stream)); + if (!JS_WrapValue(cx, &streamVal)) + return false; + reader->setFixedSlot(ReaderSlot_Stream, streamVal); // Step 2: Set stream.[[reader]] to reader. - stream->setFixedSlot(StreamSlot_Reader, ObjectValue(*reader)); + RootedValue readerVal(cx, ObjectValue(*reader)); + { + JSAutoCompartment ac(cx, stream); + if (!JS_WrapValue(cx, &readerVal)) + return false; + stream->setFixedSlot(StreamSlot_Reader, readerVal); + } // Step 3: If stream.[[state]] is "readable", RootedObject promise(cx); @@ -2080,6 +2301,8 @@ ReadableStreamReaderGenericInitialize(JSContext* cx, HandleNativeObject reader, // Step ii: Set reader.[[closedPromise]] to a new promise rejected with // stream.[[storedError]]. RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError)); + if (!JS_WrapValue(cx, &storedError)) + return false; promise = PromiseObject::unforgeableReject(cx, storedError); } @@ -2098,7 +2321,7 @@ ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader) Rooted stream(cx, StreamFromReader(reader)); // Step 2: Assert: reader.[[ownerReadableStream]].[[reader]] is reader. - MOZ_ASSERT(&stream->getFixedSlot(StreamSlot_Reader).toObject() == reader); + MOZ_ASSERT(ReaderFromStream(stream) == reader); // Create an exception to reject promises with below. We don't have a // clean way to do this, unfortunately. @@ -2156,6 +2379,8 @@ ReadableStreamBYOBReader::read(JSContext* cx, Handle // stream.[[storedError]]. if (stream->errored()) { RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError)); + if (!JS_WrapValue(cx, &storedError)) + return nullptr; return PromiseObject::unforgeableReject(cx, storedError); } @@ -2347,40 +2572,12 @@ CreateReadableStreamDefaultController(JSContext* cx, Handle str bool ReadableStreamDefaultController::constructor(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - - if (!ThrowIfNotConstructing(cx, args, "ReadableStreamDefaultController")) - return false; - - // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception. - HandleValue streamVal = args.get(0); - if (!Is(streamVal)) { - ReportArgTypeError(cx, "ReadableStreamDefaultController", "ReadableStream", - args.get(0)); - return false; - } - - Rooted stream(cx, &streamVal.toObject().as()); - - // Step 2: If stream.[[readableStreamController]] is not undefined, throw a - // TypeError exception. - if (HasController(stream)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_READABLESTREAM_CONTROLLER_SET); - return false; - } - - // Steps 3-11. - RootedObject controller(cx, CreateReadableStreamDefaultController(cx, stream, args.get(1), - args.get(2), args.get(3))); - if (!controller) - return false; - - args.rval().setObject(*controller); - return true; + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "ReadableStreamDefaultController"); + return false; } -static MOZ_MUST_USE double +[[nodiscard]] static double ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller); // Streams spec, 3.8.4.1. get desiredSize @@ -2390,7 +2587,8 @@ static MOZ_MUST_USE bool ReadableStreamController_desiredSize_impl(JSContext* cx, const CallArgs& args) { RootedNativeObject controller(cx); - controller = &args.thisv().toObject().as(); + controller = UnwrapMaybeWrappedReadableStreamController(args.thisv()); + MOZ_ASSERT(controller); // Streams spec, 3.9.8. steps 1-4. // 3.9.8. Step 1: Let stream be controller.[[controlledReadableStream]]. @@ -2420,8 +2618,12 @@ ReadableStreamDefaultController_desiredSize(JSContext* cx, unsigned argc, Value* // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a // TypeError exception. CallArgs args = CallArgsFromVp(argc, vp); - return CallNonGenericMethod, - ReadableStreamController_desiredSize_impl>(cx, args); + if (!IsMaybeWrapped(args.thisv())) { + ReportValueError3(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(), + nullptr, "ReadableStreamDefaultController", "get desiredSize"); + return false; + } + return ReadableStreamController_desiredSize_impl(cx, args); } static MOZ_MUST_USE bool @@ -2456,7 +2658,8 @@ static MOZ_MUST_USE bool ReadableStreamDefaultController_close_impl(JSContext* cx, const CallArgs& args) { Rooted controller(cx); - controller = &args.thisv().toObject().as(); + controller = UnwrapMaybeWrappedArg(args.thisv()); + MOZ_ASSERT(controller); // Steps 2-3. if (!VerifyControllerStateForClosing(cx, controller)) @@ -2476,8 +2679,12 @@ ReadableStreamDefaultController_close(JSContext* cx, unsigned argc, Value* vp) // TypeError exception. CallArgs args = CallArgsFromVp(argc, vp); - return CallNonGenericMethod, - ReadableStreamDefaultController_close_impl>(cx, args); + if (!IsMaybeWrapped(args.thisv())) { + ReportValueError3(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(), + nullptr, "ReadableStreamDefaultController", "close"); + return false; + } + return ReadableStreamDefaultController_close_impl(cx, args); } static MOZ_MUST_USE bool @@ -2490,7 +2697,8 @@ static MOZ_MUST_USE bool ReadableStreamDefaultController_enqueue_impl(JSContext* cx, const CallArgs& args) { Rooted controller(cx); - controller = &args.thisv().toObject().as(); + controller = UnwrapMaybeWrappedArg(args.thisv()); + MOZ_ASSERT(controller); // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception. if (ControllerFlags(controller) & ControllerFlag_CloseRequested) { @@ -2522,8 +2730,12 @@ ReadableStreamDefaultController_enqueue(JSContext* cx, unsigned argc, Value* vp) // TypeError exception. CallArgs args = CallArgsFromVp(argc, vp); - return CallNonGenericMethod, - ReadableStreamDefaultController_enqueue_impl>(cx, args); + if (!IsMaybeWrapped(args.thisv())) { + ReportValueError3(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(), + nullptr, "ReadableStreamDefaultController", "enqueue"); + return false; + } + return ReadableStreamDefaultController_enqueue_impl(cx, args); } // Streams spec, 3.8.4.4. error ( e ) @@ -2531,7 +2743,8 @@ static MOZ_MUST_USE bool ReadableStreamDefaultController_error_impl(JSContext* cx, const CallArgs& args) { Rooted controller(cx); - controller = &args.thisv().toObject().as(); + controller = UnwrapMaybeWrappedArg(args.thisv()); + MOZ_ASSERT(controller); // Step 2: Let stream be this.[[controlledReadableStream]]. // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception. @@ -2555,8 +2768,12 @@ ReadableStreamDefaultController_error(JSContext* cx, unsigned argc, Value* vp) // TypeError exception. CallArgs args = CallArgsFromVp(argc, vp); - return CallNonGenericMethod, - ReadableStreamDefaultController_error_impl>(cx, args); + if (!IsMaybeWrapped(args.thisv())) { + ReportValueError3(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(), + nullptr, "ReadableStreamDefaultController", "error"); + return false; + } + return ReadableStreamDefaultController_error_impl(cx, args); } static const JSPropertySpec ReadableStreamDefaultController_properties[] = { @@ -2571,8 +2788,7 @@ static const JSFunctionSpec ReadableStreamDefaultController_methods[] = { JS_FS_END }; -CLASS_SPEC(ReadableStreamDefaultController, 4, 7, ClassSpec::DontDefineConstructor, 0, - JS_NULL_CLASS_OPS); +CLASS_SPEC(ReadableStreamDefaultController, 4, 7, 0, 0, JS_NULL_CLASS_OPS); /** * Unified implementation of ReadableStream controllers' [[CancelSteps]] internal @@ -3116,36 +3332,9 @@ ReadableByteStreamController::hasExternalSource() { bool ReadableByteStreamController::constructor(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - - if (!ThrowIfNotConstructing(cx, args, "ReadableByteStreamController")) - return false; - - // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception. - HandleValue streamVal = args.get(0); - if (!Is(streamVal)) { - ReportArgTypeError(cx, "ReadableStreamDefaultController", "ReadableStream", - args.get(0)); - return false; - } - - Rooted stream(cx, &streamVal.toObject().as()); - - // Step 2: If stream.[[readableStreamController]] is not undefined, throw a - // TypeError exception. - if (HasController(stream)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_READABLESTREAM_CONTROLLER_SET); - return false; - } - - RootedObject controller(cx, CreateReadableByteStreamController(cx, stream, args.get(1), - args.get(2))); - if (!controller) - return false; - - args.rval().setObject(*controller); - return true; + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "ReadableByteStreamController"); + return false; } // Version of the ReadableByteStreamConstructor that's specialized for @@ -3451,8 +3640,8 @@ static const ClassOps ReadableByteStreamControllerClassOps = { nullptr, /* trace */ }; -CLASS_SPEC(ReadableByteStreamController, 3, 9, ClassSpec::DontDefineConstructor, - JSCLASS_BACKGROUND_FINALIZE, &ReadableByteStreamControllerClassOps); +CLASS_SPEC(ReadableByteStreamController, 3, 9, 0, JSCLASS_BACKGROUND_FINALIZE, + &ReadableByteStreamControllerClassOps); // Streams spec, 3.10.5.1. [[PullSteps]] () // Unified with 3.8.5.1 above. @@ -3638,38 +3827,9 @@ CreateReadableStreamBYOBRequest(JSContext* cx, Handle(controllerVal)) { - ReportArgTypeError(cx, "ReadableStreamBYOBRequest", - "ReadableByteStreamController", args.get(0)); - return false; - } - - Rooted controller(cx); - controller = &controllerVal.toObject().as(); - - if (!viewVal.isObject() || !JS_IsArrayBufferViewObject(&viewVal.toObject())) { - ReportArgTypeError(cx, "ReadableStreamBYOBRequest", "ArrayBuffer view", - args.get(1)); - return false; - } - - RootedArrayBufferObject view(cx, &viewVal.toObject().as()); - - RootedObject request(cx, CreateReadableStreamBYOBRequest(cx, controller, view)); - if (!request) - return false; - - args.rval().setObject(*request); - return true; + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "ReadableStreamBYOBRequest"); + return false; } // Streams spec, 3.11.4.1 get view @@ -3804,8 +3964,7 @@ static const JSFunctionSpec ReadableStreamBYOBRequest_methods[] = { JS_FS_END }; -CLASS_SPEC(ReadableStreamBYOBRequest, 3, 2, ClassSpec::DontDefineConstructor, 0, - JS_NULL_CLASS_OPS); +CLASS_SPEC(ReadableStreamBYOBRequest, 3, 2, 0, 0, JS_NULL_CLASS_OPS); // Streams spec, 3.12.1. IsReadableStreamBYOBRequest ( x ) // Implemented via is() @@ -4899,7 +5058,15 @@ js::ByteLengthQueuingStrategy::constructor(JSContext* cx, unsigned argc, Value* { CallArgs args = CallArgsFromVp(argc, vp); - RootedObject strategy(cx, NewBuiltinClassInstance(cx)); + if (!ThrowIfNotConstructing(cx, args, "ByteLengthQueuingStrategy")) + return false; + + RootedObject proto(cx); + RootedObject newTarget(cx, &args.newTarget().toObject()); + if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + return false; + + RootedObject strategy(cx, NewObjectWithClassProto(cx, proto)); if (!strategy) return false; @@ -4945,7 +5112,16 @@ js::CountQueuingStrategy::constructor(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - Rooted strategy(cx, NewBuiltinClassInstance(cx)); + if (!ThrowIfNotConstructing(cx, args, "CountQueuingStrategy")) + return false; + + RootedObject proto(cx); + RootedObject newTarget(cx, &args.newTarget().toObject()); + if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + return false; + + Rooted strategy(cx); + strategy = NewObjectWithClassProto(cx, proto); if (!strategy) return false; @@ -4998,7 +5174,8 @@ DequeueValue(JSContext* cx, HandleNativeObject container, MutableHandleValue chu // Step 2: Assert: queue is not empty. RootedValue val(cx, container->getFixedSlot(QueueContainerSlot_Queue)); - RootedNativeObject queue(cx, &val.toObject().as()); + RootedNativeObject queue(cx); + queue = UnwrapInternalSlotObject(&val.toObject()); MOZ_ASSERT(queue->getDenseInitializedLength() > 0); // Step 3. Let pair be the first element of queue. @@ -5020,7 +5197,10 @@ DequeueValue(JSContext* cx, HandleNativeObject container, MutableHandleValue chu container->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(totalSize)); // Step 7: Return pair.[[value]]. - chunk.set(pair->value()); + RootedValue chunkVal(cx, pair->value()); + if (!JS_WrapValue(cx, &chunkVal)) + return false; + chunk.set(chunkVal); return true; } @@ -5049,19 +5229,27 @@ EnqueueValueWithSize(JSContext* cx, HandleNativeObject container, HandleValue va // Step 4: Append Record {[[value]]: value, [[size]]: size} as the last element // of container.[[queue]]. RootedValue val(cx, container->getFixedSlot(QueueContainerSlot_Queue)); - RootedNativeObject queue(cx, &val.toObject().as()); + RootedNativeObject queue(cx); + queue = UnwrapInternalSlotObject(&val.toObject()); - QueueEntry* entry = QueueEntry::create(cx, value, size); - if (!entry) - return false; - val = ObjectValue(*entry); - if (!AppendToList(cx, queue, val)) - return false; + { + JSAutoCompartment ac(cx, queue); + RootedValue wrappedValue(cx, value); + if (!JS_WrapValue(cx, &wrappedValue)) + return false; - // Step 5: Set container.[[queueTotalSize]] to - // container.[[queueTotalSize]] + size. - double totalSize = container->getFixedSlot(QueueContainerSlot_TotalSize).toNumber(); - container->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(totalSize + size)); + QueueEntry* entry = QueueEntry::create(cx, wrappedValue, size); + if (!entry) + return false; + val = ObjectValue(*entry); + if (!AppendToList(cx, queue, val)) + return false; + + // Step 5: Set container.[[queueTotalSize]] to + // container.[[queueTotalSize]] + size. + double totalSize = container->getFixedSlot(QueueContainerSlot_TotalSize).toNumber(); + container->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(totalSize + size)); + } return true; } @@ -5486,3 +5674,2285 @@ js::UnwrapReadableStream(JSObject* obj) return unwrapped->is() ? unwrapped : nullptr; return nullptr; } + +namespace { + +enum class StreamCompressionFormat { + Deflate, + DeflateRaw, + Gzip +}; + +struct StreamCompressionContext +{ + z_stream mStream; + StreamCompressionFormat mFormat; + bool mCompress; + bool mFinished; + bool mInitialized; + + StreamCompressionContext(StreamCompressionFormat format, bool compress) + : mFormat(format), + mCompress(compress), + mFinished(false), + mInitialized(false) + { + memset(&mStream, 0, sizeof(mStream)); + } + + ~StreamCompressionContext() + { + if (!mInitialized) + return; + if (mCompress) + deflateEnd(&mStream); + else + inflateEnd(&mStream); + } +}; + +static int +WindowBitsForFormat(StreamCompressionFormat format) +{ + switch (format) { + case StreamCompressionFormat::Deflate: + return MAX_WBITS; + case StreamCompressionFormat::DeflateRaw: + return -MAX_WBITS; + case StreamCompressionFormat::Gzip: + return MAX_WBITS + 16; + } + MOZ_CRASH("Unknown compression stream format"); +} + +static bool +ParseCompressionFormat(JSContext* cx, HandleValue value, + StreamCompressionFormat* format) +{ + RootedString str(cx, ToString(cx, value)); + if (!str) + return false; + + bool match = false; + if (!JS_StringEqualsAscii(cx, str, "deflate", &match)) + return false; + if (match) { + *format = StreamCompressionFormat::Deflate; + return true; + } + + if (!JS_StringEqualsAscii(cx, str, "deflate-raw", &match)) + return false; + if (match) { + *format = StreamCompressionFormat::DeflateRaw; + return true; + } + + if (!JS_StringEqualsAscii(cx, str, "gzip", &match)) + return false; + if (match) { + *format = StreamCompressionFormat::Gzip; + return true; + } + + if (!JS_StringEqualsAscii(cx, str, "brotli", &match)) + return false; + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, + "CompressionStream", "supported format", + match ? "unsupported brotli format" : "unknown format"); + return false; +} + +static void +StreamCompressionContextFinalize(FreeOp* fop, JSObject* obj) +{ + auto* context = static_cast(JS_GetPrivate(obj)); + if (context) + js_delete(context); +} + +static const ClassOps StreamCompressionContextClassOps = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + StreamCompressionContextFinalize, + nullptr, /* call */ + nullptr, /* hasInstance */ + nullptr, /* construct */ + nullptr /* trace */ +}; + +static const Class StreamCompressionContextClass = { + "StreamCompressionContext", + JSCLASS_HAS_PRIVATE, + &StreamCompressionContextClassOps +}; + +static bool +CreateStreamCompressionContext(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 2) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_MORE_ARGS_NEEDED, + "__uxpCreateCompressionContext", "1", "s"); + return false; + } + + StreamCompressionFormat format; + if (!ParseCompressionFormat(cx, args.get(0), &format)) + return false; + + bool compress = ToBoolean(args.get(1)); + auto* context = js_new(format, compress); + if (!context) { + ReportOutOfMemory(cx); + return false; + } + + int windowBits = WindowBitsForFormat(format); + int zerr = compress + ? deflateInit2(&context->mStream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + windowBits, 8, Z_DEFAULT_STRATEGY) + : inflateInit2(&context->mStream, windowBits); + if (zerr != Z_OK) { + js_delete(context); + JS_ReportErrorASCII(cx, "Failed to initialize compression stream"); + return false; + } + context->mInitialized = true; + + RootedObject obj(cx, NewObjectWithGivenProto(cx, &StreamCompressionContextClass, nullptr)); + if (!obj) { + js_delete(context); + return false; + } + + JS_SetPrivate(obj, context); + args.rval().setObject(*obj); + return true; +} + +static bool +GetBufferSourceBytes(JSContext* cx, HandleValue value, Vector* bytes) +{ + if (!value.isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, + "CompressionStream", "BufferSource", + InformalValueTypeName(value)); + return false; + } + + RootedObject obj(cx, &value.toObject()); + if (JS_IsArrayBufferViewObject(obj)) { + uint32_t length = JS_GetArrayBufferViewByteLength(obj); + if (length == 0) + return true; + + JS::AutoCheckCannotGC nogc(cx); + bool isShared; + uint8_t* data = static_cast(JS_GetArrayBufferViewData(obj, &isShared, nogc)); + return bytes->append(data, length); + } + + if (JS_IsArrayBufferObject(obj)) { + uint32_t length = JS_GetArrayBufferByteLength(obj); + if (length == 0) + return true; + + JS::AutoCheckCannotGC nogc(cx); + bool isShared; + uint8_t* data = JS_GetArrayBufferData(obj, &isShared, nogc); + return bytes->append(data, length); + } + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, + "CompressionStream", "BufferSource", + obj->getClass()->name); + return false; +} + +static bool +AppendUint8Array(JSContext* cx, HandleObject array, uint32_t* index, + const uint8_t* data, size_t length) +{ + if (length == 0) + return true; + + if (length > UINT32_MAX) { + ReportAllocationOverflow(cx); + return false; + } + + RootedObject view(cx, JS_NewUint8Array(cx, length)); + if (!view) + return false; + + { + JS::AutoCheckCannotGC nogc(cx); + bool isShared; + uint8_t* viewData = static_cast(JS_GetArrayBufferViewData(view, &isShared, nogc)); + memcpy(viewData, data, length); + } + + RootedValue viewVal(cx, ObjectValue(*view)); + return JS_SetElement(cx, array, (*index)++, viewVal); +} + +static bool +StreamCompressionProcess(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 3 || !args[0].isObject() || + !args[0].toObject().hasClass(&StreamCompressionContextClass)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, + "__uxpCompressionProcess", + "StreamCompressionContext", "other"); + return false; + } + + auto* context = + static_cast(JS_GetPrivate(&args[0].toObject())); + if (!context) { + JS_ReportErrorASCII(cx, "Compression stream is already finished"); + return false; + } + + bool finish = ToBoolean(args.get(2)); + + if (context->mFinished) { + if (finish && args.get(1).isUndefined()) { + RootedObject emptyChunks(cx, JS_NewArrayObject(cx, 0)); + if (!emptyChunks) + return false; + args.rval().setObject(*emptyChunks); + return true; + } + JS_ReportErrorASCII(cx, "Compression stream is already finished"); + return false; + } + + Vector input(cx); + if (!args.get(1).isUndefined() && !GetBufferSourceBytes(cx, args.get(1), &input)) + return false; + + RootedObject chunks(cx, JS_NewArrayObject(cx, 0)); + if (!chunks) + return false; + + z_stream& stream = context->mStream; + stream.next_in = input.empty() ? nullptr : input.begin(); + stream.avail_in = input.length(); + + uint32_t chunkIndex = 0; + const int flush = finish ? Z_FINISH : Z_NO_FLUSH; + bool madeProgress; + + do { + uint8_t output[16384]; + stream.next_out = output; + stream.avail_out = sizeof(output); + + uint32_t beforeIn = stream.avail_in; + uint32_t beforeOut = stream.avail_out; + int zerr = context->mCompress ? deflate(&stream, flush) : inflate(&stream, flush); + + size_t produced = sizeof(output) - stream.avail_out; + if (!AppendUint8Array(cx, chunks, &chunkIndex, output, produced)) + return false; + + madeProgress = produced > 0 || stream.avail_in != beforeIn || + stream.avail_out != beforeOut; + + if (zerr == Z_STREAM_END) { + context->mFinished = true; + if (!context->mCompress && stream.avail_in != 0) { + JS_ReportErrorASCII(cx, "Compressed input has trailing data"); + return false; + } + break; + } + + if (zerr == Z_BUF_ERROR && !madeProgress) + break; + + if (zerr != Z_OK && zerr != Z_BUF_ERROR) { + JS_ReportErrorASCII(cx, stream.msg ? stream.msg : "Compression stream error"); + return false; + } + } while (stream.avail_out == 0 || stream.avail_in != 0 || + (finish && !context->mFinished && madeProgress)); + + if (finish && !context->mFinished) { + JS_ReportErrorASCII(cx, "Compressed input ended before the end of stream"); + return false; + } + + args.rval().setObject(*chunks); + return true; +} + +bool +ReleaseReadableStreamReaderSilently(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(true); + + if (!args.get(0).isObject()) + return true; + + RootedObject readerObj(cx, &args[0].toObject()); + if (!readerObj->is() && + !readerObj->is()) + return true; + + RootedNativeObject reader(cx, &readerObj->as()); + if (!ReaderHasStream(reader)) + return true; + + Value requestsVal = reader->getFixedSlot(ReaderSlot_Requests); + if (!requestsVal.isUndefined()) { + NativeObject* requests = &requestsVal.toObject().as(); + if (requests->getDenseInitializedLength() != 0) { + args.rval().setBoolean(false); + return true; + } + } + + Rooted stream(cx, StreamFromReader(reader)); + MOZ_ASSERT(&stream->getFixedSlot(StreamSlot_Reader).toObject() == reader); + + RootedObject closedPromise(cx, PromiseObject::unforgeableResolve(cx, UndefinedHandleValue)); + if (!closedPromise) + return false; + reader->setFixedSlot(ReaderSlot_ClosedPromise, ObjectValue(*closedPromise)); + + stream->setFixedSlot(StreamSlot_Reader, UndefinedValue()); + reader->setFixedSlot(ReaderSlot_Stream, UndefinedValue()); + return true; +} + +} // anonymous namespace + +bool +js::InitStreamExtras(JSContext* cx, HandleObject global) +{ + if (!JS_DefineFunction(cx, global, "__uxpCreateCompressionContext", + CreateStreamCompressionContext, 2, 0)) + return false; + + if (!JS_DefineFunction(cx, global, "__uxpCompressionProcess", + StreamCompressionProcess, 3, 0)) + return false; + + if (!JS_DefineFunction(cx, global, "__uxpReleaseReadableStreamReaderSilently", + ReleaseReadableStreamReaderSilently, 1, 0)) + return false; + + static const char source[] = + R"JS( +(function(global) { + "use strict"; + + if (global.__uxpStreamsInitialized) + return; + Object.defineProperty(global, "__uxpStreamsInitialized", + { value: true, configurable: true }); + + var objectToString = Object.prototype.toString; + var hasOwn = Object.prototype.hasOwnProperty; + + function isObject(value) { + return (typeof value === "object" && value !== null) || + typeof value === "function"; + } + + function requiredObject(value, name) { + if (!isObject(value)) + throw new TypeError(name + " is not an object"); + return value; + } + + function newPromiseCapability() { + var resolve, reject; + var promise = new Promise(function(res, rej) { + resolve = res; + reject = rej; + }); + return { promise: promise, resolve: resolve, reject: reject }; + } + + function promiseCall(fn, receiver, args) { + try { + return Promise.resolve(fn.apply(receiver, args || [])); + } catch (e) { + return Promise.reject(e); + } + } + + function getMethod(value, name) { + var method = value[name]; + if (method === undefined || method === null) + return undefined; + if (typeof method !== "function") + throw new TypeError(name + " is not callable"); + return method; + } + + function makeDOMException(message, name) { + if (typeof global.DOMException === "function") + return new global.DOMException(message, name); + var error = new Error(message); + error.name = name; + return error; + } + + function define(name, value) { + if (hasOwn.call(global, name) && global[name] !== undefined) + return; + Object.defineProperty(global, name, { + value: value, + writable: true, + configurable: true + }); + } + + var writableState = new WeakMap(); + var writerState = new WeakMap(); + var writableControllerState = new WeakMap(); + + function isWritableStream(stream) { + return writableState.has(stream); + } + + function isWritableStreamDefaultWriter(writer) { + return writerState.has(writer); + } + + function rejectPromise(error) { + return Promise.reject(error); + } + + function silenceRejection(promise) { + try { + if (promise && typeof promise.catch === "function") + promise.catch(function() {}); + } catch (e) {} + } + + function releaseReaderLock(reader) { + try { + if (global.__uxpReleaseReadableStreamReaderSilently(reader)) + return; + } catch (e) {} + Promise.resolve().then(function() { + try { + if (global.__uxpReleaseReadableStreamReaderSilently(reader)) + return; + } catch (e) {} + try { silenceRejection(reader.closed); } catch (e) {} + try { reader.releaseLock(); } catch (e) {} + }); + } + + function releaseWriterLock(writer) { + try { silenceRejection(writer.closed); } catch (e) {} + try { silenceRejection(writer.ready); } catch (e) {} + try { writer.releaseLock(); } catch (e) {} + } + + function syncWritableWriterWithStream(writer, state) { + var ws = writerState.get(writer); + if (!ws || ws.stream === undefined) + return; + + ws.readyPromise = state.readyPromise; + if (state.state === "closed") { + ws.closedPromise = Promise.resolve(undefined); + } else if (state.state === "errored") { + ws.closedPromise = Promise.reject(state.storedError); + ws.closedPromise.catch(function() {}); + } else { + ws.closedPromise = state.closedPromise; + } + } + + function writableError(stream, error) { + var state = writableState.get(stream); + if (!state || state.state === "closed" || state.state === "errored") + return; + state.state = "errored"; + state.storedError = error; + state.ready.reject(error); + state.closed.reject(error); + state.readyPromise.catch(function() {}); + state.closedPromise.catch(function() {}); + if (state.writer) + syncWritableWriterWithStream(state.writer, state); + } + + function writableClose(stream) { + var state = writableState.get(stream); + if (!state || state.state === "closed") + return; + state.state = "closed"; + state.ready.resolve(undefined); + state.closed.resolve(undefined); + if (state.writer) + syncWritableWriterWithStream(state.writer, state); + } + + function writableStreamWrite(stream, chunk) { + var state = writableState.get(stream); + if (!state) + return rejectPromise(new TypeError("WritableStream expected")); + if (state.state === "closed") + return rejectPromise(new TypeError("WritableStream is closed")); + if (state.state === "errored") + return rejectPromise(state.storedError); + if (state.state === "closing") + return rejectPromise(new TypeError("WritableStream is closing")); + + var size = 1; + try { + size = state.sizeAlgorithm(chunk); + if (!Number.isFinite(size) || size < 0) + throw new RangeError("Invalid chunk size"); + } catch (e) { + writableError(stream, e); + return rejectPromise(e); + } + + state.queueTotalSize += size; + if (state.queueTotalSize > state.highWaterMark && state.backpressure === false) { + state.backpressure = true; + state.ready = newPromiseCapability(); + state.readyPromise = state.ready.promise; + if (state.writer) + syncWritableWriterWithStream(state.writer, state); + } + + var write = state.sink.write; + var controller = state.controller; + var writePromise = state.writeChain.then(function() { + if (state.state === "errored") + throw state.storedError; + if (write) + return write.call(state.sink, chunk, controller); + return undefined; + }); + state.writeChain = writePromise.then(function(value) { + state.queueTotalSize -= size; + if (state.queueTotalSize <= state.highWaterMark && state.backpressure) { + state.backpressure = false; + state.ready.resolve(undefined); + state.ready = newPromiseCapability(); + state.ready.resolve(undefined); + state.readyPromise = state.ready.promise; + if (state.writer) + syncWritableWriterWithStream(state.writer, state); + } + return value; + }, function(error) { + writableError(stream, error); + throw error; + }); + state.writeChain.catch(function() {}); + return writePromise; + } + + function writableStreamClose(stream) { + var state = writableState.get(stream); + if (!state) + return rejectPromise(new TypeError("WritableStream expected")); + if (state.state === "closed") + return rejectPromise(new TypeError("WritableStream is already closed")); + if (state.state === "errored") + return rejectPromise(state.storedError); + if (state.state === "closing") + return rejectPromise(new TypeError("WritableStream is already closing")); + + state.state = "closing"; + var close = state.sink.close; + var closePromise = state.writeChain.then(function() { + if (close) + return close.call(state.sink); + return undefined; + }).then(function(value) { + writableClose(stream); + return value; + }, function(error) { + writableError(stream, error); + throw error; + }); + state.writeChain = closePromise.catch(function() {}); + return closePromise; + } + + function writableStreamAbort(stream, reason) { + var state = writableState.get(stream); + if (!state) + return rejectPromise(new TypeError("WritableStream expected")); + if (state.state === "closed") + return Promise.resolve(undefined); + if (state.state === "errored") + return rejectPromise(state.storedError); + + var abort = state.sink.abort; + state.state = "errored"; + state.storedError = reason; + if (state.abortController) { + try { state.abortController.abort(); } catch (e) {} + } + state.ready.reject(reason); + state.closed.reject(reason); + state.readyPromise.catch(function() {}); + state.closedPromise.catch(function() {}); + if (state.writer) + syncWritableWriterWithStream(state.writer, state); + + var abortPromise = abort ? promiseCall(abort, state.sink, [reason]) + : Promise.resolve(undefined); + return abortPromise; + } + + function WritableStreamDefaultController() { + throw new TypeError("Illegal constructor"); + } + + WritableStreamDefaultController.prototype.error = function(error) { + var state = writableControllerState.get(this); + if (!state) + throw new TypeError("WritableStreamDefaultController expected"); + writableError(state.stream, error); + }; + + try { + Object.defineProperty(WritableStreamDefaultController.prototype, "signal", { + configurable: true, + get: function() { + var state = writableControllerState.get(this); + if (!state) + throw new TypeError("WritableStreamDefaultController expected"); + return state.abortController ? state.abortController.signal : undefined; + } + }); + } catch (e) {} + + Object.defineProperty(WritableStreamDefaultController.prototype, + Symbol.toStringTag, + { value: "WritableStreamDefaultController", + configurable: true }); + + function WritableStream(underlyingSink, strategy) { + if (!(this instanceof WritableStream)) + throw new TypeError("WritableStream must be constructed with new"); + + underlyingSink = underlyingSink === undefined ? {} : requiredObject(underlyingSink, "underlyingSink"); + strategy = strategy === undefined ? {} : requiredObject(strategy, "strategy"); + + if (underlyingSink.type !== undefined) + throw new RangeError("Invalid writable stream type"); + + var highWaterMark = strategy.highWaterMark === undefined ? 1 : Number(strategy.highWaterMark); + if (Number.isNaN(highWaterMark) || highWaterMark < 0) + throw new RangeError("Invalid highWaterMark"); + + var sizeAlgorithm = strategy.size; + if (sizeAlgorithm === undefined) + sizeAlgorithm = function() { return 1; }; + if (typeof sizeAlgorithm !== "function") + throw new TypeError("strategy.size is not callable"); + + var controller = Object.create(WritableStreamDefaultController.prototype); + var abortController = typeof global.AbortController === "function" + ? new global.AbortController() + : undefined; + var ready = newPromiseCapability(); + ready.resolve(undefined); + var closed = newPromiseCapability(); + var state = { + sink: underlyingSink, + controller: controller, + signal: undefined, + state: "writable", + storedError: undefined, + writer: undefined, + abortController: abortController, + writeChain: Promise.resolve(undefined), + ready: ready, + readyPromise: ready.promise, + closed: closed, + closedPromise: closed.promise, + highWaterMark: highWaterMark, + sizeAlgorithm: sizeAlgorithm, + queueTotalSize: 0, + backpressure: false + }; + writableState.set(this, state); + writableControllerState.set(controller, { + stream: this, + abortController: abortController + }); + + var start = underlyingSink.start; + if (start !== undefined) { + if (typeof start !== "function") + throw new TypeError("underlyingSink.start is not callable"); + state.writeChain = promiseCall(start, underlyingSink, [controller]).catch(function(error) { + writableError(this, error); + throw error; + }.bind(this)); + state.writeChain.catch(function() {}); + } + } + + Object.defineProperty(WritableStream.prototype, "locked", { + configurable: true, + get: function() { + var state = writableState.get(this); + if (!state) + throw new TypeError("WritableStream expected"); + return state.writer !== undefined; + } + }); + + WritableStream.prototype.abort = function(reason) { + if (!isWritableStream(this)) + return rejectPromise(new TypeError("WritableStream expected")); + if (writableState.get(this).writer !== undefined) + return rejectPromise(new TypeError("WritableStream is locked")); + return writableStreamAbort(this, reason); + }; + + WritableStream.prototype.close = function() { + if (!isWritableStream(this)) + return rejectPromise(new TypeError("WritableStream expected")); + if (writableState.get(this).writer !== undefined) + return rejectPromise(new TypeError("WritableStream is locked")); + return writableStreamClose(this); + }; + + WritableStream.prototype.getWriter = function() { + if (!isWritableStream(this)) + throw new TypeError("WritableStream expected"); + return new WritableStreamDefaultWriter(this); + }; + + Object.defineProperty(WritableStream.prototype, Symbol.toStringTag, + { value: "WritableStream", configurable: true }); + + function WritableStreamDefaultWriter(stream) { + if (!(this instanceof WritableStreamDefaultWriter)) + throw new TypeError("WritableStreamDefaultWriter must be constructed with new"); + if (!isWritableStream(stream)) + throw new TypeError("WritableStream expected"); + + var state = writableState.get(stream); + if (state.writer !== undefined) + throw new TypeError("WritableStream is locked"); + + state.writer = this; + writerState.set(this, { + stream: stream, + readyPromise: state.readyPromise, + closedPromise: state.closedPromise + }); + } + + Object.defineProperty(WritableStreamDefaultWriter.prototype, "closed", { + configurable: true, + get: function() { + var state = writerState.get(this); + if (!state) + return rejectPromise(new TypeError("WritableStreamDefaultWriter expected")); + return state.closedPromise; + } + }); + + Object.defineProperty(WritableStreamDefaultWriter.prototype, "desiredSize", { + configurable: true, + get: function() { + var state = writerState.get(this); + if (!state) + throw new TypeError("WritableStreamDefaultWriter expected"); + if (state.stream === undefined) + throw new TypeError("Writer lock is released"); + var streamState = writableState.get(state.stream); + if (streamState.state === "errored") + return null; + if (streamState.state === "closed") + return 0; + return streamState.highWaterMark - streamState.queueTotalSize; + } + }); + + Object.defineProperty(WritableStreamDefaultWriter.prototype, "ready", { + configurable: true, + get: function() { + var state = writerState.get(this); + if (!state) + return rejectPromise(new TypeError("WritableStreamDefaultWriter expected")); + return state.readyPromise; + } + }); + + WritableStreamDefaultWriter.prototype.abort = function(reason) { + var state = writerState.get(this); + if (!state || state.stream === undefined) + return rejectPromise(new TypeError("Writer lock is released")); + return writableStreamAbort(state.stream, reason); + }; + + WritableStreamDefaultWriter.prototype.close = function() { + var state = writerState.get(this); + if (!state || state.stream === undefined) + return rejectPromise(new TypeError("Writer lock is released")); + return writableStreamClose(state.stream); + }; + + WritableStreamDefaultWriter.prototype.releaseLock = function() { + var state = writerState.get(this); + if (!state) + throw new TypeError("WritableStreamDefaultWriter expected"); + if (state.stream === undefined) + return; + var streamState = writableState.get(state.stream); + if (streamState.writer === this) + streamState.writer = undefined; + state.stream = undefined; + state.readyPromise = rejectPromise(new TypeError("Writer lock is released")); + state.closedPromise = rejectPromise(new TypeError("Writer lock is released")); + state.readyPromise.catch(function() {}); + state.closedPromise.catch(function() {}); + }; + + WritableStreamDefaultWriter.prototype.write = function(chunk) { + var state = writerState.get(this); + if (!state || state.stream === undefined) + return rejectPromise(new TypeError("Writer lock is released")); + return writableStreamWrite(state.stream, chunk); + }; + +)JS" +R"JS( + + Object.defineProperty(WritableStreamDefaultWriter.prototype, Symbol.toStringTag, + { value: "WritableStreamDefaultWriter", + configurable: true }); + + var transformState = new WeakMap(); + var transformControllerState = new WeakMap(); + + function TransformStreamDefaultController() { + throw new TypeError("Illegal constructor"); + } + + Object.defineProperty(TransformStreamDefaultController.prototype, "desiredSize", { + configurable: true, + get: function() { + var state = transformControllerState.get(this); + if (!state) + throw new TypeError("TransformStreamDefaultController expected"); + return state.readableController.desiredSize; + } + }); + + TransformStreamDefaultController.prototype.enqueue = function(chunk) { + var state = transformControllerState.get(this); + if (!state) + throw new TypeError("TransformStreamDefaultController expected"); + state.readableController.enqueue(chunk); + }; + + TransformStreamDefaultController.prototype.error = function(reason) { + var state = transformControllerState.get(this); + if (!state) + throw new TypeError("TransformStreamDefaultController expected"); + state.readableController.error(reason); + writableError(state.stream.writable, reason); + }; + + TransformStreamDefaultController.prototype.terminate = function() { + var state = transformControllerState.get(this); + if (!state) + throw new TypeError("TransformStreamDefaultController expected"); + try { + state.readableController.close(); + } catch (e) {} + writableError(state.stream.writable, new TypeError("TransformStream terminated")); + }; + + Object.defineProperty(TransformStreamDefaultController.prototype, + Symbol.toStringTag, + { value: "TransformStreamDefaultController", + configurable: true }); + + function errorReadableController(controller, error) { + try { + controller.error(error); + } catch (e) {} + } + + function TransformStream(transformer, writableStrategy, readableStrategy) { + if (!(this instanceof TransformStream)) + throw new TypeError("TransformStream must be constructed with new"); + + transformer = transformer === undefined ? {} : requiredObject(transformer, "transformer"); + writableStrategy = writableStrategy === undefined ? {} : requiredObject(writableStrategy, "writableStrategy"); + readableStrategy = readableStrategy === undefined ? {} : requiredObject(readableStrategy, "readableStrategy"); + + if (transformer.readableType !== undefined) + throw new RangeError("Invalid readableType"); + if (transformer.writableType !== undefined) + throw new RangeError("Invalid writableType"); + + var controller = Object.create(TransformStreamDefaultController.prototype); + var readableController; + var stream = this; + var startAlgorithm = getMethod(transformer, "start"); + var transformAlgorithm = getMethod(transformer, "transform"); + var flushAlgorithm = getMethod(transformer, "flush"); + var cancelAlgorithm = getMethod(transformer, "cancel"); + + var readable = new ReadableStream({ + start: function(c) { + readableController = c; + transformControllerState.set(controller, { + stream: stream, + readableController: c + }); + }, + cancel: function(reason) { + if (cancelAlgorithm) + return cancelAlgorithm.call(transformer, reason); + return undefined; + } + }, readableStrategy); + + var startPromise = startAlgorithm + ? promiseCall(startAlgorithm, transformer, [controller]) + : Promise.resolve(undefined); + startPromise = startPromise.then(undefined, function(error) { + if (readableController) + errorReadableController(readableController, error); + throw error; + }); + + var writable = new WritableStream({ + start: function() { + return startPromise; + }, + write: function(chunk) { + var result; + try { + if (transformAlgorithm) { + result = transformAlgorithm.call(transformer, chunk, controller); + } else { + controller.enqueue(chunk); + result = undefined; + } + } catch (error) { + errorReadableController(readableController, error); + throw error; + } + return Promise.resolve(result).then(undefined, function(error) { + errorReadableController(readableController, error); + throw error; + }); + }, + close: function() { + var result = flushAlgorithm + ? promiseCall(flushAlgorithm, transformer, [controller]) + : Promise.resolve(undefined); + return result.then(function() { + readableController.close(); + }, function(error) { + errorReadableController(readableController, error); + throw error; + }); + }, + abort: function(reason) { + readableController.error(reason); + } + }, writableStrategy); + + transformState.set(this, { + readable: readable, + writable: writable, + controller: controller + }); + } + + Object.defineProperty(TransformStream.prototype, "readable", { + configurable: true, + get: function() { + var state = transformState.get(this); + if (!state) + throw new TypeError("TransformStream expected"); + return state.readable; + } + }); + + Object.defineProperty(TransformStream.prototype, "writable", { + configurable: true, + get: function() { + var state = transformState.get(this); + if (!state) + throw new TypeError("TransformStream expected"); + return state.writable; + } + }); + + Object.defineProperty(TransformStream.prototype, Symbol.toStringTag, + { value: "TransformStream", configurable: true }); + + function isReadableStreamObject(value) { + if (!isObject(value) || !global.ReadableStream) + return false; + try { + return value instanceof global.ReadableStream; + } catch (e) { + return false; + } + } + + function isUncloneableStreamChunk(value) { + return isObject(value) && + (isWritableStream(value) || + isReadableStreamObject(value) || + transformState.has(value)); + } + + function makeDataCloneError() { + return makeDOMException("The object could not be cloned.", "DataCloneError"); + } + + function cloneTransferReasonError(value) { + var constructors = [ + global.EvalError, + global.RangeError, + global.ReferenceError, + global.SyntaxError, + global.TypeError, + global.URIError, + global.Error + ]; + + for (var i = 0; i < constructors.length; ++i) { + var C = constructors[i]; + if (typeof C !== "function") + continue; + try { + if (!(value instanceof C)) + continue; + } catch (e) { + continue; + } + + var message = ""; + var descriptor; + try { + descriptor = Object.getOwnPropertyDescriptor(value, "message"); + } catch (e) { + descriptor = undefined; + } + if (descriptor && hasOwn.call(descriptor, "value")) + message = String(descriptor.value); + return new C(message); + } + + return undefined; + } + + function cloneTransferReasonValue(value, memory) { + if (typeof value === "function" || typeof value === "symbol") + throw makeDataCloneError(); + if (!isObject(value)) + return value; + if (isUncloneableStreamChunk(value)) + throw makeDataCloneError(); + + memory = memory || new WeakMap(); + if (memory.has(value)) + return memory.get(value); + + if (typeof global.DOMException === "function") { + try { + if (value instanceof global.DOMException) { + return new global.DOMException(String(value.message), + String(value.name)); + } + } catch (e) {} + } + + var errorClone = cloneTransferReasonError(value); + if (errorClone !== undefined) + return errorClone; + + if (typeof global.ArrayBuffer === "function") { + try { + if (value instanceof global.ArrayBuffer) { + var bufferClone = value.slice(0); + memory.set(value, bufferClone); + return bufferClone; + } + } catch (e) {} + } + + if (global.ArrayBuffer && global.ArrayBuffer.isView && + global.ArrayBuffer.isView(value)) { + var clonedBuffer = cloneTransferReasonValue(value.buffer, memory); + var viewClone; + if (objectToString.call(value) === "[object DataView]") { + viewClone = new global.DataView(clonedBuffer, value.byteOffset, + value.byteLength); + } else { + viewClone = new value.constructor(clonedBuffer, value.byteOffset, + value.length); + } + memory.set(value, viewClone); + return viewClone; + } + + if (typeof global.Date === "function") { + try { + if (value instanceof global.Date) { + var dateClone = new global.Date(value.getTime()); + memory.set(value, dateClone); + return dateClone; + } + } catch (e) {} + } + + if (typeof global.RegExp === "function") { + try { + if (value instanceof global.RegExp) { + var flags = ""; + flags += value.global ? "g" : ""; + flags += value.ignoreCase ? "i" : ""; + flags += value.multiline ? "m" : ""; + flags += value.unicode ? "u" : ""; + flags += value.sticky ? "y" : ""; + var regexpClone = new global.RegExp(value.source, flags); + regexpClone.lastIndex = value.lastIndex; + memory.set(value, regexpClone); + return regexpClone; + } + } catch (e) {} + } + + if (typeof global.Map === "function") { + try { + if (value instanceof global.Map) { + var mapClone = new global.Map(); + memory.set(value, mapClone); + value.forEach(function(v, k) { + mapClone.set(cloneTransferReasonValue(k, memory), + cloneTransferReasonValue(v, memory)); + }); + return mapClone; + } + } catch (e) { + throw e; + } + } + + if (typeof global.Set === "function") { + try { + if (value instanceof global.Set) { + var setClone = new global.Set(); + memory.set(value, setClone); + value.forEach(function(v) { + setClone.add(cloneTransferReasonValue(v, memory)); + }); + return setClone; + } + } catch (e) { + throw e; + } + } + + var clone = Array.isArray(value) ? [] : {}; + memory.set(value, clone); + + var keys = Object.keys(value); + for (var i = 0; i < keys.length; ++i) { + var key = keys[i]; + clone[key] = cloneTransferReasonValue(value[key], memory); + } + + return clone; + } + + function abortTransferredWritable(record, reason) { + try { + return record.writer.abort(reason); + } catch (e) { + return Promise.reject(e); + } + } + + function rejectTransferredWritableWrite(record, error) { + var abortPromise = abortTransferredWritable(record, error); + silenceRejection(abortPromise); + return Promise.reject(error); + } + + function forwardTransferredWritableWrite(record, chunk) { + if (isUncloneableStreamChunk(chunk)) + return rejectTransferredWritableWrite(record, makeDataCloneError()); + + var prior = record.tail || Promise.resolve(undefined); + var firstWrite = !record.started; + record.started = true; + + var forwarding = prior.then(function() { + var writePromise; + try { + writePromise = Promise.resolve(record.writer.write(chunk)); + } catch (e) { + writePromise = Promise.reject(e); + } + record.inFlight = writePromise; + silenceRejection(writePromise); + return writePromise; + }); + + record.tail = forwarding; + silenceRejection(forwarding); + + if (firstWrite) + return undefined; + + return prior.then(function() { return undefined; }); + } + + function closeTransferredWritable(record) { + var tail = record.tail || Promise.resolve(undefined); + var closePromise = tail.then(function() { + return record.writer.close(); + }); + record.tail = closePromise; + silenceRejection(closePromise); + return closePromise; + } + +)JS" +R"JS( + + function __uxpTransferWritableStream(stream) { + if (!isWritableStream(stream)) + return undefined; + if (stream.locked) + return null; + try { + return { + writer: stream.getWriter(), + started: false, + inFlight: undefined, + tail: undefined + }; + } catch (e) { + return null; + } + } + + function __uxpReceiveWritableStreamTransfer(record) { + return new WritableStream({ + write: function(chunk) { + return forwardTransferredWritableWrite(record, chunk); + }, + close: function() { + return closeTransferredWritable(record); + }, + abort: function(reason) { + return abortTransferredWritable(record, reason); + } + }); + } + + function __uxpTransferReadableStream(stream) { + if (!isReadableStreamObject(stream)) + return undefined; + if (stream.locked) + return null; + try { + var record = { + reader: stream.getReader(), + pendingRead: undefined, + pumping: undefined, + closed: false, + errored: false, + canceled: false + }; + ensureReadableTransferRead(record); + return record; + } catch (e) { + return null; + } + } + + function ensureReadableTransferRead(record) { + if (!record.pendingRead && !record.closed && !record.errored && + !record.canceled) { + try { + record.pendingRead = Promise.resolve(record.reader.read()); + } catch (e) { + record.pendingRead = Promise.reject(e); + } + silenceRejection(record.pendingRead); + } + return record.pendingRead; + } + + function errorReadableTransfer(record, controller, error) { + if (record.canceled || record.closed || record.errored) + return; + record.errored = true; + try { controller.error(error); } catch (e) {} + } + + function pumpReadableTransfer(record, controller) { + if (record.pumping || record.closed || record.errored || record.canceled) + return; + + var pendingRead = ensureReadableTransferRead(record); + if (!pendingRead) + return; + + record.pumping = pendingRead.then(function(result) { + record.pendingRead = undefined; + record.pumping = undefined; + + if (record.canceled || record.errored || record.closed) + return; + + if (result.done) { + record.closed = true; + try { controller.close(); } catch (e) {} + return; + } + + if (isUncloneableStreamChunk(result.value)) { + var cloneError = makeDataCloneError(); + silenceRejection(record.reader.cancel(cloneError)); + errorReadableTransfer(record, controller, cloneError); + return; + } + + try { + controller.enqueue(result.value); + } catch (e) { + silenceRejection(record.reader.cancel(e)); + errorReadableTransfer(record, controller, e); + return; + } + + if (controller.desiredSize > 0) + pumpReadableTransfer(record, controller); + }, function(error) { + record.pendingRead = undefined; + record.pumping = undefined; + errorReadableTransfer(record, controller, error); + }); + silenceRejection(record.pumping); + } + + function cancelReadableTransfer(record, reason) { + record.canceled = true; + var clonedReason; + try { + clonedReason = cloneTransferReasonValue(reason); + } catch (e) { + return Promise.reject(e); + } + + Promise.resolve().then(function() { + try { + silenceRejection(record.reader.cancel(clonedReason)); + } catch (e) {} + }); + return undefined; + } + + function closeReadableTransferPort(port) { + try { port.close(); } catch (e) {} + } + + function startReadableTransferPort(port) { + try { + if (port.start) + port.start(); + } catch (e) {} + } + + function tryPostReadableTransferPort(port, message) { + try { + port.postMessage(message); + return true; + } catch (e) { + return false; + } + } + +)JS" +R"JS( + + function postReadableTransferPortError(record, error) { + var posted = tryPostReadableTransferPort(record.port, + { type: "error", value: error }); + if (!posted) { + posted = tryPostReadableTransferPort(record.port, { + type: "error", + name: error && error.name, + message: error && error.message + }); + } + record.closed = true; + if (!posted) + closeReadableTransferPort(record.port); + } + +)JS" +R"JS( + + function requestReadableTransferPortChunk(state) { + if (state.closed || state.errored || state.canceled) + return; + if (!tryPostReadableTransferPort(state.port, { type: "pull" })) { + state.errored = true; + if (state.controller) + try { state.controller.error(makeDataCloneError()); } catch (e) {} + closeReadableTransferPort(state.port); + } + } + + function errorReadableTransferPortState(state, error) { + if (state.errored) + return; + state.errored = true; + try { + if (state.controller) + state.controller.error(error); + } catch (e) {} + closeReadableTransferPort(state.port); + } + + function pumpReadableTransferPort(record) { + if (record.reading || record.closed || record.canceled || + record.credits <= 0) + return; + + --record.credits; + record.reading = true; + + var readPromise; + try { + readPromise = Promise.resolve(record.reader.read()); + } catch (e) { + readPromise = Promise.reject(e); + } + silenceRejection(readPromise); + + readPromise.then(function(result) { + record.reading = false; + + if (record.canceled || record.closed) + return; + + if (result.done) { + record.closed = true; + if (!tryPostReadableTransferPort(record.port, { type: "close" })) + closeReadableTransferPort(record.port); + return; + } + + if (isUncloneableStreamChunk(result.value)) { + var cloneError = makeDataCloneError(); + silenceRejection(record.reader.cancel(cloneError)); + postReadableTransferPortError(record, cloneError); + return; + } + + if (!tryPostReadableTransferPort(record.port, + { type: "chunk", value: result.value })) { + record.closed = true; + closeReadableTransferPort(record.port); + return; + } + + pumpReadableTransferPort(record); + }, function(error) { + record.reading = false; + if (record.canceled || record.closed) + return; + postReadableTransferPortError(record, error); + }); + } + + function __uxpTransferReadableStreamPort(stream) { + +)JS" +R"JS( + if (!isReadableStreamObject(stream)) + return undefined; + if (stream.locked || typeof global.MessageChannel !== "function") + return null; + + var channel; + try { + channel = new global.MessageChannel(); + var record = { + reader: stream.getReader(), + port: channel.port1, + reading: false, + credits: 1, + closed: false, + canceled: false + }; + + record.port.onmessage = function(event) { + var message = event.data; + if (!message || record.canceled) + return; + + if (message.type === "closed") { + record.closed = true; + closeReadableTransferPort(record.port); + return; + } + + if (record.closed) + return; + + if (message.type === "pull") { + ++record.credits; + pumpReadableTransferPort(record); + return; + } + + if (message.type === "cancel") { + record.canceled = true; + Promise.resolve().then(function() { + try { + silenceRejection(record.reader.cancel(message.reason)); + } catch (e) {} + }); + closeReadableTransferPort(record.port); + } + }; + + startReadableTransferPort(record.port); + pumpReadableTransferPort(record); + return channel.port2; + } catch (e) { + if (channel) { + closeReadableTransferPort(channel.port1); + closeReadableTransferPort(channel.port2); + } + return null; + } + } + + function errorFromReadableTransferPortMessage(message) { + if ("value" in message) + return message.value; + return makeDOMException(message.message || "ReadableStream transfer failed", + message.name || "DataCloneError"); + } + + function __uxpReceiveReadableStreamTransferFromPort(port) { + var state = { + port: port, + controller: undefined, + closed: false, + errored: false, + canceled: false + }; + + port.onmessage = function(event) { + var message = event.data; + if (!message || state.closed || state.errored || state.canceled) + return; + + if (message.type === "chunk") { + if (isUncloneableStreamChunk(message.value)) { + errorReadableTransferPortState(state, makeDataCloneError()); + return; + } + + try { + state.controller.enqueue(message.value); + } catch (e) { + errorReadableTransferPortState(state, e); + return; + } + + if (state.controller.desiredSize > 0) + requestReadableTransferPortChunk(state); + return; + } + + if (message.type === "close") { + state.closed = true; + try { state.controller.close(); } catch (e) {} + tryPostReadableTransferPort(port, { type: "closed" }); + closeReadableTransferPort(port); + return; + } + + if (message.type === "error") { + if (!state.errored) { + state.errored = true; + try { + if (state.controller) + state.controller.error( + errorFromReadableTransferPortMessage(message)); + } catch (e) {} + } + tryPostReadableTransferPort(port, { type: "closed" }); + closeReadableTransferPort(port); + } + }; + + return new ReadableStream({ + start: function(controller) { + state.controller = controller; + startReadableTransferPort(port); + requestReadableTransferPortChunk(state); + }, + pull: function() { + requestReadableTransferPortChunk(state); + }, + cancel: function(reason) { + state.canceled = true; + try { + port.postMessage({ type: "cancel", reason: reason }); + } catch (e) { + closeReadableTransferPort(port); + return Promise.reject(e); + } + closeReadableTransferPort(port); + return undefined; + } + }); + } + + function __uxpTransferTransformStream(stream) { + if (!isObject(stream) || !transformState.has(stream)) + return undefined; + + var state = transformState.get(stream); + if (state.readable.locked || state.writable.locked) + return null; + + var readableRecord = __uxpTransferReadableStream(state.readable); + if (!readableRecord) + return null; + + var writableRecord = __uxpTransferWritableStream(state.writable); + if (!writableRecord) + return null; + + return { + readable: readableRecord, + writable: writableRecord + }; + } + + function __uxpReceiveTransformStreamTransfer(record) { + var stream = Object.create(global.TransformStream.prototype); + transformState.set(stream, { + readable: __uxpReceiveReadableStreamTransfer(record.readable), + writable: __uxpReceiveWritableStreamTransfer(record.writable), + controller: undefined + }); + return stream; + } + + function __uxpReceiveReadableStreamTransfer(record) { + return new ReadableStream({ + start: function(controller) { + pumpReadableTransfer(record, controller); + }, + pull: function(controller) { + pumpReadableTransfer(record, controller); + }, + cancel: function(reason) { + return cancelReadableTransfer(record, reason); + } + }); + } + + function pipeTo(readable, destination, options) { + options = options === undefined ? {} : requiredObject(options, "options"); + var signal = options.signal; + var preventClose = Boolean(options.preventClose); + var preventAbort = Boolean(options.preventAbort); + var preventCancel = Boolean(options.preventCancel); + var reader; + var writer; + + try { + destination = requiredObject(destination, "destination"); + reader = readable.getReader(); + writer = destination.getWriter(); + } catch (e) { + return rejectPromise(e); + } + + var closed = false; + function cleanup() { + if (closed) + return; + closed = true; + releaseReaderLock(reader); + releaseWriterLock(writer); + if (signal && signal.removeEventListener) + signal.removeEventListener("abort", onAbort); + } + + function abortReason() { + return signal && "reason" in signal + ? signal.reason + : makeDOMException("The operation was aborted.", "AbortError"); + } + + function shutdownWithAction(action, originalError) { + return action().then(function() { + cleanup(); + throw originalError; + }, function(actionError) { + cleanup(); + throw actionError; + }); + } + + function onAbort() {} + + var pipePromise = new Promise(function(resolve, reject) { + onAbort = function() { + var reason = abortReason(); + var actions = []; + if (!preventAbort) + actions.push(writer.abort(reason)); + if (!preventCancel) + actions.push(reader.cancel(reason)); + Promise.all(actions).then(function() { + cleanup(); + reject(reason); + }, function(error) { + cleanup(); + reject(error); + }); + }; + + if (signal && signal.aborted) { + onAbort(); + return; + } + if (signal && signal.addEventListener) + signal.addEventListener("abort", onAbort); + + function pump() { + if (closed) + return; + reader.read().then(function(result) { + if (closed) + return; + if (result.done) { + var closePromise = preventClose ? Promise.resolve(undefined) + : writer.close(); + closePromise.then(function() { + cleanup(); + resolve(undefined); + }, function(error) { + cleanup(); + reject(error); + }); + return; + } + + writer.write(result.value).then(pump, function(error) { + if (closed) + return; + var cancel = preventCancel + ? Promise.resolve(undefined) + : reader.cancel(error); + cancel.then(function() { + cleanup(); + reject(error); + }, function(cancelError) { + cleanup(); + reject(cancelError); + }); + }); + }, function(error) { + if (closed) + return; + var abort = preventAbort + ? Promise.resolve(undefined) + : writer.abort(error); + abort.then(function() { + cleanup(); + reject(error); + }, function(abortError) { + cleanup(); + reject(abortError); + }); + }); + } + + pump(); + }); + + return pipePromise; + } + + function pipeThrough(readable, pair, options) { + pair = requiredObject(pair, "transform"); + var writable = pair.writable; + var output = pair.readable; + if (readable.locked) + throw new TypeError("ReadableStream is locked"); + if (!isWritableStream(writable)) + throw new TypeError("transform.writable must be a WritableStream"); + if (!(output instanceof global.ReadableStream)) + throw new TypeError("transform.readable must be a ReadableStream"); + if (writable.locked) + throw new TypeError("WritableStream is locked"); + readable.pipeTo(writable, options).catch(function() {}); + return output; + } + + Object.defineProperty(global, "__uxpStreamsPipeTo", + { value: pipeTo, configurable: true }); + Object.defineProperty(global, "__uxpStreamsPipeThrough", + { value: pipeThrough, configurable: true }); + Object.defineProperty(global, "__uxpTransferWritableStream", + { value: __uxpTransferWritableStream, + configurable: true }); + Object.defineProperty(global, "__uxpReceiveWritableStreamTransfer", + { value: __uxpReceiveWritableStreamTransfer, + configurable: true }); + Object.defineProperty(global, "__uxpTransferReadableStream", + { value: __uxpTransferReadableStream, + configurable: true }); + Object.defineProperty(global, "__uxpReceiveReadableStreamTransfer", + { value: __uxpReceiveReadableStreamTransfer, + configurable: true }); + Object.defineProperty(global, "__uxpTransferReadableStreamPort", + { value: __uxpTransferReadableStreamPort, + configurable: true }); + Object.defineProperty(global, "__uxpReceiveReadableStreamTransferFromPort", + { value: __uxpReceiveReadableStreamTransferFromPort, + configurable: true }); + Object.defineProperty(global, "__uxpTransferTransformStream", + { value: __uxpTransferTransformStream, + configurable: true }); + Object.defineProperty(global, "__uxpReceiveTransformStreamTransfer", + { value: __uxpReceiveTransformStreamTransfer, + configurable: true }); + + if (global.ReadableStream && !global.ReadableStream.from) { + Object.defineProperty(global.ReadableStream, "from", { + value: function from(asyncIterable) { + if (asyncIterable == null) + throw new TypeError("ReadableStream.from requires an iterable"); + var method = asyncIterable[Symbol.asyncIterator] || + asyncIterable[Symbol.iterator]; + if (typeof method !== "function") + throw new TypeError("ReadableStream.from requires an iterable"); + var iterator = method.call(asyncIterable); + return new ReadableStream({ + pull: function(controller) { + return Promise.resolve(iterator.next()).then(function(result) { + if (!isObject(result)) + throw new TypeError("Iterator result is not an object"); + if (result.done) { + controller.close(); + } else { + controller.enqueue(result.value); + } + }); + }, + cancel: function(reason) { + var returnMethod = iterator.return; + if (returnMethod) + return returnMethod.call(iterator, reason); + return undefined; + } + }); + }, + writable: true, + configurable: true + }); + } + + if (global.ReadableStream && typeof Symbol === "function" && + Symbol.asyncIterator && !global.ReadableStream.prototype.values) { + function ReadableStreamAsyncIterator(stream, preventCancel) { + this._reader = stream.getReader(); + this._preventCancel = preventCancel; + this._done = false; + } + + ReadableStreamAsyncIterator.prototype.next = function() { + if (this._done) + return Promise.resolve({ value: undefined, done: true }); + var self = this; + return this._reader.read().then(function(result) { + if (result.done) { + self._done = true; + releaseReaderLock(self._reader); + } + return result; + }, function(error) { + self._done = true; + releaseReaderLock(self._reader); + throw error; + }); + }; + + ReadableStreamAsyncIterator.prototype.return = function(value) { + if (this._done) + return Promise.resolve({ value: value, done: true }); + this._done = true; + var reader = this._reader; + if (this._preventCancel) { + releaseReaderLock(reader); + return Promise.resolve({ value: value, done: true }); + } + return reader.cancel(value).then(function() { + releaseReaderLock(reader); + return { value: value, done: true }; + }, function(error) { + releaseReaderLock(reader); + throw error; + }); + }; + + ReadableStreamAsyncIterator.prototype[Symbol.asyncIterator] = function() { + return this; + }; + + Object.defineProperty(global.ReadableStream.prototype, "values", { + value: function values(options) { + var preventCancel = Boolean(options && options.preventCancel); + return new ReadableStreamAsyncIterator(this, preventCancel); + }, + writable: true, + configurable: true + }); + + Object.defineProperty(global.ReadableStream.prototype, Symbol.asyncIterator, { + value: global.ReadableStream.prototype.values, + writable: true, + configurable: true + }); + } + + )JS" + R"JS( + + var textEncoderStreamState = new WeakMap(); + var textDecoderStreamState = new WeakMap(); + + function enqueueEncoded(controller, encoder, string) { + if (string.length === 0) + return; + var chunk = encoder.encode(string); + if (chunk.byteLength) + controller.enqueue(chunk); + } + + function TextEncoderStream() { + if (!(this instanceof TextEncoderStream)) + throw new TypeError("TextEncoderStream must be constructed with new"); + + var encoder = new TextEncoder(); + var leadingSurrogate = null; + var transform = new TransformStream({ + transform: function(chunk, controller) { + var input = String(chunk); + var output = ""; + var i = 0; + + if (leadingSurrogate !== null) { + if (input.length > 0) { + var first = input.charCodeAt(0); + if (first >= 0xDC00 && first <= 0xDFFF) { + output += String.fromCharCode(leadingSurrogate) + input.charAt(0); + i = 1; + } else { + output += "\uFFFD"; + } + } else { + return; + } + leadingSurrogate = null; + } + + for (; i < input.length; ++i) { + var code = input.charCodeAt(i); + if (code >= 0xD800 && code <= 0xDBFF) { + if (i + 1 < input.length) { + var next = input.charCodeAt(i + 1); + if (next >= 0xDC00 && next <= 0xDFFF) { + output += input.charAt(i) + input.charAt(i + 1); + ++i; + } else { + output += "\uFFFD"; + } + } else { + leadingSurrogate = code; + break; + } + } else if (code >= 0xDC00 && code <= 0xDFFF) { + output += "\uFFFD"; + } else { + output += input.charAt(i); + } + } + + enqueueEncoded(controller, encoder, output); + }, + flush: function(controller) { + if (leadingSurrogate !== null) { + leadingSurrogate = null; + enqueueEncoded(controller, encoder, "\uFFFD"); + } + } + }); + + textEncoderStreamState.set(this, { transform: transform }); + } + + Object.defineProperty(TextEncoderStream.prototype, "encoding", { + configurable: true, + get: function() { + if (!textEncoderStreamState.has(this)) + throw new TypeError("TextEncoderStream expected"); + return "utf-8"; + } + }); + + Object.defineProperty(TextEncoderStream.prototype, "readable", { + configurable: true, + get: function() { + var state = textEncoderStreamState.get(this); + if (!state) + throw new TypeError("TextEncoderStream expected"); + return state.transform.readable; + } + }); + + Object.defineProperty(TextEncoderStream.prototype, "writable", { + configurable: true, + get: function() { + var state = textEncoderStreamState.get(this); + if (!state) + throw new TypeError("TextEncoderStream expected"); + return state.transform.writable; + } + }); + + Object.defineProperty(TextEncoderStream.prototype, Symbol.toStringTag, + { value: "TextEncoderStream", configurable: true }); + + function TextDecoderStream(label, options) { + if (!(this instanceof TextDecoderStream)) + throw new TypeError("TextDecoderStream must be constructed with new"); + label = label === undefined ? "utf-8" : label; + options = options === undefined ? {} : options; + + var decoder = new TextDecoder(label, options); + var transform = new TransformStream({ + transform: function(chunk, controller) { + var output = decoder.decode(chunk, { stream: true }); + if (output.length) + controller.enqueue(output); + }, + flush: function(controller) { + var output = decoder.decode(); + if (output.length) + controller.enqueue(output); + } + }); + + textDecoderStreamState.set(this, { + transform: transform, + encoding: decoder.encoding, + fatal: decoder.fatal, + ignoreBOM: Boolean(options && options.ignoreBOM) + }); + } + + Object.defineProperty(TextDecoderStream.prototype, "encoding", { + configurable: true, + get: function() { + var state = textDecoderStreamState.get(this); + if (!state) + throw new TypeError("TextDecoderStream expected"); + return state.encoding; + } + }); + + Object.defineProperty(TextDecoderStream.prototype, "fatal", { + configurable: true, + get: function() { + var state = textDecoderStreamState.get(this); + if (!state) + throw new TypeError("TextDecoderStream expected"); + return state.fatal; + } + }); + + Object.defineProperty(TextDecoderStream.prototype, "ignoreBOM", { + configurable: true, + get: function() { + var state = textDecoderStreamState.get(this); + if (!state) + throw new TypeError("TextDecoderStream expected"); + return state.ignoreBOM; + } + }); + + Object.defineProperty(TextDecoderStream.prototype, "readable", { + configurable: true, + get: function() { + var state = textDecoderStreamState.get(this); + if (!state) + throw new TypeError("TextDecoderStream expected"); + return state.transform.readable; + } + }); + + Object.defineProperty(TextDecoderStream.prototype, "writable", { + configurable: true, + get: function() { + var state = textDecoderStreamState.get(this); + if (!state) + throw new TypeError("TextDecoderStream expected"); + return state.transform.writable; + } + }); + + Object.defineProperty(TextDecoderStream.prototype, Symbol.toStringTag, + { value: "TextDecoderStream", configurable: true }); + + var compressionStreamState = new WeakMap(); + var decompressionStreamState = new WeakMap(); + + function enqueueCompressionChunks(nativeContext, chunk, finish, controller) { + var chunks = global.__uxpCompressionProcess(nativeContext, chunk, finish); + for (var i = 0; i < chunks.length; ++i) + controller.enqueue(chunks[i]); + } + + function CompressionStream(format) { + if (!(this instanceof CompressionStream)) + throw new TypeError("CompressionStream must be constructed with new"); + var nativeContext = global.__uxpCreateCompressionContext(format, true); + var transform = new TransformStream({ + transform: function(chunk, controller) { + enqueueCompressionChunks(nativeContext, chunk, false, controller); + }, + flush: function(controller) { + enqueueCompressionChunks(nativeContext, undefined, true, controller); + } + }); + compressionStreamState.set(this, { transform: transform }); + } + + Object.defineProperty(CompressionStream.prototype, "readable", { + configurable: true, + get: function() { + var state = compressionStreamState.get(this); + if (!state) + throw new TypeError("CompressionStream expected"); + return state.transform.readable; + } + }); + + Object.defineProperty(CompressionStream.prototype, "writable", { + configurable: true, + get: function() { + var state = compressionStreamState.get(this); + if (!state) + throw new TypeError("CompressionStream expected"); + return state.transform.writable; + } + }); + + Object.defineProperty(CompressionStream.prototype, Symbol.toStringTag, + { value: "CompressionStream", configurable: true }); + + function DecompressionStream(format) { + if (!(this instanceof DecompressionStream)) + throw new TypeError("DecompressionStream must be constructed with new"); + var nativeContext = global.__uxpCreateCompressionContext(format, false); + var transform = new TransformStream({ + transform: function(chunk, controller) { + enqueueCompressionChunks(nativeContext, chunk, false, controller); + }, + flush: function(controller) { + enqueueCompressionChunks(nativeContext, undefined, true, controller); + } + }); + decompressionStreamState.set(this, { transform: transform }); + } + + Object.defineProperty(DecompressionStream.prototype, "readable", { + configurable: true, + get: function() { + var state = decompressionStreamState.get(this); + if (!state) + throw new TypeError("DecompressionStream expected"); + return state.transform.readable; + } + }); + + Object.defineProperty(DecompressionStream.prototype, "writable", { + configurable: true, + get: function() { + var state = decompressionStreamState.get(this); + if (!state) + throw new TypeError("DecompressionStream expected"); + return state.transform.writable; + } + }); + + Object.defineProperty(DecompressionStream.prototype, Symbol.toStringTag, + { value: "DecompressionStream", configurable: true }); + + define("WritableStream", WritableStream); + define("WritableStreamDefaultWriter", WritableStreamDefaultWriter); + define("WritableStreamDefaultController", WritableStreamDefaultController); + define("TransformStream", TransformStream); + define("TransformStreamDefaultController", TransformStreamDefaultController); + define("TextEncoderStream", TextEncoderStream); + define("TextDecoderStream", TextDecoderStream); + define("CompressionStream", CompressionStream); + define("DecompressionStream", DecompressionStream); +})(this); +)JS"; + + RootedValue rval(cx); + JS::CompileOptions options(cx); + options.setFileAndLine("builtin/StreamExtras.js", 1); + options.setNoScriptRval(true); + return JS::Evaluate(cx, options, source, strlen(source), &rval); +} diff --git a/js/src/builtin/Stream.h b/js/src/builtin/Stream.h index 0726e78361..50bd7a52a1 100644 --- a/js/src/builtin/Stream.h +++ b/js/src/builtin/Stream.h @@ -165,6 +165,8 @@ class CountQueuingStrategy : public NativeObject static const Class protoClass_; }; +[[nodiscard]] bool InitStreamExtras(JSContext* cx, HandleObject global); + } // namespace js #endif /* builtin_Stream_h */ diff --git a/js/src/builtin/String.js b/js/src/builtin/String.js index e1c32482ae..a83a06ad2b 100644 --- a/js/src/builtin/String.js +++ b/js/src/builtin/String.js @@ -654,6 +654,82 @@ function String_repeat(count) { return T; } +// ES2024 +// String.prototype.isWellFormed ( ) +function String_isWellFormed() { + // Steps 1-2. + RequireObjectCoercible(this); + var S = ToString(this); + + // Step 3. + var length = S.length; + for (var k = 0; k < length; k++) { + var c = callFunction(std_String_charCodeAt, S, k); + if (c >= 0xD800 && c <= 0xDBFF) { + if (k + 1 >= length) + return false; + + var d = callFunction(std_String_charCodeAt, S, k + 1); + if (d < 0xDC00 || d > 0xDFFF) + return false; + + k++; + } else if (c >= 0xDC00 && c <= 0xDFFF) { + return false; + } + } + + // Step 4. + return true; +} + +// ES2024 +// String.prototype.toWellFormed ( ) +function String_toWellFormed() { + // Steps 1-2. + RequireObjectCoercible(this); + var S = ToString(this); + + // Step 3. + var length = S.length; + var result = ""; + var copied = 0; + + for (var k = 0; k < length; k++) { + var c = callFunction(std_String_charCodeAt, S, k); + var isUnpairedSurrogate = false; + + if (c >= 0xD800 && c <= 0xDBFF) { + if (k + 1 < length) { + var d = callFunction(std_String_charCodeAt, S, k + 1); + if (d >= 0xDC00 && d <= 0xDFFF) { + k++; + continue; + } + } + isUnpairedSurrogate = true; + } else if (c >= 0xDC00 && c <= 0xDFFF) { + isUnpairedSurrogate = true; + } + + if (isUnpairedSurrogate) { + if (copied < k) + result += callFunction(String_substring, S, copied, k); + result += "\uFFFD"; + copied = k + 1; + } + } + + if (copied === 0) + return S; + + if (copied < length) + result += callFunction(String_substring, S, copied, length); + + // Step 4. + return result; +} + // ES6 draft specification, section 21.1.3.27, version 2013-09-27. function String_iterator() { RequireObjectCoercible(this); diff --git a/js/src/builtin/TypedArray.js b/js/src/builtin/TypedArray.js index c831b74614..4311f27079 100644 --- a/js/src/builtin/TypedArray.js +++ b/js/src/builtin/TypedArray.js @@ -37,10 +37,25 @@ function TypedArrayLengthMethod() { return TypedArrayLength(this); } +function TypedArrayContentTypeIsBigIntMethod() { + return IsBigInt64TypedArray(this) || IsBigUint64TypedArray(this); +} + +function ThrowIfTypedArrayOutOfBounds(tarray) { + if (TypedArrayIsOutOfBounds(tarray)) + ThrowTypeError(JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); +} + +function ThrowIfPossiblyWrappedTypedArrayOutOfBounds(tarray) { + if (PossiblyWrappedTypedArrayIsOutOfBounds(tarray)) + ThrowTypeError(JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); +} + function GetAttachedArrayBuffer(tarray) { var buffer = ViewedArrayBufferIfReified(tarray); if (IsDetachedBuffer(buffer)) ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + ThrowIfTypedArrayOutOfBounds(tarray); return buffer; } @@ -60,6 +75,13 @@ function IsTypedArrayEnsuringArrayBuffer(arg) { return true; } + if (IsObject(arg) && IsPossiblyWrappedTypedArray(arg)) { + if (PossiblyWrappedTypedArrayHasDetachedBuffer(arg)) + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + ThrowIfPossiblyWrappedTypedArrayOutOfBounds(arg); + return false; + } + callFunction(CallTypedArrayMethodIfWrapped, arg, "GetAttachedArrayBufferMethod"); return false; } @@ -79,6 +101,7 @@ function ValidateTypedArray(obj, error) { if (IsPossiblyWrappedTypedArray(obj)) { if (PossiblyWrappedTypedArrayHasDetachedBuffer(obj)) ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + ThrowIfPossiblyWrappedTypedArrayOutOfBounds(obj); return false; } } @@ -1024,12 +1047,18 @@ function TypedArraySet(overloaded, offset = 0) { // Steps 9-10. var targetBuffer = GetAttachedArrayBuffer(target); + ThrowIfTypedArrayOutOfBounds(target); + // Step 11. var targetLength = TypedArrayLength(target); // Steps 12 et seq. - if (IsPossiblyWrappedTypedArray(overloaded)) + if (IsPossiblyWrappedTypedArray(overloaded)) { + if (PossiblyWrappedTypedArrayHasDetachedBuffer(overloaded)) + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + ThrowIfPossiblyWrappedTypedArrayOutOfBounds(overloaded); return SetFromTypedArray(target, overloaded, targetOffset, targetLength); + } return SetFromNonTypedArray(target, overloaded, targetOffset, targetLength, targetBuffer); } @@ -1366,6 +1395,8 @@ function TypedArraySubarray(begin, end) { "TypedArraySubarray"); } + GetAttachedArrayBuffer(obj); + // Steps 4-6. var buffer = TypedArrayBuffer(obj); var srcLength = TypedArrayLength(obj); @@ -1846,7 +1877,10 @@ function ArrayBufferSlice(start, end) { ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); // Steps 19-21. - ArrayBufferCopyData(new_, 0, O, first | 0, newLen | 0, isWrapped); + var currentLen = ArrayBufferByteLength(O); + var copyLen = first >= currentLen ? 0 : std_Math_min(newLen, currentLen - first); + if (copyLen > 0) + ArrayBufferCopyData(new_, 0, O, first | 0, copyLen | 0, isWrapped); // Step 22. return new_; diff --git a/js/src/builtin/WeakMapObject.cpp b/js/src/builtin/WeakMapObject.cpp index 82b6ef1a7c..d68b9e05fd 100644 --- a/js/src/builtin/WeakMapObject.cpp +++ b/js/src/builtin/WeakMapObject.cpp @@ -8,6 +8,7 @@ #include "jsapi.h" #include "jscntxt.h" +#include "builtin/WeakRefObject.h" #include "vm/SelfHosting.h" #include "vm/Interpreter-inl.h" @@ -21,21 +22,80 @@ IsWeakMap(HandleValue v) return v.isObject() && v.toObject().is(); } +struct WeakMapObject::Data +{ + ObjectValueMap objectMap; + SymbolValueMap symbolMap; + + Data(JSContext* cx, JSObject* owner) + : objectMap(cx, owner), + symbolMap(cx, owner) + {} + + bool init() { + return objectMap.init() && symbolMap.init(); + } +}; + +ObjectValueMap* +WeakMapObject::getMap() +{ + Data* data = getData(); + return data ? &data->objectMap : nullptr; +} + +SymbolValueMap* +WeakMapObject::getSymbolMap() +{ + Data* data = getData(); + return data ? &data->symbolMap : nullptr; +} + +static bool +EnsureWeakMapData(JSContext* cx, Handle mapObj) +{ + if (mapObj->getData()) + return true; + + auto data = cx->make_unique(cx, mapObj.get()); + if (!data) + return false; + + if (!data->init()) { + JS_ReportOutOfMemory(cx); + return false; + } + + mapObj->setPrivate(data.release()); + return true; +} + MOZ_ALWAYS_INLINE bool WeakMap_has_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(IsWeakMap(args.thisv())); - if (!args.get(0).isObject()) { + if (!CanBeHeldWeakly(args.get(0))) { args.rval().setBoolean(false); return true; } - if (ObjectValueMap* map = args.thisv().toObject().as().getMap()) { - JSObject* key = &args[0].toObject(); - if (map->has(key)) { - args.rval().setBoolean(true); - return true; + WeakMapObject& weakMap = args.thisv().toObject().as(); + if (args.get(0).isObject()) { + if (ObjectValueMap* map = weakMap.getMap()) { + JSObject* key = &args[0].toObject(); + if (map->has(key)) { + args.rval().setBoolean(true); + return true; + } + } + } else { + if (SymbolValueMap* map = weakMap.getSymbolMap()) { + JS::Symbol* key = args[0].toSymbol(); + if (map->has(key)) { + args.rval().setBoolean(true); + return true; + } } } @@ -55,16 +115,27 @@ WeakMap_get_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(IsWeakMap(args.thisv())); - if (!args.get(0).isObject()) { + if (!CanBeHeldWeakly(args.get(0))) { args.rval().setUndefined(); return true; } - if (ObjectValueMap* map = args.thisv().toObject().as().getMap()) { - JSObject* key = &args[0].toObject(); - if (ObjectValueMap::Ptr ptr = map->lookup(key)) { - args.rval().set(ptr->value()); - return true; + WeakMapObject& weakMap = args.thisv().toObject().as(); + if (args.get(0).isObject()) { + if (ObjectValueMap* map = weakMap.getMap()) { + JSObject* key = &args[0].toObject(); + if (ObjectValueMap::Ptr ptr = map->lookup(key)) { + args.rval().set(ptr->value()); + return true; + } + } + } else { + if (SymbolValueMap* map = weakMap.getSymbolMap()) { + JS::Symbol* key = args[0].toSymbol(); + if (SymbolValueMap::Ptr ptr = map->lookup(key)) { + args.rval().set(ptr->value()); + return true; + } } } @@ -84,17 +155,29 @@ WeakMap_delete_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(IsWeakMap(args.thisv())); - if (!args.get(0).isObject()) { + if (!CanBeHeldWeakly(args.get(0))) { args.rval().setBoolean(false); return true; } - if (ObjectValueMap* map = args.thisv().toObject().as().getMap()) { - JSObject* key = &args[0].toObject(); - if (ObjectValueMap::Ptr ptr = map->lookup(key)) { - map->remove(ptr); - args.rval().setBoolean(true); - return true; + WeakMapObject& weakMap = args.thisv().toObject().as(); + if (args.get(0).isObject()) { + if (ObjectValueMap* map = weakMap.getMap()) { + JSObject* key = &args[0].toObject(); + if (ObjectValueMap::Ptr ptr = map->lookup(key)) { + map->remove(ptr); + args.rval().setBoolean(true); + return true; + } + } + } else { + if (SymbolValueMap* map = weakMap.getSymbolMap()) { + JS::Symbol* key = args[0].toSymbol(); + if (SymbolValueMap::Ptr ptr = map->lookup(key)) { + map->remove(ptr); + args.rval().setBoolean(true); + return true; + } } } @@ -126,22 +209,13 @@ TryPreserveReflector(JSContext* cx, HandleObject obj) return true; } -static MOZ_ALWAYS_INLINE bool -SetWeakMapEntryInternal(JSContext* cx, Handle mapObj, - HandleObject key, HandleValue value) +static bool +SetWeakMapObjectEntryInternal(JSContext* cx, Handle mapObj, + HandleObject key, HandleValue value) { + if (!EnsureWeakMapData(cx, mapObj)) + return false; ObjectValueMap* map = mapObj->getMap(); - if (!map) { - auto newMap = cx->make_unique(cx, mapObj.get()); - if (!newMap) - return false; - if (!newMap->init()) { - JS_ReportOutOfMemory(cx); - return false; - } - map = newMap.release(); - mapObj->setPrivate(map); - } // Preserve wrapped native keys to prevent wrapper optimization. if (!TryPreserveReflector(cx, key)) @@ -162,13 +236,28 @@ SetWeakMapEntryInternal(JSContext* cx, Handle mapObj, return true; } -MOZ_ALWAYS_INLINE bool -WeakMap_set_impl(JSContext* cx, const CallArgs& args) +static bool +SetWeakMapSymbolEntryInternal(JSContext* cx, Handle mapObj, + Handle key, HandleValue value) { - MOZ_ASSERT(IsWeakMap(args.thisv())); + if (!EnsureWeakMapData(cx, mapObj)) + return false; + SymbolValueMap* map = mapObj->getSymbolMap(); - if (!args.get(0).isObject()) { - UniqueChars bytes = DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, args.get(0), nullptr); + MOZ_ASSERT_IF(value.isObject(), value.toObject().compartment() == mapObj->compartment()); + if (!map->put(key, value)) { + JS_ReportOutOfMemory(cx); + return false; + } + return true; +} + +static MOZ_ALWAYS_INLINE bool +SetWeakMapEntryInternal(JSContext* cx, Handle mapObj, + HandleValue key, HandleValue value) +{ + if (!CanBeHeldWeakly(key)) { + UniqueChars bytes = DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, key, nullptr); if (!bytes) return false; JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT, @@ -176,11 +265,24 @@ WeakMap_set_impl(JSContext* cx, const CallArgs& args) return false; } - RootedObject key(cx, &args[0].toObject()); + if (key.isObject()) { + RootedObject objectKey(cx, &key.toObject()); + return SetWeakMapObjectEntryInternal(cx, mapObj, objectKey, value); + } + + Rooted symbolKey(cx, key.toSymbol()); + return SetWeakMapSymbolEntryInternal(cx, mapObj, symbolKey, value); +} + +MOZ_ALWAYS_INLINE bool +WeakMap_set_impl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsWeakMap(args.thisv())); + Rooted thisObj(cx, &args.thisv().toObject()); Rooted map(cx, &thisObj->as()); - if (!SetWeakMapEntryInternal(cx, map, key, args.get(1))) + if (!SetWeakMapEntryInternal(cx, map, args.get(0), args.get(1))) return false; args.rval().set(args.thisv()); return true; @@ -193,6 +295,13 @@ js::WeakMap_set(JSContext* cx, unsigned argc, Value* vp) return CallNonGenericMethod(cx, args); } +bool +js::SetWeakMapEntryValue(JSContext* cx, HandleObject mapObj, HandleValue key, HandleValue val) +{ + Rooted rootedMap(cx, &mapObj->as()); + return SetWeakMapEntryInternal(cx, rootedMap, key, val); +} + JS_FRIEND_API(bool) JS_NondeterministicGetWeakMapKeys(JSContext* cx, HandleObject objArg, MutableHandleObject ret) { @@ -205,11 +314,11 @@ JS_NondeterministicGetWeakMapKeys(JSContext* cx, HandleObject objArg, MutableHan RootedObject arr(cx, NewDenseEmptyArray(cx)); if (!arr) return false; - ObjectValueMap* map = obj->as().getMap(); - if (map) { + WeakMapObject::Data* data = obj->as().getData(); + if (data) { // Prevent GC from mutating the weakmap while iterating. AutoSuppressGC suppress(cx); - for (ObjectValueMap::Base::Range r = map->all(); !r.empty(); r.popFront()) { + for (ObjectValueMap::Base::Range r = data->objectMap.all(); !r.empty(); r.popFront()) { JS::ExposeObjectToActiveJS(r.front().key()); RootedObject key(cx, r.front().key()); if (!cx->compartment()->wrap(cx, &key)) @@ -217,6 +326,14 @@ JS_NondeterministicGetWeakMapKeys(JSContext* cx, HandleObject objArg, MutableHan if (!NewbornArrayPush(cx, arr, ObjectValue(*key))) return false; } + for (SymbolValueMap::Base::Range r = data->symbolMap.all(); !r.empty(); r.popFront()) { + gc::ExposeGCThingToActiveJS(JS::GCCellPtr(r.front().key().get())); + RootedValue key(cx, SymbolValue(r.front().key())); + if (!cx->compartment()->wrap(cx, &key)) + return false; + if (!NewbornArrayPush(cx, arr, key)) + return false; + } } ret.set(arr); return true; @@ -225,21 +342,23 @@ JS_NondeterministicGetWeakMapKeys(JSContext* cx, HandleObject objArg, MutableHan static void WeakMap_mark(JSTracer* trc, JSObject* obj) { - if (ObjectValueMap* map = obj->as().getMap()) - map->trace(trc); + if (WeakMapObject::Data* data = obj->as().getData()) { + data->objectMap.trace(trc); + data->symbolMap.trace(trc); + } } static void WeakMap_finalize(FreeOp* fop, JSObject* obj) { MOZ_ASSERT(fop->maybeOffMainThread()); - if (ObjectValueMap* map = obj->as().getMap()) { + if (WeakMapObject::Data* data = obj->as().getData()) { #ifdef DEBUG - map->~ObjectValueMap(); - memset(static_cast(map), 0xdc, sizeof(*map)); - fop->free_(map); + data->~Data(); + memset(static_cast(data), 0xdc, sizeof(*data)); + fop->free_(data); #else - fop->delete_(map); + fop->delete_(data); #endif } } @@ -282,7 +401,8 @@ JS::SetWeakMapEntry(JSContext* cx, HandleObject mapObj, HandleObject key, CHECK_REQUEST(cx); assertSameCompartment(cx, key, val); Rooted rootedMap(cx, &mapObj->as()); - return SetWeakMapEntryInternal(cx, rootedMap, key, val); + RootedValue keyValue(cx, ObjectValue(*key)); + return SetWeakMapEntryInternal(cx, rootedMap, keyValue, val); } static bool @@ -386,4 +506,3 @@ js::InitBareWeakMapCtor(JSContext* cx, HandleObject obj) { return InitWeakMapClass(cx, obj, false); } - diff --git a/js/src/builtin/WeakMapObject.h b/js/src/builtin/WeakMapObject.h index 507758d2fa..f19ad1d4df 100644 --- a/js/src/builtin/WeakMapObject.h +++ b/js/src/builtin/WeakMapObject.h @@ -16,7 +16,11 @@ class WeakMapObject : public NativeObject public: static const Class class_; - ObjectValueMap* getMap() { return static_cast(getPrivate()); } + struct Data; + + Data* getData() { return static_cast(getPrivate()); } + ObjectValueMap* getMap(); + SymbolValueMap* getSymbolMap(); }; } // namespace js diff --git a/js/src/builtin/WeakRefObject.cpp b/js/src/builtin/WeakRefObject.cpp new file mode 100644 index 0000000000..8e8feb3f3d --- /dev/null +++ b/js/src/builtin/WeakRefObject.cpp @@ -0,0 +1,208 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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 "builtin/WeakRefObject.h" + +#include "jsapi.h" +#include "jscntxt.h" + +#include "gc/Nursery.h" +#include "gc/Tracer.h" +#include "vm/GlobalObject.h" + +#include "jsobjinlines.h" + +#include "vm/Interpreter-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +static WeakRefObject::Referent* +GetReferent(JSObject* obj) +{ + return obj->as().getData(); +} + +static MOZ_ALWAYS_INLINE bool +IsWeakRef(HandleValue v) +{ + return v.isObject() && v.toObject().is(); +} + +static bool +WeakRef_deref_impl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsWeakRef(args.thisv())); + + WeakRefObject::Referent* data = GetReferent(&args.thisv().toObject()); + JSObject* target = data ? data->target.get() : nullptr; + if (target) + args.rval().setObject(*target); + else + args.rval().setUndefined(); + return true; +} + +const JSPropertySpec WeakRefObject::properties[] = { + JS_PS_END +}; + +const JSFunctionSpec WeakRefObject::methods[] = { + JS_FN("deref", WeakRefObject::deref, 0, 0), + JS_FS_END +}; + +static JSObject* +InitWeakRefClass(JSContext* cx, HandleObject obj, bool defineMembers) +{ + Handle global = obj.as(); + RootedPlainObject proto(cx, NewBuiltinClassInstance(cx)); + if (!proto) + return nullptr; + + RootedFunction ctor(cx, GlobalObject::createConstructor(cx, WeakRefObject::construct, + ClassName(JSProto_WeakRef, cx), 1)); + if (!ctor) + return nullptr; + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + if (defineMembers) { + if (!DefinePropertiesAndFunctions(cx, proto, WeakRefObject::properties, WeakRefObject::methods)) + return nullptr; + if (!DefineToStringTag(cx, proto, cx->names().WeakRef)) + return nullptr; + } + + if (!GlobalObject::initBuiltinConstructor(cx, global, JSProto_WeakRef, ctor, proto)) + return nullptr; + return proto; +} + +/* static */ WeakRefObject* +WeakRefObject::create(JSContext* cx, HandleObject target, HandleObject proto /* = nullptr */) +{ + Rooted obj(cx, NewObjectWithClassProto(cx, proto)); + if (!obj) + return nullptr; + + Referent* data = cx->new_(target, cx->options().weakRefs()); + if (!data) + return nullptr; + + obj->setPrivate(data); + return obj; +} + +/* static */ bool +WeakRefObject::construct(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "WeakRef")) + return false; + + if (!args.get(0).isObject()) { + UniqueChars bytes = + DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, args.get(0), nullptr); + if (!bytes) + return false; + + JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT, + bytes.get()); + return false; + } + + RootedObject target(cx, &args[0].toObject()); + + RootedObject proto(cx); + RootedObject newTarget(cx, &args.newTarget().toObject()); + if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + return false; + + Rooted obj(cx, WeakRefObject::create(cx, target, proto)); + if (!obj) + return false; + + args.rval().setObject(*obj); + return true; +} + +/* static */ bool +WeakRefObject::deref(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +/* static */ void +WeakRefObject::trace(JSTracer* trc, JSObject* obj) +{ + if (Referent* data = GetReferent(obj)) { + JSObject* target = data->target.unbarrieredGet(); + if (!target) + return; + + // When pref-disabled, keep referent alive via strong trace so deref() + // stays usable as a stub without touching GC internals. + if (!data->enabled) { + TraceManuallyBarrieredEdge(trc, data->target.unsafeGet(), "WeakRef stub referent"); + } else if (IsInsideNursery(target)) { + // Weak edges must be tenured; trace strongly while referent is in the nursery. + TraceManuallyBarrieredEdge(trc, data->target.unsafeGet(), "WeakRef nursery referent"); + } else { + TraceWeakEdge(trc, &data->target, "WeakRef referent"); + } + } +} + +/* static */ void +WeakRefObject::finalize(FreeOp* fop, JSObject* obj) +{ + if (Referent* data = GetReferent(obj)) + fop->delete_(data); +} + +static const ClassOps WeakRefObjectClassOps = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + WeakRefObject::finalize, + nullptr, /* call */ + nullptr, /* hasInstance */ + nullptr, /* construct */ + WeakRefObject::trace +}; + +const Class WeakRefObject::class_ = { + "WeakRef", + JSCLASS_HAS_PRIVATE | + JSCLASS_HAS_CACHED_PROTO(JSProto_WeakRef) | + JSCLASS_BACKGROUND_FINALIZE, + &WeakRefObjectClassOps +}; + +/* static */ JSObject* +WeakRefObject::initClass(JSContext* cx, HandleObject obj) +{ + return ::InitWeakRefClass(cx, obj, true); +} + +JSObject* +js::InitWeakRefClass(JSContext* cx, HandleObject obj) +{ + return WeakRefObject::initClass(cx, obj); +} + +JSObject* +js::InitBareWeakRefCtor(JSContext* cx, HandleObject obj) +{ + return ::InitWeakRefClass(cx, obj, false); +} diff --git a/js/src/builtin/WeakRefObject.h b/js/src/builtin/WeakRefObject.h new file mode 100644 index 0000000000..6f2a2b4590 --- /dev/null +++ b/js/src/builtin/WeakRefObject.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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/. */ + +#ifndef builtin_WeakRefObject_h +#define builtin_WeakRefObject_h + +#include "gc/Barrier.h" +#include "vm/NativeObject.h" + +namespace js { + +class WeakRefObject : public NativeObject +{ + public: + struct Referent { + explicit Referent(JSObject* obj, bool enabled) + : target(obj), enabled(enabled) {} + WeakRef target; + bool enabled; + }; + + static const Class class_; + + static JSObject* initClass(JSContext* cx, HandleObject obj); + static WeakRefObject* create(JSContext* cx, HandleObject target, HandleObject proto = nullptr); + + static void trace(JSTracer* trc, JSObject* obj); + static void finalize(FreeOp* fop, JSObject* obj); + [[nodiscard]] static bool construct(JSContext* cx, unsigned argc, Value* vp); + static bool deref(JSContext* cx, unsigned argc, Value* vp); + + Referent* getData() const { + return static_cast(getPrivate()); + } + + WeakRef& target() { + MOZ_ASSERT(getData()); + return getData()->target; + } + + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; + + private: +}; + +extern JSObject* +InitWeakRefClass(JSContext* cx, HandleObject obj); + +extern JSObject* +InitBareWeakRefCtor(JSContext* cx, HandleObject obj); + +static inline bool +CanBeHeldWeakly(const Value& value) +{ + return value.isObject() || value.isSymbol(); +} + +} // namespace js + +#endif /* builtin_WeakRefObject_h */ diff --git a/js/src/builtin/WeakSet.js b/js/src/builtin/WeakSet.js index b16b4634dd..88f7929c72 100644 --- a/js/src/builtin/WeakSet.js +++ b/js/src/builtin/WeakSet.js @@ -32,7 +32,7 @@ function WeakSet_add(value) { ThrowTypeError(JSMSG_INCOMPATIBLE_PROTO, "WeakSet", "add", typeof S); // Step 5. - if (!IsObject(value)) + if (!CanBeHeldWeakly(value)) ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, DecompileArg(0, value)); // Steps 7-8. @@ -55,7 +55,7 @@ function WeakSet_delete(value) { ThrowTypeError(JSMSG_INCOMPATIBLE_PROTO, "WeakSet", "delete", typeof S); // Step 5. - if (!IsObject(value)) + if (!CanBeHeldWeakly(value)) return false; // Steps 7-8. @@ -75,7 +75,7 @@ function WeakSet_has(value) { ThrowTypeError(JSMSG_INCOMPATIBLE_PROTO, "WeakSet", "has", typeof S); // Step 6. - if (!IsObject(value)) + if (!CanBeHeldWeakly(value)) return false; // Steps 7-8. diff --git a/js/src/builtin/WeakSetObject.cpp b/js/src/builtin/WeakSetObject.cpp index bb9708f6f9..055f6b1abd 100644 --- a/js/src/builtin/WeakSetObject.cpp +++ b/js/src/builtin/WeakSetObject.cpp @@ -12,6 +12,7 @@ #include "builtin/MapObject.h" #include "builtin/SelfHostingDefines.h" #include "builtin/WeakMapObject.h" +#include "builtin/WeakRefObject.h" #include "vm/GlobalObject.h" #include "vm/SelfHosting.h" @@ -108,7 +109,6 @@ WeakSetObject::construct(JSContext* cx, unsigned argc, Value* vp) if (optimized) { RootedValue keyVal(cx); - RootedObject keyObject(cx); RootedValue placeholder(cx, BooleanValue(true)); RootedObject map(cx, &obj->getReservedSlot(WEAKSET_MAP_SLOT).toObject()); RootedArrayObject array(cx, &iterable.toObject().as()); @@ -116,7 +116,7 @@ WeakSetObject::construct(JSContext* cx, unsigned argc, Value* vp) keyVal.set(array->getDenseElement(index)); MOZ_ASSERT(!keyVal.isMagic(JS_ELEMENTS_HOLE)); - if (keyVal.isPrimitive()) { + if (!CanBeHeldWeakly(keyVal)) { UniqueChars bytes = DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, keyVal, nullptr); if (!bytes) @@ -126,8 +126,7 @@ WeakSetObject::construct(JSContext* cx, unsigned argc, Value* vp) return false; } - keyObject = &keyVal.toObject(); - if (!SetWeakMapEntry(cx, map, keyObject, placeholder)) + if (!SetWeakMapEntryValue(cx, map, keyVal, placeholder)) return false; } } else { diff --git a/js/src/jit/BaselineIC.cpp b/js/src/jit/BaselineIC.cpp index adef1f46f9..4fc540e626 100644 --- a/js/src/jit/BaselineIC.cpp +++ b/js/src/jit/BaselineIC.cpp @@ -1470,6 +1470,12 @@ TryAttachGetElemStub(JSContext* cx, JSScript* script, jsbytecode* pc, ICGetElem_ res.isNumber() && !TypedArrayGetElemStubExists(stub, obj)) { + if (obj->is() && + obj->as().hasResizableOrGrowableBuffer()) + { + return true; + } + if (!cx->runtime()->jitSupportsFloatingPoint && (TypedThingRequiresFloatingPoint(obj) || rhs.isDouble())) { @@ -2160,6 +2166,8 @@ ICGetElem_TypedArray::Compiler::generateStubCode(MacroAssembler& masm) Register obj = masm.extractObject(R0, ExtractTemp0); masm.loadPtr(Address(ICStubReg, ICGetElem_TypedArray::offsetOfShape()), scratchReg); masm.branchTestObjShape(Assembler::NotEqual, obj, scratchReg, &failure); + if (layout_ == Layout_TypedArray) + GuardResizableOrGrowableTypedArray(masm, obj, scratchReg, &failure); // Ensure the index is an integer. if (cx->runtime()->jitSupportsFloatingPoint) { @@ -2631,6 +2639,9 @@ DoSetElemFallback(JSContext* cx, BaselineFrame* frame, ICSetElem_Fallback* stub_ bool expectOutOfBounds; double idx = index.toNumber(); if (obj->is()) { + if (obj->as().hasResizableOrGrowableBuffer()) + return true; + expectOutOfBounds = (idx < 0 || idx >= double(obj->as().length())); } else { // Typed objects throw on out of bounds accesses. Don't attach @@ -3221,6 +3232,8 @@ ICSetElem_TypedArray::Compiler::generateStubCode(MacroAssembler& masm) Register obj = masm.extractObject(R0, ExtractTemp0); masm.loadPtr(Address(ICStubReg, ICSetElem_TypedArray::offsetOfShape()), scratchReg); masm.branchTestObjShape(Assembler::NotEqual, obj, scratchReg, &failure); + if (layout_ == Layout_TypedArray) + GuardResizableOrGrowableTypedArray(masm, obj, scratchReg, &failure); // Ensure the index is an integer. if (cx->runtime()->jitSupportsFloatingPoint) { diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp index a37c3079ef..1ef6e607e8 100644 --- a/js/src/jit/CodeGenerator.cpp +++ b/js/src/jit/CodeGenerator.cpp @@ -8889,9 +8889,17 @@ CodeGenerator::branchIfNotEmptyObjectElements(Register obj, Label* target) Address(obj, NativeObject::offsetOfElements()), ImmPtr(js::emptyObjectElements), &emptyObj); - masm.branchPtr(Assembler::NotEqual, + masm.branchPtr(Assembler::Equal, Address(obj, NativeObject::offsetOfElements()), ImmPtr(js::emptyObjectElementsShared), + &emptyObj); + masm.branchPtr(Assembler::Equal, + Address(obj, NativeObject::offsetOfElements()), + ImmPtr(js::emptyObjectElementsResizableOrGrowable), + &emptyObj); + masm.branchPtr(Assembler::NotEqual, + Address(obj, NativeObject::offsetOfElements()), + ImmPtr(js::emptyObjectElementsSharedResizableOrGrowable), target); masm.bind(&emptyObj); } diff --git a/js/src/jit/CodeGenerator.h b/js/src/jit/CodeGenerator.h index 02fda9d577..0e554c17af 100644 --- a/js/src/jit/CodeGenerator.h +++ b/js/src/jit/CodeGenerator.h @@ -537,8 +537,7 @@ class CodeGenerator final : public CodeGeneratorSpecific Label* ifDoesntEmulateUndefined, Register scratch, OutOfLineTestObject* ool); - // Branch to target unless obj has an emptyObjectElements or emptyObjectElementsShared - // elements pointer. + // Branch to target unless obj has one of the empty elements pointers. void branchIfNotEmptyObjectElements(Register obj, Label* target); void emitStoreElementTyped(const LAllocation* value, MIRType valueType, MIRType elementType, diff --git a/js/src/jit/IonCaches.cpp b/js/src/jit/IonCaches.cpp index 5a7e437285..095dc14175 100644 --- a/js/src/jit/IonCaches.cpp +++ b/js/src/jit/IonCaches.cpp @@ -1245,6 +1245,7 @@ GenerateTypedArrayLength(JSContext* cx, MacroAssembler& masm, IonCache::StubAtta masm.branchPtr(Assembler::AboveOrEqual, tmpReg, ImmPtr(&TypedArrayObject::classes[Scalar::MaxTypedArrayViewType]), failures); + GuardResizableOrGrowableTypedArray(masm, object, tmpReg, failures); // Load length. masm.loadTypedOrValue(Address(object, TypedArrayObject::lengthOffset()), output); @@ -1644,6 +1645,9 @@ GetPropertyIC::tryAttachTypedArrayLength(JSContext* cx, HandleScript outerScript if (!JSID_IS_ATOM(id, cx->names().length)) return true; + if (obj->as().hasResizableOrGrowableBuffer()) + return true; + if (hasTypedArrayLengthStub(obj)) return true; @@ -4038,6 +4042,12 @@ GetPropertyIC::canAttachTypedOrUnboxedArrayElement(JSObject* obj, const Value& i if (!obj->is() && !obj->is()) return false; + if (obj->is() && + obj->as().hasResizableOrGrowableBuffer()) + { + return false; + } + MOZ_ASSERT(idval.isInt32() || idval.isString()); // Don't emit a stub if the access is out of bounds. We make to make @@ -4092,6 +4102,9 @@ GenerateGetTypedOrUnboxedArrayElement(JSContext* cx, MacroAssembler& masm, // Decide to what type index the stub should be optimized Register tmpReg = output.scratchReg().gpr(); MOZ_ASSERT(tmpReg != InvalidReg); + if (array->is()) + GuardResizableOrGrowableTypedArray(masm, object, tmpReg, &failures); + Register indexReg = tmpReg; if (idval.isString()) { MOZ_ASSERT(GetIndexFromString(idval.toString()) != UINT32_MAX); @@ -4575,6 +4588,7 @@ GenerateSetTypedArrayElement(JSContext* cx, MacroAssembler& masm, IonCache::Stub if (!shape) return false; masm.branchTestObjShape(Assembler::NotEqual, object, shape, &failures); + GuardResizableOrGrowableTypedArray(masm, object, temp, &failures); // Ensure the index is an int32. Register indexReg; @@ -4661,6 +4675,9 @@ SetPropertyIC::tryAttachTypedArrayElement(JSContext* cx, HandleScript outerScrip if (!IsTypedArrayElementSetInlineable(obj, idval, val)) return true; + if (obj->as().hasResizableOrGrowableBuffer()) + return true; + *emitted = true; MacroAssembler masm(cx, ion, outerScript, profilerLeavePc_); diff --git a/js/src/jit/MCallOptimize.cpp b/js/src/jit/MCallOptimize.cpp index 0f78ef3f2f..4585a838b4 100644 --- a/js/src/jit/MCallOptimize.cpp +++ b/js/src/jit/MCallOptimize.cpp @@ -365,13 +365,9 @@ IonBuilder::inlineNativeGetter(CallInfo& callInfo, JSFunction* target) // Try to optimize typed array lengths. if (TypedArrayObject::isOriginalLengthGetter(native)) { - Scalar::Type type = thisTypes->getTypedArrayType(constraints()); - if (type == Scalar::MaxTypedArrayViewType) - return InliningStatus_NotInlined; - - MInstruction* length = addTypedArrayLength(thisArg); - current->push(length); - return InliningStatus_Inlined; + // RAB/GSAB views can have dynamic length or temporarily become + // out-of-bounds. Let the property IC/VM path handle the getter. + return InliningStatus_NotInlined; } // Try to optimize RegExp getters. @@ -2480,21 +2476,10 @@ IsTypedArrayObject(CompilerConstraintList* constraints, MDefinition* def) IonBuilder::InliningStatus IonBuilder::inlinePossiblyWrappedTypedArrayLength(CallInfo& callInfo) { - MOZ_ASSERT(!callInfo.constructing()); - MOZ_ASSERT(callInfo.argc() == 1); - if (callInfo.getArg(0)->type() != MIRType::Object) - return InliningStatus_NotInlined; - if (getInlineReturnType() != MIRType::Int32) - return InliningStatus_NotInlined; + (void) callInfo; - if (!IsTypedArrayObject(constraints(), callInfo.getArg(0))) - return InliningStatus_NotInlined; - - MInstruction* length = addTypedArrayLength(callInfo.getArg(0)); - current->push(length); - - callInfo.setImplicitlyUsedUnchecked(); - return InliningStatus_Inlined; + // RAB/GSAB views require dynamic length semantics. + return InliningStatus_NotInlined; } IonBuilder::InliningStatus @@ -3251,45 +3236,14 @@ bool IonBuilder::atomicsMeetsPreconditions(CallInfo& callInfo, Scalar::Type* arrayType, bool* requiresTagCheck, AtomicCheckResult checkResult) { - if (!JitSupportsAtomics()) - return false; + (void) callInfo; + (void) arrayType; + (void) requiresTagCheck; + (void) checkResult; - if (callInfo.getArg(0)->type() != MIRType::Object) - return false; - - if (callInfo.getArg(1)->type() != MIRType::Int32) - return false; - - // Ensure that the first argument is a TypedArray that maps shared - // memory. - // - // Then check both that the element type is something we can - // optimize and that the return type is suitable for that element - // type. - - TemporaryTypeSet* arg0Types = callInfo.getArg(0)->resultTypeSet(); - if (!arg0Types) - return false; - - TemporaryTypeSet::TypedArraySharedness sharedness; - *arrayType = arg0Types->getTypedArrayType(constraints(), &sharedness); - *requiresTagCheck = sharedness != TemporaryTypeSet::KnownShared; - switch (*arrayType) { - case Scalar::Int8: - case Scalar::Uint8: - case Scalar::Int16: - case Scalar::Uint16: - case Scalar::Int32: - return checkResult == DontCheckAtomicResult || getInlineReturnType() == MIRType::Int32; - case Scalar::Uint32: - // Bug 1077305: it would be attractive to allow inlining even - // if the inline return type is Int32, which it will frequently - // be. - return checkResult == DontCheckAtomicResult || getInlineReturnType() == MIRType::Double; - default: - // Excludes floating types and Uint8Clamped. - return false; - } + // Atomics bounds checks through addTypedArrayLengthAndData assume stable + // SharedArrayBuffer lengths. Avoid this path now that growable SAB exists. + return false; } void diff --git a/js/src/jit/MIR.cpp b/js/src/jit/MIR.cpp index 197737c1ef..8f13179bfd 100644 --- a/js/src/jit/MIR.cpp +++ b/js/src/jit/MIR.cpp @@ -5505,25 +5505,15 @@ jit::ElementAccessIsTypedArray(CompilerConstraintList* constraints, MDefinition* obj, MDefinition* id, Scalar::Type* arrayType) { - if (obj->mightBeType(MIRType::String)) - return false; + (void) constraints; + (void) obj; + (void) id; + (void) arrayType; - if (id->type() != MIRType::Int32 && id->type() != MIRType::Double) - return false; - - TemporaryTypeSet* types = obj->resultTypeSet(); - if (!types) - return false; - - *arrayType = types->getTypedArrayType(constraints); - - // FIXME: https://bugzil.la/1536699 - if (*arrayType == Scalar::MaxTypedArrayViewType || - Scalar::isBigIntType(*arrayType)) { - return false; - } - - return true; + // Resizable ArrayBuffer and growable SharedArrayBuffer views need dynamic + // length/out-of-bounds handling. This older MIR path assumes stable + // typed-array length/data slots, so keep element accesses on IC/VM paths. + return false; } bool diff --git a/js/src/jit/SharedIC.cpp b/js/src/jit/SharedIC.cpp index 2805651f12..edf39837cd 100644 --- a/js/src/jit/SharedIC.cpp +++ b/js/src/jit/SharedIC.cpp @@ -3631,6 +3631,17 @@ CheckForTypedObjectWithDetachedStorage(JSContext* cx, MacroAssembler& masm, Labe masm.branch32(Assembler::NotEqual, AbsoluteAddress(address), Imm32(0), failure); } +void +GuardResizableOrGrowableTypedArray(MacroAssembler& masm, Register obj, Register scratch, + Label* failure) +{ + masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch); + masm.branchTest32(Assembler::NonZero, + Address(scratch, ObjectElements::offsetOfFlags()), + Imm32(ObjectElements::RESIZABLE_OR_GROWABLE_BUFFER), + failure); +} + void LoadTypedThingData(MacroAssembler& masm, TypedThingLayout layout, Register obj, Register result) { diff --git a/js/src/jit/SharedIC.h b/js/src/jit/SharedIC.h index ab379d3039..cf23490942 100644 --- a/js/src/jit/SharedIC.h +++ b/js/src/jit/SharedIC.h @@ -2307,7 +2307,11 @@ CheckDOMProxyExpandoDoesNotShadow(JSContext* cx, MacroAssembler& masm, Register void CheckForTypedObjectWithDetachedStorage(JSContext* cx, MacroAssembler& masm, Label* failure); -MOZ_MUST_USE bool +void +GuardResizableOrGrowableTypedArray(MacroAssembler& masm, Register obj, Register scratch, + Label* failure); + +[[nodiscard]] bool DoCallNativeGetter(JSContext* cx, HandleFunction callee, HandleObject obj, MutableHandleValue result); diff --git a/js/src/js.msg b/js/src/js.msg index 499e7a1623..c635bb54ed 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -548,6 +548,8 @@ MSG_DEF(JSMSG_TOO_LONG_ARRAY, 0, JSEXN_TYPEERR, "Too long array") // Typed array MSG_DEF(JSMSG_BAD_INDEX, 0, JSEXN_RANGEERR, "invalid or out-of-range index") +MSG_DEF(JSMSG_ARRAYBUFFER_NOT_RESIZABLE, 0, JSEXN_TYPEERR, "ArrayBuffer is not resizable") +MSG_DEF(JSMSG_ARRAYBUFFER_CANNOT_DETACH, 0, JSEXN_TYPEERR, "ArrayBuffer cannot be detached") MSG_DEF(JSMSG_NON_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected ArrayBuffer, but species constructor returned non-ArrayBuffer") MSG_DEF(JSMSG_SAME_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected different ArrayBuffer, but species constructor returned same ArrayBuffer") MSG_DEF(JSMSG_SHORT_ARRAY_BUFFER_RETURNED, 2, JSEXN_TYPEERR, "expected ArrayBuffer with at least {0} bytes, but species constructor returns ArrayBuffer with {1} bytes") @@ -555,12 +557,15 @@ MSG_DEF(JSMSG_TYPED_ARRAY_BAD_ARGS, 0, JSEXN_TYPEERR, "invalid arguments") MSG_DEF(JSMSG_TYPED_ARRAY_NEGATIVE_ARG,1, JSEXN_RANGEERR, "argument {0} must be >= 0") MSG_DEF(JSMSG_TYPED_ARRAY_DETACHED, 0, JSEXN_TYPEERR, "attempting to access detached ArrayBuffer") MSG_DEF(JSMSG_TYPED_ARRAY_CONSTRUCT_BOUNDS, 0, JSEXN_RANGEERR, "attempting to construct out-of-bounds TypedArray on ArrayBuffer") +MSG_DEF(JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS, 0, JSEXN_TYPEERR, "attempting to access out-of-bounds TypedArray") +MSG_DEF(JSMSG_DATA_VIEW_OUT_OF_BOUNDS, 0, JSEXN_TYPEERR, "attempting to access out-of-bounds DataView") MSG_DEF(JSMSG_TYPED_ARRAY_CALL_OR_CONSTRUCT, 1, JSEXN_TYPEERR, "cannot directly {0} builtin %TypedArray%") MSG_DEF(JSMSG_NON_TYPED_ARRAY_RETURNED, 0, JSEXN_TYPEERR, "constructor didn't return TypedArray object") MSG_DEF(JSMSG_SHORT_TYPED_ARRAY_RETURNED, 2, JSEXN_TYPEERR, "expected TypedArray of at least length {0}, but constructor returned TypedArray of length {1}") // Shared array buffer MSG_DEF(JSMSG_SHARED_ARRAY_BAD_LENGTH, 0, JSEXN_RANGEERR, "length argument out of range") +MSG_DEF(JSMSG_SHARED_ARRAY_NOT_GROWABLE, 0, JSEXN_TYPEERR, "SharedArrayBuffer is not growable") MSG_DEF(JSMSG_NON_SHARED_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected SharedArrayBuffer, but species constructor returned non-SharedArrayBuffer") MSG_DEF(JSMSG_SAME_SHARED_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected different SharedArrayBuffer, but species constructor returned same SharedArrayBuffer") MSG_DEF(JSMSG_SHORT_SHARED_ARRAY_BUFFER_RETURNED, 2, JSEXN_TYPEERR, "expected SharedArrayBuffer with at least {0} bytes, but species constructor returns SharedArrayBuffer with {1} bytes") diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index f1627e481e..3ea147eaff 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -951,6 +951,20 @@ LookupStdName(const JSAtomState& names, JSAtom* name, const JSStdName* table) return nullptr; } +static bool +IsStreamExtraName(JSFlatString* name) +{ + return JS_FlatStringEqualsAscii(name, "WritableStream") || + JS_FlatStringEqualsAscii(name, "WritableStreamDefaultWriter") || + JS_FlatStringEqualsAscii(name, "WritableStreamDefaultController") || + JS_FlatStringEqualsAscii(name, "TransformStream") || + JS_FlatStringEqualsAscii(name, "TransformStreamDefaultController") || + JS_FlatStringEqualsAscii(name, "TextEncoderStream") || + JS_FlatStringEqualsAscii(name, "TextDecoderStream") || + JS_FlatStringEqualsAscii(name, "CompressionStream") || + JS_FlatStringEqualsAscii(name, "DecompressionStream"); +} + /* * Table of standard classes, indexed by JSProtoKey. For entries where the * JSProtoKey does not correspond to a class with a meaningful constructor, we @@ -1041,11 +1055,27 @@ JS_ResolveStandardClass(JSContext* cx, HandleObject obj, HandleId id, bool* reso if (!GlobalObject::ensureConstructor(cx, global, key)) return false; + if (key == JSProto_ReadableStream) { + RootedObject globalObj(cx, global); + if (!InitStreamExtras(cx, globalObj)) + return false; + } + *resolved = true; return true; } } + if (cx->options().streams() && IsStreamExtraName(idAtom)) { + RootedObject globalObj(cx, global); + if (!InitStreamExtras(cx, globalObj)) + return false; + + if (!HasOwnProperty(cx, globalObj, id, resolved)) + return false; + return true; + } + // There is no such property to resolve. An ordinary resolve hook would // just return true at this point. But the global object is special in one // more way: its prototype chain is lazily initialized. That is, @@ -1075,6 +1105,7 @@ JS_MayResolveStandardClass(const JSAtomState& names, jsid id, JSObject* maybeObj return atom == names.undefined || atom == names.globalThis || + IsStreamExtraName(atom) || LookupStdName(names, atom, standard_class_names) || LookupStdName(names, atom, builtin_property_names); } diff --git a/js/src/jsapi.h b/js/src/jsapi.h index cae2297ae4..7744bdd592 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -998,7 +998,9 @@ class JS_PUBLIC_API(ContextOptions) { werror_(false), strictMode_(false), extraWarnings_(false), - arrayProtoValues_(true) + arrayProtoValues_(true), + streams_(true), + weakRefs_(false) { } @@ -1138,6 +1140,16 @@ class JS_PUBLIC_API(ContextOptions) { return *this; } + bool weakRefs() const { return weakRefs_; } + ContextOptions& setWeakRefs(bool flag) { + weakRefs_ = flag; + return *this; + } + ContextOptions& toggleWeakRefs() { + weakRefs_ = !weakRefs_; + return *this; + } + private: bool baseline_ : 1; bool ion_ : 1; @@ -1155,6 +1167,7 @@ class JS_PUBLIC_API(ContextOptions) { bool extraWarnings_ : 1; bool arrayProtoValues_ : 1; bool streams_ : 1; + bool weakRefs_ : 1; }; JS_PUBLIC_API(ContextOptions&) diff --git a/js/src/jsfriendapi.h b/js/src/jsfriendapi.h index c463019dac..2697cb5337 100644 --- a/js/src/jsfriendapi.h +++ b/js/src/jsfriendapi.h @@ -1748,6 +1748,9 @@ JS_IsFloat64Array(JSObject* obj); extern JS_FRIEND_API(bool) JS_GetTypedArraySharedness(JSObject* obj); +extern JS_FRIEND_API(uint32_t) +JS_GetTypedArrayLength(JSObject* obj); + /* * Test for specific typed array types (ArrayBufferView subtypes) and return * the unwrapped object if so, else nullptr. Never throws. @@ -1814,8 +1817,7 @@ inline void \ Get ## Type ## ArrayLengthAndData(JSObject* obj, uint32_t* length, bool* isSharedMemory, type** data) \ { \ MOZ_ASSERT(GetObjectClass(obj) == detail::Type ## ArrayClassPtr); \ - const JS::Value& lenSlot = GetReservedSlot(obj, detail::TypedArrayLengthSlot); \ - *length = mozilla::AssertedCast(lenSlot.toInt32()); \ + *length = JS_GetTypedArrayLength(obj); \ *isSharedMemory = JS_GetTypedArraySharedness(obj); \ *data = static_cast(GetObjectPrivate(obj)); \ } diff --git a/js/src/jsprototypes.h b/js/src/jsprototypes.h index 9822a47b6c..6005724f89 100644 --- a/js/src/jsprototypes.h +++ b/js/src/jsprototypes.h @@ -119,6 +119,7 @@ IF_SAB(real,imaginary)(Atomics, InitAtomicsClass, OCLASP(Atomics)) \ imaginary(WritableStreamDefaultController,dummy, dummy) \ real(ByteLengthQueuingStrategy, InitViaClassSpec, &js::ByteLengthQueuingStrategy::class_) \ real(CountQueuingStrategy, InitViaClassSpec, &js::CountQueuingStrategy::class_) \ + real(WeakRef, InitWeakRefClass, OCLASP(WeakRef)) \ #define JS_FOR_EACH_PROTOTYPE(macro) JS_FOR_PROTOTYPES(macro,macro) diff --git a/js/src/jsstr.cpp b/js/src/jsstr.cpp index 5209e979de..67de5d7236 100644 --- a/js/src/jsstr.cpp +++ b/js/src/jsstr.cpp @@ -3344,6 +3344,8 @@ static const JSFunctionSpec string_methods[] = { JS_SELF_HOSTED_FN("toLocaleUpperCase", "String_toLocaleUpperCase", 0,0), JS_SELF_HOSTED_FN("localeCompare", "String_localeCompare", 1,0), JS_SELF_HOSTED_FN("repeat", "String_repeat", 1,0), + JS_SELF_HOSTED_FN("isWellFormed", "String_isWellFormed", 0,0), + JS_SELF_HOSTED_FN("toWellFormed", "String_toWellFormed", 0,0), JS_FN("normalize", str_normalize, 0,0), /* Perl-ish methods (search is actually Python-esque). */ diff --git a/js/src/jsweakmap.h b/js/src/jsweakmap.h index c13baa7825..ec0fd8ddc6 100644 --- a/js/src/jsweakmap.h +++ b/js/src/jsweakmap.h @@ -16,11 +16,25 @@ #include "gc/Marking.h" #include "gc/StoreBuffer.h" #include "js/HashTable.h" +#include "vm/Symbol.h" namespace js { class WeakMapBase; +template <> +struct MovableCellHasher +{ + using Key = JS::Symbol*; + using Lookup = JS::Symbol*; + + static bool hasHash(const Lookup& l) { return true; } + static bool ensureHash(const Lookup& l) { return true; } + static HashNumber hash(const Lookup& l) { return l->hash(); } + static bool match(const Key& k, const Lookup& l) { return k == l; } + static void rekey(Key& k, const Key& newKey) { k = newKey; } +}; + // A subclass template of js::HashMap whose keys and values may be garbage-collected. When // a key is collected, the table entry disappears, dropping its reference to the value. // @@ -296,9 +310,16 @@ class WeakMap : public HashMap, return nullptr; } + JSObject* getDelegate(JS::Symbol* sym) const { + return nullptr; + } + private: void exposeGCThingToActiveJS(const JS::Value& v) const { JS::ExposeValueToActiveJS(v); } void exposeGCThingToActiveJS(JSObject* obj) const { JS::ExposeObjectToActiveJS(obj); } + void exposeGCThingToActiveJS(JS::Symbol* sym) const { + gc::ExposeGCThingToActiveJS(JS::GCCellPtr(sym)); + } bool keyNeedsMark(JSObject* key) const { JSObject* delegate = getDelegate(key); @@ -313,6 +334,10 @@ class WeakMap : public HashMap, return false; } + bool keyNeedsMark(JS::Symbol* sym) const { + return false; + } + bool findZoneEdges() override { // This is overridden by ObjectValueMap. return true; @@ -378,6 +403,9 @@ WeakMap_set(JSContext* cx, unsigned argc, Value* vp); extern bool WeakMap_delete(JSContext* cx, unsigned argc, Value* vp); +extern bool +SetWeakMapEntryValue(JSContext* cx, HandleObject mapObj, HandleValue key, HandleValue val); + extern JSObject* InitWeakMapClass(JSContext* cx, HandleObject obj); @@ -394,6 +422,16 @@ class ObjectValueMap : public WeakMap, HeapPtr, virtual bool findZoneEdges(); }; +class SymbolValueMap : public WeakMap, HeapPtr, + MovableCellHasher>> +{ + public: + SymbolValueMap(JSContext* cx, JSObject* obj) + : WeakMap, HeapPtr, + MovableCellHasher>>(cx, obj) + {} +}; + // Generic weak map for mapping objects to other objects. class ObjectWeakMap diff --git a/js/src/moz.build b/js/src/moz.build index baa4e1a19e..6672e41220 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -142,6 +142,7 @@ main_deunified_sources = [ 'builtin/TestingFunctions.cpp', 'builtin/TypedObject.cpp', 'builtin/WeakMapObject.cpp', + 'builtin/WeakRefObject.cpp', 'builtin/WeakSetObject.cpp', 'devtools/sharkctl.cpp', 'ds/LifoAlloc.cpp', diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index ed30afa7ef..8f075ce228 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -3330,6 +3330,15 @@ static const JSClass sandbox_class = { &sandbox_classOps }; +enum GlobalAppSlot { + GlobalAppSlotModuleMetadataHook, + GlobalAppSlotModuleDynamicImportHook, + GlobalAppSlotCount +}; + +static_assert(GlobalAppSlotCount <= JSCLASS_GLOBAL_APPLICATION_SLOTS, + "global application slots overflow"); + static void SetStandardCompartmentOptions(JS::CompartmentOptions& options) { @@ -4158,7 +4167,7 @@ ParseModule(JSContext* cx, unsigned argc, Value* vp) const char16_t* chars = stableChars.twoByteRange().begin().get(); JS::SourceBufferHolder srcBuf(chars, scriptContents->length(), - SourceBufferHolder::NoOwnership); + JS::SourceBufferHolder::NoOwnership); RootedObject module(cx, frontend::CompileModule(cx, options, srcBuf)); if (!module) @@ -4369,7 +4378,7 @@ AbortDynamicModuleImport(JSContext* cx, unsigned argc, Value* vp) RootedString specifier(cx, args[1].toString()); Rooted promise(cx, &args[2].toObject().as()); - cx->setPendingException(args[3]); + cx->setPendingException(args[3], nullptr); return js::FinishDynamicModuleImport(cx, args[0], specifier, promise); } @@ -8422,7 +8431,7 @@ main(int argc, char** argv, char** envp) JS::SetModuleResolveHook(cx->runtime(), ShellModuleResolveHook); JS::SetModuleDynamicImportHook(cx, ShellModuleDynamicImportHook); - JS::SetModuleMetadataHook(cx, ShellModuleMetadataHook); + JS::SetModuleMetadataHook(cx, CallModuleMetadataHook); result = Shell(cx, &op, envp); diff --git a/js/src/tests/non262/ArrayBuffer/resizable-transfer.js b/js/src/tests/non262/ArrayBuffer/resizable-transfer.js new file mode 100644 index 0000000000..f603edabff --- /dev/null +++ b/js/src/tests/non262/ArrayBuffer/resizable-transfer.js @@ -0,0 +1,98 @@ +// |reftest| skip-if(!ArrayBuffer.prototype.transfer) + +var fixed = new ArrayBuffer(4); +assertEq(fixed.byteLength, 4); +assertEq(fixed.maxByteLength, 4); +assertEq(fixed.resizable, false); +assertEq(fixed.detached, false); +new Uint8Array(fixed).set([1, 2, 3, 4]); +fixed.extra = 17; +assertEq(Array.from(new Uint8Array(fixed)).join(","), "1,2,3,4"); +assertThrowsInstanceOf(() => fixed.resize(2), TypeError); +assertEq(ArrayBuffer.prototype.transfer.length, 0); +assertEq(ArrayBuffer.prototype.transferToFixedLength.length, 0); +var resizeArgumentConverted = false; +assertThrowsInstanceOf(() => fixed.resize({ valueOf() { resizeArgumentConverted = true; return 1; } }), + TypeError); +assertEq(resizeArgumentConverted, false); + +var resizable = new ArrayBuffer(4, { maxByteLength: 8 }); +assertEq(resizable.byteLength, 4); +assertEq(resizable.maxByteLength, 8); +assertEq(resizable.resizable, true); + +var bytes = new Uint8Array(resizable); +bytes[0] = 11; +bytes[3] = 44; +resizable.resize(6); +assertEq(resizable.byteLength, 6); +assertEq(new Uint8Array(resizable)[0], 11); +assertEq(new Uint8Array(resizable)[3], 44); +assertEq(new Uint8Array(resizable)[4], 0); +assertThrowsInstanceOf(() => resizable.resize(9), RangeError); + +var source = new ArrayBuffer(4); +var sourceBytes = new Uint8Array(source); +sourceBytes[0] = 1; +sourceBytes[1] = 2; +var sourceView = new Uint8Array(source); +var moved = source.transfer(6); +assertEq(source.detached, true); +assertEq(source.byteLength, 0); +assertEq(source.maxByteLength, 0); +assertEq(sourceView.length, 0); +assertEq(moved.byteLength, 6); +assertEq(moved.resizable, false); +assertEq(moved.maxByteLength, 6); +assertEq(new Uint8Array(moved)[0], 1); +assertEq(new Uint8Array(moved)[1], 2); +assertEq(new Uint8Array(moved)[4], 0); + +var resizableSource = new ArrayBuffer(4, { maxByteLength: 8 }); +new Uint8Array(resizableSource)[0] = 7; +var resizableMoved = resizableSource.transfer(); +assertEq(resizableSource.detached, true); +assertEq(resizableSource.resizable, true); +assertEq(resizableMoved.byteLength, 4); +assertEq(resizableMoved.maxByteLength, 8); +assertEq(resizableMoved.resizable, true); +assertEq(new Uint8Array(resizableMoved)[0], 7); + +var fixedMoved = resizableMoved.transferToFixedLength(10); +assertEq(resizableMoved.detached, true); +assertEq(fixedMoved.byteLength, 10); +assertEq(fixedMoved.maxByteLength, 10); +assertEq(fixedMoved.resizable, false); +assertEq(new Uint8Array(fixedMoved)[0], 7); + +var sliceSource = new ArrayBuffer(8, { maxByteLength: 8 }); +var sliceSourceBytes = new Uint8Array(sliceSource); +for (var i = 0; i < sliceSourceBytes.length; i++) + sliceSourceBytes[i] = i + 1; + +sliceSource.constructor = { + [Symbol.species]: function(byteLength) { + sliceSource.resize(4); + return new ArrayBuffer(byteLength); + } +}; + +var sliced = sliceSource.slice(2, 8); +var slicedBytes = new Uint8Array(sliced); +assertEq(sliced.byteLength, 6); +assertEq(slicedBytes[0], 3); +assertEq(slicedBytes[1], 4); +assertEq(slicedBytes[2], 0); +assertEq(slicedBytes[5], 0); + +sliceSource.resize(8); +for (var i = 0; i < sliceSourceBytes.length; i++) + sliceSourceBytes[i] = i + 1; +var zeroCopied = sliceSource.slice(6, 8); +assertEq(zeroCopied.byteLength, 2); +assertEq(new Uint8Array(zeroCopied)[0], 0); + +assertThrowsInstanceOf(() => new ArrayBuffer(4, { maxByteLength: 3 }), RangeError); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/tests/non262/ArrayBuffer/resizable-views.js b/js/src/tests/non262/ArrayBuffer/resizable-views.js new file mode 100644 index 0000000000..e8d810b165 --- /dev/null +++ b/js/src/tests/non262/ArrayBuffer/resizable-views.js @@ -0,0 +1,218 @@ +// |reftest| skip-if(!this.SharedArrayBuffer) + +var rab = new ArrayBuffer(4, { maxByteLength: 16 }); +var tracking = new Uint8Array(rab); +var fixed = new Uint8Array(rab, 1, 2); +var bytes = new Uint8Array(rab); + +bytes[1] = 11; +bytes[2] = 22; + +assertEq(tracking.length, 4); +assertEq(tracking.byteLength, 4); +assertEq(tracking.byteOffset, 0); +assertEq(fixed.length, 2); +assertEq(fixed.byteLength, 2); +assertEq(fixed.byteOffset, 1); + +rab.resize(2); +assertEq(tracking.length, 2); +assertEq(tracking.byteLength, 2); +assertEq(fixed.length, 0); +assertEq(fixed.byteLength, 0); +assertEq(fixed.byteOffset, 0); +assertEq(fixed[0], undefined); + +rab.resize(8); +assertEq(tracking.length, 8); +assertEq(tracking.byteLength, 8); +assertEq(fixed.length, 2); +assertEq(fixed.byteLength, 2); +assertEq(fixed.byteOffset, 1); +assertEq(fixed[0], 11); +assertEq(fixed[1], 0); +tracking[6] = 66; +assertEq(new Uint8Array(rab)[6], 66); + +var dv = new DataView(rab, 4); +assertEq(dv.byteOffset, 4); +assertEq(dv.byteLength, 4); +dv.setUint8(0, 44); +assertEq(tracking[4], 44); + +rab.resize(3); +assertThrowsInstanceOf(() => dv.byteOffset, TypeError); +assertThrowsInstanceOf(() => dv.byteLength, TypeError); +assertThrowsInstanceOf(() => dv.getUint8(0), TypeError); + +rab.resize(6); +assertEq(dv.byteOffset, 4); +assertEq(dv.byteLength, 2); +assertEq(dv.getUint8(0), 0); + +var fixedDv = new DataView(rab, 4, 2); +rab.resize(5); +assertThrowsInstanceOf(() => fixedDv.byteOffset, TypeError); +assertThrowsInstanceOf(() => fixedDv.byteLength, TypeError); +assertThrowsInstanceOf(() => fixedDv.getUint8(0), TypeError); +rab.resize(6); +assertEq(fixedDv.byteOffset, 4); +assertEq(fixedDv.byteLength, 2); + +var methodRab = new ArrayBuffer(4, { maxByteLength: 8 }); +var methodFixed = new Uint8Array(methodRab, 2, 2); +methodRab.resize(2); +[ + () => methodFixed.at(0), + () => methodFixed.copyWithin(0, 0), + () => methodFixed.entries(), + () => methodFixed.every(x => true), + () => methodFixed.fill(1), + () => methodFixed.filter(x => true), + () => methodFixed.find(x => true), + () => methodFixed.findIndex(x => true), + () => methodFixed.findLast(x => true), + () => methodFixed.findLastIndex(x => true), + () => methodFixed.forEach(x => x), + () => methodFixed.includes(0), + () => methodFixed.indexOf(0), + () => methodFixed.join(","), + () => methodFixed.keys(), + () => methodFixed.lastIndexOf(0), + () => methodFixed.map(x => x), + () => methodFixed.reduce((a, b) => a + b, 0), + () => methodFixed.reduceRight((a, b) => a + b, 0), + () => methodFixed.reverse(), + () => methodFixed.set([1], 0), + () => methodFixed.slice(), + () => methodFixed.some(x => true), + () => methodFixed.sort(), + () => methodFixed.subarray(), + () => methodFixed.toLocaleString(), + () => methodFixed.toReversed(), + () => methodFixed.toSorted(), + () => methodFixed.toString(), + () => methodFixed.values(), + () => methodFixed.with(0, 1), + () => methodFixed[Symbol.iterator](), +].forEach(fn => assertThrowsInstanceOf(fn, TypeError)); + +methodRab.resize(4); +assertEq(methodFixed.length, 2); + +var sourceRab = new ArrayBuffer(4, { maxByteLength: 8 }); +var oobSource = new Uint8Array(sourceRab, 2, 2); +sourceRab.resize(2); +assertThrowsInstanceOf(() => new Uint8Array(oobSource), TypeError); +assertThrowsInstanceOf(() => new Uint16Array(oobSource), TypeError); +assertThrowsInstanceOf(() => new Uint8Array(4).set(oobSource), TypeError); +sourceRab.resize(4); +assertEq(new Uint8Array(oobSource).length, 2); + +var ctorRab = new ArrayBuffer(8, { maxByteLength: 8 }); +var ShrinkingNewTarget = new Proxy(function() {}, { + get(target, prop, receiver) { + if (prop === "prototype") { + ctorRab.resize(2); + return DataView.prototype; + } + return Reflect.get(target, prop, receiver); + } +}); +assertThrowsInstanceOf(() => Reflect.construct(DataView, [ctorRab, 4], ShrinkingNewTarget), + RangeError); + +var fixedCtorRab = new ArrayBuffer(8, { maxByteLength: 8 }); +var FixedShrinkingNewTarget = new Proxy(function() {}, { + get(target, prop, receiver) { + if (prop === "prototype") { + fixedCtorRab.resize(5); + return DataView.prototype; + } + return Reflect.get(target, prop, receiver); + } +}); +assertThrowsInstanceOf(() => Reflect.construct(DataView, [fixedCtorRab, 4, 2], + FixedShrinkingNewTarget), + RangeError); + +var gsab = new SharedArrayBuffer(4, { maxByteLength: 16 }); +var sharedTracking = new Uint8Array(gsab); +assertEq(sharedTracking.length, 4); +gsab.grow(8); +assertEq(sharedTracking.length, 8); +sharedTracking[6] = 33; +assertEq(new Uint8Array(gsab)[6], 33); + +var sharedDv = new DataView(gsab); +assertEq(sharedDv.buffer, gsab); +assertEq(sharedDv.byteOffset, 0); +assertEq(sharedDv.byteLength, 8); +sharedDv.setUint8(7, 99); +assertEq(sharedTracking[7], 99); +gsab.grow(12); +assertEq(sharedDv.byteLength, 12); +assertEq(sharedDv.getUint8(7), 99); + +var fixedSharedDv = new DataView(gsab, 4, 2); +assertEq(fixedSharedDv.byteOffset, 4); +assertEq(fixedSharedDv.byteLength, 2); +gsab.grow(16); +assertEq(fixedSharedDv.byteOffset, 4); +assertEq(fixedSharedDv.byteLength, 2); + +function readLength(view) { + return view.length; +} + +function readElement(view, index) { + return view[index]; +} + +function writeElement(view, index, value) { + view[index] = value; +} + +var normal = new Uint8Array(4); +normal[0] = 7; +for (var i = 0; i < 2000; i++) { + assertEq(readLength(normal), 4); + assertEq(readElement(normal, 0), 7); + writeElement(normal, 1, 8); +} + +var icRab = new ArrayBuffer(4, { maxByteLength: 8 }); +var icTracking = new Uint8Array(icRab); +icTracking[0] = 9; +assertEq(readLength(icTracking), 4); +assertEq(readElement(icTracking, 0), 9); +icRab.resize(0); +assertEq(readLength(icTracking), 0); +assertEq(readElement(icTracking, 0), undefined); +writeElement(icTracking, 0, 1); +icRab.resize(4); +assertEq(readLength(icTracking), 4); +assertEq(readElement(icTracking, 0), 0); +writeElement(icTracking, 0, 12); +assertEq(readElement(icTracking, 0), 12); + +var icFixed = new Uint8Array(icRab, 1, 2); +assertEq(readLength(icFixed), 2); +icRab.resize(2); +assertEq(readLength(icFixed), 0); +assertEq(readElement(icFixed, 0), undefined); +writeElement(icFixed, 0, 55); +icRab.resize(4); +assertEq(readLength(icFixed), 2); +assertEq(readElement(icFixed, 0), 0); + +var icGsab = new SharedArrayBuffer(4, { maxByteLength: 8 }); +var icSharedTracking = new Uint8Array(icGsab); +assertEq(readLength(icSharedTracking), 4); +icGsab.grow(8); +assertEq(readLength(icSharedTracking), 8); +writeElement(icSharedTracking, 5, 77); +assertEq(readElement(icSharedTracking, 5), 77); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/non262/Atomics/waitAsync.js b/js/src/tests/non262/Atomics/waitAsync.js new file mode 100644 index 0000000000..a2b23a4f80 --- /dev/null +++ b/js/src/tests/non262/Atomics/waitAsync.js @@ -0,0 +1,77 @@ +// |reftest| skip-if(!this.SharedArrayBuffer || !this.Atomics || !this.drainJobQueue) + +if (typeof SharedArrayBuffer === "function" && typeof Atomics === "object" && + typeof drainJobQueue === "function") { + const sab = new SharedArrayBuffer(4); + const i32 = new Int32Array(sab); + + let result = Atomics.waitAsync(i32, 0, 1, 10); + assertEq(result.async, false); + assertEq(result.value, "not-equal"); + + result = Atomics.waitAsync(i32, 0, 0, 0); + assertEq(result.async, false); + assertEq(result.value, "timed-out"); + + result = Atomics.waitAsync(i32, 0, 0); + assertEq(result.async, true); + let notified; + result.value.then(value => { + notified = value; + }); + assertEq(Atomics.notify(i32, 0, 1), 1); + drainJobQueue(); + assertEq(notified, "ok"); + + result = Atomics.waitAsync(i32, 0, 0, 1); + assertEq(result.async, true); + let timedOut; + result.value.then(value => { + timedOut = value; + }); + drainJobQueue(); + assertEq(timedOut, "timed-out"); + + if (typeof BigInt64Array === "function") { + const bigSab = new SharedArrayBuffer(16); + const i64 = new BigInt64Array(bigSab); + const u64 = new BigUint64Array(bigSab); + + assertEq(Atomics.store(i64, 0, -1n), -1n); + assertEq(Atomics.load(i64, 0), -1n); + assertEq(Atomics.load(u64, 0), 18446744073709551615n); + assertEq(Atomics.exchange(i64, 0, 7n), -1n); + assertEq(Atomics.compareExchange(i64, 0, 7n, 10n), 7n); + assertEq(Atomics.add(i64, 0, 5n), 10n); + assertEq(Atomics.load(i64, 0), 15n); + assertEq(Atomics.sub(i64, 0, 20n), 15n); + assertEq(Atomics.load(i64, 0), -5n); + assertEq(Atomics.and(u64, 0, 7n), 18446744073709551611n); + assertEq(Atomics.or(u64, 0, 8n), 3n); + assertEq(Atomics.xor(u64, 0, 15n), 11n); + + assertThrowsInstanceOf(() => Atomics.waitAsync(u64, 0, 0n), TypeError); + + result = Atomics.waitAsync(i64, 1, 1n, 10); + assertEq(result.async, false); + assertEq(result.value, "not-equal"); + + result = Atomics.waitAsync(i64, 1, 0n, 0); + assertEq(result.async, false); + assertEq(result.value, "timed-out"); + + let offsetView = new BigInt64Array(bigSab, 8); + result = Atomics.waitAsync(offsetView, 0, 0n); + assertEq(result.async, true); + notified = undefined; + result.value.then(value => { + notified = value; + }); + assertEq(Atomics.notify(i64, 1, 1), 1); + drainJobQueue(); + assertEq(notified, "ok"); + } +} + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/non262/Promise/with-resolvers.js b/js/src/tests/non262/Promise/with-resolvers.js new file mode 100644 index 0000000000..985d458622 --- /dev/null +++ b/js/src/tests/non262/Promise/with-resolvers.js @@ -0,0 +1,31 @@ +// |reftest| skip-if(!Promise.withResolvers) + +var desc = Object.getOwnPropertyDescriptor(Promise, "withResolvers"); +assertEq(desc.enumerable, false); +assertEq(desc.configurable, true); +assertEq(desc.writable, true); +assertEq(Promise.withResolvers.length, 0); +assertEq(Promise.withResolvers.name, "withResolvers"); + +var capability = Promise.withResolvers(); +assertEq(capability.promise instanceof Promise, true); +assertEq(typeof capability.resolve, "function"); +assertEq(typeof capability.reject, "function"); +assertEqArray(Object.keys(capability), ["promise", "resolve", "reject"]); + +capability.resolve(42); +capability.promise.then(v => assertEq(v, 42)); + +class MyPromise extends Promise {} +var subCapability = Promise.withResolvers.call(MyPromise); +assertEq(subCapability.promise instanceof MyPromise, true); +subCapability.reject("rejected"); +subCapability.promise.then( + () => { throw new Error("expected rejection"); }, + reason => assertEq(reason, "rejected") +); + +assertThrowsInstanceOf(() => Promise.withResolvers.call({}), TypeError); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/tests/non262/SharedArrayBuffer/growable.js b/js/src/tests/non262/SharedArrayBuffer/growable.js new file mode 100644 index 0000000000..7b9ce85ffe --- /dev/null +++ b/js/src/tests/non262/SharedArrayBuffer/growable.js @@ -0,0 +1,40 @@ +// |reftest| skip-if(!this.SharedArrayBuffer) + +if (typeof SharedArrayBuffer === "function") { + const fixed = new SharedArrayBuffer(4); + assertEq(fixed.byteLength, 4); + assertEq(fixed.maxByteLength, 4); + assertEq(fixed.growable, false); + assertThrowsInstanceOf(() => fixed.grow(4), TypeError); + + assertThrowsInstanceOf(() => new SharedArrayBuffer(-1), RangeError); + assertThrowsInstanceOf(() => new SharedArrayBuffer(8, {maxByteLength: 4}), RangeError); + + let optionGetterCalled = false; + const growable = new SharedArrayBuffer(4, { + get maxByteLength() { + optionGetterCalled = true; + return 16; + } + }); + assertEq(optionGetterCalled, true); + assertEq(growable.byteLength, 4); + assertEq(growable.maxByteLength, 16); + assertEq(growable.growable, true); + + const before = new Uint8Array(growable); + before[0] = 37; + assertEq(growable.grow(12), undefined); + assertEq(growable.byteLength, 12); + assertEq(growable.maxByteLength, 16); + + const after = new Uint8Array(growable); + assertEq(after.length, 12); + assertEq(after[0], 37); + + assertThrowsInstanceOf(() => growable.grow(11), RangeError); + assertThrowsInstanceOf(() => growable.grow(17), RangeError); +} + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/non262/String/well-formed.js b/js/src/tests/non262/String/well-formed.js new file mode 100644 index 0000000000..7472f6afa8 --- /dev/null +++ b/js/src/tests/non262/String/well-formed.js @@ -0,0 +1,26 @@ +// |reftest| skip-if(!String.prototype.isWellFormed||!String.prototype.toWellFormed) + +assertEq("".isWellFormed(), true); +assertEq("abc".isWellFormed(), true); +assertEq("\uD83D\uDE00".isWellFormed(), true); +assertEq("\uD800".isWellFormed(), false); +assertEq("\uDC00".isWellFormed(), false); +assertEq("\uD800a".isWellFormed(), false); +assertEq("a\uDC00".isWellFormed(), false); +assertEq("\uD800\uD800\uDC00".isWellFormed(), false); + +assertEq("abc".toWellFormed(), "abc"); +assertEq("\uD83D\uDE00".toWellFormed(), "\uD83D\uDE00"); +assertEq("\uD800".toWellFormed(), "\uFFFD"); +assertEq("\uDC00".toWellFormed(), "\uFFFD"); +assertEq("\uD800a\uDC00".toWellFormed(), "\uFFFDa\uFFFD"); +assertEq("\uD800\uD800\uDC00".toWellFormed(), "\uFFFD\uD800\uDC00"); + +assertEq(String.prototype.isWellFormed.call(123), true); +assertEq(String.prototype.toWellFormed.call({ toString() { return "\uD800x"; } }), "\uFFFDx"); + +assertThrowsInstanceOf(() => String.prototype.isWellFormed.call(null), TypeError); +assertThrowsInstanceOf(() => String.prototype.toWellFormed.call(undefined), TypeError); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/tests/non262/WeakMap/symbol-keys.js b/js/src/tests/non262/WeakMap/symbol-keys.js new file mode 100644 index 0000000000..0edb542974 --- /dev/null +++ b/js/src/tests/non262/WeakMap/symbol-keys.js @@ -0,0 +1,36 @@ +var key = Symbol("weak"); +var map = new WeakMap(); +assertEq(map.has(key), false); +assertEq(map.get(key), undefined); +assertEq(map.set(key, 13), map); +assertEq(map.has(key), true); +assertEq(map.get(key), 13); +assertEq(map.delete(key), true); +assertEq(map.has(key), false); + +var constructedKey = Symbol("constructed"); +var constructed = new WeakMap([[constructedKey, 7]]); +assertEq(constructed.get(constructedKey), 7); + +var registered = Symbol.for("registered"); +assertEq(map.has(registered), false); +assertEq(map.get(registered), undefined); +assertEq(map.delete(registered), false); +assertThrowsInstanceOf(() => map.set(registered, 1), TypeError); +assertThrowsInstanceOf(() => new WeakMap([[registered, 1]]), TypeError); + +var setKey = Symbol("set"); +var set = new WeakSet([setKey]); +assertEq(set.has(setKey), true); +assertEq(set.delete(setKey), true); +assertEq(set.has(setKey), false); +assertEq(set.add(setKey), set); +assertEq(set.has(setKey), true); + +assertEq(set.has(registered), false); +assertEq(set.delete(registered), false); +assertThrowsInstanceOf(() => set.add(registered), TypeError); +assertThrowsInstanceOf(() => new WeakSet([registered]), TypeError); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/tests/non262/collections/groupBy.js b/js/src/tests/non262/collections/groupBy.js new file mode 100644 index 0000000000..121a899ede --- /dev/null +++ b/js/src/tests/non262/collections/groupBy.js @@ -0,0 +1,34 @@ +// |reftest| skip-if(!Object.groupBy||!Map.groupBy) + +var objectGroups = Object.groupBy(["a", "bb", "c"], value => value.length); +assertEq(Object.getPrototypeOf(objectGroups), null); +assertEqArray(objectGroups["1"], ["a", "c"]); +assertEqArray(objectGroups["2"], ["bb"]); + +var symbol = Symbol(); +var symbolGroups = Object.groupBy([1, 2], value => value === 1 ? symbol : "__proto__"); +assertEqArray(symbolGroups[symbol], [1]); +assertEqArray(symbolGroups.__proto__, [2]); + +var indexes = []; +var mapGroups = Map.groupBy(["a", "bb", "c"], (value, index) => { + indexes.push(index); + return value.length; +}); +assertEq(mapGroups instanceof Map, true); +assertEqArray(indexes, [0, 1, 2]); +assertEqArray(mapGroups.get(1), ["a", "c"]); +assertEqArray(mapGroups.get(2), ["bb"]); + +var key = {}; +var objectKeyGroups = Map.groupBy([1, 2], value => value === 1 ? key : NaN); +assertEqArray(objectKeyGroups.get(key), [1]); +assertEqArray(objectKeyGroups.get(NaN), [2]); + +assertThrowsInstanceOf(() => Object.groupBy(null, x => x), TypeError); +assertThrowsInstanceOf(() => Map.groupBy(undefined, x => x), TypeError); +assertThrowsInstanceOf(() => Object.groupBy([], null), TypeError); +assertThrowsInstanceOf(() => Map.groupBy([], null), TypeError); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/vm/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index e43edab690..666fc774df 100644 --- a/js/src/vm/ArrayBufferObject.cpp +++ b/js/src/vm/ArrayBufferObject.cpp @@ -22,6 +22,7 @@ # include #endif +#include #include "jsapi.h" #include "jsarray.h" #include "jscntxt.h" @@ -45,6 +46,7 @@ #include "vm/Interpreter.h" #include "vm/SelfHosting.h" #include "vm/SharedArrayObject.h" +#include "vm/TypedArrayObject.h" #include "vm/WrapperObject.h" #include "wasm/WasmSignalHandlers.h" #include "wasm/WasmTypes.h" @@ -170,12 +172,18 @@ static const JSPropertySpec static_properties[] = { static const JSFunctionSpec prototype_functions[] = { + JS_FN("resize", ArrayBufferObject::fun_resize, 1, 0), JS_SELF_HOSTED_FN("slice", "ArrayBufferSlice", 2, 0), + JS_FN("transfer", ArrayBufferObject::fun_transfer, 0, 0), + JS_FN("transferToFixedLength", ArrayBufferObject::fun_transferToFixedLength, 0, 0), JS_FS_END }; static const JSPropertySpec prototype_properties[] = { JS_PSG("byteLength", ArrayBufferObject::byteLengthGetter, 0), + JS_PSG("detached", ArrayBufferObject::detachedGetter, 0), + JS_PSG("maxByteLength", ArrayBufferObject::maxByteLengthGetter, 0), + JS_PSG("resizable", ArrayBufferObject::resizableGetter, 0), JS_STRING_SYM_PS(toStringTag, "ArrayBuffer", JSPROP_READONLY), JS_PS_END }; @@ -252,6 +260,52 @@ ArrayBufferObject::byteLengthGetter(JSContext* cx, unsigned argc, Value* vp) return CallNonGenericMethod(cx, args); } +MOZ_ALWAYS_INLINE bool +ArrayBufferObject::detachedGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsArrayBuffer(args.thisv())); + args.rval().setBoolean(args.thisv().toObject().as().isDetached()); + return true; +} + +bool +ArrayBufferObject::detachedGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +MOZ_ALWAYS_INLINE bool +ArrayBufferObject::maxByteLengthGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsArrayBuffer(args.thisv())); + ArrayBufferObject& buffer = args.thisv().toObject().as(); + args.rval().setInt32(buffer.isDetached() ? 0 : int32_t(buffer.maxByteLength())); + return true; +} + +bool +ArrayBufferObject::maxByteLengthGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +MOZ_ALWAYS_INLINE bool +ArrayBufferObject::resizableGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsArrayBuffer(args.thisv())); + args.rval().setBoolean(args.thisv().toObject().as().isResizable()); + return true; +} + +bool +ArrayBufferObject::resizableGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + /* * ArrayBuffer.isView(obj); ES6 (Dec 2013 draft) 24.1.3.1 */ @@ -264,6 +318,39 @@ ArrayBufferObject::fun_isView(JSContext* cx, unsigned argc, Value* vp) return true; } +static bool +GetArrayBufferMaxByteLengthOption(JSContext* cx, HandleValue options, + uint32_t byteLength, uint32_t* maxByteLength, + bool* resizable) +{ + *maxByteLength = byteLength; + *resizable = false; + + if (!options.isObject()) + return true; + + RootedObject opts(cx, &options.toObject()); + RootedValue maxByteLengthValue(cx); + if (!GetProperty(cx, opts, opts, cx->names().maxByteLength, &maxByteLengthValue)) + return false; + + if (maxByteLengthValue.isUndefined()) + return true; + + uint64_t max; + if (!ToIndex(cx, maxByteLengthValue, &max)) + return false; + + if (max > INT32_MAX || max < byteLength) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); + return false; + } + + *maxByteLength = uint32_t(max); + *resizable = true; + return true; +} + // ES2017 draft 24.1.2.1 bool @@ -287,12 +374,22 @@ ArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* vp) } // Step 3. + uint32_t maxByteLength; + bool resizable; + if (!GetArrayBufferMaxByteLengthOption(cx, args.get(1), uint32_t(byteLength), + &maxByteLength, &resizable)) + { + return false; + } + + // Step 4. RootedObject proto(cx); RootedObject newTarget(cx, &args.newTarget().toObject()); if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) return false; - JSObject* bufobj = create(cx, uint32_t(byteLength), proto); + JSObject* bufobj = create(cx, uint32_t(byteLength), BufferContents::createPlain(nullptr), + OwnsData, proto, GenericObject, maxByteLength, resizable); if (!bufobj) return false; args.rval().setObject(*bufobj); @@ -302,13 +399,66 @@ ArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* vp) static ArrayBufferObject::BufferContents AllocateArrayBufferContents(JSContext* cx, uint32_t nbytes) { - uint8_t* p = cx->runtime()->pod_callocCanGC(nbytes); + uint8_t* p = cx->runtime()->pod_callocCanGC(nbytes ? nbytes : 1); if (!p) ReportOutOfMemory(cx); return ArrayBufferObject::BufferContents::create(p); } +static bool +ReportArrayBufferNotResizable(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARRAYBUFFER_NOT_RESIZABLE); + return false; +} + +static bool +ReportArrayBufferCannotDetach(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARRAYBUFFER_CANNOT_DETACH); + return false; +} + +static bool +ReportArrayBufferLengthOutOfRange(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); + return false; +} + +static bool +ArrayBufferViewFits(ArrayBufferViewObject* view, uint32_t newByteLength) +{ + if (view->is()) { + DataViewObject& dataView = view->as(); + uint32_t byteOffset = dataView.byteOffsetMaybeOutOfBounds(); + if (byteOffset > newByteLength) + return false; + uint32_t byteLength = dataView.isLengthTracking() + ? newByteLength - byteOffset + : dataView.fixedByteLengthMaybeOutOfBounds(); + return byteOffset <= newByteLength && byteLength <= newByteLength - byteOffset; + } + + if (view->is()) { + TypedArrayObject& typedArray = view->as(); + if (typedArray.isSharedMemory()) + return true; + uint32_t byteOffset = typedArray.byteOffsetMaybeOutOfBounds(); + if (byteOffset > newByteLength) + return false; + uint32_t byteLength = typedArray.isLengthTracking() + ? newByteLength - byteOffset + : typedArray.fixedLengthMaybeOutOfBounds() * + typedArray.bytesPerElement(); + return byteOffset <= newByteLength && byteLength <= newByteLength - byteOffset; + } + + // Outline typed objects don't have a recoverable fixed byte range here. + return false; +} + static void NoteViewBufferWasDetached(ArrayBufferViewObject* view, ArrayBufferObject::BufferContents newContents, @@ -391,7 +541,8 @@ ArrayBufferObject::setNewData(FreeOp* fop, BufferContents newContents, OwnsState void ArrayBufferObject::changeViewContents(JSContext* cx, ArrayBufferViewObject* view, - uint8_t* oldDataPointer, BufferContents newContents) + uint8_t* oldDataPointer, BufferContents newContents, + uint32_t newByteLength) { MOZ_ASSERT(!view->isSharedMemory()); @@ -402,7 +553,18 @@ ArrayBufferObject::changeViewContents(JSContext* cx, ArrayBufferViewObject* view uint8_t* viewDataPointer = view->dataPointerUnshared(nogc); if (viewDataPointer) { MOZ_ASSERT(newContents); - ptrdiff_t offset = viewDataPointer - oldDataPointer; + uint32_t offset; + if (view->is()) { + offset = view->as().byteOffsetMaybeOutOfBounds(); + } else if (view->is()) { + offset = view->as().byteOffsetMaybeOutOfBounds(); + } else { + ptrdiff_t oldOffset = viewDataPointer - oldDataPointer; + MOZ_ASSERT(oldOffset >= 0); + offset = uint32_t(oldOffset); + } + if (offset > newByteLength) + offset = 0; viewDataPointer = static_cast(newContents.data()) + offset; view->setDataPointerUnshared(viewDataPointer); } @@ -428,10 +590,180 @@ ArrayBufferObject::changeContents(JSContext* cx, BufferContents newContents, auto& innerViews = cx->compartment()->innerViews.get(); if (InnerViewTable::ViewVector* views = innerViews.maybeViewsUnbarriered(this)) { for (size_t i = 0; i < views->length(); i++) - changeViewContents(cx, (*views)[i], oldDataPointer, newContents); + changeViewContents(cx, (*views)[i], oldDataPointer, newContents, byteLength()); } if (firstView()) - changeViewContents(cx, firstView(), oldDataPointer, newContents); + changeViewContents(cx, firstView(), oldDataPointer, newContents, byteLength()); +} + +void +ArrayBufferObject::changeContentsForResize(JSContext* cx, BufferContents newContents, + OwnsState ownsState, uint32_t newByteLength) +{ + MOZ_RELEASE_ASSERT(!isWasm()); + MOZ_ASSERT(!forInlineTypedObject()); + + uint8_t* oldDataPointer = dataPointer(); + setNewData(cx->runtime()->defaultFreeOp(), newContents, ownsState); + setByteLength(newByteLength); + + auto& innerViews = cx->compartment()->innerViews.get(); + if (InnerViewTable::ViewVector* views = innerViews.maybeViewsUnbarriered(this)) { + for (size_t i = 0; i < views->length(); i++) { + ArrayBufferViewObject* view = (*views)[i]; + if (view->is() || view->is()) + changeViewContents(cx, view, oldDataPointer, newContents, newByteLength); + else if (ArrayBufferViewFits(view, newByteLength)) + changeViewContents(cx, view, oldDataPointer, newContents, newByteLength); + else + NoteViewBufferWasDetached(view, newContents, cx); + } + } + + if (firstView()) { + if (firstView()->is() || firstView()->is()) + changeViewContents(cx, firstView(), oldDataPointer, newContents, newByteLength); + else if (ArrayBufferViewFits(firstView(), newByteLength)) + changeViewContents(cx, firstView(), oldDataPointer, newContents, newByteLength); + else + NoteViewBufferWasDetached(firstView(), newContents, cx); + } +} + +static bool +ResizeArrayBuffer(JSContext* cx, Handle buffer, uint32_t newByteLength) +{ + if (!buffer->isResizable()) + return ReportArrayBufferNotResizable(cx); + + if (buffer->isDetached()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + + if (newByteLength > buffer->maxByteLength()) + return ReportArrayBufferLengthOutOfRange(cx); + + if (!buffer->isPlain() || buffer->isPreparedForAsmJS() || buffer->forInlineTypedObject()) + return ReportArrayBufferCannotDetach(cx); + + if (newByteLength == buffer->byteLength()) + return true; + + ArrayBufferObject::BufferContents newContents = AllocateArrayBufferContents(cx, newByteLength); + if (!newContents) + return false; + + uint32_t copyLength = std::min(newByteLength, buffer->byteLength()); + if (copyLength > 0) + memcpy(newContents.data(), buffer->dataPointer(), copyLength); + + buffer->changeContentsForResize(cx, newContents, ArrayBufferObject::OwnsData, newByteLength); + return true; +} + +bool +ArrayBufferObject::fun_resize_impl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsArrayBuffer(args.thisv())); + + Rooted buffer(cx, &args.thisv().toObject().as()); + if (!buffer->isResizable()) + return ReportArrayBufferNotResizable(cx); + + uint64_t newByteLength; + if (!ToIndex(cx, args.get(0), &newByteLength)) + return false; + if (newByteLength > INT32_MAX) + return ReportArrayBufferLengthOutOfRange(cx); + + if (!ResizeArrayBuffer(cx, buffer, uint32_t(newByteLength))) + return false; + + args.rval().setUndefined(); + return true; +} + +bool +ArrayBufferObject::fun_resize(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +static bool +ArrayBufferTransfer(JSContext* cx, const CallArgs& args, bool preserveResizability) +{ + MOZ_ASSERT(IsArrayBuffer(args.thisv())); + Rooted buffer(cx, &args.thisv().toObject().as()); + + uint32_t newByteLength = buffer->byteLength(); + if (args.hasDefined(0)) { + uint64_t newLength; + if (!ToIndex(cx, args.get(0), &newLength)) + return false; + if (newLength > INT32_MAX) + return ReportArrayBufferLengthOutOfRange(cx); + newByteLength = uint32_t(newLength); + } + + if (buffer->isDetached()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + + if (buffer->isWasm() || buffer->isPreparedForAsmJS()) + return ReportArrayBufferCannotDetach(cx); + + bool newResizable = preserveResizability && buffer->isResizable(); + uint32_t newMaxByteLength = newResizable ? buffer->maxByteLength() : newByteLength; + if (newResizable && newByteLength > newMaxByteLength) + return ReportArrayBufferLengthOutOfRange(cx); + + Rooted newBuffer(cx, + ArrayBufferObject::create(cx, newByteLength, ArrayBufferObject::BufferContents::createPlain(nullptr), + ArrayBufferObject::OwnsData, nullptr, GenericObject, + newMaxByteLength, newResizable)); + if (!newBuffer) + return false; + + uint32_t copyLength = std::min(newByteLength, buffer->byteLength()); + if (copyLength > 0) + memcpy(newBuffer->dataPointer(), buffer->dataPointer(), copyLength); + + ArrayBufferObject::BufferContents detachedContents = + buffer->hasStealableContents() ? ArrayBufferObject::BufferContents::createPlain(nullptr) + : buffer->contents(); + ArrayBufferObject::detach(cx, buffer, detachedContents); + + args.rval().setObject(*newBuffer); + return true; +} + +bool +ArrayBufferObject::fun_transfer_impl(JSContext* cx, const CallArgs& args) +{ + return ArrayBufferTransfer(cx, args, true); +} + +bool +ArrayBufferObject::fun_transfer(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +bool +ArrayBufferObject::fun_transferToFixedLength_impl(JSContext* cx, const CallArgs& args) +{ + return ArrayBufferTransfer(cx, args, false); +} + +bool +ArrayBufferObject::fun_transferToFixedLength(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); } /* @@ -887,6 +1219,14 @@ ArrayBufferObject::byteLength() const return getSlot(BYTE_LENGTH_SLOT).toInt32(); } +uint32_t +ArrayBufferObject::maxByteLength() const +{ + if (!isResizable()) + return byteLength(); + return getSlot(MAX_BYTE_LENGTH_SLOT).toInt32(); +} + void ArrayBufferObject::setByteLength(uint32_t length) { @@ -929,7 +1269,7 @@ js::WasmArrayBufferMaxSize(const ArrayBufferObjectMaybeShared* buf) if (buf->is()) return buf->as().wasmMaxSize(); - return Some(buf->as().byteLength()); + return Some(buf->as().maxByteLength()); } /* static */ bool @@ -1035,9 +1375,12 @@ ArrayBufferObject* ArrayBufferObject::create(JSContext* cx, uint32_t nbytes, BufferContents contents, OwnsState ownsState /* = OwnsData */, HandleObject proto /* = nullptr */, - NewObjectKind newKind /* = GenericObject */) + NewObjectKind newKind /* = GenericObject */, + uint32_t maxByteLength /* = 0 */, + bool resizable /* = false */) { MOZ_ASSERT_IF(contents.kind() == MAPPED, contents); + MOZ_ASSERT_IF(resizable, maxByteLength >= nbytes); // 24.1.1.1, step 3 (Inlined 6.2.6.1 CreateByteDataBlock, step 2). // Refuse to allocate too large buffers, currently limited to ~2 GiB. @@ -1046,10 +1389,6 @@ ArrayBufferObject::create(JSContext* cx, uint32_t nbytes, BufferContents content return nullptr; } - // If we need to allocate data, try to use a larger object size class so - // that the array buffer's data can be allocated inline with the object. - // The extra space will be left unused by the object's fixed slots and - // available for the buffer's data, see NewObject(). size_t reservedSlots = JSCLASS_RESERVED_SLOTS(&class_); size_t nslots = reservedSlots; @@ -1066,18 +1405,10 @@ ArrayBufferObject::create(JSContext* cx, uint32_t nbytes, BufferContents content } } else { MOZ_ASSERT(ownsState == OwnsData); - size_t usableSlots = NativeObject::MAX_FIXED_SLOTS - reservedSlots; - if (nbytes <= usableSlots * sizeof(Value)) { - int newSlots = (nbytes - 1) / sizeof(Value) + 1; - MOZ_ASSERT(int(nbytes) <= newSlots * int(sizeof(Value))); - nslots = reservedSlots + newSlots; - contents = BufferContents::createPlain(nullptr); - } else { - contents = AllocateArrayBufferContents(cx, nbytes); - if (!contents) - return nullptr; - allocated = true; - } + contents = AllocateArrayBufferContents(cx, nbytes); + if (!contents) + return nullptr; + allocated = true; } MOZ_ASSERT(!(class_.flags & JSCLASS_HAS_PRIVATE)); @@ -1097,10 +1428,11 @@ ArrayBufferObject::create(JSContext* cx, uint32_t nbytes, BufferContents content if (!contents) { void* data = obj->inlineDataPointer(); - js_memset(data, 0, nbytes); - obj->initialize(nbytes, BufferContents::createPlain(data), DoesntOwnData); + memset(data, 0, nbytes); + obj->initialize(nbytes, BufferContents::createPlain(data), DoesntOwnData, + maxByteLength, resizable); } else { - obj->initialize(nbytes, contents, ownsState); + obj->initialize(nbytes, contents, ownsState, maxByteLength, resizable); } return obj; @@ -1130,25 +1462,28 @@ ArrayBufferObject::createEmpty(JSContext* cx) bool ArrayBufferObject::createDataViewForThisImpl(JSContext* cx, const CallArgs& args) { - MOZ_ASSERT(IsArrayBuffer(args.thisv())); + MOZ_ASSERT(IsAnyArrayBuffer(args.thisv())); /* * This method is only called for |DataView(alienBuf, ...)| which calls * this as |createDataViewForThis.call(alienBuf, byteOffset, byteLength, - * DataView.prototype)|, - * ergo there must be exactly 3 arguments. + * DataView.prototype, lengthTracking)|, + * ergo there must be exactly 4 arguments. */ - MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args.length() == 4); uint32_t byteOffset = args[0].toPrivateUint32(); uint32_t byteLength = args[1].toPrivateUint32(); - Rooted buffer(cx, &args.thisv().toObject().as()); + bool lengthTracking = args[3].toBoolean(); + Rooted buffer(cx, + &args.thisv().toObject().as()); /* * Pop off the passed-along prototype and delegate to normal DataViewObject * construction. */ - JSObject* obj = DataViewObject::create(cx, byteOffset, byteLength, buffer, &args[2].toObject()); + JSObject* obj = DataViewObject::create(cx, byteOffset, byteLength, buffer, + &args[2].toObject(), lengthTracking); if (!obj) return false; args.rval().setObject(*obj); @@ -1159,7 +1494,7 @@ bool ArrayBufferObject::createDataViewForThis(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - return CallNonGenericMethod(cx, args); + return CallNonGenericMethod(cx, args); } /* static */ ArrayBufferObject::BufferContents @@ -1528,6 +1863,8 @@ ArrayBufferViewObject::trace(JSTracer* trc, JSObject* objArg) // The data may or may not be inline with the buffer. The buffer // can only move during a compacting GC, in which case its // objectMoved hook has already updated the buffer's data pointer. + if (offset > buf.byteLength()) + offset = 0; obj->initPrivate(buf.dataPointer() + offset); } } @@ -1578,6 +1915,8 @@ ArrayBufferViewObject::dataPointerUnshared(const JS::AutoRequireNoGC& nogc) bool ArrayBufferViewObject::isSharedMemory() { + if (is()) + return as().isSharedMemory(); if (is()) return as().isSharedMemory(); return false; @@ -1609,7 +1948,7 @@ ArrayBufferViewObject::bufferObject(JSContext* cx, Handleas().bufferEither(); } MOZ_ASSERT(thisObject->is()); - return &thisObject->as().arrayBuffer(); + return &thisObject->as().arrayBufferEither(); } /* JS Friend API */ @@ -1860,7 +2199,7 @@ JS_GetArrayBufferViewData(JSObject* obj, bool* isSharedMemory, const JS::AutoChe if (!obj) return nullptr; if (obj->is()) { - *isSharedMemory = false; + *isSharedMemory = obj->as().isSharedMemory(); return obj->as().dataPointer(); } TypedArrayObject& ta = obj->as(); @@ -1930,7 +2269,7 @@ js::GetArrayBufferViewLengthAndData(JSObject* obj, uint32_t* length, bool* isSha : obj->as().byteLength(); if (obj->is()) { - *isSharedMemory = false; + *isSharedMemory = obj->as().isSharedMemory(); *data = static_cast(obj->as().dataPointer()); } else { diff --git a/js/src/vm/ArrayBufferObject.h b/js/src/vm/ArrayBufferObject.h index f4010c6c77..f9e3353b7e 100644 --- a/js/src/vm/ArrayBufferObject.h +++ b/js/src/vm/ArrayBufferObject.h @@ -82,7 +82,7 @@ ArrayBufferObjectMaybeShared& AsAnyArrayBuffer(HandleValue val); class ArrayBufferObjectMaybeShared : public NativeObject { public: - uint32_t byteLength() { + uint32_t byteLength() const { return AnyArrayBufferByteLength(this); } @@ -128,15 +128,22 @@ typedef MutableHandle MutableHandleArrayBufferObj class ArrayBufferObject : public ArrayBufferObjectMaybeShared { static bool byteLengthGetterImpl(JSContext* cx, const CallArgs& args); + static bool maxByteLengthGetterImpl(JSContext* cx, const CallArgs& args); + static bool resizableGetterImpl(JSContext* cx, const CallArgs& args); + static bool detachedGetterImpl(JSContext* cx, const CallArgs& args); static bool fun_slice_impl(JSContext* cx, const CallArgs& args); + static bool fun_resize_impl(JSContext* cx, const CallArgs& args); + static bool fun_transfer_impl(JSContext* cx, const CallArgs& args); + static bool fun_transferToFixedLength_impl(JSContext* cx, const CallArgs& args); public: static const uint8_t DATA_SLOT = 0; static const uint8_t BYTE_LENGTH_SLOT = 1; static const uint8_t FIRST_VIEW_SLOT = 2; static const uint8_t FLAGS_SLOT = 3; + static const uint8_t MAX_BYTE_LENGTH_SLOT = 4; - static const uint8_t RESERVED_SLOTS = 4; + static const uint8_t RESERVED_SLOTS = 5; static const size_t ARRAY_BUFFER_ALIGNMENT = 8; @@ -189,7 +196,11 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared // This PLAIN or WASM buffer has been prepared for asm.js and cannot // henceforth be transferred/detached. - FOR_ASMJS = 0x40 + FOR_ASMJS = 0x40, + + // This buffer was created with [[ArrayBufferMaxByteLength]] and can + // be resized up to that maximum. + RESIZABLE = 0x80 }; static_assert(JS_ARRAYBUFFER_DETACHED_FLAG == DETACHED, @@ -230,8 +241,14 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared static const Class class_; static bool byteLengthGetter(JSContext* cx, unsigned argc, Value* vp); + static bool maxByteLengthGetter(JSContext* cx, unsigned argc, Value* vp); + static bool resizableGetter(JSContext* cx, unsigned argc, Value* vp); + static bool detachedGetter(JSContext* cx, unsigned argc, Value* vp); static bool fun_slice(JSContext* cx, unsigned argc, Value* vp); + static bool fun_resize(JSContext* cx, unsigned argc, Value* vp); + static bool fun_transfer(JSContext* cx, unsigned argc, Value* vp); + static bool fun_transferToFixedLength(JSContext* cx, unsigned argc, Value* vp); static bool fun_isView(JSContext* cx, unsigned argc, Value* vp); @@ -243,7 +260,9 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared BufferContents contents, OwnsState ownsState = OwnsData, HandleObject proto = nullptr, - NewObjectKind newKind = GenericObject); + NewObjectKind newKind = GenericObject, + uint32_t maxByteLength = 0, + bool resizable = false); static ArrayBufferObject* create(JSContext* cx, uint32_t nbytes, HandleObject proto = nullptr, NewObjectKind newKind = GenericObject); @@ -294,6 +313,8 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared void setNewData(FreeOp* fop, BufferContents newContents, OwnsState ownsState); void changeContents(JSContext* cx, BufferContents newContents, OwnsState ownsState); + void changeContentsForResize(JSContext* cx, BufferContents newContents, + OwnsState ownsState, uint32_t newByteLength); // Detach this buffer from its original memory. (This necessarily makes // views of this buffer unusable for modifying that original memory.) @@ -302,7 +323,8 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared private: void changeViewContents(JSContext* cx, ArrayBufferViewObject* view, - uint8_t* oldDataPointer, BufferContents newContents); + uint8_t* oldDataPointer, BufferContents newContents, + uint32_t newByteLength); void setFirstView(ArrayBufferViewObject* view); uint8_t* inlineDataPointer() const; @@ -311,6 +333,7 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared uint8_t* dataPointer() const; SharedMem dataPointerShared() const; uint32_t byteLength() const; + uint32_t maxByteLength() const; BufferContents contents() const { return BufferContents(dataPointer(), bufferKind()); @@ -334,6 +357,7 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared bool isWasm() const { return bufferKind() == WASM; } bool isMapped() const { return bufferKind() == MAPPED; } bool isDetached() const { return flags() & DETACHED; } + bool isResizable() const { return flags() & RESIZABLE; } bool isPreparedForAsmJS() const { return flags() & FOR_ASMJS; } // WebAssembly support: @@ -391,12 +415,17 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared void setIsDetached() { setFlags(flags() | DETACHED); } void setIsPreparedForAsmJS() { setFlags(flags() | FOR_ASMJS); } + void setIsResizable() { setFlags(flags() | RESIZABLE); } - void initialize(size_t byteLength, BufferContents contents, OwnsState ownsState) { + void initialize(size_t byteLength, BufferContents contents, OwnsState ownsState, + uint32_t maxByteLength = 0, bool resizable = false) { setByteLength(byteLength); setFlags(0); + setFixedSlot(MAX_BYTE_LENGTH_SLOT, Int32Value(maxByteLength ? maxByteLength : byteLength)); setFirstView(nullptr); setDataPointer(contents, ownsState); + if (resizable) + setIsResizable(); } // Note: initialize() may be called after initEmpty(); initEmpty() must @@ -404,6 +433,7 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared void initEmpty() { setByteLength(0); setFlags(0); + setFixedSlot(MAX_BYTE_LENGTH_SLOT, Int32Value(0)); setFirstView(nullptr); setDataPointer(BufferContents::createPlain(nullptr), DoesntOwnData); } diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index 6edd49c6c0..82d49b201a 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -248,6 +248,7 @@ macro(lookupSetter, lookupSetter, "__lookupSetter__") \ macro(MapConstructorInit, MapConstructorInit, "MapConstructorInit") \ macro(MapIterator, MapIterator, "Map Iterator") \ + macro(maxByteLength, maxByteLength, "maxByteLength") \ macro(maximumFractionDigits, maximumFractionDigits, "maximumFractionDigits") \ macro(maximumSignificantDigits, maximumSignificantDigits, "maximumSignificantDigits") \ macro(message, message, "message") \ diff --git a/js/src/vm/GlobalObject.cpp b/js/src/vm/GlobalObject.cpp index b3080d6edd..9d1dab322e 100644 --- a/js/src/vm/GlobalObject.cpp +++ b/js/src/vm/GlobalObject.cpp @@ -27,6 +27,7 @@ #include "builtin/SymbolObject.h" #include "builtin/TypedObject.h" #include "builtin/WeakMapObject.h" +#include "builtin/WeakRefObject.h" #include "builtin/WeakSetObject.h" #include "vm/Debugger.h" #include "vm/EnvironmentObject.h" @@ -464,6 +465,7 @@ GlobalObject::initStandardClasses(JSContext* cx, Handle global) if (!ensureConstructor(cx, global, static_cast(k))) return false; } + return true; } @@ -538,6 +540,7 @@ GlobalObject::initSelfHostingBuiltins(JSContext* cx, Handle globa InitBareBuiltinCtor(cx, global, JSProto_Int32Array) && InitBareSymbolCtor(cx, global) && InitBareWeakMapCtor(cx, global) && + InitBareWeakRefCtor(cx, global) && InitStopIterationClass(cx, global) && DefineFunctions(cx, global, builtins, AsIntrinsic); } diff --git a/js/src/vm/NativeObject.cpp b/js/src/vm/NativeObject.cpp index cde86fb829..b34a20c91b 100644 --- a/js/src/vm/NativeObject.cpp +++ b/js/src/vm/NativeObject.cpp @@ -43,6 +43,22 @@ static const ObjectElements emptyElementsHeaderShared(0, 0, ObjectElements::Shar HeapSlot* const js::emptyObjectElementsShared = reinterpret_cast(uintptr_t(&emptyElementsHeaderShared) + sizeof(ObjectElements)); +static const ObjectElements emptyElementsHeaderResizableOrGrowable( + 0, 0, ObjectElements::RESIZABLE_OR_GROWABLE_BUFFER); + +/* Objects with no elements share one empty set of elements. */ +HeapSlot* const js::emptyObjectElementsResizableOrGrowable = + reinterpret_cast(uintptr_t(&emptyElementsHeaderResizableOrGrowable) + + sizeof(ObjectElements)); + +static const ObjectElements emptyElementsHeaderSharedResizableOrGrowable( + 0, 0, ObjectElements::SHARED_MEMORY | ObjectElements::RESIZABLE_OR_GROWABLE_BUFFER); + +/* Objects with no elements share one empty set of elements. */ +HeapSlot* const js::emptyObjectElementsSharedResizableOrGrowable = + reinterpret_cast(uintptr_t(&emptyElementsHeaderSharedResizableOrGrowable) + + sizeof(ObjectElements)); + #ifdef DEBUG @@ -61,10 +77,12 @@ ObjectElements::ConvertElementsToDoubles(JSContext* cx, uintptr_t elementsPtr) * This function is infallible, but has a fallible interface so that it can * be called directly from Ion code. Only arrays can have their dense * elements converted to doubles, and arrays never have empty elements. - */ + */ HeapSlot* elementsHeapPtr = (HeapSlot*) elementsPtr; MOZ_ASSERT(elementsHeapPtr != emptyObjectElements && - elementsHeapPtr != emptyObjectElementsShared); + elementsHeapPtr != emptyObjectElementsShared && + elementsHeapPtr != emptyObjectElementsResizableOrGrowable && + elementsHeapPtr != emptyObjectElementsSharedResizableOrGrowable); ObjectElements* header = ObjectElements::fromElements(elementsHeapPtr); MOZ_ASSERT(!header->shouldConvertDoubleElements()); diff --git a/js/src/vm/NativeObject.h b/js/src/vm/NativeObject.h index 86977d109f..67fd3a7a46 100644 --- a/js/src/vm/NativeObject.h +++ b/js/src/vm/NativeObject.h @@ -185,6 +185,11 @@ class ObjectElements // These elements are set to integrity level "frozen". FROZEN = 0x10, + + // For TypedArrays only: this TypedArray views a resizable + // ArrayBuffer or growable SharedArrayBuffer. JIT fast paths with + // cached length/data assumptions must fall back for these objects. + RESIZABLE_OR_GROWABLE_BUFFER = 0x20, }; private: @@ -255,6 +260,10 @@ class ObjectElements : flags(SHARED_MEMORY), initializedLength(0), capacity(capacity), length(length) {} + constexpr ObjectElements(uint32_t capacity, uint32_t length, uint32_t flags) + : flags(flags), initializedLength(0), capacity(capacity), length(length) + {} + HeapSlot* elements() { return reinterpret_cast(uintptr_t(this) + sizeof(ObjectElements)); } @@ -269,6 +278,10 @@ class ObjectElements return flags & SHARED_MEMORY; } + bool hasResizableOrGrowableBuffer() const { + return flags & RESIZABLE_OR_GROWABLE_BUFFER; + } + GCPtrNativeObject& ownerObject() const { MOZ_ASSERT(isCopyOnWrite()); return *(GCPtrNativeObject*)(&elements()[initializedLength]); @@ -326,6 +339,8 @@ static_assert(ObjectElements::VALUES_PER_HEADER * sizeof(HeapSlot) == sizeof(Obj */ extern HeapSlot* const emptyObjectElements; extern HeapSlot* const emptyObjectElementsShared; +extern HeapSlot* const emptyObjectElementsResizableOrGrowable; +extern HeapSlot* const emptyObjectElementsSharedResizableOrGrowable; struct Class; class GCMarker; @@ -480,6 +495,12 @@ class NativeObject : public ShapedObject elements_ = emptyObjectElementsShared; } + void setHasResizableOrGrowableBuffer() { + MOZ_ASSERT(elements_ == emptyObjectElements || elements_ == emptyObjectElementsShared); + elements_ = isSharedMemory() ? emptyObjectElementsSharedResizableOrGrowable + : emptyObjectElementsResizableOrGrowable; + } + bool isInWholeCellBuffer() const { const gc::TenuredCell* cell = &asTenured(); gc::ArenaCellSet* cells = cell->arena()->bufferedCells; @@ -1254,7 +1275,10 @@ class NativeObject : public ShapedObject } inline bool hasEmptyElements() const { - return elements_ == emptyObjectElements || elements_ == emptyObjectElementsShared; + return elements_ == emptyObjectElements || + elements_ == emptyObjectElementsShared || + elements_ == emptyObjectElementsResizableOrGrowable || + elements_ == emptyObjectElementsSharedResizableOrGrowable; } /* diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 26fd6cc9e1..24b23f19ff 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -38,6 +38,7 @@ #include "builtin/SelfHostingDefines.h" #include "builtin/Stream.h" #include "builtin/TypedObject.h" +#include "builtin/WeakRefObject.h" #include "builtin/WeakSetObject.h" #include "gc/Marking.h" #include "gc/Policy.h" @@ -105,6 +106,14 @@ intrinsic_IsObject(JSContext* cx, unsigned argc, Value* vp) return true; } +static bool +intrinsic_CanBeHeldWeakly(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(CanBeHeldWeakly(args.get(0))); + return true; +} + static bool intrinsic_IsArray(JSContext* cx, unsigned argc, Value* vp) { @@ -270,6 +279,20 @@ intrinsic_GetBuiltinConstructor(JSContext* cx, unsigned argc, Value* vp) return true; } +static bool +intrinsic_NewMap(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + Rooted map(cx, MapObject::create(cx)); + if (!map) + return false; + + args.rval().setObject(*map); + return true; +} + static bool intrinsic_SubstringKernel(JSContext* cx, unsigned argc, Value* vp) { @@ -1287,6 +1310,36 @@ intrinsic_PossiblyWrappedTypedArrayHasDetachedBuffer(JSContext* cx, unsigned arg return true; } +static bool +intrinsic_TypedArrayIsOutOfBounds(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + RootedObject obj(cx, &args[0].toObject()); + MOZ_ASSERT(obj->is()); + args.rval().setBoolean(obj->as().isOutOfBounds()); + return true; +} + +static bool +intrinsic_PossiblyWrappedTypedArrayIsOutOfBounds(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isObject()); + + JSObject* obj = CheckedUnwrap(&args[0].toObject()); + if (!obj) { + JS_ReportErrorASCII(cx, "Permission denied to access object"); + return false; + } + + MOZ_ASSERT(obj->is()); + args.rval().setBoolean(obj->as().isOutOfBounds()); + return true; +} + static bool intrinsic_MoveTypedArrayElements(JSContext* cx, unsigned argc, Value* vp) { @@ -1409,6 +1462,10 @@ intrinsic_SetFromTypedArrayApproach(JSContext* cx, unsigned argc, Value* vp) JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } + if (unsafeTypedArrayCrossCompartment->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return false; + } // Steps 21, 23. uint32_t unsafeSrcLengthCrossCompartment = unsafeTypedArrayCrossCompartment->length(); @@ -2268,7 +2325,10 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_INLINABLE_FN("std_Math_min", math_min, 2,0, MathMin), JS_INLINABLE_FN("std_Math_abs", math_abs, 1,0, MathAbs), + JS_FN("std_Map_create", intrinsic_NewMap, 0,0), + JS_FN("std_Map_get", MapObject::get, 1,0), JS_FN("std_Map_has", MapObject::has, 1,0), + JS_FN("std_Map_set", MapObject::set, 2,0), JS_FN("std_Map_iterator", MapObject::entries, 0,0), JS_FN("std_Number_valueOf", num_valueOf, 0,0), @@ -2316,6 +2376,7 @@ static const JSFunctionSpec intrinsic_functions[] = { // Helper funtions after this point. JS_INLINABLE_FN("ToObject", intrinsic_ToObject, 1,0, IntrinsicToObject), JS_INLINABLE_FN("IsObject", intrinsic_IsObject, 1,0, IntrinsicIsObject), + JS_FN("CanBeHeldWeakly", intrinsic_CanBeHeldWeakly, 1,0), JS_INLINABLE_FN("IsArray", intrinsic_IsArray, 1,0, ArrayIsArray), JS_INLINABLE_FN("IsWrappedArrayConstructor", intrinsic_IsWrappedArrayConstructor, 1,0, IntrinsicIsWrappedArrayConstructor), @@ -2475,6 +2536,10 @@ static const JSFunctionSpec intrinsic_functions[] = { 1, 0, IntrinsicPossiblyWrappedTypedArrayLength), JS_FN("PossiblyWrappedTypedArrayHasDetachedBuffer", intrinsic_PossiblyWrappedTypedArrayHasDetachedBuffer, 1, 0), + JS_FN("TypedArrayIsOutOfBounds", + intrinsic_TypedArrayIsOutOfBounds, 1, 0), + JS_FN("PossiblyWrappedTypedArrayIsOutOfBounds", + intrinsic_PossiblyWrappedTypedArrayIsOutOfBounds, 1, 0), JS_FN("MoveTypedArrayElements", intrinsic_MoveTypedArrayElements, 4,0), JS_FN("SetFromTypedArrayApproach",intrinsic_SetFromTypedArrayApproach, 4, 0), diff --git a/js/src/vm/SharedArrayObject.cpp b/js/src/vm/SharedArrayObject.cpp index 6a3c6a91c3..a597564695 100644 --- a/js/src/vm/SharedArrayObject.cpp +++ b/js/src/vm/SharedArrayObject.cpp @@ -8,6 +8,7 @@ #include "mozilla/Atomics.h" #include "jsfriendapi.h" +#include "jsnum.h" #include "jsprf.h" #ifdef XP_WIN @@ -104,15 +105,18 @@ SharedArrayAllocSize(uint32_t length) } SharedArrayRawBuffer* -SharedArrayRawBuffer::New(JSContext* cx, uint32_t length) +SharedArrayRawBuffer::New(JSContext* cx, uint32_t length, uint32_t maxLength, bool growable) { // The value (uint32_t)-1 is used as a signal in various places, // so guard against it on principle. MOZ_ASSERT(length != (uint32_t)-1); + MOZ_ASSERT(maxLength != (uint32_t)-1); + MOZ_ASSERT(maxLength >= length); // Add a page for the header and round to a page boundary. - uint32_t allocSize = SharedArrayAllocSize(length); - if (allocSize <= length) + uint32_t allocationLength = growable ? maxLength : length; + uint32_t allocSize = SharedArrayAllocSize(allocationLength); + if (allocSize <= allocationLength) return nullptr; // Test >= to guard against the case where multiple extant runtimes @@ -127,7 +131,8 @@ SharedArrayRawBuffer::New(JSContext* cx, uint32_t length) } } - bool preparedForAsmJS = jit::JitOptions.asmJSAtomicsEnable && IsValidAsmJSHeapLength(length); + bool preparedForAsmJS = + !growable && jit::JitOptions.asmJSAtomicsEnable && IsValidAsmJSHeapLength(length); void* p = nullptr; if (preparedForAsmJS) { @@ -161,8 +166,9 @@ SharedArrayRawBuffer::New(JSContext* cx, uint32_t length) uint8_t* buffer = reinterpret_cast(p) + gc::SystemPageSize(); uint8_t* base = buffer - sizeof(SharedArrayRawBuffer); - SharedArrayRawBuffer* rawbuf = new (base) SharedArrayRawBuffer(buffer, length, preparedForAsmJS); - MOZ_ASSERT(rawbuf->length == length); // Deallocation needs this + SharedArrayRawBuffer* rawbuf = + new (base) SharedArrayRawBuffer(buffer, length, maxLength, growable, preparedForAsmJS); + MOZ_ASSERT(rawbuf->allocatedByteLength() == allocationLength); // Deallocation needs this. return rawbuf; } @@ -201,7 +207,7 @@ SharedArrayRawBuffer::dropReference() MOZ_ASSERT(p.asValue() % gc::SystemPageSize() == 0); uint8_t* address = p.unwrap(/*safe - only reference*/); - uint32_t allocSize = SharedArrayAllocSize(this->length); + uint32_t allocSize = SharedArrayAllocSize(this->allocatedByteLength()); if (this->preparedForAsmJS) { uint32_t mappedSize = SharedArrayMappedSize(allocSize); @@ -221,6 +227,23 @@ SharedArrayRawBuffer::dropReference() numLive--; } +bool +SharedArrayRawBuffer::growTo(uint32_t newLength) +{ + MOZ_ASSERT(growable); + MOZ_ASSERT(newLength <= maxLength); + + for (;;) { + uint32_t oldLength = length; + if (newLength < oldLength) + return false; + if (newLength == oldLength) + return true; + if (length.compareExchange(oldLength, newLength)) + return true; + } +} + MOZ_ALWAYS_INLINE bool SharedArrayBufferObject::byteLengthGetterImpl(JSContext* cx, const CallArgs& args) @@ -237,6 +260,112 @@ SharedArrayBufferObject::byteLengthGetter(JSContext* cx, unsigned argc, Value* v return CallNonGenericMethod(cx, args); } +MOZ_ALWAYS_INLINE bool +SharedArrayBufferObject::maxByteLengthGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsSharedArrayBuffer(args.thisv())); + args.rval().setInt32(args.thisv().toObject().as().maxByteLength()); + return true; +} + +bool +SharedArrayBufferObject::maxByteLengthGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +MOZ_ALWAYS_INLINE bool +SharedArrayBufferObject::growableGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsSharedArrayBuffer(args.thisv())); + args.rval().setBoolean(args.thisv().toObject().as().isGrowable()); + return true; +} + +bool +SharedArrayBufferObject::growableGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +static bool +ReportSharedArrayBufferNotGrowable(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_NOT_GROWABLE); + return false; +} + +static bool +ReportSharedArrayBufferLengthOutOfRange(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_BAD_LENGTH); + return false; +} + +static bool +GetSharedArrayBufferMaxByteLengthOption(JSContext* cx, HandleValue options, + uint32_t byteLength, uint32_t* maxByteLength, + bool* growable) +{ + *maxByteLength = byteLength; + *growable = false; + + if (!options.isObject()) + return true; + + RootedObject opts(cx, &options.toObject()); + RootedValue maxByteLengthValue(cx); + if (!GetProperty(cx, opts, opts, cx->names().maxByteLength, &maxByteLengthValue)) + return false; + + if (maxByteLengthValue.isUndefined()) + return true; + + uint64_t max; + if (!ToIndex(cx, maxByteLengthValue, &max)) + return false; + + if (max > INT32_MAX || max < byteLength) + return ReportSharedArrayBufferLengthOutOfRange(cx); + + *maxByteLength = uint32_t(max); + *growable = true; + return true; +} + +MOZ_ALWAYS_INLINE bool +SharedArrayBufferObject::fun_grow_impl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsSharedArrayBuffer(args.thisv())); + + Rooted buffer(cx, + &args.thisv().toObject().as()); + if (!buffer->isGrowable()) + return ReportSharedArrayBufferNotGrowable(cx); + + uint64_t newByteLength; + if (!ToIndex(cx, args.get(0), &newByteLength)) + return false; + if (newByteLength > INT32_MAX) + return ReportSharedArrayBufferLengthOutOfRange(cx); + + uint32_t newLength = uint32_t(newByteLength); + if (newLength > buffer->maxByteLength() || !buffer->growTo(newLength)) + return ReportSharedArrayBufferLengthOutOfRange(cx); + + args.rval().setUndefined(); + return true; +} + +bool +SharedArrayBufferObject::fun_grow(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + bool SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* vp) { @@ -245,11 +374,19 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* if (!ThrowIfNotConstructing(cx, args, "SharedArrayBuffer")) return false; + uint64_t length64; + if (!ToIndex(cx, args.get(0), &length64)) + return false; // Bugs 1068458, 1161298: Limit length to 2^31-1. - uint32_t length; - bool overflow_unused; - if (!ToLengthClamped(cx, args.get(0), &length, &overflow_unused) || length > INT32_MAX) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_BAD_LENGTH); + if (length64 > INT32_MAX) + return ReportSharedArrayBufferLengthOutOfRange(cx); + uint32_t length = uint32_t(length64); + + uint32_t maxByteLength; + bool growable; + if (!GetSharedArrayBufferMaxByteLengthOption(cx, args.get(1), length, + &maxByteLength, &growable)) + { return false; } @@ -258,7 +395,7 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) return false; - JSObject* bufobj = New(cx, length, proto); + JSObject* bufobj = New(cx, length, maxByteLength, growable, proto); if (!bufobj) return false; args.rval().setObject(*bufobj); @@ -266,9 +403,10 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* } SharedArrayBufferObject* -SharedArrayBufferObject::New(JSContext* cx, uint32_t length, HandleObject proto) +SharedArrayBufferObject::New(JSContext* cx, uint32_t length, uint32_t maxLength, bool growable, + HandleObject proto) { - SharedArrayRawBuffer* buffer = SharedArrayRawBuffer::New(cx, length); + SharedArrayRawBuffer* buffer = SharedArrayRawBuffer::New(cx, length, maxLength, growable); if (!buffer) return nullptr; @@ -341,7 +479,7 @@ SharedArrayBufferObject::addSizeOfExcludingThis(JSObject* obj, mozilla::MallocSi // just live with the risk. const SharedArrayBufferObject& buf = obj->as(); info->objectsNonHeapElementsShared += - buf.byteLength() / buf.rawBufferObject()->refcount(); + buf.rawBufferObject()->allocatedByteLength() / buf.rawBufferObject()->refcount(); } /* static */ void @@ -409,12 +547,15 @@ static const JSPropertySpec static_properties[] = { }; static const JSFunctionSpec prototype_functions[] = { + JS_FN("grow", SharedArrayBufferObject::fun_grow, 1, 0), JS_SELF_HOSTED_FN("slice", "SharedArrayBufferSlice", 2, 0), JS_FS_END }; static const JSPropertySpec prototype_properties[] = { JS_PSG("byteLength", SharedArrayBufferObject::byteLengthGetter, 0), + JS_PSG("growable", SharedArrayBufferObject::growableGetter, 0), + JS_PSG("maxByteLength", SharedArrayBufferObject::maxByteLengthGetter, 0), JS_STRING_SYM_PS(toStringTag, "SharedArrayBuffer", JSPROP_READONLY), JS_PS_END }; diff --git a/js/src/vm/SharedArrayObject.h b/js/src/vm/SharedArrayObject.h index 81fb49f3a8..01872aaff6 100644 --- a/js/src/vm/SharedArrayObject.h +++ b/js/src/vm/SharedArrayObject.h @@ -44,7 +44,9 @@ class SharedArrayRawBuffer { private: mozilla::Atomic refcount_; - uint32_t length; + mozilla::Atomic length; + uint32_t maxLength; + bool growable; bool preparedForAsmJS; // A list of structures representing tasks waiting on some @@ -52,9 +54,12 @@ class SharedArrayRawBuffer FutexWaiter* waiters_; protected: - SharedArrayRawBuffer(uint8_t* buffer, uint32_t length, bool preparedForAsmJS) + SharedArrayRawBuffer(uint8_t* buffer, uint32_t length, uint32_t maxLength, bool growable, + bool preparedForAsmJS) : refcount_(1), length(length), + maxLength(maxLength), + growable(growable), preparedForAsmJS(preparedForAsmJS), waiters_(nullptr) { @@ -62,7 +67,11 @@ class SharedArrayRawBuffer } public: - static SharedArrayRawBuffer* New(JSContext* cx, uint32_t length); + static SharedArrayRawBuffer* New(JSContext* cx, uint32_t length, uint32_t maxLength, + bool growable); + static SharedArrayRawBuffer* New(JSContext* cx, uint32_t length) { + return New(cx, length, length, false); + } // This may be called from multiple threads. The caller must take // care of mutual exclusion. @@ -85,6 +94,20 @@ class SharedArrayRawBuffer return length; } + uint32_t maxByteLength() const { + return growable ? maxLength : byteLength(); + } + + uint32_t allocatedByteLength() const { + return growable ? maxLength : byteLength(); + } + + bool isGrowable() const { + return growable; + } + + [[nodiscard]] bool growTo(uint32_t newLength); + bool isPreparedForAsmJS() const { return preparedForAsmJS; } @@ -117,6 +140,9 @@ class SharedArrayRawBuffer class SharedArrayBufferObject : public ArrayBufferObjectMaybeShared { static bool byteLengthGetterImpl(JSContext* cx, const CallArgs& args); + static bool maxByteLengthGetterImpl(JSContext* cx, const CallArgs& args); + static bool growableGetterImpl(JSContext* cx, const CallArgs& args); + static bool fun_grow_impl(JSContext* cx, const CallArgs& args); public: // RAWBUF_SLOT holds a pointer (as "private" data) to the @@ -128,13 +154,23 @@ class SharedArrayBufferObject : public ArrayBufferObjectMaybeShared static const Class class_; static bool byteLengthGetter(JSContext* cx, unsigned argc, Value* vp); + static bool maxByteLengthGetter(JSContext* cx, unsigned argc, Value* vp); + static bool growableGetter(JSContext* cx, unsigned argc, Value* vp); + static bool fun_grow(JSContext* cx, unsigned argc, Value* vp); static bool class_constructor(JSContext* cx, unsigned argc, Value* vp); // Create a SharedArrayBufferObject with a new SharedArrayRawBuffer. static SharedArrayBufferObject* New(JSContext* cx, uint32_t length, + uint32_t maxLength, + bool growable, HandleObject proto = nullptr); + static SharedArrayBufferObject* New(JSContext* cx, + uint32_t length, + HandleObject proto = nullptr) { + return New(cx, length, length, false, proto); + } // Create a SharedArrayBufferObject using an existing SharedArrayRawBuffer. static SharedArrayBufferObject* New(JSContext* cx, @@ -164,6 +200,15 @@ class SharedArrayBufferObject : public ArrayBufferObjectMaybeShared uint32_t byteLength() const { return rawBufferObject()->byteLength(); } + uint32_t maxByteLength() const { + return rawBufferObject()->maxByteLength(); + } + bool isGrowable() const { + return rawBufferObject()->isGrowable(); + } + [[nodiscard]] bool growTo(uint32_t newLength) { + return rawBufferObject()->growTo(newLength); + } bool isPreparedForAsmJS() const { return rawBufferObject()->isPreparedForAsmJS(); } diff --git a/js/src/vm/StructuredClone.cpp b/js/src/vm/StructuredClone.cpp index e1f1b2777e..50e6162923 100644 --- a/js/src/vm/StructuredClone.cpp +++ b/js/src/vm/StructuredClone.cpp @@ -45,6 +45,7 @@ #include "builtin/MapObject.h" #include "js/Date.h" #include "js/GCHashTable.h" +#include "vm/ArrayBufferObject-inl.h" #include "vm/SavedFrame.h" #include "vm/SharedArrayObject.h" #include "vm/TypedArrayObject.h" diff --git a/js/src/vm/TypedArrayCommon.h b/js/src/vm/TypedArrayCommon.h index fab7210417..056f8b52be 100644 --- a/js/src/vm/TypedArrayCommon.h +++ b/js/src/vm/TypedArrayCommon.h @@ -1065,16 +1065,40 @@ class TypedArrayMethods if (!ToInt32(cx, args[1], &offset)) return false; - if (offset < 0 || uint32_t(offset) > target->length()) { - // the given offset is bogus + if (offset < 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX); return false; } } + if (target->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + if (target->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return false; + } + + uint32_t targetLength = target->length(); + if (uint32_t(offset) > targetLength) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX); + return false; + } + RootedObject arg0(cx, &args[0].toObject()); if (arg0->is()) { - if (arg0->as().length() > target->length() - offset) { + Rooted source(cx, &arg0->as()); + if (source->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + if (source->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return false; + } + + if (source->length() > targetLength - offset) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); return false; } @@ -1086,7 +1110,7 @@ class TypedArrayMethods if (!GetLengthProperty(cx, arg0, &len)) return false; - if (uint32_t(offset) > target->length() || len > target->length() - offset) { + if (len > targetLength - offset) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); return false; } @@ -1099,13 +1123,32 @@ class TypedArrayMethods return true; } - static bool - setFromTypedArray(JSContext* cx, Handle target, HandleObject source, - uint32_t offset = 0) - { - MOZ_ASSERT(source->is(), "use setFromNonTypedArray"); + static bool + setFromTypedArray(JSContext* cx, Handle target, HandleObject source, + uint32_t offset = 0) + { + MOZ_ASSERT(source->is(), "use setFromNonTypedArray"); - bool isShared = target->isSharedMemory() || source->as().isSharedMemory(); + if (target->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + if (target->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return false; + } + + Rooted sourceArray(cx, &source->as()); + if (sourceArray->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + if (sourceArray->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return false; + } + + bool isShared = target->isSharedMemory() || sourceArray->isSharedMemory(); switch (target->type()) { case Scalar::Int8: diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp index faf3dc40df..22d21c8550 100644 --- a/js/src/vm/TypedArrayObject.cpp +++ b/js/src/vm/TypedArrayObject.cpp @@ -484,8 +484,9 @@ class TypedArrayObjectTemplate : public TypedArrayObject } static TypedArrayObject* - makeInstance(JSContext* cx, Handle buffer, uint32_t byteOffset, uint32_t len, - HandleObject proto) + makeInstance(JSContext* cx, Handle buffer, + uint32_t byteOffset, uint32_t len, HandleObject proto, + bool lengthTracking = false) { MOZ_ASSERT_IF(!buffer, byteOffset == 0); @@ -510,12 +511,19 @@ class TypedArrayObjectTemplate : public TypedArrayObject return nullptr; bool isSharedMemory = buffer && IsSharedArrayBuffer(buffer.get()); + bool hasResizableOrGrowableBuffer = + buffer && + ((buffer->is() && buffer->as().isResizable()) || + (buffer->is() && + buffer->as().isGrowable())); obj->setFixedSlot(TypedArrayObject::BUFFER_SLOT, ObjectOrNullValue(buffer)); // This is invariant. Self-hosting code that sets BUFFER_SLOT // (if it does) must maintain it, should it need to. if (isSharedMemory) obj->setIsSharedMemory(); + if (hasResizableOrGrowableBuffer) + obj->setHasResizableOrGrowableBuffer(); if (buffer) { obj->initViewData(buffer->dataPointerEither() + byteOffset); @@ -549,7 +557,9 @@ class TypedArrayObjectTemplate : public TypedArrayObject #endif } - obj->setFixedSlot(TypedArrayObject::LENGTH_SLOT, Int32Value(len)); + obj->setFixedSlot(TypedArrayObject::LENGTH_SLOT, + Int32Value(lengthTracking ? TypedArrayObject::LENGTH_TRACKING + : int32_t(len))); obj->setFixedSlot(TypedArrayObject::BYTEOFFSET_SLOT, Int32Value(byteOffset)); #ifdef DEBUG @@ -898,6 +908,7 @@ class TypedArrayObjectTemplate : public TypedArrayObject return nullptr; // invalid byteOffset } + bool lengthTracking = false; uint32_t len; if (lengthInt == -1) { len = (buffer->byteLength() - byteOffset) / sizeof(NativeType); @@ -906,6 +917,12 @@ class TypedArrayObjectTemplate : public TypedArrayObject JSMSG_TYPED_ARRAY_CONSTRUCT_BOUNDS); return nullptr; // given byte array doesn't map exactly to sizeof(NativeType) * N } + if ((buffer->is() && buffer->as().isResizable()) || + (buffer->is() && + buffer->as().isGrowable())) + { + lengthTracking = true; + } } else { len = uint32_t(lengthInt); } @@ -924,7 +941,7 @@ class TypedArrayObjectTemplate : public TypedArrayObject return nullptr; // byteOffset + len is too big for the arraybuffer } - return makeInstance(cx, buffer, byteOffset, len, proto); + return makeInstance(cx, buffer, byteOffset, len, proto, lengthTracking); } static bool @@ -1320,6 +1337,10 @@ TypedArrayObjectTemplate::fromTypedArray(JSContext* cx, HandleObject other, b JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return nullptr; } + if (srcArray->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return nullptr; + } // Step 9. uint32_t elementLength = srcArray->length(); @@ -1859,7 +1880,8 @@ DataViewNewObjectKind(JSContext* cx, uint32_t byteLength, JSObject* proto) DataViewObject* DataViewObject::create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, - Handle arrayBuffer, JSObject* protoArg) + Handle arrayBuffer, JSObject* protoArg, + bool lengthTracking) { if (arrayBuffer->isDetached()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); @@ -1868,8 +1890,18 @@ DataViewObject::create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, MOZ_ASSERT(byteOffset <= INT32_MAX); MOZ_ASSERT(byteLength <= INT32_MAX); - MOZ_ASSERT(byteOffset + byteLength < UINT32_MAX); - MOZ_ASSERT(!arrayBuffer || !arrayBuffer->is()); + + uint32_t bufferByteLength = arrayBuffer->byteLength(); + if (byteOffset > bufferByteLength || + (!lengthTracking && byteLength > bufferByteLength - byteOffset)) + { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARG_INDEX_OUT_OF_RANGE, + "1"); + return nullptr; + } + + if (lengthTracking) + byteLength = bufferByteLength - byteOffset; RootedObject proto(cx, protoArg); RootedObject obj(cx); @@ -1893,57 +1925,72 @@ DataViewObject::create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, } } - // Caller should have established these preconditions, and no - // (non-self-hosted) JS code has had an opportunity to run so nothing can - // have invalidated them. MOZ_ASSERT(byteOffset <= arrayBuffer->byteLength()); MOZ_ASSERT(byteOffset + byteLength <= arrayBuffer->byteLength()); DataViewObject& dvobj = obj->as(); dvobj.setFixedSlot(TypedArrayObject::BYTEOFFSET_SLOT, Int32Value(byteOffset)); - dvobj.setFixedSlot(TypedArrayObject::LENGTH_SLOT, Int32Value(byteLength)); + dvobj.setFixedSlot(TypedArrayObject::LENGTH_SLOT, + Int32Value(lengthTracking ? TypedArrayObject::LENGTH_TRACKING + : int32_t(byteLength))); dvobj.setFixedSlot(TypedArrayObject::BUFFER_SLOT, ObjectValue(*arrayBuffer)); - dvobj.initPrivate(arrayBuffer->dataPointer() + byteOffset); + auto dataPointer = arrayBuffer->dataPointerEither(); + dvobj.initPrivate((dataPointer + byteOffset).unwrap(/*safe - stored as private data*/)); // Include a barrier if the data view's data pointer is in the nursery, as // is done for typed arrays. - if (!IsInsideNursery(obj) && cx->runtime()->gc.nursery.isInside(arrayBuffer->dataPointer())) + if (arrayBuffer->is() && + !IsInsideNursery(obj) && + cx->runtime()->gc.nursery.isInside(dataPointer)) + { cx->runtime()->gc.storeBuffer.putWholeCell(obj); + } // Verify that the private slot is at the expected place MOZ_ASSERT(dvobj.numFixedSlots() == TypedArrayObject::DATA_SLOT); - if (!arrayBuffer->addView(cx, &dvobj)) - return nullptr; + if (arrayBuffer->is()) { + if (!arrayBuffer->as().addView(cx, &dvobj)) + return nullptr; + } return &dvobj; } bool DataViewObject::getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, const CallArgs& args, - uint32_t* byteOffsetPtr, uint32_t* byteLengthPtr) + uint32_t* byteOffsetPtr, uint32_t* byteLengthPtr, + bool* lengthTrackingPtr) { - if (!IsArrayBuffer(bufobj)) { + if (!IsAnyArrayBuffer(bufobj)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, - "DataView", "ArrayBuffer", bufobj->getClass()->name); + "DataView", "ArrayBuffer or SharedArrayBuffer", + bufobj->getClass()->name); return false; } - Rooted buffer(cx, &AsArrayBuffer(bufobj)); + Rooted buffer(cx, &bufobj->as()); uint32_t byteOffset = 0; uint32_t byteLength = buffer->byteLength(); + bool isResizableOrGrowable = + (buffer->is() && buffer->as().isResizable()) || + (buffer->is() && + buffer->as().isGrowable()); + bool lengthTracking = isResizableOrGrowable && !args.hasDefined(2); if (args.length() > 1) { - if (!ToUint32(cx, args[1], &byteOffset)) + uint64_t offset; + if (!ToIndex(cx, args[1], &offset)) return false; - if (byteOffset > INT32_MAX) { + if (offset > INT32_MAX) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARG_INDEX_OUT_OF_RANGE, "1"); return false; } + byteOffset = uint32_t(offset); } - if (buffer->isDetached()) { + if (buffer->is() && buffer->as().isDetached()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } @@ -1957,14 +2004,18 @@ DataViewObject::getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, cons if (args.get(2).isUndefined()) { byteLength -= byteOffset; + lengthTracking = isResizableOrGrowable; } else { - if (!ToUint32(cx, args[2], &byteLength)) + uint64_t viewByteLength; + if (!ToIndex(cx, args[2], &viewByteLength)) return false; - if (byteLength > INT32_MAX) { + lengthTracking = false; + if (viewByteLength > INT32_MAX) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARG_INDEX_OUT_OF_RANGE, "2"); return false; } + byteLength = uint32_t(viewByteLength); MOZ_ASSERT(byteOffset + byteLength >= byteOffset, "can't overflow: both numbers are less than INT32_MAX"); @@ -1983,6 +2034,7 @@ DataViewObject::getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, cons *byteOffsetPtr = byteOffset; *byteLengthPtr = byteLength; + *lengthTrackingPtr = lengthTracking; return true; } @@ -1994,7 +2046,8 @@ DataViewObject::constructSameCompartment(JSContext* cx, HandleObject bufobj, con assertSameCompartment(cx, bufobj); uint32_t byteOffset, byteLength; - if (!getAndCheckConstructorArgs(cx, bufobj, args, &byteOffset, &byteLength)) + bool lengthTracking; + if (!getAndCheckConstructorArgs(cx, bufobj, args, &byteOffset, &byteLength, &lengthTracking)) return false; RootedObject proto(cx); @@ -2002,8 +2055,9 @@ DataViewObject::constructSameCompartment(JSContext* cx, HandleObject bufobj, con if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) return false; - Rooted buffer(cx, &AsArrayBuffer(bufobj)); - JSObject* obj = DataViewObject::create(cx, byteOffset, byteLength, buffer, proto); + Rooted buffer(cx, &bufobj->as()); + JSObject* obj = DataViewObject::create(cx, byteOffset, byteLength, buffer, proto, + lengthTracking); if (!obj) return false; args.rval().setObject(*obj); @@ -2043,8 +2097,12 @@ DataViewObject::constructWrapped(JSContext* cx, HandleObject bufobj, const CallA // NB: This entails the IsArrayBuffer check uint32_t byteOffset, byteLength; - if (!getAndCheckConstructorArgs(cx, unwrapped, args, &byteOffset, &byteLength)) + bool lengthTracking; + if (!getAndCheckConstructorArgs(cx, unwrapped, args, &byteOffset, &byteLength, + &lengthTracking)) + { return false; + } // Make sure to get the [[Prototype]] for the created view from this // compartment. @@ -2060,11 +2118,12 @@ DataViewObject::constructWrapped(JSContext* cx, HandleObject bufobj, const CallA return false; } - FixedInvokeArgs<3> args2(cx); + FixedInvokeArgs<4> args2(cx); args2[0].set(PrivateUint32Value(byteOffset)); args2[1].set(PrivateUint32Value(byteLength)); args2[2].setObject(*proto); + args2[3].setBoolean(lengthTracking); RootedValue fval(cx, global->createDataViewForThis()); RootedValue thisv(cx, ObjectValue(*bufobj)); @@ -2092,6 +2151,11 @@ template /* static */ uint8_t* DataViewObject::getDataPointer(JSContext* cx, Handle obj, uint64_t offset) { + if (obj->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATA_VIEW_OUT_OF_BOUNDS); + return nullptr; + } + const size_t TypeSize = sizeof(NativeType); if (offset > UINT32_MAX - TypeSize || offset + TypeSize > obj->byteLength()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARG_INDEX_OUT_OF_RANGE, @@ -2255,7 +2319,7 @@ DataViewObject::read(JSContext* cx, Handle obj, bool isLittleEndian = args.length() >= 2 && ToBoolean(args[1]); // Steps 6-7. - if (obj->arrayBuffer().isDetached()) { + if (obj->arrayBufferEither().isDetached()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } @@ -2354,7 +2418,7 @@ DataViewObject::write(JSContext* cx, Handle obj, bool isLittleEndian = args.length() >= 3 && ToBoolean(args[2]); // Steps 7-8. - if (obj->arrayBuffer().isDetached()) { + if (obj->arrayBufferEither().isDetached()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } @@ -3179,7 +3243,13 @@ template bool DataViewObject::getterImpl(JSContext* cx, const CallArgs& args) { - args.rval().set(ValueGetter(&args.thisv().toObject().as())); + Rooted view(cx, &args.thisv().toObject().as()); + if (ValueGetter != bufferValue && view->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATA_VIEW_OUT_OF_BOUNDS); + return false; + } + + args.rval().set(ValueGetter(view)); return true; } diff --git a/js/src/vm/TypedArrayObject.h b/js/src/vm/TypedArrayObject.h index 8b4b0b5092..8c08bb870e 100644 --- a/js/src/vm/TypedArrayObject.h +++ b/js/src/vm/TypedArrayObject.h @@ -52,6 +52,8 @@ class TypedArrayObject : public NativeObject "right buffer slot"); // Slot containing length of the view in number of typed elements. + // Length-tracking views on resizable/growable buffers store + // LENGTH_TRACKING here and compute their visible length from the buffer. static const size_t LENGTH_SLOT = 1; static_assert(LENGTH_SLOT == JS_TYPEDARRAYLAYOUT_LENGTH_SLOT, "self-hosted code with burned-in constants must get the " @@ -65,6 +67,8 @@ class TypedArrayObject : public NativeObject static const size_t RESERVED_SLOTS = 3; + static const int32_t LENGTH_TRACKING = -1; + #ifdef DEBUG static const uint8_t ZeroLengthArrayData = 0x4A; #endif @@ -139,15 +143,13 @@ class TypedArrayObject : public NativeObject return tarr->getFixedSlot(BUFFER_SLOT); } static Value byteOffsetValue(TypedArrayObject* tarr) { - Value v = tarr->getFixedSlot(BYTEOFFSET_SLOT); - MOZ_ASSERT(v.toInt32() >= 0); - return v; + return Int32Value(tarr->byteOffset()); } static Value byteLengthValue(TypedArrayObject* tarr) { - return Int32Value(tarr->getFixedSlot(LENGTH_SLOT).toInt32() * tarr->bytesPerElement()); + return Int32Value(tarr->byteLength()); } static Value lengthValue(TypedArrayObject* tarr) { - return tarr->getFixedSlot(LENGTH_SLOT); + return Int32Value(tarr->length()); } static bool @@ -159,14 +161,70 @@ class TypedArrayObject : public NativeObject JSObject* bufferObject() const { return bufferValue(const_cast(this)).toObjectOrNull(); } + bool isLengthTracking() const { + return getFixedSlot(LENGTH_SLOT).toInt32() == LENGTH_TRACKING; + } + bool hasResizableOrGrowableBuffer() const { + if (!hasBuffer()) + return false; + if (isSharedMemory()) + return bufferShared()->isGrowable(); + return bufferUnshared()->isResizable(); + } + uint32_t byteOffsetMaybeOutOfBounds() const { + Value v = getFixedSlot(BYTEOFFSET_SLOT); + MOZ_ASSERT(v.toInt32() >= 0); + return v.toInt32(); + } + uint32_t fixedLengthMaybeOutOfBounds() const { + int32_t length = getFixedSlot(LENGTH_SLOT).toInt32(); + MOZ_ASSERT(length >= 0); + return length; + } + uint32_t bufferByteLength() const { + MOZ_ASSERT(hasBuffer()); + if (isSharedMemory()) + return bufferShared()->byteLength(); + return bufferUnshared()->byteLength(); + } + bool isOutOfBounds() const { + if (!hasBuffer()) + return false; + if (hasDetachedBuffer()) + return true; + + uint32_t bufferByteLength = this->bufferByteLength(); + uint32_t offset = byteOffsetMaybeOutOfBounds(); + if (offset > bufferByteLength) + return true; + + if (isLengthTracking()) + return false; + + uint32_t byteLength = fixedLengthMaybeOutOfBounds() * bytesPerElement(); + return byteLength > bufferByteLength - offset; + } uint32_t byteOffset() const { - return byteOffsetValue(const_cast(this)).toInt32(); + if (isOutOfBounds()) + return 0; + return byteOffsetMaybeOutOfBounds(); } uint32_t byteLength() const { - return byteLengthValue(const_cast(this)).toInt32(); + return length() * bytesPerElement(); } uint32_t length() const { - return lengthValue(const_cast(this)).toInt32(); + if (!isLengthTracking()) { + if (isOutOfBounds()) + return 0; + return fixedLengthMaybeOutOfBounds(); + } + + if (isOutOfBounds()) + return 0; + + uint32_t bufferByteLength = this->bufferByteLength(); + uint32_t offset = byteOffsetMaybeOutOfBounds(); + return (bufferByteLength - offset) / bytesPerElement(); } bool hasInlineElements() const; @@ -466,28 +524,26 @@ class DataViewObject : public NativeObject defineGetter(JSContext* cx, PropertyName* name, HandleNativeObject proto); static bool getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, const CallArgs& args, - uint32_t *byteOffset, uint32_t* byteLength); + uint32_t *byteOffset, uint32_t* byteLength, + bool* lengthTracking); static bool constructSameCompartment(JSContext* cx, HandleObject bufobj, const CallArgs& args); static bool constructWrapped(JSContext* cx, HandleObject bufobj, const CallArgs& args); friend bool ArrayBufferObject::createDataViewForThisImpl(JSContext* cx, const CallArgs& args); static DataViewObject* create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, - Handle arrayBuffer, JSObject* proto); + Handle arrayBuffer, JSObject* proto, + bool lengthTracking = false); public: static const Class class_; static Value byteOffsetValue(DataViewObject* view) { - Value v = view->getFixedSlot(TypedArrayObject::BYTEOFFSET_SLOT); - MOZ_ASSERT(v.toInt32() >= 0); - return v; + return Int32Value(view->byteOffset()); } static Value byteLengthValue(DataViewObject* view) { - Value v = view->getFixedSlot(TypedArrayObject::LENGTH_SLOT); - MOZ_ASSERT(v.toInt32() >= 0); - return v; + return Int32Value(view->byteLength()); } static Value bufferValue(DataViewObject* view) { @@ -495,21 +551,66 @@ class DataViewObject : public NativeObject } uint32_t byteOffset() const { - return byteOffsetValue(const_cast(this)).toInt32(); + if (isOutOfBounds()) + return 0; + return byteOffsetMaybeOutOfBounds(); } uint32_t byteLength() const { - return byteLengthValue(const_cast(this)).toInt32(); + if (isOutOfBounds()) + return 0; + if (isLengthTracking()) + return arrayBufferEither().byteLength() - byteOffsetMaybeOutOfBounds(); + return fixedByteLengthMaybeOutOfBounds(); } - ArrayBufferObject& arrayBuffer() const { - return bufferValue(const_cast(this)).toObject().as(); + ArrayBufferObjectMaybeShared& arrayBufferEither() const { + return bufferValue(const_cast(this)). + toObject().as(); + } + + bool isSharedMemory() const { + return bufferValue(const_cast(this)). + toObject().is(); } void* dataPointer() const { return getPrivate(); } + bool isLengthTracking() const { + return getFixedSlot(TypedArrayObject::LENGTH_SLOT).toInt32() == + TypedArrayObject::LENGTH_TRACKING; + } + + uint32_t byteOffsetMaybeOutOfBounds() const { + Value v = getFixedSlot(TypedArrayObject::BYTEOFFSET_SLOT); + MOZ_ASSERT(v.toInt32() >= 0); + return v.toInt32(); + } + + uint32_t fixedByteLengthMaybeOutOfBounds() const { + int32_t length = getFixedSlot(TypedArrayObject::LENGTH_SLOT).toInt32(); + MOZ_ASSERT(length >= 0); + return length; + } + + bool isOutOfBounds() const { + const ArrayBufferObjectMaybeShared& buffer = arrayBufferEither(); + if (buffer.isDetached()) + return true; + + uint32_t bufferByteLength = buffer.byteLength(); + uint32_t offset = byteOffsetMaybeOutOfBounds(); + if (offset > bufferByteLength) + return true; + + if (isLengthTracking()) + return false; + + return fixedByteLengthMaybeOutOfBounds() > bufferByteLength - offset; + } + static bool class_constructor(JSContext* cx, unsigned argc, Value* vp); static bool getInt8Impl(JSContext* cx, const CallArgs& args); diff --git a/js/xpconnect/src/XPCJSContext.cpp b/js/xpconnect/src/XPCJSContext.cpp index 3ca3da6b8b..9365937fdd 100644 --- a/js/xpconnect/src/XPCJSContext.cpp +++ b/js/xpconnect/src/XPCJSContext.cpp @@ -1446,6 +1446,7 @@ ReloadPrefsCallback(const char* pref, void* data) bool extraWarnings = Preferences::GetBool(JS_OPTIONS_DOT_STR "strict"); bool streams = Preferences::GetBool(JS_OPTIONS_DOT_STR "streams"); + bool weakRefs = Preferences::GetBool(JS_OPTIONS_DOT_STR "weakrefs"); bool unboxedObjects = Preferences::GetBool(JS_OPTIONS_DOT_STR "unboxed_objects"); @@ -1472,7 +1473,8 @@ ReloadPrefsCallback(const char* pref, void* data) .setWerror(werror) .setExtraWarnings(extraWarnings) .setArrayProtoValues(arrayProtoValues) - .setStreams(streams); + .setStreams(streams) + .setWeakRefs(weakRefs); JS_SetParallelParsingEnabled(cx, parallelParsing); JS_SetOffthreadIonCompilationEnabled(cx, offthreadIonCompilation); diff --git a/layout/style/Loader.cpp b/layout/style/Loader.cpp index 581fe9622d..a6a471a0b8 100644 --- a/layout/style/Loader.cpp +++ b/layout/style/Loader.cpp @@ -885,9 +885,10 @@ SheetLoadData::OnStreamComplete(nsIUnicharStreamLoader* aLoader, } // In standards mode, a style sheet must have one of these MIME - // types to be processed at all. In quirks mode, we accept any - // MIME type, but only if the style sheet is same-origin with the - // requesting document or parent sheet. See bug 524223. + // types to be processed at all, unless it is same-origin. + // For same-origin sheets, we accept any MIME type to match modern + // browser behavior (Chrome, Firefox). Cross-origin sheets require + // a valid CSS MIME type for security. bool validType = contentType.EqualsLiteral("text/css") || contentType.EqualsLiteral(UNKNOWN_CONTENT_TYPE) || @@ -906,7 +907,7 @@ SheetLoadData::OnStreamComplete(nsIUnicharStreamLoader* aLoader, } } - if (sameOrigin && mLoader->mCompatMode == eCompatibility_NavQuirks) { + if (sameOrigin) { errorMessage = "MimeNotCssWarn"; errorFlag = nsIScriptError::warningFlag; } else { diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index e13c9e94c9..54d89a7abb 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -9,7 +9,7 @@ * * Platform-specific #ifdefs at the end of this file override the generic * entries at the top. - */ + * /* * SYNTAX HINTS: @@ -639,7 +639,7 @@ pref("layout.event-regions.enabled", false); // gfx/layers/apz/src/AsyncPanZoomController.cpp. pref("apz.allow_checkerboarding", true); pref("apz.allow_immediate_handoff", true); -pref("apz.allow_zooming", false); +pref("apz.allow_zooming", true); // Whether to lock touch scrolling to one axis at a time // 0 = FREE (No locking at all) @@ -651,7 +651,7 @@ pref("apz.axis_lock.breakout_threshold", "0.03125"); // 1/32 inches pref("apz.axis_lock.breakout_angle", "0.3926991"); // PI / 8 (22.5 degrees) pref("apz.axis_lock.direct_pan_angle", "1.047197"); // PI / 3 (60 degrees) pref("apz.content_response_timeout", 400); -pref("apz.drag.enabled", false); +pref("apz.drag.enabled", true); pref("apz.danger_zone_x", 50); pref("apz.danger_zone_y", 100); pref("apz.disable_for_scroll_linked_effects", false); @@ -710,7 +710,7 @@ pref("apz.zoom_animation_duration_ms", 250); pref("apz.scale_repaint_delay_ms", 500); #if !defined(MOZ_WIDGET_UIKIT) -pref("apz.desktop.enabled", false); +pref("apz.desktop.enabled", true); #endif #ifdef XP_MACOSX @@ -1348,6 +1348,9 @@ pref("javascript.options.dynamicImport", true); // Streams API pref("javascript.options.streams", true); +// Enable garbage collection of weakrefed objects +pref("javascript.options.weakrefs", false); + // advanced prefs pref("advanced.mailftp", false); pref("image.animation_mode", "normal");