From 90ca1f569a364363a0985cb038725e698a43406f Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 09:36:44 -0500 Subject: [PATCH 01/31] Issue #1442 - Part 1: Change ArrayBufferCopyData self-hosting intrinsic to take a start offset for the destination. This will be needed to manage stream chunks. https://bugzilla.mozilla.org/show_bug.cgi?id=1272697 --- js/src/builtin/TypedArray.js | 6 ++---- js/src/builtin/Utilities.js | 1 + js/src/vm/ArrayBufferObject.cpp | 9 +++++---- js/src/vm/ArrayBufferObject.h | 6 +++--- js/src/vm/SelfHosting.cpp | 17 +++++++++-------- js/src/vm/SharedArrayObject.cpp | 9 +++++---- js/src/vm/SharedArrayObject.h | 6 +++--- 7 files changed, 28 insertions(+), 26 deletions(-) diff --git a/js/src/builtin/TypedArray.js b/js/src/builtin/TypedArray.js index 57f6d738ca..a8510faa71 100644 --- a/js/src/builtin/TypedArray.js +++ b/js/src/builtin/TypedArray.js @@ -2,8 +2,6 @@ * 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 "TypedObjectConstants.h" - function ViewedArrayBufferIfReified(tarray) { assert(IsTypedArray(tarray), "non-typed array asked for its buffer"); @@ -1781,7 +1779,7 @@ function ArrayBufferSlice(start, end) { ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); // Steps 19-21. - ArrayBufferCopyData(new_, O, first | 0, newLen | 0, isWrapped); + ArrayBufferCopyData(new_, 0, O, first | 0, newLen | 0, isWrapped); // Step 22. return new_; @@ -1863,7 +1861,7 @@ function SharedArrayBufferSlice(start, end) { ThrowTypeError(JSMSG_SHORT_SHARED_ARRAY_BUFFER_RETURNED, newLen, actualLen); // Steps 16-18. - SharedArrayBufferCopyData(new_, O, first | 0, newLen | 0, isWrapped); + SharedArrayBufferCopyData(new_, 0, O, first | 0, newLen | 0, isWrapped); // Step 19. return new_; diff --git a/js/src/builtin/Utilities.js b/js/src/builtin/Utilities.js index 51c5a574fd..1cc15ed68e 100644 --- a/js/src/builtin/Utilities.js +++ b/js/src/builtin/Utilities.js @@ -23,6 +23,7 @@ */ #include "SelfHostingDefines.h" +#include "TypedObjectConstants.h" // Assertions and debug printing, defined here instead of in the header above // to make `assert` invisible to C++. diff --git a/js/src/vm/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index db1d7c798f..9e0bf3f4da 100644 --- a/js/src/vm/ArrayBufferObject.cpp +++ b/js/src/vm/ArrayBufferObject.cpp @@ -1255,15 +1255,16 @@ ArrayBufferObject::finalize(FreeOp* fop, JSObject* obj) } /* static */ void -ArrayBufferObject::copyData(Handle toBuffer, - Handle fromBuffer, - uint32_t fromIndex, uint32_t count) +ArrayBufferObject::copyData(Handle toBuffer, uint32_t toIndex, + Handle fromBuffer, uint32_t fromIndex, + uint32_t count) { MOZ_ASSERT(toBuffer->byteLength() >= count); + MOZ_ASSERT(toBuffer->byteLength() >= toIndex + count); MOZ_ASSERT(fromBuffer->byteLength() >= fromIndex); MOZ_ASSERT(fromBuffer->byteLength() >= fromIndex + count); - memcpy(toBuffer->dataPointer(), fromBuffer->dataPointer() + fromIndex, count); + memcpy(toBuffer->dataPointer() + toIndex, fromBuffer->dataPointer() + fromIndex, count); } /* static */ void diff --git a/js/src/vm/ArrayBufferObject.h b/js/src/vm/ArrayBufferObject.h index 4ff7962cfb..f4010c6c77 100644 --- a/js/src/vm/ArrayBufferObject.h +++ b/js/src/vm/ArrayBufferObject.h @@ -261,9 +261,9 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared template static bool createTypedArrayFromBuffer(JSContext* cx, unsigned argc, Value* vp); - static void copyData(Handle toBuffer, - Handle fromBuffer, - uint32_t fromIndex, uint32_t count); + static void copyData(Handle toBuffer, uint32_t toIndex, + Handle fromBuffer, uint32_t fromIndex, + uint32_t count); static void trace(JSTracer* trc, JSObject* obj); static void objectMoved(JSObject* obj, const JSObject* old); diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index e9c72d1bf1..e8e6ad937a 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -1098,9 +1098,9 @@ static bool intrinsic_ArrayBufferCopyData(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 5); + MOZ_ASSERT(args.length() == 6); - bool isWrapped = args[4].toBoolean(); + bool isWrapped = args[5].toBoolean(); Rooted toBuffer(cx); if (!isWrapped) { toBuffer = &args[0].toObject().as(); @@ -1114,11 +1114,12 @@ intrinsic_ArrayBufferCopyData(JSContext* cx, unsigned argc, Value* vp) } toBuffer = toBufferObj.as(); } - Rooted fromBuffer(cx, &args[1].toObject().as()); - uint32_t fromIndex = uint32_t(args[2].toInt32()); - uint32_t count = uint32_t(args[3].toInt32()); + uint32_t toIndex = uint32_t(args[1].toInt32()); + Rooted fromBuffer(cx, &args[2].toObject().as()); + uint32_t fromIndex = uint32_t(args[3].toInt32()); + uint32_t count = uint32_t(args[4].toInt32()); - T::copyData(toBuffer, fromBuffer, fromIndex, count); + T::copyData(toBuffer, toIndex, fromBuffer, fromIndex, count); args.rval().setUndefined(); return true; @@ -2426,14 +2427,14 @@ static const JSFunctionSpec intrinsic_functions[] = { intrinsic_PossiblyWrappedArrayBufferByteLength, 1,0, IntrinsicPossiblyWrappedArrayBufferByteLength), JS_FN("ArrayBufferCopyData", - intrinsic_ArrayBufferCopyData, 5,0), + intrinsic_ArrayBufferCopyData, 6,0), JS_FN("SharedArrayBufferByteLength", intrinsic_ArrayBufferByteLength, 1,0), JS_FN("PossiblyWrappedSharedArrayBufferByteLength", intrinsic_PossiblyWrappedArrayBufferByteLength, 1,0), JS_FN("SharedArrayBufferCopyData", - intrinsic_ArrayBufferCopyData, 5,0), + intrinsic_ArrayBufferCopyData, 6,0), JS_FN("IsUint8TypedArray", intrinsic_IsUint8TypedArray, 1,0), JS_FN("IsInt8TypedArray", intrinsic_IsInt8TypedArray, 1,0), diff --git a/js/src/vm/SharedArrayObject.cpp b/js/src/vm/SharedArrayObject.cpp index 8e55001d13..44fe3b790d 100644 --- a/js/src/vm/SharedArrayObject.cpp +++ b/js/src/vm/SharedArrayObject.cpp @@ -331,15 +331,16 @@ SharedArrayBufferObject::addSizeOfExcludingThis(JSObject* obj, mozilla::MallocSi } /* static */ void -SharedArrayBufferObject::copyData(Handle toBuffer, - Handle fromBuffer, - uint32_t fromIndex, uint32_t count) +SharedArrayBufferObject::copyData(Handle toBuffer, uint32_t toIndex, + Handle fromBuffer, uint32_t fromIndex, + uint32_t count) { MOZ_ASSERT(toBuffer->byteLength() >= count); + MOZ_ASSERT(toBuffer->byteLength() >= toIndex + count); MOZ_ASSERT(fromBuffer->byteLength() >= fromIndex); MOZ_ASSERT(fromBuffer->byteLength() >= fromIndex + count); - jit::AtomicOperations::memcpySafeWhenRacy(toBuffer->dataPointerShared(), + jit::AtomicOperations::memcpySafeWhenRacy(toBuffer->dataPointerShared() + toIndex, fromBuffer->dataPointerShared() + fromIndex, count); } diff --git a/js/src/vm/SharedArrayObject.h b/js/src/vm/SharedArrayObject.h index 05db688184..19048336cb 100644 --- a/js/src/vm/SharedArrayObject.h +++ b/js/src/vm/SharedArrayObject.h @@ -146,9 +146,9 @@ class SharedArrayBufferObject : public ArrayBufferObjectMaybeShared static void addSizeOfExcludingThis(JSObject* obj, mozilla::MallocSizeOf mallocSizeOf, JS::ClassInfo* info); - static void copyData(Handle toBuffer, - Handle fromBuffer, - uint32_t fromIndex, uint32_t count); + static void copyData(Handle toBuffer, uint32_t toIndex, + Handle fromBuffer, uint32_t fromIndex, + uint32_t count); SharedArrayRawBuffer* rawBufferObject() const; From 62467428a3f05b86cd4ee529403a5ba62b4320f8 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 10:45:00 -0500 Subject: [PATCH 02/31] Issue #1442 - Part 2: Add a run-time preference for toggling the streams API. https://bugzilla.mozilla.org/show_bug.cgi?id=1272697 --- dom/workers/RuntimeService.cpp | 1 + js/src/jsapi.h | 11 +++++++++++ js/src/shell/js.cpp | 9 +++++++-- js/xpconnect/src/XPCJSContext.cpp | 5 ++++- modules/libpref/init/all.js | 3 +++ 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp index 239207efc9..6138a3c6de 100644 --- a/dom/workers/RuntimeService.cpp +++ b/dom/workers/RuntimeService.cpp @@ -299,6 +299,7 @@ LoadContextOptions(const char* aPrefName, void* /* aClosure */) .setNativeRegExp(GetWorkerPref(NS_LITERAL_CSTRING("native_regexp"))) .setAsyncStack(GetWorkerPref(NS_LITERAL_CSTRING("asyncstack"))) .setWerror(GetWorkerPref(NS_LITERAL_CSTRING("werror"))) + .setStreams(GetWorkerPref(NS_LITERAL_CSTRING("streams"))) .setExtraWarnings(GetWorkerPref(NS_LITERAL_CSTRING("strict"))) .setArrayProtoValues(GetWorkerPref( NS_LITERAL_CSTRING("array_prototype_values"))); diff --git a/js/src/jsapi.h b/js/src/jsapi.h index f80d2602e6..a8e28a587b 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -1161,6 +1161,16 @@ class JS_PUBLIC_API(ContextOptions) { return *this; } + bool streams() const { return streams_; } + ContextOptions& setStreams(bool flag) { + streams_ = flag; + return *this; + } + ContextOptions& toggleStreams() { + streams_ = !streams_; + return *this; + } + bool nativeRegExp() const { return nativeRegExp_; } ContextOptions& setNativeRegExp(bool flag) { nativeRegExp_ = flag; @@ -1243,6 +1253,7 @@ class JS_PUBLIC_API(ContextOptions) { bool strictMode_ : 1; bool extraWarnings_ : 1; bool arrayProtoValues_ : 1; + bool streams_ : 1; }; JS_PUBLIC_API(ContextOptions&) diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index a77e128a52..1f89e38482 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -318,6 +318,7 @@ static bool enableUnboxedArrays = false; static bool enableSharedMemory = SHARED_MEMORY_DEFAULT; static bool enableWasmAlwaysBaseline = false; static bool enableArrayProtoValues = true; +static bool enableStreams = false; static bool printTiming = false; static const char* jsCacheDir = nullptr; static const char* jsCacheAsmJSPath = nullptr; @@ -7565,6 +7566,7 @@ SetContextOptions(JSContext* cx, const OptionParser& op) enableUnboxedArrays = op.getBoolOption("unboxed-arrays"); enableWasmAlwaysBaseline = op.getBoolOption("wasm-always-baseline"); enableArrayProtoValues = !op.getBoolOption("no-array-proto-values"); + enableStreams = op.getBoolOption("enable-streams"); JS::ContextOptionsRef(cx).setBaseline(enableBaseline) .setIon(enableIon) @@ -7573,7 +7575,8 @@ SetContextOptions(JSContext* cx, const OptionParser& op) .setWasmAlwaysBaseline(enableWasmAlwaysBaseline) .setNativeRegExp(enableNativeRegExp) .setUnboxedArrays(enableUnboxedArrays) - .setArrayProtoValues(enableArrayProtoValues); + .setArrayProtoValues(enableArrayProtoValues) + .setStreams(enableStreams); if (op.getBoolOption("wasm-check-bce")) jit::JitOptions.wasmAlwaysCheckBounds = true; @@ -7845,7 +7848,8 @@ SetWorkerContextOptions(JSContext* cx) .setWasmAlwaysBaseline(enableWasmAlwaysBaseline) .setNativeRegExp(enableNativeRegExp) .setUnboxedArrays(enableUnboxedArrays) - .setArrayProtoValues(enableArrayProtoValues); + .setArrayProtoValues(enableArrayProtoValues) + .setStreams(enableStreams); cx->setOffthreadIonCompilationEnabled(offthreadCompilation); cx->profilingScripts = enableCodeCoverage || enableDisassemblyDumps; @@ -8019,6 +8023,7 @@ main(int argc, char** argv, char** envp) || !op.addBoolOption('\0', "wasm-always-baseline", "Enable wasm baseline compiler when possible") || !op.addBoolOption('\0', "wasm-check-bce", "Always generate wasm bounds check, even redundant ones.") || !op.addBoolOption('\0', "no-array-proto-values", "Remove Array.prototype.values") + || !op.addBoolOption('\0', "enable-streams", "Enable WHATWG Streams") #ifdef ENABLE_SHARED_ARRAY_BUFFER || !op.addStringOption('\0', "shared-memory", "on/off", "SharedArrayBuffer and Atomics " diff --git a/js/xpconnect/src/XPCJSContext.cpp b/js/xpconnect/src/XPCJSContext.cpp index e76cd9f9fe..be9a75f799 100644 --- a/js/xpconnect/src/XPCJSContext.cpp +++ b/js/xpconnect/src/XPCJSContext.cpp @@ -1438,6 +1438,8 @@ ReloadPrefsCallback(const char* pref, void* data) bool werror = Preferences::GetBool(JS_OPTIONS_DOT_STR "werror"); bool extraWarnings = Preferences::GetBool(JS_OPTIONS_DOT_STR "strict"); + + bool streams = Preferences::GetBool(JS_OPTIONS_DOT_STR "streams"); bool unboxedObjects = Preferences::GetBool(JS_OPTIONS_DOT_STR "unboxed_objects"); @@ -1463,7 +1465,8 @@ ReloadPrefsCallback(const char* pref, void* data) .setDumpStackOnDebuggeeWouldRun(dumpStackOnDebuggeeWouldRun) .setWerror(werror) .setExtraWarnings(extraWarnings) - .setArrayProtoValues(arrayProtoValues); + .setArrayProtoValues(arrayProtoValues) + .setStreams(streams); JS_SetParallelParsingEnabled(cx, parallelParsing); JS_SetOffthreadIonCompilationEnabled(cx, offthreadIonCompilation); diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index ab9ec4000d..d1e2ddf1e2 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -1334,6 +1334,9 @@ pref("javascript.options.main_thread_stack_quota_cap", 2097152); // Dynamic module import. pref("javascript.options.dynamicImport", true); +// Streams API +pref("javascript.options.streams", false); + // advanced prefs pref("advanced.mailftp", false); pref("image.animation_mode", "normal"); From 54e84f0f1d1a509d1c75d86f835134c8efa29d5c Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 16:00:10 -0500 Subject: [PATCH 03/31] Issue #1442 - Part 3: Implement ReadableStream and associated classes in the JS engine. https://bugzilla.mozilla.org/show_bug.cgi?id=1272697 --- js/src/builtin/Stream.cpp | 4957 +++++++++++++++++++++++++++ js/src/builtin/Stream.h | 118 + js/src/builtin/TestingFunctions.cpp | 12 + js/src/js.msg | 29 +- js/src/jsfriendapi.h | 6 + js/src/jsnum.cpp | 11 +- js/src/jsnum.h | 4 + js/src/jsprototypes.h | 11 + js/src/moz.build | 1 + js/src/vm/ArrayBufferObject.cpp | 11 + js/src/vm/CommonPropertyNames.h | 7 + js/src/vm/GlobalObject.cpp | 11 + js/src/vm/SelfHosting.cpp | 5 +- 13 files changed, 5178 insertions(+), 5 deletions(-) create mode 100644 js/src/builtin/Stream.cpp create mode 100644 js/src/builtin/Stream.h diff --git a/js/src/builtin/Stream.cpp b/js/src/builtin/Stream.cpp new file mode 100644 index 0000000000..8eae95bcf7 --- /dev/null +++ b/js/src/builtin/Stream.cpp @@ -0,0 +1,4957 @@ +/* -*- 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/Stream.h" + +#include "jscntxt.h" + +#include "gc/Heap.h" +#include "vm/SelfHosting.h" + +#include "jsobjinlines.h" + +#include "vm/NativeObject-inl.h" + +using namespace js; + +enum StreamSlots { + StreamSlot_Controller, + StreamSlot_Reader, + StreamSlot_State, + StreamSlot_StoredError, + StreamSlotCount +}; + +enum ReaderSlots { + ReaderSlot_Stream, + ReaderSlot_Requests, + ReaderSlot_ClosedPromise, + ReaderSlotCount, +}; + +enum ReaderType { + ReaderType_Default, + ReaderType_BYOB +}; + +// ReadableStreamDefaultController and ReadableByteStreamController are both +// queue containers and must have these slots at identical offsets. +enum QueueContainerSlots { + QueueContainerSlot_Queue, + QueueContainerSlot_TotalSize, + QueueContainerSlotCount +}; + +// These slots are identical between the two types of ReadableStream +// controllers. +enum ControllerSlots { + ControllerSlot_Stream = QueueContainerSlotCount, + ControllerSlot_UnderlyingSource, + ControllerSlot_StrategyHWM, + ControllerSlot_Flags, + ControllerSlotCount +}; + +enum DefaultControllerSlots { + DefaultControllerSlot_StrategySize = ControllerSlotCount, + DefaultControllerSlotCount +}; + +enum ByteControllerSlots { + ByteControllerSlot_BYOBRequest = ControllerSlotCount, + ByteControllerSlot_PendingPullIntos, + ByteControllerSlot_AutoAllocateSize, + ByteControllerSlotCount +}; + +enum ControllerFlags { + ControllerFlag_Started = 1 << 0, + ControllerFlag_Pulling = 1 << 1, + ControllerFlag_PullAgain = 1 << 2, + ControllerFlag_CloseRequested = 1 << 3, + ControllerFlag_TeeBranch = 1 << 4, + ControllerFlag_TeeBranch1 = 1 << 5, + ControllerFlag_TeeBranch2 = 1 << 6 +}; + +enum BYOBRequestSlots { + BYOBRequestSlot_Controller, + BYOBRequestSlot_View, + BYOBRequestSlotCount +}; + +template +MOZ_ALWAYS_INLINE bool +Is(HandleValue v) +{ + return v.isObject() && v.toObject().is(); +} + +#ifdef DEBUG +static bool +IsReadableStreamController(const JSObject* controller) +{ + return controller->is() || + controller->is(); +} +#endif // DEBUG + +static bool +IsReadableStreamReader(const JSObject* reader) +{ + return reader->is() || + reader->is(); +} + +static inline uint32_t +ControllerFlags(const NativeObject* controller) +{ + MOZ_ASSERT(IsReadableStreamController(controller)); + return controller->getFixedSlot(ControllerSlot_Flags).toInt32(); +} + +static inline void +AddControllerFlags(NativeObject* controller, uint32_t flags) +{ + MOZ_ASSERT(IsReadableStreamController(controller)); + controller->setFixedSlot(ControllerSlot_Flags, + Int32Value(ControllerFlags(controller) | flags)); +} + +static inline void +RemoveControllerFlags(NativeObject* controller, uint32_t flags) +{ + MOZ_ASSERT(IsReadableStreamController(controller)); + controller->setFixedSlot(ControllerSlot_Flags, + Int32Value(ControllerFlags(controller) & ~flags)); +} + +static inline uint32_t +StreamState(const ReadableStream* stream) +{ + return stream->getFixedSlot(StreamSlot_State).toInt32(); +} + +static inline void +SetStreamState(ReadableStream* stream, uint32_t state) +{ + MOZ_ASSERT_IF(stream->disturbed(), state & ReadableStream::Disturbed); + MOZ_ASSERT_IF(stream->closed() || stream->errored(), !(state & ReadableStream::Readable)); + stream->setFixedSlot(StreamSlot_State, Int32Value(state)); +} + +inline bool +ReadableStream::readable() const +{ + return StreamState(this) & Readable; +} + +inline bool +ReadableStream::closed() const +{ + return StreamState(this) & Closed; +} + +inline bool +ReadableStream::errored() const +{ + return StreamState(this) & Errored; +} + +inline bool +ReadableStream::disturbed() const +{ + return StreamState(this) & Disturbed; +} + +inline static MOZ_MUST_USE ReadableStream* +StreamFromController(const NativeObject* controller) +{ + MOZ_ASSERT(IsReadableStreamController(controller)); + return &controller->getFixedSlot(ControllerSlot_Stream).toObject().as(); +} + +inline static MOZ_MUST_USE NativeObject* +ControllerFromStream(ReadableStream* stream) +{ + Value controllerVal = stream->getFixedSlot(StreamSlot_Controller); + MOZ_ASSERT(IsReadableStreamController(&controllerVal.toObject())); + return &controllerVal.toObject().as(); +} + +inline static MOZ_MUST_USE ReadableStream* +StreamFromReader(const NativeObject* reader) +{ + MOZ_ASSERT(IsReadableStreamReader(reader)); + return &reader->getFixedSlot(ReaderSlot_Stream).toObject().as(); +} + +inline static MOZ_MUST_USE NativeObject* +ReaderFromStream(NativeObject* stream) +{ + Value readerVal = stream->getFixedSlot(StreamSlot_Reader); + MOZ_ASSERT(IsReadableStreamReader(&readerVal.toObject())); + return &readerVal.toObject().as(); +} + +inline static MOZ_MUST_USE JSFunction* +NewHandler(JSContext *cx, Native handler, HandleObject target) +{ + RootedAtom funName(cx, cx->names().empty); + RootedFunction handlerFun(cx, NewNativeFunction(cx, handler, 0, funName, + gc::AllocKind::FUNCTION_EXTENDED, + GenericObject)); + if (!handlerFun) + return nullptr; + handlerFun->setExtendedSlot(0, ObjectValue(*target)); + return handlerFun; +} + +template +inline static MOZ_MUST_USE T* +TargetFromHandler(JSObject& handler) +{ + return &handler.as().getExtendedSlot(0).toObject().as(); +} + +inline static MOZ_MUST_USE bool +ResetQueue(JSContext* cx, HandleNativeObject container); + +inline static MOZ_MUST_USE bool +InvokeOrNoop(JSContext* cx, HandleValue O, HandlePropertyName P, HandleValue arg, + MutableHandleValue rval); + +static MOZ_MUST_USE JSObject* +PromiseInvokeOrNoop(JSContext* cx, HandleValue O, HandlePropertyName P, HandleValue arg); + +static MOZ_MUST_USE JSObject* +PromiseRejectedWithPendingError(JSContext* cx) { + // Not much we can do about uncatchable exceptions, just bail. + RootedValue exn(cx); + if (!GetAndClearException(cx, &exn)) + return nullptr; + return PromiseObject::unforgeableReject(cx, exn); +} + +static bool +ReportArgTypeError(JSContext* cx, const char* funName, const char* expectedType, + HandleValue arg) +{ + UniqueChars bytes = DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, arg, nullptr); + if (!bytes) + return false; + + return JS_ReportErrorFlagsAndNumberLatin1(cx, JSREPORT_ERROR, GetErrorMessage, + nullptr, JSMSG_NOT_EXPECTED_TYPE, + funName, expectedType, bytes.get()); +} + +static MOZ_MUST_USE bool +RejectWithPendingError(JSContext* cx, Handle promise) { + // Not much we can do about uncatchable exceptions, just bail. + RootedValue exn(cx); + if (!GetAndClearException(cx, &exn)) + return false; + return PromiseObject::reject(cx, promise, exn); +} + +static MOZ_MUST_USE bool +ReturnPromiseRejectedWithPendingError(JSContext* cx, const CallArgs& args) +{ + JSObject* promise = PromiseRejectedWithPendingError(cx); + if (!promise) + return false; + + args.rval().setObject(*promise); + return true; +} + +static MOZ_MUST_USE bool +RejectNonGenericMethod(JSContext* cx, const CallArgs& args, + const char* className, const char* methodName) +{ + ReportValueError3(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(), + nullptr, className, methodName); + + return ReturnPromiseRejectedWithPendingError(cx, args); +} + +inline static MOZ_MUST_USE NativeObject* +SetNewList(JSContext* cx, HandleNativeObject container, uint32_t slot) +{ + NativeObject* 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) +{ + uint32_t length = list->getDenseInitializedLength(); + + if (!list->ensureElements(cx, length + 1)) + return false; + + list->ensureDenseInitializedLength(cx, length, 1); + list->setDenseElement(length, value); + + return true; +} + +template +inline static MOZ_MUST_USE T* +PeekList(NativeObject* list) +{ + MOZ_ASSERT(list->getDenseInitializedLength() > 0); + return &list->getDenseElement(0).toObject().as(); +} + +template +inline static MOZ_MUST_USE T* +ShiftFromList(JSContext* cx, HandleNativeObject list) +{ + uint32_t length = list->getDenseInitializedLength(); + MOZ_ASSERT(length > 0); + + Rooted entry(cx, &list->getDenseElement(0).toObject().as()); + list->moveDenseElements(0, 1, length - 1); + list->shrinkElements(cx, length - 1); + + list->setDenseInitializedLength(length - 1); + + return entry; +} + +class ByteStreamChunk : public NativeObject +{ + private: + enum Slots { + Slot_Buffer = 0, + Slot_ByteOffset, + Slot_ByteLength, + SlotCount + }; + + public: + static const Class class_; + + ArrayBufferObject* buffer() { + return &getFixedSlot(Slot_Buffer).toObject().as(); + } + uint32_t byteOffset() { return getFixedSlot(Slot_ByteOffset).toInt32(); } + void SetByteOffset(uint32_t offset) { + setFixedSlot(Slot_ByteOffset, Int32Value(offset)); + } + uint32_t byteLength() { return getFixedSlot(Slot_ByteLength).toInt32(); } + void SetByteLength(uint32_t length) { + setFixedSlot(Slot_ByteLength, Int32Value(length)); + } + + static ByteStreamChunk* create(JSContext* cx, HandleObject buffer, uint32_t byteOffset, + uint32_t byteLength) + { + Rooted chunk(cx, NewObjectWithClassProto(cx)); + if (!chunk) + return nullptr; + + chunk->setFixedSlot(Slot_Buffer, ObjectValue(*buffer)); + chunk->setFixedSlot(Slot_ByteOffset, Int32Value(byteOffset)); + chunk->setFixedSlot(Slot_ByteLength, Int32Value(byteLength)); + return chunk; + } +}; + +const Class ByteStreamChunk::class_ = { + "ByteStreamChunk", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) +}; + +class PullIntoDescriptor : public NativeObject +{ + private: + enum Slots { + Slot_buffer, + Slot_ByteOffset, + Slot_ByteLength, + Slot_BytesFilled, + Slot_ElementSize, + Slot_Ctor, + Slot_ReaderType, + SlotCount + }; + public: + static const Class class_; + + ArrayBufferObject* buffer() { + return &getFixedSlot(Slot_buffer).toObject().as(); + } + void setBuffer(ArrayBufferObject* buffer) { setFixedSlot(Slot_buffer, ObjectValue(*buffer)); } + JSObject* ctor() { return getFixedSlot(Slot_Ctor).toObjectOrNull(); } + uint32_t byteOffset() const { return getFixedSlot(Slot_ByteOffset).toInt32(); } + uint32_t byteLength() const { return getFixedSlot(Slot_ByteLength).toInt32(); } + uint32_t bytesFilled() const { return getFixedSlot(Slot_BytesFilled).toInt32(); } + void setBytesFilled(int32_t bytes) { setFixedSlot(Slot_BytesFilled, Int32Value(bytes)); } + uint32_t elementSize() const { return getFixedSlot(Slot_ElementSize).toInt32(); } + uint32_t readerType() const { return getFixedSlot(Slot_ReaderType).toInt32(); } + + static PullIntoDescriptor* create(JSContext* cx, HandleArrayBufferObject buffer, + uint32_t byteOffset, uint32_t byteLength, + uint32_t bytesFilled, uint32_t elementSize, + HandleObject ctor, uint32_t readerType) + { + Rooted descriptor(cx, NewObjectWithClassProto(cx)); + if (!descriptor) + return nullptr; + + descriptor->setFixedSlot(Slot_buffer, ObjectValue(*buffer)); + descriptor->setFixedSlot(Slot_Ctor, ObjectOrNullValue(ctor)); + descriptor->setFixedSlot(Slot_ByteOffset, Int32Value(byteOffset)); + descriptor->setFixedSlot(Slot_ByteLength, Int32Value(byteLength)); + descriptor->setFixedSlot(Slot_BytesFilled, Int32Value(bytesFilled)); + descriptor->setFixedSlot(Slot_ElementSize, Int32Value(elementSize)); + descriptor->setFixedSlot(Slot_ReaderType, Int32Value(readerType)); + return descriptor; + } +}; + +const Class PullIntoDescriptor::class_ = { + "PullIntoDescriptor", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) +}; + +class QueueEntry : public NativeObject +{ + private: + enum Slots { + Slot_Value = 0, + Slot_Size, + SlotCount + }; + + public: + static const Class class_; + + Value value() { return getFixedSlot(Slot_Value); } + double size() { return getFixedSlot(Slot_Size).toNumber(); } + + static QueueEntry* create(JSContext* cx, HandleValue value, double size) + { + Rooted entry(cx, NewObjectWithClassProto(cx)); + if (!entry) + return nullptr; + + entry->setFixedSlot(Slot_Value, value); + entry->setFixedSlot(Slot_Size, NumberValue(size)); + + return entry; + } +}; + +const Class QueueEntry::class_ = { + "QueueEntry", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) +}; + +class TeeState : public NativeObject +{ + private: + enum Slots { + Slot_Flags = 0, + Slot_Reason1, + Slot_Reason2, + Slot_Promise, + Slot_Stream, + Slot_Branch1, + Slot_Branch2, + SlotCount + }; + + enum Flags + { + Flag_ClosedOrErrored = 1 << 0, + Flag_Canceled1 = 1 << 1, + Flag_Canceled2 = 1 << 2, + Flag_CloneForBranch2 = 1 << 3, + }; + uint32_t flags() const { return getFixedSlot(Slot_Flags).toInt32(); } + void setFlags(uint32_t flags) { setFixedSlot(Slot_Flags, Int32Value(flags)); } + + public: + static const Class class_; + + bool cloneForBranch2() const { return flags() & Flag_CloneForBranch2; } + + bool closedOrErrored() const { return flags() & Flag_ClosedOrErrored; } + void setClosedOrErrored() { + MOZ_ASSERT(!(flags() & Flag_ClosedOrErrored)); + setFlags(flags() | Flag_ClosedOrErrored); + } + + bool canceled1() const { return flags() & Flag_Canceled1; } + void setCanceled1(HandleValue reason) { + MOZ_ASSERT(!(flags() & Flag_Canceled1)); + setFlags(flags() | Flag_Canceled1); + setFixedSlot(Slot_Reason1, reason); + } + + bool canceled2() const { return flags() & Flag_Canceled2; } + void setCanceled2(HandleValue reason) { + MOZ_ASSERT(!(flags() & Flag_Canceled2)); + setFlags(flags() | Flag_Canceled2); + setFixedSlot(Slot_Reason2, reason); + } + + Value reason1() const { + MOZ_ASSERT(canceled1()); + return getFixedSlot(Slot_Reason1); + } + + Value reason2() const { + MOZ_ASSERT(canceled2()); + return getFixedSlot(Slot_Reason2); + } + + PromiseObject* promise() { + return &getFixedSlot(Slot_Promise).toObject().as(); + } + ReadableStream* stream() { + return &getFixedSlot(Slot_Stream).toObject().as(); + } + ReadableStreamDefaultReader* reader() { + return &ReaderFromStream(stream())->as(); + } + + ReadableStreamDefaultController* branch1() { + ReadableStreamDefaultController* controller = &getFixedSlot(Slot_Branch1).toObject() + .as(); + MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch); + MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch1); + return controller; + } + void setBranch1(ReadableStreamDefaultController* controller) { + MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch); + MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch1); + setFixedSlot(Slot_Branch1, ObjectValue(*controller)); + } + + ReadableStreamDefaultController* branch2() { + ReadableStreamDefaultController* controller = &getFixedSlot(Slot_Branch2).toObject() + .as(); + MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch); + MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch2); + return controller; + } + void setBranch2(ReadableStreamDefaultController* controller) { + MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch); + MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch2); + setFixedSlot(Slot_Branch2, ObjectValue(*controller)); + } + + static TeeState* create(JSContext* cx, Handle stream) { + Rooted state(cx, NewObjectWithClassProto(cx)); + if (!state) + return nullptr; + + Rooted promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) + return nullptr; + + state->setFixedSlot(Slot_Flags, Int32Value(0)); + state->setFixedSlot(Slot_Promise, ObjectValue(*promise)); + state->setFixedSlot(Slot_Stream, ObjectValue(*stream)); + + return state; + } +}; + +const Class TeeState::class_ = { + "TeeState", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) +}; + +#define CLASS_SPEC(cls, nCtorArgs, nSlots, specFlags) \ +const ClassSpec cls::classSpec_ = { \ + GenericCreateConstructor, \ + GenericCreatePrototype, \ + nullptr, \ + nullptr, \ + cls##_methods, \ + cls##_properties, \ + nullptr, \ + specFlags \ +}; \ +\ +const Class cls::class_ = { \ + #cls, \ + JSCLASS_HAS_RESERVED_SLOTS(nSlots) | \ + JSCLASS_HAS_CACHED_PROTO(JSProto_##cls), \ + JS_NULL_CLASS_OPS, \ + &cls::classSpec_ \ +}; \ +\ +const Class cls::protoClass_ = { \ + "object", \ + JSCLASS_HAS_CACHED_PROTO(JSProto_##cls), \ + JS_NULL_CLASS_OPS, \ + &cls::classSpec_ \ +}; + +// Streams spec, 3.2.3., steps 1-4. +ReadableStream* +ReadableStream::createStream(JSContext* cx) +{ + Rooted stream(cx, NewBuiltinClassInstance(cx)); + if (!stream) + return nullptr; + + // Step 1 (reordered): Set this.[[state]] to "readable". + // Step 2: Set this.[[reader]] and this.[[storedError]] to undefined (implicit). + // Step 3: Set this.[[disturbed]] to false (implicit). + // Step 4: Set this.[[readableStreamController]] to undefined (implicit). + stream->setFixedSlot(StreamSlot_State, Int32Value(Readable)); + + return stream; +} + +static MOZ_MUST_USE ReadableStreamDefaultController* +CreateReadableStreamDefaultController(JSContext* cx, Handle stream, + HandleValue underlyingSource, HandleValue size, + HandleValue highWaterMarkVal); + +// Streams spec, 3.2.3., steps 1-4, 8. +ReadableStream* +ReadableStream::createDefaultStream(JSContext* cx, HandleValue underlyingSource, + HandleValue size, HandleValue highWaterMark) +{ + + Rooted stream(cx, createStream(cx)); + if (!stream) + return nullptr; + + // Step b: Set this.[[readableStreamController]] to + // ? Construct(ReadableStreamDefaultController, + // « this, underlyingSource, size, + // highWaterMark »). + RootedObject controller(cx, CreateReadableStreamDefaultController(cx, stream, + underlyingSource, + size, + highWaterMark)); + if (!controller) + return nullptr; + + stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller)); + + return stream; +} + +static MOZ_MUST_USE ReadableByteStreamController* +CreateReadableByteStreamController(JSContext* cx, Handle stream, + HandleValue underlyingByteSource, + HandleValue highWaterMarkVal); + +// Streams spec, 3.2.3., steps 1-4, 7. +ReadableStream* +ReadableStream::createByteStream(JSContext* cx, HandleValue underlyingSource, + HandleValue highWaterMark) +{ + + Rooted stream(cx, createStream(cx)); + if (!stream) + return nullptr; + + // Step b: Set this.[[readableStreamController]] to + // ? Construct(ReadableByteStreamController, « this, underlyingSource, + // highWaterMark »). + RootedObject controller(cx, CreateReadableByteStreamController(cx, stream, + underlyingSource, + highWaterMark)); + if (!controller) + return nullptr; + + stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller)); + + return stream; +} + +// Streams spec, 3.2.3. +bool +ReadableStream::constructor(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + RootedValue val(cx, args.get(0)); + RootedValue underlyingSource(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + // Do argument handling first to keep the right order of error reporting. + if (underlyingSource.isUndefined()) { + RootedObject sourceObj(cx, NewBuiltinClassInstance(cx)); + if (!sourceObj) + return false; + underlyingSource = ObjectValue(*sourceObj); + } + RootedValue size(cx); + RootedValue highWaterMark(cx); + + if (!options.isUndefined()) { + if (!GetProperty(cx, options, cx->names().size, &size)) + return false; + + if (!GetProperty(cx, options, cx->names().highWaterMark, &highWaterMark)) + return false; + } + + if (!ThrowIfNotConstructing(cx, args, "ReadableStream")) + return false; + + // Step 5: Let type be ? GetV(underlyingSource, "type"). + RootedValue typeVal(cx); + if (!GetProperty(cx, underlyingSource, cx->names().type, &typeVal)) + return false; + + // Step 6: Let typeString be ? ToString(type). + RootedString type(cx, ToString(cx, typeVal)); + if (!type) + return false; + + int32_t notByteStream; + if (!CompareStrings(cx, type, cx->names().bytes, ¬ByteStream)) + return false; + + // Step 7 & 8.a (reordered): If highWaterMark is undefined, let + // highWaterMark be 1 (or 0 for byte streams). + if (highWaterMark.isUndefined()) + highWaterMark = Int32Value(notByteStream ? 1 : 0); + + Rooted stream(cx); + + // Step 7: If typeString is "bytes", + if (!notByteStream) { + stream = createByteStream(cx, underlyingSource, highWaterMark); + } else if (typeVal.isUndefined()) { + // Step 8: Otherwise, if type is undefined, + stream = createDefaultStream(cx, underlyingSource, size, highWaterMark); + } else { + // Step 9: Otherwise, throw a RangeError exception. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG); + return false; + } + if (!stream) + return false; + + args.rval().setObject(*stream); + return true; +} + +static MOZ_ALWAYS_INLINE bool +IsReadableStreamLocked(ReadableStream* stream); + +// Streams spec, 3.2.4.1. get locked +static MOZ_MUST_USE bool +ReadableStream_locked_impl(JSContext* cx, const CallArgs& args) +{ + Rooted stream(cx, &args.thisv().toObject().as()); + + // Step 2: Return ! IsReadableStreamLocked(this). + args.rval().setBoolean(IsReadableStreamLocked(stream)); + return true; +} + +static bool +ReadableStream_locked(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, ReadableStream_locked_impl>(cx, args); +} + +static MOZ_MUST_USE JSObject* +ReadableStreamCancel(JSContext* cx, Handle stream, HandleValue reason); + +// Streams spec, 3.2.4.2. cancel ( reason ) +static MOZ_MUST_USE bool +ReadableStream_cancel(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + // Step 1: If ! IsReadableStream(this) is false, return a promise rejected + // with a TypeError exception. + if (!Is(args.thisv())) { + ReportValueError3(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(), + nullptr, "cancel", ""); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + Rooted stream(cx, &args.thisv().toObject().as()); + + // Step 2: If ! IsReadableStreamLocked(this) is true, return a promise + // rejected with a TypeError exception. + if (IsReadableStreamLocked(stream)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_NOT_LOCKED, "cancel"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: Return ! ReadableStreamCancel(this, reason). + RootedObject cancelPromise(cx, ReadableStreamCancel(cx, stream, args.get(0))); + if (!cancelPromise) + return false; + args.rval().setObject(*cancelPromise); + return true; +} + +static MOZ_MUST_USE ReadableStreamDefaultReader* +CreateReadableStreamDefaultReader(JSContext* cx, Handle stream); + +static MOZ_MUST_USE ReadableStreamBYOBReader* +CreateReadableStreamBYOBReader(JSContext* cx, Handle stream); + +// Streams spec, 3.2.4.3. getReader() +static MOZ_MUST_USE bool +ReadableStream_getReader_impl(JSContext* cx, const CallArgs& args) +{ + Rooted stream(cx, &args.thisv().toObject().as()); + RootedObject reader(cx); + + // Step 2: If mode is undefined, return + // ? AcquireReadableStreamDefaultReader(this). + RootedValue modeVal(cx); + HandleValue optionsVal = args.get(0); + if (!optionsVal.isUndefined()) { + if (!GetProperty(cx, optionsVal, cx->names().mode, &modeVal)) + return false; + } + + if (modeVal.isUndefined()) { + reader = CreateReadableStreamDefaultReader(cx, stream); + } else { + // Step 3: Set mode to ? ToString(mode) (implicit). + RootedString mode(cx, ToString(cx, modeVal)); + if (!mode) + return false; + + // Step 4: If mode is "byob", return ? AcquireReadableStreamBYOBReader(this). + int32_t notByob; + if (!CompareStrings(cx, mode, cx->names().byob, ¬Byob)) + return false; + if (notByob) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_INVALID_READER_MODE); + // Step 5: Throw a RangeError exception. + return false; + + } + reader = CreateReadableStreamBYOBReader(cx, stream); + } + + // Reordered second part of steps 2 and 4. + if (!reader) + return false; + args.rval().setObject(*reader); + return true; +} + +static bool +ReadableStream_getReader(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, ReadableStream_getReader_impl>(cx, args); +} + +// Streams spec, 3.2.4.4. pipeThrough({ writable, readable }, options) +static MOZ_MUST_USE 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 »). + + // // Step 2: Return readable. + // return readable; +} + +// Streams spec, 3.2.4.5. pipeTo(dest, { preventClose, preventAbort, preventCancel } = {}) +// TODO: Unimplemented since spec is not complete yet. +static MOZ_MUST_USE bool +ReadableStream_pipeTo(JSContext* cx, unsigned argc, Value* vp) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, "pipeTo"); + return false; +} + +static MOZ_MUST_USE bool +ReadableStreamTee(JSContext* cx, Handle stream, bool cloneForBranch2, + MutableHandle branch1, MutableHandle branch2); + +// Streams spec, 3.2.4.6. tee() +static MOZ_MUST_USE bool +ReadableStream_tee_impl(JSContext* cx, const CallArgs& args) +{ + Rooted stream(cx, &args.thisv().toObject().as()); + + // Step 2: Let branches be ? ReadableStreamTee(this, false). + Rooted branch1(cx); + Rooted branch2(cx); + if (!ReadableStreamTee(cx, stream, false, &branch1, &branch2)) + return false; + + // Step 3: Return ! CreateArrayFromList(branches). + RootedNativeObject branches(cx, NewDenseFullyAllocatedArray(cx, 2)); + if (!branches) + return false; + branches->setDenseInitializedLength(2); + branches->initDenseElement(0, ObjectValue(*branch1)); + branches->initDenseElement(1, ObjectValue(*branch2)); + + args.rval().setObject(*branches); + return true; +} + +static bool +ReadableStream_tee(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, ReadableStream_tee_impl>(cx, args); +} + +static const JSFunctionSpec ReadableStream_methods[] = { + JS_FN("cancel", ReadableStream_cancel, 1, 0), + JS_FN("getReader", ReadableStream_getReader, 0, 0), + JS_FN("pipeThrough", ReadableStream_pipeThrough, 2, 0), + JS_FN("pipeTo", ReadableStream_pipeTo, 1, 0), + JS_FN("tee", ReadableStream_tee, 0, 0), + JS_FS_END +}; + +static const JSPropertySpec ReadableStream_properties[] = { + JS_PSG("locked", ReadableStream_locked, 0), + JS_PS_END +}; + +CLASS_SPEC(ReadableStream, 0, StreamSlotCount, 0); + +// Streams spec, 3.3.1. AcquireReadableStreamBYOBReader ( stream ) +// Always inlined. + +// Streams spec, 3.3.2. AcquireReadableStreamDefaultReader ( stream ) +// Always inlined. + +// Streams spec, 3.3.3. IsReadableStream ( x ) +// Using is instead. + +// Streams spec, 3.3.4. IsReadableStreamDisturbed ( stream ) +static MOZ_ALWAYS_INLINE bool +IsReadableStreamDisturbed(ReadableStream* stream) +{ + // Step 1: Assert: ! IsReadableStream(stream) is true (implicit). + // Step 2: Return stream.[[disturbed]]. + return stream->disturbed(); +} + +// Streams spec, 3.3.5. IsReadableStreamLocked ( stream ) +static MOZ_ALWAYS_INLINE bool +IsReadableStreamLocked(ReadableStream* stream) +{ + // Step 1: Assert: ! IsReadableStream(stream) is true (implicit). + // Step 2: If stream.[[reader]] is undefined, return false. + // Step 3: Return true. + return !stream->getFixedSlot(StreamSlot_Reader).isUndefined(); +} + +static MOZ_MUST_USE bool +ReadableStreamDefaultControllerClose(JSContext* cx, + Handle controller); +static MOZ_MUST_USE bool +ReadableStreamDefaultControllerEnqueue(JSContext* cx, + Handle controller, + HandleValue chunk); + +static bool +TeeReaderReadHandler(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + Rooted teeState(cx, TargetFromHandler(args.callee())); + HandleValue resultVal = args.get(0); + + // Step a: Assert: Type(result) is Object. + RootedObject result(cx, &resultVal.toObject()); + + // Step b: Let value be ? Get(result, "value"). + RootedValue value(cx); + if (!GetPropertyPure(cx, result, NameToId(cx->names().value), value.address())) + return false; + + // Step c: Let done be ? Get(result, "done"). + RootedValue doneVal(cx); + if (!GetPropertyPure(cx, result, NameToId(cx->names().done), doneVal.address())) + return false; + + // Step d: Assert: Type(done) is Boolean. + bool done = doneVal.toBoolean(); + + // Step e: If done is true and teeState.[[closedOrErrored]] is false, + if (done && !teeState->closedOrErrored()) { + // Step i: If teeState.[[canceled1]] is false, + if (!teeState->canceled1()) { + // Step 1: Perform ! ReadableStreamDefaultControllerClose(branch1). + Rooted branch1(cx, teeState->branch1()); + if (!ReadableStreamDefaultControllerClose(cx, branch1)) + return false; + } + + // Step ii: If teeState.[[canceled2]] is false, + if (!teeState->canceled2()) { + // Step 1: Perform ! ReadableStreamDefaultControllerClose(branch1). + Rooted branch2(cx, teeState->branch2()); + if (!ReadableStreamDefaultControllerClose(cx, branch2)) + return false; + } + + // Step iii: Set teeState.[[closedOrErrored]] to true. + teeState->setClosedOrErrored(); + } + + // Step f: If teeState.[[closedOrErrored]] is true, return. + if (teeState->closedOrErrored()) + return true; + + // Step g: Let value1 and value2 be value. + RootedValue value1(cx, value); + RootedValue value2(cx, value); + + // Step h: If teeState.[[canceled2]] is false and cloneForBranch2 is + // true, set value2 to + // ? StructuredDeserialize(StructuredSerialize(value2), + // the current Realm Record). + // TODO: add StructuredClone() intrinsic. + MOZ_ASSERT(!teeState->cloneForBranch2(), "tee(cloneForBranch2=true) should not be exposed"); + + // Step i: If teeState.[[canceled1]] is false, perform + // ? ReadableStreamDefaultControllerEnqueue(branch1, value1). + Rooted controller(cx); + if (!teeState->canceled1()) { + controller = teeState->branch1(); + if (!ReadableStreamDefaultControllerEnqueue(cx, controller, value1)) + return false; + } + + // Step j: If teeState.[[canceled2]] is false, + // perform ? ReadableStreamDefaultControllerEnqueue(branch2, value2). + if (!teeState->canceled2()) { + controller = teeState->branch2(); + if (!ReadableStreamDefaultControllerEnqueue(cx, controller, value2)) + return false; + } + + args.rval().setUndefined(); + return true; +} + +static MOZ_MUST_USE JSObject* +ReadableStreamDefaultReaderRead(JSContext* cx, HandleNativeObject reader); + +static MOZ_MUST_USE JSObject* +ReadableStreamTee_Pull(JSContext* cx, Handle teeState, + Handle branchStream) +{ + // Step 1: Let reader be F.[[reader]], branch1 be F.[[branch1]], + // branch2 be F.[[branch2]], teeState be F.[[teeState]], and + // cloneForBranch2 be F.[[cloneForBranch2]]. + + // Step 2: Return the result of transforming + // ! ReadableStreamDefaultReaderRead(reader) by a fulfillment + // handler which takes the argument result and performs the + // following steps: + Rooted reader(cx, teeState->reader()); + RootedObject readPromise(cx, ReadableStreamDefaultReaderRead(cx, reader)); + if (!readPromise) + return nullptr; + + RootedObject onFulfilled(cx, NewHandler(cx, TeeReaderReadHandler, teeState)); + if (!onFulfilled) + return nullptr; + + return JS::CallOriginalPromiseThen(cx, readPromise, onFulfilled, nullptr); +} + +static MOZ_MUST_USE JSObject* +ReadableStreamTee_Cancel(JSContext* cx, Handle teeState, + Handle branch, HandleValue reason) +{ + // Step 1: Let stream be F.[[stream]] and teeState be F.[[teeState]]. + Rooted stream(cx, teeState->stream()); + + bool bothBranchesCanceled = false; + + // Step 2: Set teeState.[[canceled1]] to true. + // Step 3: Set teeState.[[reason1]] to reason. + if (ControllerFlags(branch) & ControllerFlag_TeeBranch1) { + teeState->setCanceled1(reason); + bothBranchesCanceled = teeState->canceled2(); + } else { + MOZ_ASSERT(ControllerFlags(branch) & ControllerFlag_TeeBranch2); + teeState->setCanceled2(reason); + bothBranchesCanceled = teeState->canceled1(); + } + + // Step 4: If teeState.[[canceled1]] is true, + // Step 4: If teeState.[[canceled2]] is true, + if (bothBranchesCanceled) { + // Step a: Let compositeReason be + // ! CreateArrayFromList(« teeState.[[reason1]], teeState.[[reason2]] »). + RootedNativeObject compositeReason(cx, NewDenseFullyAllocatedArray(cx, 2)); + if (!compositeReason) + return nullptr; + + compositeReason->setDenseInitializedLength(2); + compositeReason->initDenseElement(0, teeState->reason1()); + compositeReason->initDenseElement(1, teeState->reason2()); + RootedValue compositeReasonVal(cx, ObjectValue(*compositeReason)); + + Rooted promise(cx, teeState->promise()); + + // Step b: Let cancelResult be ! ReadableStreamCancel(stream, compositeReason). + RootedObject cancelResult(cx, ReadableStreamCancel(cx, stream, compositeReasonVal)); + if (!cancelResult) { + if (!RejectWithPendingError(cx, promise)) + return nullptr; + } else { + // Step c: Resolve teeState.[[promise]] with cancelResult. + RootedValue resultVal(cx, ObjectValue(*cancelResult)); + if (!PromiseObject::resolve(cx, promise, resultVal)) + return nullptr; + } + } + + // Step 5: Return teeState.[[promise]]. + return teeState->promise(); +} + +static MOZ_MUST_USE bool +ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, HandleValue e); + +// Streams spec, 3.3.6. step 21: +// Upon rejection of reader.[[closedPromise]] with reason r, +static bool +TeeReaderClosedHandler(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + Rooted teeState(cx, TargetFromHandler(args.callee())); + HandleValue reason = args.get(0); + + // Step a: If teeState.[[closedOrErrored]] is false, then: + if (!teeState->closedOrErrored()) { + // Step a.i: Perform ! ReadableStreamDefaultControllerError(pull.[[branch1]], r). + Rooted branch1(cx, teeState->branch1()); + if (!ReadableStreamControllerError(cx, branch1, reason)) + return false; + + // Step a.ii: Perform ! ReadableStreamDefaultControllerError(pull.[[branch2]], r). + Rooted branch2(cx, teeState->branch2()); + if (!ReadableStreamControllerError(cx, branch2, reason)) + return false; + + // Step a.iii: Set teeState.[[closedOrErrored]] to true. + teeState->setClosedOrErrored(); + } + + return true; +} + +// Streams spec, 3.3.6. ReadableStreamTee ( stream, cloneForBranch2 ) +static MOZ_MUST_USE bool +ReadableStreamTee(JSContext* cx, Handle stream, bool cloneForBranch2, + MutableHandle branch1Stream, + MutableHandle branch2Stream) +{ + // Step 1: Assert: ! IsReadableStream(stream) is true (implicit). + // Step 2: Assert: Type(cloneForBranch2) is Boolean (implicit). + + // Step 3: Let reader be ? AcquireReadableStreamDefaultReader(stream). + Rooted reader(cx, CreateReadableStreamDefaultReader(cx, stream)); + if (!reader) + return false; + + // Step 4: Let teeState be Record {[[closedOrErrored]]: false, + // [[canceled1]]: false, + // [[canceled2]]: false, + // [[reason1]]: undefined, + // [[reason2]]: undefined, + // [[promise]]: a new promise}. + Rooted teeState(cx, TeeState::create(cx, stream)); + if (!teeState) + return false; + + // Steps 5-10 omitted because our implementation works differently. + + // Step 5: Let pull be a new ReadableStreamTee pull function. + // Step 6: Set pull.[[reader]] to reader, pull.[[teeState]] to teeState, and + // pull.[[cloneForBranch2]] to cloneForBranch2. + // Step 7: Let cancel1 be a new ReadableStreamTee branch 1 cancel function. + // Step 8: Set cancel1.[[stream]] to stream and cancel1.[[teeState]] to + // teeState. + + // Step 9: Let cancel2 be a new ReadableStreamTee branch 2 cancel function. + // Step 10: Set cancel2.[[stream]] to stream and cancel2.[[teeState]] to + // teeState. + + // Step 11: Let underlyingSource1 be ! ObjectCreate(%ObjectPrototype%). + // Step 12: Perform ! CreateDataProperty(underlyingSource1, "pull", pull). + // Step 13: Perform ! CreateDataProperty(underlyingSource1, "cancel", cancel1). + + // Step 14: Let branch1Stream be ! Construct(ReadableStream, underlyingSource1). + RootedValue hwmValue(cx, NumberValue(1)); + RootedValue underlyingSource(cx, ObjectValue(*teeState)); + branch1Stream.set(ReadableStream::createDefaultStream(cx, underlyingSource, + UndefinedHandleValue, + hwmValue)); + if (!branch1Stream) + return false; + + Rooted branch1(cx); + branch1 = &ControllerFromStream(branch1Stream)->as(); + AddControllerFlags(branch1, ControllerFlag_TeeBranch | ControllerFlag_TeeBranch1); + teeState->setBranch1(branch1); + + // Step 15: Let underlyingSource2 be ! ObjectCreate(%ObjectPrototype%). + // Step 16: Perform ! CreateDataProperty(underlyingSource2, "pull", pull). + // Step 17: Perform ! CreateDataProperty(underlyingSource2, "cancel", cancel2). + + // Step 18: Let branch2Stream be ! Construct(ReadableStream, underlyingSource2). + branch2Stream.set(ReadableStream::createDefaultStream(cx, underlyingSource, + UndefinedHandleValue, + hwmValue)); + if (!branch2Stream) + return false; + + Rooted branch2(cx); + branch2 = &ControllerFromStream(branch2Stream)->as(); + AddControllerFlags(branch2, ControllerFlag_TeeBranch | ControllerFlag_TeeBranch2); + teeState->setBranch2(branch2); + + // Step 19: Set pull.[[branch1]] to branch1Stream.[[readableStreamController]]. + // Step 20: Set pull.[[branch2]] to branch2Stream.[[readableStreamController]]. + + // Step 21: Upon rejection of reader.[[closedPromise]] with reason r, + RootedObject closedPromise(cx, &reader->getFixedSlot(ReaderSlot_ClosedPromise).toObject()); + + RootedObject onRejected(cx, NewHandler(cx, TeeReaderClosedHandler, teeState)); + if (!onRejected) + return false; + + if (!JS::AddPromiseReactions(cx, closedPromise, nullptr, onRejected)) + return false; + + // Step 22: Return « branch1, branch2 ». + return true; +} + +// Streams spec, 3.4.1. ReadableStreamAddReadIntoRequest ( stream ) +static MOZ_MUST_USE PromiseObject* +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()); + + // Step 2: MOZ_ASSERT: stream.[[state]] is "readable" or "closed". + MOZ_ASSERT(stream->readable() || stream->closed()); + + // Step 3: Let promise be a new promise. + Rooted promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) + return nullptr; + + // 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()); + // Since [[promise]] is the Record's only field, we store it directly. + val = ObjectValue(*promise); + if (!AppendToList(cx, readIntoRequests, val)) + return nullptr; + + // Step 6: Return promise. + return promise; +} + +// Streams spec, 3.4.2. ReadableStreamAddReadRequest ( stream ) +static MOZ_MUST_USE PromiseObject* +ReadableStreamAddReadRequest(JSContext* cx, Handle stream) +{ + MOZ_ASSERT(stream->is()); + + // Step 1: Assert: ! IsReadableStreamDefaultReader(stream.[[reader]]) is true. + RootedNativeObject reader(cx, ReaderFromStream(stream)); + + // Step 2: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(stream->readable()); + + // Step 3: Let promise be a new promise. + Rooted promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) + return nullptr; + + // 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()); + + // Since [[promise]] is the Record's only field, we store it directly. + val = ObjectValue(*promise); + if (!AppendToList(cx, readRequests, val)) + return nullptr; + + // Step 6: Return promise. + return promise; +} + +static MOZ_MUST_USE bool +ReadableStreamClose(JSContext* cx, Handle stream); + +static MOZ_MUST_USE JSObject* +ReadableStreamControllerCancelSteps(JSContext* cx, + HandleNativeObject controller, HandleValue reason); + +// Used for transforming the result of promise fulfillment/rejection. +static bool +ReturnUndefined(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + return true; +} + +// Streams spec, 3.4.3. ReadableStreamCancel ( stream, reason ) +static MOZ_MUST_USE JSObject* +ReadableStreamCancel(JSContext* cx, Handle stream, HandleValue reason) +{ + // Step 1: Set stream.[[disturbed]] to true. + uint32_t state = StreamState(stream) | ReadableStream::Disturbed; + SetStreamState(stream, state); + + // Step 2: If stream.[[state]] is "closed", return a new promise resolved + // with undefined. + if (stream->closed()) + return PromiseObject::unforgeableResolve(cx, UndefinedHandleValue); + + // Step 3: If stream.[[state]] is "errored", return a new promise rejected + // with stream.[[storedError]]. + if (stream->errored()) { + RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError)); + return PromiseObject::unforgeableReject(cx, storedError); + } + + // Step 4: Perform ! ReadableStreamClose(stream). + if (!ReadableStreamClose(cx, stream)) + return nullptr; + + // Step 5: Let sourceCancelPromise be + // ! stream.[[readableStreamController]].[[CancelSteps]](reason). + RootedNativeObject controller(cx, ControllerFromStream(stream)); + RootedObject sourceCancelPromise(cx); + sourceCancelPromise = ReadableStreamControllerCancelSteps(cx, controller, reason); + if (!sourceCancelPromise) + return nullptr; + + // Step 6: Return the result of transforming sourceCancelPromise by a + // fulfillment handler that returns undefined. + RootedAtom funName(cx, cx->names().empty); + RootedFunction returnUndefined(cx, + NewNativeFunction(cx, ReturnUndefined, 0, funName)); + if (!returnUndefined) + return nullptr; + return JS::CallOriginalPromiseThen(cx, sourceCancelPromise, returnUndefined, nullptr); +} + +// Streams spec, 3.4.4. ReadableStreamClose ( stream ) +static MOZ_MUST_USE bool +ReadableStreamClose(JSContext* cx, Handle stream) +{ + // Step 1: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(stream->readable()); + + uint32_t state = StreamState(stream); + // Step 2: Set stream.[[state]] to "closed". + SetStreamState(stream, (state & ReadableStream::Disturbed) | ReadableStream::Closed); + + // Step 3: Let reader be stream.[[reader]]. + RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader)); + + // Step 4: If reader is undefined, return. + if (val.isUndefined()) + return true; + + // Step 5: If ! IsReadableStreamDefaultReader(reader) is true, + RootedNativeObject reader(cx, &val.toObject().as()); + 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()); + uint32_t len = readRequests->getDenseInitializedLength(); + RootedObject readRequest(cx); + RootedObject resultObj(cx); + RootedValue resultVal(cx); + for (uint32_t i = 0; i < len; i++) { + // Step i: Resolve readRequest.[[promise]] with + // ! CreateIterResultObject(undefined, true). + readRequest = &readRequests->getDenseElement(i).toObject(); + resultObj = CreateIterResultObject(cx, UndefinedHandleValue, true); + if (!resultObj) + return false; + resultVal = ObjectValue(*resultObj); + if (!ResolvePromise(cx, readRequest, resultVal)) + return false; + } + + // Step b: Set reader.[[readRequests]] to an empty List. + reader->setFixedSlot(ReaderSlot_Requests, UndefinedValue()); + } + } + + // Step 6: Resolve reader.[[closedPromise]] with undefined. + // Step 7: Return (implicit). + RootedObject closedPromise(cx, &reader->getFixedSlot(ReaderSlot_ClosedPromise).toObject()); + return ResolvePromise(cx, closedPromise, UndefinedHandleValue); +} + +// Streams spec, 3.4.5. ReadableStreamError ( stream, e ) +static MOZ_MUST_USE bool +ReadableStreamError(JSContext* cx, Handle stream, HandleValue e) +{ + // Step 1: Assert: ! IsReadableStream(stream) is true (implicit). + + // Step 2: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(stream->readable()); + + // Step 3: Set stream.[[state]] to "errored". + uint32_t state = StreamState(stream); + SetStreamState(stream, (state & ReadableStream::Disturbed) | ReadableStream::Errored); + + // Step 4: Set stream.[[storedError]] to e. + stream->setFixedSlot(StreamSlot_StoredError, e); + + // Step 5: Let reader be stream.[[reader]]. + RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader)); + + // Step 6: If reader is undefined, return. + if (val.isUndefined()) + return true; + RootedNativeObject reader(cx, &val.toObject().as()); + + // 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); + 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)) + return false; + } + + // Step b: Set reader.[[readRequests]] to a new empty List. + if (!SetNewList(cx, reader, ReaderSlot_Requests)) + return false; + + // Step 9: Reject reader.[[closedPromise]] with e. + val = reader->getFixedSlot(ReaderSlot_ClosedPromise); + Rooted closedPromise(cx, &val.toObject().as()); + return PromiseObject::reject(cx, closedPromise, e); +} + +// Streams spec, 3.4.6. ReadableStreamFulfillReadIntoRequest( stream, chunk, done ) +// Streams spec, 3.4.7. ReadableStreamFulfillReadRequest ( stream, chunk, done ) +// These two spec functions are identical in our implementation. +static MOZ_MUST_USE bool +ReadableStreamFulfillReadOrReadIntoRequest(JSContext* cx, Handle stream, + HandleValue chunk, bool done) +{ + // Step 1: Let reader be stream.[[reader]]. + RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader)); + RootedNativeObject reader(cx, &val.toObject().as()); + + // Step 2: Let readIntoRequest be the first element of + // reader.[[readIntoRequests]]. + // Step 3: Remove readIntoRequest from reader.[[readIntoRequests]], shifting + // all other elements downward (so that the second becomes the first, + // and so on). + val = reader->getFixedSlot(ReaderSlot_Requests); + RootedNativeObject readIntoRequests(cx, &val.toObject().as()); + Rooted readIntoRequest(cx); + readIntoRequest = ShiftFromList(cx, readIntoRequests); + MOZ_ASSERT(readIntoRequest); + + // Step 4: Resolve readIntoRequest.[[promise]] with + // ! CreateIterResultObject(chunk, done). + RootedObject iterResult(cx, CreateIterResultObject(cx, chunk, done)); + if (!iterResult) + return false; + val = ObjectValue(*iterResult); + return PromiseObject::resolve(cx, readIntoRequest, val); +} + +// Streams spec, 3.4.8. ReadableStreamGetNumReadIntoRequests ( stream ) +// Streams spec, 3.4.9. ReadableStreamGetNumReadRequests ( stream ) +// (Identical implementation.) +static uint32_t +ReadableStreamGetNumReadRequests(NativeObject* stream) +{ + MOZ_ASSERT(stream->is()); + + // Step 1: Return the number of elements in + // stream.[[reader]].[[readRequests]]. + Value readerVal = stream->getFixedSlot(StreamSlot_Reader); + NativeObject* reader = &readerVal.toObject().as(); + MOZ_ASSERT(reader->is() || + reader->is()); + Value readRequests = reader->getFixedSlot(ReaderSlot_Requests); + return readRequests.toObject().as().getDenseInitializedLength(); +} + +// Stream spec 3.4.10. ReadableStreamHasBYOBReader ( stream ) +static MOZ_MUST_USE bool +ReadableStreamHasBYOBReader(ReadableStream* stream) +{ + // Step 1: Let reader be stream.[[reader]]. + // Step 2: If reader is undefined, return false. + // 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(); +} + +// Streap spec 3.4.11. ReadableStreamHasDefaultReader ( stream ) +static MOZ_MUST_USE bool +ReadableStreamHasDefaultReader(ReadableStream* stream) +{ + // Step 1: Let reader be stream.[[reader]]. + // Step 2: If reader is undefined, return false. + // 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(); +} + +static MOZ_MUST_USE bool +ReadableStreamReaderGenericInitialize(JSContext* cx, + HandleNativeObject reader, + Handle stream); + +// Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream ) +// Steps 2-4. +static MOZ_MUST_USE ReadableStreamDefaultReader* +CreateReadableStreamDefaultReader(JSContext* cx, Handle stream) +{ + Rooted reader(cx); + reader = NewBuiltinClassInstance(cx); + if (!reader) + return nullptr; + + // Step 2: If ! IsReadableStreamLocked(stream) is true, throw a TypeError + // exception. + if (IsReadableStreamLocked(stream)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_LOCKED); + return nullptr; + } + + // Step 3: Perform ! ReadableStreamReaderGenericInitialize(this, stream). + if (!ReadableStreamReaderGenericInitialize(cx, reader, stream)) + return nullptr; + + // Step 4: Set this.[[readRequests]] to a new empty List. + if (!SetNewList(cx, reader, ReaderSlot_Requests)) + return nullptr; + + return reader; +} + +// Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream ) +bool +ReadableStreamDefaultReader::constructor(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "ReadableStreamDefaultReader")) + return false; + + // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception. + if (!Is(args.get(0))) { + 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; + + args.rval().setObject(*reader); + return true; +} + +// Streams spec, 3.5.4.1 get closed +static MOZ_MUST_USE 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", "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) +{ + 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"); + + // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise + // rejected with a TypeError exception. + RootedNativeObject reader(cx, &args.thisv().toObject().as()); + if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMREADER_NOT_OWNED, "cancel"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: Return ! ReadableStreamReaderGenericCancel(this, reason). + JSObject* cancelPromise = ReadableStreamReaderGenericCancel(cx, reader, args.get(0)); + if (!cancelPromise) + return false; + args.rval().setObject(*cancelPromise); + return true; +} + +// Streams spec, 3.5.4.3 read ( ) +static MOZ_MUST_USE 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())) + return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "read"); + + // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise + // rejected with a TypeError exception. + RootedNativeObject reader(cx, &args.thisv().toObject().as()); + if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMREADER_NOT_OWNED, "read"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: Return ! ReadableStreamDefaultReaderRead(this). + JSObject* readPromise = ReadableStreamDefaultReaderRead(cx, reader); + if (!readPromise) + return false; + args.rval().setObject(*readPromise); + return true; +} + +static MOZ_MUST_USE bool +ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader); + +// Streams spec, 3.5.4.4. releaseLock ( ) +static MOZ_MUST_USE bool +ReadableStreamDefaultReader_releaseLock_impl(JSContext* cx, const CallArgs& args) +{ + Rooted reader(cx); + reader = &args.thisv().toObject().as(); + + // Step 2: If this.[[ownerReadableStream]] is undefined, return. + if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + args.rval().setUndefined(); + return true; + } + + // Step 3: If this.[[readRequests]] is not empty, throw a TypeError exception. + Value val = reader->getFixedSlot(ReaderSlot_Requests); + if (!val.isUndefined()) { + NativeObject* readRequests = &val.toObject().as(); + uint32_t len = readRequests->getDenseInitializedLength(); + if (len != 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMREADER_NOT_EMPTY, + "releaseLock"); + return false; + } + } + + // Step 4: Perform ! ReadableStreamReaderGenericRelease(this). + return ReadableStreamReaderGenericRelease(cx, reader); +} + +static bool +ReadableStreamDefaultReader_releaseLock(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStreamDefaultReader(this) is false, + // throw a TypeError exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableStreamDefaultReader_releaseLock_impl>(cx, args); +} + +static const JSFunctionSpec ReadableStreamDefaultReader_methods[] = { + JS_FN("cancel", ReadableStreamDefaultReader_cancel, 1, 0), + JS_FN("read", ReadableStreamDefaultReader_read, 0, 0), + JS_FN("releaseLock", ReadableStreamDefaultReader_releaseLock, 0, 0), + JS_FS_END +}; + +static const JSPropertySpec ReadableStreamDefaultReader_properties[] = { + JS_PSG("closed", ReadableStreamDefaultReader_closed, 0), + JS_PS_END +}; + +CLASS_SPEC(ReadableStreamDefaultReader, 1, ReaderSlotCount, ClassSpec::DontDefineConstructor); + + +// Streams spec, 3.6.3 new ReadableStreamBYOBReader ( stream ) +// Steps 2-5. +static MOZ_MUST_USE ReadableStreamBYOBReader* +CreateReadableStreamBYOBReader(JSContext* cx, Handle stream) +{ + // Step 2: If ! IsReadableByteStreamController(stream.[[readableStreamController]]) + // is false, throw a TypeError exception. + if (!ControllerFromStream(stream)->is()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER); + return nullptr; + } + + // Step 3: If ! IsReadableStreamLocked(stream) is true, throw a TypeError + // exception. + if (IsReadableStreamLocked(stream)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED); + return nullptr; + } + + Rooted reader(cx); + reader = NewBuiltinClassInstance(cx); + if (!reader) + return nullptr; + + // Step 4: Perform ! ReadableStreamReaderGenericInitialize(this, stream). + if (!ReadableStreamReaderGenericInitialize(cx, reader, stream)) + return nullptr; + + // Step 5: Set this.[[readIntoRequests]] to a new empty List. + if (!SetNewList(cx, reader, ReaderSlot_Requests)) + return nullptr; + + return reader; +} + +// Streams spec, 3.6.3 new ReadableStreamBYOBReader ( stream ) +bool +ReadableStreamBYOBReader::constructor(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "ReadableStreamBYOBReader")) + return false; + + // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception. + if (!Is(args.get(0))) { + 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; + + args.rval().setObject(*reader); + return true; +} + +// Streams spec, 3.6.4.1 get closed +static MOZ_MUST_USE bool +ReadableStreamBYOBReader_closed(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsReadableStreamBYOBReader(this) is false, return a promise + // rejected with a TypeError exception. + if (!Is(args.thisv())) + return RejectNonGenericMethod(cx, args, "ReadableStreamBYOBReader", "get closed"); + + // Step 2: Return this.[[closedPromise]]. + NativeObject* reader = &args.thisv().toObject().as(); + args.rval().set(reader->getFixedSlot(ReaderSlot_ClosedPromise)); + return true; +} + +// Streams spec, 3.6.4.2. cancel ( reason ) +static MOZ_MUST_USE bool +ReadableStreamBYOBReader_cancel(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsReadableStreamBYOBReader(this) is false, return a promise + // rejected with a TypeError exception. + if (!Is(args.thisv())) + return RejectNonGenericMethod(cx, args, "ReadableStreamBYOBReader", "cancel"); + + // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise + // rejected with a TypeError exception. + RootedNativeObject reader(cx, &args.thisv().toObject().as()); + if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMREADER_NOT_OWNED, "cancel"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: Return ! ReadableStreamReaderGenericCancel(this, reason). + JSObject* cancelPromise = ReadableStreamReaderGenericCancel(cx, reader, args.get(0)); + if (!cancelPromise) + return false; + args.rval().setObject(*cancelPromise); + return true; +} + +static MOZ_MUST_USE JSObject* +ReadableStreamBYOBReaderRead(JSContext* cx, HandleNativeObject reader, + Handle view); + +// Streams spec, 3.6.4.3 read ( ) +static MOZ_MUST_USE bool +ReadableStreamBYOBReader_read(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue viewVal = args.get(0); + + // Step 1: If ! IsReadableStreamBYOBReader(this) is false, return a promise + // rejected with a TypeError exception. + if (!Is(args.thisv())) + return RejectNonGenericMethod(cx, args, "ReadableStreamBYOBReader", "read"); + + // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise + // rejected with a TypeError exception. + RootedNativeObject reader(cx, &args.thisv().toObject().as()); + if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMREADER_NOT_OWNED, "read"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: If Type(view) is not Object, return a promise rejected with a + // TypeError exception. + // Step 4: If view does not have a [[ViewedArrayBuffer]] internal slot, + // return a promise rejected with a TypeError exception. + if (!Is(viewVal)) { + ReportArgTypeError(cx, "ReadableStreamBYOBReader.read", "Typed Array", viewVal); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + Rooted view(cx, &viewVal.toObject().as()); + + // Step 5: If view.[[ByteLength]] is 0, return a promise rejected with a + // TypeError exception. + // Note: It's ok to use the length in number of elements here because all we + // want to know is whether it's < 0. + if (view->length() == 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMBYOBREADER_READ_EMPTY_VIEW); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 6: Return ! ReadableStreamBYOBReaderRead(this, view). + JSObject* readPromise = ReadableStreamBYOBReaderRead(cx, reader, view); + if (!readPromise) + return false; + args.rval().setObject(*readPromise); + return true; +} + +static MOZ_MUST_USE bool +ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader); + +// Streams spec, 3.6.4.4. releaseLock ( ) +static MOZ_MUST_USE bool +ReadableStreamBYOBReader_releaseLock_impl(JSContext* cx, const CallArgs& args) +{ + Rooted reader(cx); + reader = &args.thisv().toObject().as(); + + // Step 2: If this.[[ownerReadableStream]] is undefined, return. + if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + args.rval().setUndefined(); + return true; + } + + // Step 3: If this.[[readRequests]] is not empty, throw a TypeError exception. + Value val = reader->getFixedSlot(ReaderSlot_Requests); + if (!val.isUndefined()) { + NativeObject* readRequests = &val.toObject().as(); + uint32_t len = readRequests->getDenseInitializedLength(); + if (len != 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMREADER_NOT_EMPTY, "releaseLock"); + return false; + } + } + + // Step 4: Perform ! ReadableStreamReaderGenericRelease(this). + return ReadableStreamReaderGenericRelease(cx, reader); +} + +static bool +ReadableStreamBYOBReader_releaseLock(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStreamBYOBReader(this) is false, + // throw a TypeError exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableStreamBYOBReader_releaseLock_impl>(cx, args); +} + +static const JSPropertySpec ReadableStreamBYOBReader_properties[] = { + JS_PSG("closed", ReadableStreamBYOBReader_closed, 0), + JS_PS_END +}; + +static const JSFunctionSpec ReadableStreamBYOBReader_methods[] = { + JS_FN("cancel", ReadableStreamBYOBReader_cancel, 1, 0), + JS_FN("read", ReadableStreamBYOBReader_read, 1, 0), + JS_FN("releaseLock", ReadableStreamBYOBReader_releaseLock, 0, 0), + JS_FS_END +}; + +CLASS_SPEC(ReadableStreamBYOBReader, 1, 3, ClassSpec::DontDefineConstructor); + +inline static MOZ_MUST_USE bool +ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject controller); + +// Streams spec, 3.7.1. IsReadableStreamDefaultReader ( x ) +// Implemented via intrinsic_isInstanceOfBuiltin() + +// Streams spec, 3.7.2. IsReadableStreamBYOBReader ( x ) +// Implemented via intrinsic_isInstanceOfBuiltin() + +// Streams spec, 3.7.3. ReadableStreamReaderGenericCancel ( reader, reason ) +static MOZ_MUST_USE JSObject* +ReadableStreamReaderGenericCancel(JSContext* cx, HandleNativeObject reader, HandleValue reason) +{ + // Step 1: Let stream be reader.[[ownerReadableStream]]. + Rooted stream(cx, StreamFromReader(reader)); + + // Step 2: Assert: stream is not undefined (implicit). + + // Step 3: Return ! ReadableStreamCancel(stream, reason). + return ReadableStreamCancel(cx, stream, reason); +} + +// Streams spec, 3.7.4. ReadableStreamReaderGenericInitialize ( reader, stream ) +static MOZ_MUST_USE bool +ReadableStreamReaderGenericInitialize(JSContext* cx, HandleNativeObject reader, + Handle stream) +{ + // Step 1: Set reader.[[ownerReadableStream]] to stream. + reader->setFixedSlot(ReaderSlot_Stream, ObjectValue(*stream)); + + // Step 2: Set stream.[[reader]] to reader. + stream->setFixedSlot(StreamSlot_Reader, ObjectValue(*reader)); + + // Step 3: If stream.[[state]] is "readable", + RootedObject promise(cx); + if (stream->readable()) { + // Step a: Set reader.[[closedPromise]] to a new promise. + promise = PromiseObject::createSkippingExecutor(cx); + } else if (stream->closed()) { + // Step 4: Otherwise + // Step a: If stream.[[state]] is "closed", + // Step i: Set reader.[[closedPromise]] to a new promise resolved with + // undefined. + promise = PromiseObject::unforgeableResolve(cx, UndefinedHandleValue); + } else { + // Step b: Otherwise, + // Step i: Assert: stream.[[state]] is "errored". + MOZ_ASSERT(stream->errored()); + + // Step ii: Set reader.[[closedPromise]] to a new promise rejected with + // stream.[[storedError]]. + RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError)); + promise = PromiseObject::unforgeableReject(cx, storedError); + } + + if (!promise) + return false; + + reader->setFixedSlot(ReaderSlot_ClosedPromise, ObjectValue(*promise)); + return true; +} + +// Streams spec, 3.7.5. ReadableStreamReaderGenericRelease ( reader ) +static MOZ_MUST_USE bool +ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader) +{ + // Step 1: Assert: reader.[[ownerReadableStream]] is not undefined. + Rooted stream(cx, StreamFromReader(reader)); + + // Step 2: Assert: reader.[[ownerReadableStream]].[[reader]] is reader. + MOZ_ASSERT(&stream->getFixedSlot(StreamSlot_Reader).toObject() == reader); + + // Create an exception to reject promises with below. We don't have a + // clean way to do this, unfortunately. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAMREADER_RELEASED); + RootedValue exn(cx); + // Not much we can do about uncatchable exceptions, just bail. + if (!GetAndClearException(cx, &exn)) + return false; + + // Step 3: If reader.[[ownerReadableStream]].[[state]] is "readable", reject + // reader.[[closedPromise]] with a TypeError exception. + if (stream->readable()) { + Value val = reader->getFixedSlot(ReaderSlot_ClosedPromise); + Rooted closedPromise(cx, &val.toObject().as()); + if (!PromiseObject::reject(cx, closedPromise, exn)) + return false; + } else { + // Step 4: Otherwise, set reader.[[closedPromise]] to a new promise rejected + // with a TypeError exception. + RootedObject closedPromise(cx, PromiseObject::unforgeableReject(cx, exn)); + if (!closedPromise) + return false; + reader->setFixedSlot(ReaderSlot_ClosedPromise, ObjectValue(*closedPromise)); + } + + // Step 5: Set reader.[[ownerReadableStream]].[[reader]] to undefined. + stream->setFixedSlot(StreamSlot_Reader, UndefinedValue()); + + // Step 6: Set reader.[[ownerReadableStream]] to undefined. + reader->setFixedSlot(ReaderSlot_Stream, UndefinedValue()); + + return true; +} + +static MOZ_MUST_USE JSObject* +ReadableByteStreamControllerPullInto(JSContext* cx, + Handle controller, + HandleNativeObject view); + +// Streams spec, 3.7.6. ReadableStreamBYOBReaderRead ( reader, view ) +static MOZ_MUST_USE JSObject* +ReadableStreamBYOBReaderRead(JSContext* cx, HandleNativeObject reader, + Handle view) +{ + MOZ_ASSERT(reader->is()); + + // Step 1: Let stream be reader.[[ownerReadableStream]]. + // Step 2: Assert: stream is not undefined. + Rooted stream(cx, StreamFromReader(reader)); + + // Step 3: Set stream.[[disturbed]] to true. + SetStreamState(stream, StreamState(stream) | ReadableStream::Disturbed); + + // Step 4: If stream.[[state]] is "errored", return a promise rejected with + // stream.[[storedError]]. + if (stream->errored()) { + RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError)); + return PromiseObject::unforgeableReject(cx, storedError); + } + + // Step 5: Return ! ReadableByteStreamControllerPullInto(stream.[[readableStreamController]], view). + Rooted controller(cx); + controller = &ControllerFromStream(stream)->as(); + return ReadableByteStreamControllerPullInto(cx, controller, view); +} + +static MOZ_MUST_USE JSObject* +ReadableStreamControllerPullSteps(JSContext* cx, HandleNativeObject controller); + +// Streams spec, 3.7.7. ReadableStreamDefaultReaderRead ( reader ) +static MOZ_MUST_USE JSObject* +ReadableStreamDefaultReaderRead(JSContext* cx, HandleNativeObject reader) +{ + MOZ_ASSERT(reader->is()); + + // Step 1: Let stream be reader.[[ownerReadableStream]]. + // Step 2: Assert: stream is not undefined. + Rooted stream(cx, StreamFromReader(reader)); + + // Step 3: Set stream.[[disturbed]] to true. + SetStreamState(stream, StreamState(stream) | ReadableStream::Disturbed); + + // Step 4: If stream.[[state]] is "closed", return a new promise resolved with + // ! CreateIterResultObject(undefined, true). + if (stream->closed()) { + RootedObject iterResult(cx, CreateIterResultObject(cx, UndefinedHandleValue, true)); + if (!iterResult) + return nullptr; + RootedValue iterResultVal(cx, ObjectValue(*iterResult)); + return PromiseObject::unforgeableResolve(cx, iterResultVal); + } + + // Step 5: If stream.[[state]] is "errored", return a new promise rejected with + // stream.[[storedError]]. + if (stream->errored()) { + RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError)); + return PromiseObject::unforgeableReject(cx, storedError); + } + + // Step 6: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(stream->readable()); + + // Step 7: Return ! stream.[[readableStreamController]].[[PullSteps]](). + RootedNativeObject controller(cx, ControllerFromStream(stream)); + return ReadableStreamControllerPullSteps(cx, controller); +} + +// Streams spec, 3.8.3, step 11.a. +// and +// Streams spec, 3.10.3, step 16.a. +static bool +ControllerStartHandler(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + RootedNativeObject controller(cx, TargetFromHandler(args.callee())); + + // Step i: Set controller.[[started]] to true. + AddControllerFlags(controller, ControllerFlag_Started); + + // Step ii: Assert: controller.[[pulling]] is false. + // Step iii: Assert: controller.[[pullAgain]] is false. + MOZ_ASSERT(!(ControllerFlags(controller) & + (ControllerFlag_Pulling | ControllerFlag_PullAgain))); + + // Step iv: Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). + // or + // Step iv: Perform ! ReadableByteStreamControllerCallPullIfNeeded((controller). + if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) + return false; + args.rval().setUndefined(); + return true; +} + +static MOZ_MUST_USE bool +ReadableStreamDefaultControllerErrorIfNeeded(JSContext* cx, + Handle controller, + HandleValue e); + +static MOZ_MUST_USE bool +ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, HandleValue e); + +// Streams spec, 3.8.3, step 11.b. +// and +// Streams spec, 3.10.3, step 16.b. +static bool +ControllerStartFailedHandler(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + RootedNativeObject controllerObj(cx, TargetFromHandler(args.callee())); + + // 3.8.3, Step 11.b.i: + // Perform ! ReadableStreamDefaultControllerErrorIfNeeded(controller, r). + if (controllerObj->is()) { + Rooted controller(cx); + controller = &controllerObj->as(); + return ReadableStreamDefaultControllerErrorIfNeeded(cx, controller, args.get(0)); + } + + // 3.10.3, Step 16.b.i: If stream.[[state]] is "readable", perform + // ! ReadableByteStreamControllerError(controller, r). + if (StreamFromController(controllerObj)->readable()) + return ReadableStreamControllerError(cx, controllerObj, args.get(0)); + + args.rval().setUndefined(); + return true; +} + +static MOZ_MUST_USE bool +ValidateAndNormalizeHighWaterMark(JSContext* cx, + HandleValue highWaterMarkVal, + double* highWaterMark); + +static MOZ_MUST_USE bool +ValidateAndNormalizeQueuingStrategy(JSContext* cx, + HandleValue size, + HandleValue highWaterMarkVal, + double* highWaterMark); + +// Streams spec, 3.8.3 new ReadableStreamDefaultController ( stream, underlyingSource, +// size, highWaterMark ) +// Steps 3 - 11. +static MOZ_MUST_USE ReadableStreamDefaultController* +CreateReadableStreamDefaultController(JSContext* cx, Handle stream, + HandleValue underlyingSource, HandleValue size, + HandleValue highWaterMarkVal) +{ + Rooted controller(cx); + controller = NewBuiltinClassInstance(cx); + if (!controller) + return nullptr; + + // Step 3: Set this.[[controlledReadableStream]] to stream. + controller->setFixedSlot(ControllerSlot_Stream, ObjectValue(*stream)); + + // Step 4: Set this.[[underlyingSource]] to underlyingSource. + controller->setFixedSlot(ControllerSlot_UnderlyingSource, underlyingSource); + + // Step 5: Perform ! ResetQueue(this). + if (!ResetQueue(cx, controller)) + return nullptr; + + // Step 6: Set this.[[started]], this.[[closeRequested]], this.[[pullAgain]], + // and this.[[pulling]] to false. + controller->setFixedSlot(ControllerSlot_Flags, Int32Value(0)); + + // Step 7: Let normalizedStrategy be + // ? ValidateAndNormalizeQueuingStrategy(size, highWaterMark). + double highWaterMark; + if (!ValidateAndNormalizeQueuingStrategy(cx, size, highWaterMarkVal, &highWaterMark)) + return nullptr; + + // Step 8: Set this.[[strategySize]] to normalizedStrategy.[[size]] and + // this.[[strategyHWM]] to normalizedStrategy.[[highWaterMark]]. + controller->setFixedSlot(DefaultControllerSlot_StrategySize, size); + controller->setFixedSlot(ControllerSlot_StrategyHWM, NumberValue(highWaterMark)); + + // Step 9: Let controller be this (implicit). + + // Step 10: Let startResult be + // ? InvokeOrNoop(underlyingSource, "start", « this »). + RootedValue startResult(cx); + RootedValue controllerVal(cx, ObjectValue(*controller)); + if (!InvokeOrNoop(cx, underlyingSource, cx->names().start, controllerVal, &startResult)) + return nullptr; + + // Step 11: Let startPromise be a promise resolved with startResult: + RootedObject startPromise(cx, PromiseObject::unforgeableResolve(cx, startResult)); + if (!startPromise) + return nullptr; + + RootedObject onStartFulfilled(cx, NewHandler(cx, ControllerStartHandler, controller)); + if (!onStartFulfilled) + return nullptr; + + RootedObject onStartRejected(cx, NewHandler(cx, ControllerStartFailedHandler, controller)); + if (!onStartRejected) + return nullptr; + + if (!JS::AddPromiseReactions(cx, startPromise, onStartFulfilled, onStartRejected)) + return nullptr; + + return controller; +} + +// Streams spec, 3.8.3. +// new ReadableStreamDefaultController( stream, underlyingSource, size, +// highWaterMark ) +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 (!stream->getFixedSlot(StreamSlot_Controller).isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_CONTROLLER_SET); + return false; + } + + 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; +} + +static MOZ_MUST_USE double +ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller); + +// Streams spec, 3.8.4.1. get desiredSize +// and +// Streams spec, 3.10.4.2. get desiredSize +static MOZ_MUST_USE bool +ReadableStreamController_desiredSize_impl(JSContext* cx, const CallArgs& args) +{ + RootedNativeObject controller(cx); + controller = &args.thisv().toObject().as(); + + // Streams spec, 3.9.8. steps 1-4. + // 3.9.8. Step 1: Let stream be controller.[[controlledReadableStream]]. + ReadableStream* stream = StreamFromController(controller); + + // 3.9.8. Step 2: Let state be stream.[[state]]. + // 3.9.8. Step 3: If state is "errored", return null. + if (stream->errored()) { + args.rval().setNull(); + return true; + } + + // 3.9.8. Step 4: If state is "closed", return 0. + if (stream->closed()) { + args.rval().setInt32(0); + return true; + } + + // Step 2: Return ! ReadableStreamDefaultControllerGetDesiredSize(this). + args.rval().setNumber(ReadableStreamControllerGetDesiredSizeUnchecked(controller)); + return true; +} + +static bool +ReadableStreamDefaultController_desiredSize(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a + // TypeError exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableStreamController_desiredSize_impl>(cx, args); +} + +static MOZ_MUST_USE bool +ReadableStreamDefaultControllerClose(JSContext* cx, + Handle controller); + +// Streams spec, 3.8.4.2 close() +static MOZ_MUST_USE bool +ReadableStreamDefaultController_close_impl(JSContext* cx, const CallArgs& args) +{ + Rooted controller(cx); + controller = &args.thisv().toObject().as(); + + // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception. + if (ControllerFlags(controller) & ControllerFlag_CloseRequested) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_CLOSED, "close"); + return false; + } + + // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable", + // throw a TypeError exception. + ReadableStream* stream = StreamFromController(controller); + if (!stream->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close"); + return false; + } + + // Step 4: Perform ! ReadableStreamDefaultControllerClose(this). + if (!ReadableStreamDefaultControllerClose(cx, controller)) + return false; + args.rval().setUndefined(); + return true; +} + +static bool +ReadableStreamDefaultController_close(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a + // TypeError exception. + + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableStreamDefaultController_close_impl>(cx, args); +} + +static MOZ_MUST_USE bool +ReadableStreamDefaultControllerEnqueue(JSContext* cx, + Handle controller, + HandleValue chunk); + +// Streams spec, 3.8.4.3. enqueue ( chunk ) +static MOZ_MUST_USE bool +ReadableStreamDefaultController_enqueue_impl(JSContext* cx, const CallArgs& args) +{ + Rooted controller(cx); + controller = &args.thisv().toObject().as(); + + // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception. + if (ControllerFlags(controller) & ControllerFlag_CloseRequested) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_CLOSED, "close"); + return false; + } + + // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable", + // throw a TypeError exception. + ReadableStream* stream = StreamFromController(controller); + if (!stream->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close"); + return false; + } + + // Step 4: Return ! ReadableStreamDefaultControllerEnqueue(this, chunk). + if (!ReadableStreamDefaultControllerEnqueue(cx, controller, args.get(0))) + return false; + args.rval().setUndefined(); + return true; +} + +static bool +ReadableStreamDefaultController_enqueue(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a + // TypeError exception. + + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableStreamDefaultController_enqueue_impl>(cx, args); +} + +// Streams spec, 3.8.4.4. error ( e ) +static MOZ_MUST_USE bool +ReadableStreamDefaultController_error_impl(JSContext* cx, const CallArgs& args) +{ + Rooted controller(cx); + controller = &args.thisv().toObject().as(); + + // Step 2: Let stream be this.[[controlledReadableStream]]. + ReadableStream* stream = StreamFromController(controller); + + // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception. + if (!stream->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close"); + return false; + } + + // Step 4: Perform ! ReadableStreamDefaultControllerError(this, e). + if (!ReadableStreamControllerError(cx, controller, args.get(0))) + return false; + args.rval().setUndefined(); + return true; +} + +static bool +ReadableStreamDefaultController_error(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a + // TypeError exception. + + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableStreamDefaultController_error_impl>(cx, args); +} + +static const JSPropertySpec ReadableStreamDefaultController_properties[] = { + JS_PSG("desiredSize", ReadableStreamDefaultController_desiredSize, 0), + JS_PS_END +}; + +static const JSFunctionSpec ReadableStreamDefaultController_methods[] = { + JS_FN("close", ReadableStreamDefaultController_close, 0, 0), + JS_FN("enqueue", ReadableStreamDefaultController_enqueue, 1, 0), + JS_FN("error", ReadableStreamDefaultController_error, 1, 0), + JS_FS_END +}; + +CLASS_SPEC(ReadableStreamDefaultController, 4, 7, ClassSpec::DontDefineConstructor); + +/** + * Unified implementation of ReadableStream controllers' [[CancelSteps]] internal + * methods. + * Streams spec, 3.8.5.1. [[CancelSteps]] ( reason ) + * and + * Streams spec, 3.10.5.1. [[CancelSteps]] ( reason ) + */ +static MOZ_MUST_USE JSObject* +ReadableStreamControllerCancelSteps(JSContext* cx, HandleNativeObject controller, + HandleValue reason) +{ + MOZ_ASSERT(IsReadableStreamController(controller)); + + // Step 1 of 3.10.5.1: If this.[[pendingPullIntos]] is not empty, + if (!controller->is()) { + Value val = controller->getFixedSlot(ByteControllerSlot_PendingPullIntos); + RootedNativeObject pendingPullIntos(cx, &val.toObject().as()); + + if (pendingPullIntos->getDenseInitializedLength() != 0) { + // Step a: Let firstDescriptor be the first element of + // this.[[pendingPullIntos]]. + // Step b: Set firstDescriptor.[[bytesFilled]] to 0. + Rooted firstDescriptor(cx); + firstDescriptor = PeekList(pendingPullIntos); + firstDescriptor->setBytesFilled(0); + } + } + + // Step 1 of 3.8.5.1, step 2 of 3.10.5.1: Perform ! ResetQueue(this). + if (!ResetQueue(cx, controller)) + return nullptr; + + // Step 2 of 3.8.5.1, step 3 of 3.10.5.1: + // Return ! PromiseInvokeOrNoop(this.[[underlying(Byte)Source]], + // "cancel", « reason ») + RootedValue underlyingSource(cx); + underlyingSource = controller->getFixedSlot(ControllerSlot_UnderlyingSource); + + if (Is(underlyingSource)) { + Rooted teeState(cx, &underlyingSource.toObject().as()); + Rooted defaultController(cx); + defaultController = &controller->as(); + return ReadableStreamTee_Cancel(cx, teeState, defaultController, reason); + } + + return PromiseInvokeOrNoop(cx, underlyingSource, cx->names().cancel, reason); +} + +inline static MOZ_MUST_USE bool +DequeueValue(JSContext* cx, HandleNativeObject container, MutableHandleValue chunk); + +// Streams spec, 3.8.5.2. ReadableStreamDefaultController [[PullSteps]]() +static JSObject* +ReadableStreamDefaultControllerPullSteps(JSContext* cx, HandleNativeObject controller) +{ + MOZ_ASSERT(controller->is()); + + // Step 1: Let stream be this.[[controlledReadableStream]]. + Rooted stream(cx, StreamFromController(controller)); + + // Step 2: If this.[[queue]] is not empty, + RootedNativeObject queue(cx); + RootedValue val(cx, controller->getFixedSlot(QueueContainerSlot_Queue)); + if (val.isObject()) + queue = &val.toObject().as(); + + if (queue && queue->getDenseInitializedLength() != 0) { + // Step a: Let chunk be ! DequeueValue(this.[[queue]]). + RootedValue chunk(cx); + if (!DequeueValue(cx, controller, &chunk)) + return nullptr; + + // Step b: If this.[[closeRequested]] is true and this.[[queue]] is empty, + // perform ! ReadableStreamClose(stream). + bool closeRequested = ControllerFlags(controller) & ControllerFlag_CloseRequested; + if (closeRequested && queue->getDenseInitializedLength() == 0) { + if (!ReadableStreamClose(cx, stream)) + return nullptr; + } + + // Step c: Otherwise, perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this). + else { + if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) + return nullptr; + } + + // Step d: Return a promise resolved with ! CreateIterResultObject(chunk, false). + RootedObject iterResultObj(cx, CreateIterResultObject(cx, chunk, false)); + if (!iterResultObj) + return nullptr; + RootedValue iterResult(cx, ObjectValue(*iterResultObj)); + return PromiseObject::unforgeableResolve(cx, iterResult); + } + + // Step 3: Let pendingPromise be ! ReadableStreamAddReadRequest(stream). + Rooted pendingPromise(cx, ReadableStreamAddReadRequest(cx, stream)); + if (!pendingPromise) + return nullptr; + + // Step 4: Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this). + if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) + return nullptr; + + // Step 5: Return pendingPromise. + return pendingPromise; +} + +// Streams spec, 3.9.2 and 3.12.3. step 7: +// Upon fulfillment of pullPromise, +static bool +ControllerPullHandler(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + RootedNativeObject controller(cx, TargetFromHandler(args.callee())); + uint32_t flags = ControllerFlags(controller); + + // Step a: Set controller.[[pulling]] to false. + // Step b.i: Set controller.[[pullAgain]] to false. + RemoveControllerFlags(controller, ControllerFlag_Pulling | ControllerFlag_PullAgain); + + // Step b: If controller.[[pullAgain]] is true, + if (flags & ControllerFlag_PullAgain) { + // Step ii: Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) + return false; + } + + args.rval().setUndefined(); + return true; +} + +// Streams spec, 3.9.2 and 3.12.3. step 8: +// Upon rejection of pullPromise with reason e, +static bool +ControllerPullFailedHandler(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + RootedNativeObject controller(cx, TargetFromHandler(args.callee())); + HandleValue e = args.get(0); + + // Step a: If controller.[[controlledReadableStream]].[[state]] is "readable", + // perform ! ReadableByteStreamControllerError(controller, e). + if (StreamFromController(controller)->readable()) { + if (!ReadableStreamControllerError(cx, controller, e)) + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool +ReadableStreamControllerShouldCallPull(NativeObject* controller); + +// Streams spec, 3.9.2 ReadableStreamDefaultControllerCallPullIfNeeded ( controller ) +// and +// Streams spec, 3.12.3. ReadableByteStreamControllerCallPullIfNeeded ( controller ) +inline static MOZ_MUST_USE bool +ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject controller) +{ + // Step 1: Let shouldPull be + // ! ReadableByteStreamControllerShouldCallPull(controller). + bool shouldPull = ReadableStreamControllerShouldCallPull(controller); + + // Step 2: If shouldPull is false, return. + if (!shouldPull) + return true; + + // Step 3: If controller.[[pulling]] is true, + if (ControllerFlags(controller) & ControllerFlag_Pulling) { + // Step a: Set controller.[[pullAgain]] to true. + AddControllerFlags(controller, ControllerFlag_PullAgain); + + // Step b: Return. + return true; + } + + // Step 4: Assert: controller.[[pullAgain]] is false. + MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_PullAgain)); + + // Step 5: Set controller.[[pulling]] to true. + AddControllerFlags(controller, ControllerFlag_Pulling); + + // Step 6: Let pullPromise be + // ! PromiseInvokeOrNoop(controller.[[underlyingByteSource]], "pull", controller). + RootedObject pullPromise(cx); + RootedValue underlyingSource(cx); + underlyingSource = controller->getFixedSlot(ControllerSlot_UnderlyingSource); + RootedValue controllerVal(cx, ObjectValue(*controller)); + + if (Is(underlyingSource)) { + Rooted teeState(cx, &underlyingSource.toObject().as()); + Rooted stream(cx, StreamFromController(controller)); + pullPromise = ReadableStreamTee_Pull(cx, teeState, stream); + } else { + pullPromise = PromiseInvokeOrNoop(cx, underlyingSource, cx->names().pull, controllerVal); + } + if (!pullPromise) + return false; + + RootedObject onPullFulfilled(cx, NewHandler(cx, ControllerPullHandler, controller)); + if (!onPullFulfilled) + return false; + + RootedObject onPullRejected(cx, NewHandler(cx, ControllerPullFailedHandler, controller)); + if (!onPullRejected) + return false; + + return JS::AddPromiseReactions(cx, pullPromise, onPullFulfilled, onPullRejected); + + // Steps 7-8 implemented in functions above. +} + +// Streams spec, 3.9.3. ReadableStreamDefaultControllerShouldCallPull ( controller ) +// and +// Streams spec, 3.12.24. ReadableByteStreamControllerShouldCallPull ( controller ) +static bool +ReadableStreamControllerShouldCallPull(NativeObject* controller) +{ + // Step 1: Let stream be controller.[[controlledReadableStream]]. + ReadableStream* stream = StreamFromController(controller); + + // Step 2: If stream.[[state]] is "closed" or stream.[[state]] is "errored", + // return false. + // or, equivalently + // Step 2: If stream.[[state]] is not "readable", return false. + if (!stream->readable()) + return false; + + // Step 3: If controller.[[closeRequested]] is true, return false. + uint32_t flags = ControllerFlags(controller); + if (flags & ControllerFlag_CloseRequested) + return false; + + // Step 4: If controller.[[started]] is false, return false. + if (!(flags & ControllerFlag_Started)) + return false; + + // Step 5: If ! IsReadableStreamLocked(stream) is true and + // ! ReadableStreamGetNumReadRequests(stream) > 0, return true. + // Steps 5-6 of 3.12.24 are equivalent in our implementation. + if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0) + return true; + + // Step 6: Let desiredSize be ReadableStreamDefaultControllerGetDesiredSize(controller). + double desiredSize = ReadableStreamControllerGetDesiredSizeUnchecked(controller); + + // Step 7: If desiredSize > 0, return true. + // Step 8: Return false. + // Steps 7-8 of 3.12.24 are equivalent in our implementation. + return desiredSize > 0; +} + +// Streams spec, 3.9.4. ReadableStreamDefaultControllerClose ( controller ) +static MOZ_MUST_USE bool +ReadableStreamDefaultControllerClose(JSContext* cx, + Handle controller) +{ + // Step 1: Let stream be controller.[[controlledReadableStream]]. + Rooted stream(cx, StreamFromController(controller)); + + // Step 2: Assert: controller.[[closeRequested]] is false. + MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested)); + + // Step 3: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(stream->readable()); + + // Step 4: Set controller.[[closeRequested]] to true. + AddControllerFlags(controller, ControllerFlag_CloseRequested); + + // Step 5: If controller.[[queue]] is empty, perform ! ReadableStreamClose(stream). + RootedNativeObject queue(cx); + queue = &controller->getFixedSlot(QueueContainerSlot_Queue).toObject().as(); + if (queue->getDenseInitializedLength() == 0) + return ReadableStreamClose(cx, stream); + + return true; +} + +static MOZ_MUST_USE bool +ReadableStreamFulfillReadOrReadIntoRequest(JSContext* cx, Handle stream, + HandleValue chunk, bool done); + +static MOZ_MUST_USE bool +EnqueueValueWithSize(JSContext* cx, HandleNativeObject container, HandleValue value, + HandleValue sizeVal); + +// Streams spec, 3.9.5. ReadableStreamDefaultControllerEnqueue ( controller, chunk ) +static MOZ_MUST_USE bool +ReadableStreamDefaultControllerEnqueue(JSContext* cx, + Handle controller, + HandleValue chunk) +{ + // Step 1: Let stream be controller.[[controlledReadableStream]]. + Rooted stream(cx, StreamFromController(controller)); + + // Step 2: Assert: controller.[[closeRequested]] is false. + MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested)); + + // Step 3: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(stream->readable()); + + // Step 4: If ! IsReadableStreamLocked(stream) is true and + // ! ReadableStreamGetNumReadRequests(stream) > 0, perform + // ! ReadableStreamFulfillReadRequest(stream, chunk, false). + if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0) { + if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, chunk, false)) + return false; + } else { + // Step 5: Otherwise, + // Step a: Let chunkSize be 1. + RootedValue chunkSize(cx, NumberValue(1)); + bool success = true; + + // Step b: If controller.[[strategySize]] is not undefined, + RootedValue strategySize(cx); + strategySize = controller->getFixedSlot(DefaultControllerSlot_StrategySize); + if (!strategySize.isUndefined()) { + // Step i: Set chunkSize to Call(stream.[[strategySize]], undefined, chunk). + success = Call(cx, strategySize, UndefinedHandleValue, chunk, &chunkSize); + } + + // Step c: Let enqueueResult be + // EnqueueValueWithSize(controller, chunk, chunkSize). + if (success) + success = EnqueueValueWithSize(cx, controller, chunk, chunkSize); + + if (!success) { + // Step b.ii: If chunkSize is an abrupt completion, + // and + // Step d: If enqueueResult is an abrupt completion, + RootedValue exn(cx); + if (!cx->getPendingException(&exn)) + return false; + + // Step b.ii.1: Perform + // ! ReadableStreamDefaultControllerErrorIfNeeded(controller, + // chunkSize.[[Value]]). + if (!ReadableStreamDefaultControllerErrorIfNeeded(cx, controller, exn)) + return false; + + // Step b.ii.2: Return chunkSize. + return false; + } + } + + // Step 6: Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). + if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) + return false; + + // Step 7: Return. + return true; +} + +static MOZ_MUST_USE bool +ReadableStreamError(JSContext* cx, Handle stream, HandleValue e); + +static MOZ_MUST_USE bool +ReadableByteStreamControllerClearPendingPullIntos(JSContext* cx, HandleNativeObject controller); + +// Streams spec, 3.9.6. ReadableStreamDefaultControllerError ( controller, e ) +// and +// Streams spec, 3.12.10. ReadableByteStreamControllerError ( controller, e ) +static MOZ_MUST_USE bool +ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, HandleValue e) +{ + MOZ_ASSERT(IsReadableStreamController(controller)); + + // Step 1: Let stream be controller.[[controlledReadableStream]]. + Rooted stream(cx, StreamFromController(controller)); + + // Step 2: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(stream->readable()); + + // Step 3 of 3.12.10: + // Perform ! ReadableByteStreamControllerClearPendingPullIntos(controller). + if (controller->is()) { + Rooted byteStreamController(cx); + byteStreamController = &controller->as(); + if (!ReadableByteStreamControllerClearPendingPullIntos(cx, byteStreamController)) + return false; + } + + // Step 3 (or 4): Perform ! ResetQueue(controller). + if (!ResetQueue(cx, controller)) + return false; + + // Step 4 (or 5): Perform ! ReadableStreamError(stream, e). + return ReadableStreamError(cx, stream, e); +} + +// Streams spec, 3.9.7. ReadableStreamDefaultControllerErrorIfNeeded ( controller, e ) nothrow +static MOZ_MUST_USE bool +ReadableStreamDefaultControllerErrorIfNeeded(JSContext* cx, + Handle controller, + HandleValue e) +{ + // Step 1: If controller.[[controlledReadableStream]].[[state]] is "readable", + // perform ! ReadableStreamDefaultControllerError(controller, e). + Rooted stream(cx, StreamFromController(controller)); + if (stream->readable()) + return ReadableStreamControllerError(cx, controller, e); + return true; +} + +// Streams spec, 3.9.8. ReadableStreamDefaultControllerGetDesiredSize ( controller ) +// and +// Streams spec 3.12.13. ReadableByteStreamControllerGetDesiredSize ( controller ) +static MOZ_MUST_USE double +ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller) +{ + // Steps 1-4 done at callsites, so only assert that they have been done. +#if DEBUG + ReadableStream* stream = StreamFromController(controller); + MOZ_ASSERT(!(stream->errored() || stream->closed())); +#endif // DEBUG + + // Step 5: Return controller.[[strategyHWM]] − controller.[[queueTotalSize]]. + double strategyHWM = controller->getFixedSlot(ControllerSlot_StrategyHWM).toNumber(); + double queueSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber(); + return strategyHWM - queueSize; +} + +// Streams spec, 3.10.3 new ReadableByteStreamController ( stream, underlyingSource, +// highWaterMark ) +// Steps 3 - 16. +static MOZ_MUST_USE ReadableByteStreamController* +CreateReadableByteStreamController(JSContext* cx, Handle stream, + HandleValue underlyingByteSource, + HandleValue highWaterMarkVal) +{ + Rooted controller(cx); + controller = NewBuiltinClassInstance(cx); + if (!controller) + return nullptr; + + // Step 3: Set this.[[controlledReadableStream]] to stream. + controller->setFixedSlot(ControllerSlot_Stream, ObjectValue(*stream)); + + // Step 4: Set this.[[underlyingByteSource]] to underlyingByteSource. + controller->setFixedSlot(ControllerSlot_UnderlyingSource, underlyingByteSource); + + // Step 5: Set this.[[pullAgain]], and this.[[pulling]] to false. + controller->setFixedSlot(ControllerSlot_Flags, Int32Value(0)); + + // Step 6: Perform ! ReadableByteStreamControllerClearPendingPullIntos(this). + if (!ReadableByteStreamControllerClearPendingPullIntos(cx, controller)) + return nullptr; + + // Step 7: Perform ! ResetQueue(this). + if (!ResetQueue(cx, controller)) + return nullptr; + + // Step 8: Set this.[[started]] and this.[[closeRequested]] to false. + // These should be false by default, unchanged since step 5. + MOZ_ASSERT(ControllerFlags(controller) == 0); + + // Step 9: Set this.[[strategyHWM]] to + // ? ValidateAndNormalizeHighWaterMark(highWaterMark). + double highWaterMark; + if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal, &highWaterMark)) + return nullptr; + controller->setFixedSlot(ControllerSlot_StrategyHWM, NumberValue(highWaterMark)); + + // Step 10: Let autoAllocateChunkSize be + // ? GetV(underlyingByteSource, "autoAllocateChunkSize"). + RootedValue autoAllocateChunkSize(cx); + if (!GetProperty(cx, underlyingByteSource, cx->names().autoAllocateChunkSize, + &autoAllocateChunkSize)) + { + return nullptr; + } + + // Step 11: If autoAllocateChunkSize is not undefined, + if (!autoAllocateChunkSize.isUndefined()) { + // Step a: If ! IsInteger(autoAllocateChunkSize) is false, or if + // autoAllocateChunkSize ≤ 0, throw a RangeError exception. + if (!IsInteger(autoAllocateChunkSize) || autoAllocateChunkSize.toNumber() <= 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNKSIZE); + return nullptr; + } + } + + // Step 12: Set this.[[autoAllocateChunkSize]] to autoAllocateChunkSize. + controller->setFixedSlot(ByteControllerSlot_AutoAllocateSize, autoAllocateChunkSize); + + // Step 13: Set this.[[pendingPullIntos]] to a new empty List. + if (!SetNewList(cx, controller, ByteControllerSlot_PendingPullIntos)) + return nullptr; + + // Step 14: Let controller be this (implicit). + + // Step 15: Let startResult be + // ? InvokeOrNoop(underlyingSource, "start", « this »). + RootedValue startResult(cx); + RootedValue controllerVal(cx, ObjectValue(*controller)); + if (!InvokeOrNoop(cx, underlyingByteSource, cx->names().start, controllerVal, &startResult)) + return nullptr; + + // Step 16: Let startPromise be a promise resolved with startResult: + RootedObject startPromise(cx, PromiseObject::unforgeableResolve(cx, startResult)); + if (!startPromise) + return nullptr; + + RootedObject onStartFulfilled(cx, NewHandler(cx, ControllerStartHandler, controller)); + if (!onStartFulfilled) + return nullptr; + + RootedObject onStartRejected(cx, NewHandler(cx, ControllerStartFailedHandler, controller)); + if (!onStartRejected) + return nullptr; + + if (!JS::AddPromiseReactions(cx, startPromise, onStartFulfilled, onStartRejected)) + return nullptr; + + return controller; +} + +// Streams spec, 3.10.3. +// new ReadableByteStreamController ( stream, underlyingByteSource, +// highWaterMark ) +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 (!stream->getFixedSlot(StreamSlot_Controller).isUndefined()) { + 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; +} + +static MOZ_MUST_USE ReadableStreamBYOBRequest* +CreateReadableStreamBYOBRequest(JSContext* cx, Handle controller, + HandleObject view); + +// Streams spec, 3.10.4.1. get byobRequest +static MOZ_MUST_USE bool +ReadableByteStreamController_byobRequest_impl(JSContext* cx, const CallArgs& args) +{ + Rooted controller(cx); + controller = &args.thisv().toObject().as(); + + // Step 2: If this.[[byobRequest]] is undefined and this.[[pendingPullIntos]] + // is not empty, + Value val = controller->getFixedSlot(ByteControllerSlot_BYOBRequest); + RootedValue byobRequest(cx, val); + val = controller->getFixedSlot(ByteControllerSlot_PendingPullIntos); + RootedNativeObject pendingPullIntos(cx, &val.toObject().as()); + + if (byobRequest.isUndefined() && pendingPullIntos->getDenseInitializedLength() != 0) { + // Step a: Let firstDescriptor be the first element of this.[[pendingPullIntos]]. + Rooted firstDescriptor(cx); + firstDescriptor = PeekList(pendingPullIntos); + + // Step b: Let view be ! Construct(%Uint8Array%, + // « firstDescriptor.[[buffer]], + // firstDescriptor.[[byteOffset]] + firstDescriptor.[[bytesFilled]], + // firstDescriptor.[[byteLength]] − firstDescriptor.[[bytesFilled]] »). + RootedArrayBufferObject buffer(cx, firstDescriptor->buffer()); + uint32_t bytesFilled = firstDescriptor->bytesFilled(); + RootedObject view(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, + firstDescriptor->byteOffset() + bytesFilled, + firstDescriptor->byteLength() - bytesFilled)); + if (!view) + return false; + + // Step c: Set this.[[byobRequest]] to + // ! Construct(ReadableStreamBYOBRequest, « this, view »). + RootedObject request(cx, CreateReadableStreamBYOBRequest(cx, controller, view)); + if (!request) + return false; + byobRequest = ObjectValue(*request); + controller->setFixedSlot(ByteControllerSlot_BYOBRequest, byobRequest); + } + + // Step 3: Return this.[[byobRequest]]. + args.rval().set(byobRequest); + return true; +} + +static bool +ReadableByteStreamController_byobRequest(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If IsReadableByteStreamController(this) is false, throw a TypeError + // exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableByteStreamController_byobRequest_impl>(cx, args); +} + +// Streams spec, 3.10.4.2. get desiredSize +// Combined with 3.8.4.1 above. + +static bool +ReadableByteStreamController_desiredSize(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableByteStreamController(this) is false, throw a + // TypeError exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableStreamController_desiredSize_impl>(cx, args); +} + +static MOZ_MUST_USE bool +ReadableByteStreamControllerClose(JSContext* cx, Handle controller); + +// Streams spec, 3.10.4.3. close() +static MOZ_MUST_USE bool +ReadableByteStreamController_close_impl(JSContext* cx, const CallArgs& args) +{ + Rooted controller(cx); + controller = &args.thisv().toObject().as(); + + // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception. + if (ControllerFlags(controller) & ControllerFlag_CloseRequested) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_CLOSED, "close"); + return false; + } + + // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable", + // throw a TypeError exception. + if (!StreamFromController(controller)->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close"); + return false; + } + + // Step 4: Perform ? ReadableByteStreamControllerClose(this). + if (!ReadableByteStreamControllerClose(cx, controller)) + return false; + args.rval().setUndefined(); + return true; +} + +static bool +ReadableByteStreamController_close(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableByteStreamController(this) is false, throw a + // TypeError exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableByteStreamController_close_impl>(cx, args); +} + +static MOZ_MUST_USE bool +ReadableByteStreamControllerEnqueue(JSContext* cx, + Handle controller, + HandleObject chunk); + +// Streams spec, 3.10.4.4. enqueue ( chunk ) +static MOZ_MUST_USE bool +ReadableByteStreamController_enqueue_impl(JSContext* cx, const CallArgs& args) +{ + Rooted controller(cx); + controller = &args.thisv().toObject().as(); + HandleValue chunkVal = args.get(0); + + // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception. + if (ControllerFlags(controller) & ControllerFlag_CloseRequested) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_CLOSED, "enqueue"); + return false; + } + + // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable", + // throw a TypeError exception. + if (!StreamFromController(controller)->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "enqueue"); + return false; + } + + // Step 4: If Type(chunk) is not Object, throw a TypeError exception. + // Step 5: If chunk does not have a [[ViewedArrayBuffer]] internal slot, + // throw a TypeError exception. + if (!chunkVal.isObject() || !JS_IsArrayBufferViewObject(&chunkVal.toObject())) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK); + return false; + } + RootedObject chunk(cx, &chunkVal.toObject()); + + // Step 6: Return ! ReadableByteStreamControllerEnqueue(this, chunk). + if (!ReadableByteStreamControllerEnqueue(cx, controller, chunk)) + return false; + args.rval().setUndefined(); + return true; +} + +static bool +ReadableByteStreamController_enqueue(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableByteStreamController(this) is false, throw a + // TypeError exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableByteStreamController_enqueue_impl>(cx, args); +} + +// Streams spec, 3.10.4.5. error ( e ) +static MOZ_MUST_USE bool +ReadableByteStreamController_error_impl(JSContext* cx, const CallArgs& args) +{ + Rooted controller(cx); + controller = &args.thisv().toObject().as(); + HandleValue e = args.get(0); + + // Step 2: Let stream be this.[[controlledReadableStream]]. + // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception. + if (!StreamFromController(controller)->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "error"); + return false; + } + + // Step 4: Perform ! ReadableByteStreamControllerError(this, e). + if (!ReadableStreamControllerError(cx, controller, e)) + return false; + args.rval().setUndefined(); + return true; +} + +static bool +ReadableByteStreamController_error(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableByteStreamController(this) is false, throw a + // TypeError exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableByteStreamController_error_impl>(cx, args); +} + +static const JSPropertySpec ReadableByteStreamController_properties[] = { + JS_PSG("byobRequest", ReadableByteStreamController_byobRequest, 0), + JS_PSG("desiredSize", ReadableByteStreamController_desiredSize, 0), + JS_PS_END +}; + +static const JSFunctionSpec ReadableByteStreamController_methods[] = { + JS_FN("close", ReadableByteStreamController_close, 0, 0), + JS_FN("enqueue", ReadableByteStreamController_enqueue, 1, 0), + JS_FN("error", ReadableByteStreamController_error, 1, 0), + JS_FS_END +}; + +CLASS_SPEC(ReadableByteStreamController, 3, 9, ClassSpec::DontDefineConstructor); + +// Streams spec, 3.10.5.1. [[PullSteps]] () +// Unified with 3.8.5.1 above. + +static MOZ_MUST_USE bool +ReadableByteStreamControllerHandleQueueDrain(JSContext* cx, HandleNativeObject controller); + +// Streams spec, 3.10.5.2. [[PullSteps]] () +static JSObject* +ReadableByteStreamControllerPullSteps(JSContext* cx, HandleNativeObject controller) +{ + // Step 1: Let stream be this.[[controlledReadableStream]]. + Rooted stream(cx, StreamFromController(controller)); + + // Step 2: MOZ_ASSERT: ! ReadableStreamHasDefaultReader(stream) is true. + MOZ_ASSERT(ReadableStreamHasDefaultReader(stream)); + + RootedValue val(cx); + // Step 3: If this.[[queueTotalSize]] > 0, + double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber(); + if (queueTotalSize > 0) { + // Step 3.a: MOZ_ASSERT: ! ReadableStreamGetNumReadRequests(_stream_) is 0. + MOZ_ASSERT(ReadableStreamGetNumReadRequests(stream) == 0); + + // Step 3.b: Let entry be the first element of this.[[queue]]. + // Step 3.c: Remove entry from this.[[queue]], shifting all other elements + // downward (so that the second becomes the first, and so on). + val = controller->getFixedSlot(QueueContainerSlot_Queue); + RootedNativeObject queue(cx, &val.toObject().as()); + Rooted entry(cx, ShiftFromList(cx, queue)); + MOZ_ASSERT(entry); + + // Step 3.d: Set this.[[queueTotalSize]] to this.[[queueTotalSize]] − entry.[[byteLength]]. + uint32_t byteLength = entry->byteLength(); + queueTotalSize = queueTotalSize - byteLength; + controller->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(queueTotalSize)); + + // Step 3.e: Perform ! ReadableByteStreamControllerHandleQueueDrain(this). + if (!ReadableByteStreamControllerHandleQueueDrain(cx, controller)) + return nullptr; + + // Step 3.f: Let view be ! Construct(%Uint8Array%, « entry.[[buffer]], + // entry.[[byteOffset]], entry.[[byteLength]] »). + RootedObject buffer(cx, entry->buffer()); + + uint32_t byteOffset = entry->byteOffset(); + RootedObject view(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, byteOffset, byteLength)); + if (!view) + return nullptr; + + // Step 3.g: Return a promise resolved with ! CreateIterResultObject(view, false). + val.setObject(*view); + RootedObject iterResult(cx, CreateIterResultObject(cx, val, false)); + if (!iterResult) + return nullptr; + val.setObject(*iterResult); + + return PromiseObject::unforgeableResolve(cx, val); + } + + // Step 4: Let autoAllocateChunkSize be this.[[autoAllocateChunkSize]]. + val = controller->getFixedSlot(ByteControllerSlot_AutoAllocateSize); + + // Step 5: If autoAllocateChunkSize is not undefined, + if (!val.isUndefined()) { + double autoAllocateChunkSize = val.toNumber(); + + // Step 5.a: Let buffer be Construct(%ArrayBuffer%, « autoAllocateChunkSize »). + RootedObject bufferObj(cx, JS_NewArrayBuffer(cx, autoAllocateChunkSize)); + + // Step 5.b: If buffer is an abrupt completion, + // return a promise rejected with buffer.[[Value]]. + if (!bufferObj) + return PromiseRejectedWithPendingError(cx); + + RootedArrayBufferObject buffer(cx, &bufferObj->as()); + + // Step 5.c: Let pullIntoDescriptor be Record {[[buffer]]: buffer.[[Value]], + // [[byteOffset]]: 0, + // [[byteLength]]: autoAllocateChunkSize, + // [[bytesFilled]]: 0, [[elementSize]]: 1, + // [[ctor]]: %Uint8Array%, + // [[readerType]]: `"default"`}. + RootedObject pullIntoDescriptor(cx); + pullIntoDescriptor = PullIntoDescriptor::create(cx, buffer, 0, + autoAllocateChunkSize, 0, 1, + nullptr, + ReaderType_Default); + if (!pullIntoDescriptor) + return PromiseRejectedWithPendingError(cx); + + // Step 5.d: Append pullIntoDescriptor as the last element of this.[[pendingPullIntos]]. + val = controller->getFixedSlot(ByteControllerSlot_PendingPullIntos); + RootedNativeObject pendingPullIntos(cx, &val.toObject().as()); + val = ObjectValue(*pullIntoDescriptor); + if (!AppendToList(cx, pendingPullIntos, val)) + return nullptr; + } + + // Step 6: Let promise be ! ReadableStreamAddReadRequest(stream). + Rooted promise(cx, ReadableStreamAddReadRequest(cx, stream)); + if (!promise) + return nullptr; + + // Step 7: Perform ! ReadableByteStreamControllerCallPullIfNeeded(this). + if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) + return nullptr; + + // Step 8: Return promise. + return promise; +} + +/** + * Unified implementation of ReadableStream controllers' [[PullSteps]] internal + * methods. + * Streams spec, 3.8.5.2. [[PullSteps]] () + * and + * Streams spec, 3.10.5.2. [[PullSteps]] () + */ +static MOZ_MUST_USE JSObject* +ReadableStreamControllerPullSteps(JSContext* cx, HandleNativeObject controller) +{ + MOZ_ASSERT(IsReadableStreamController(controller)); + + if (controller->is()) + return ReadableStreamDefaultControllerPullSteps(cx, controller); + + return ReadableByteStreamControllerPullSteps(cx, controller); +} + + +static MOZ_MUST_USE ReadableStreamBYOBRequest* +CreateReadableStreamBYOBRequest(JSContext* cx, Handle controller, + HandleObject view) +{ + MOZ_ASSERT(controller); + MOZ_ASSERT(JS_IsArrayBufferViewObject(view)); + + Rooted request(cx); + request = NewBuiltinClassInstance(cx); + if (!request) + return nullptr; + + // Step 1: Set this.[[associatedReadableByteStreamController]] to controller. + request->setFixedSlot(BYOBRequestSlot_Controller, ObjectValue(*controller)); + + // Step 2: Set this.[[view]] to view. + request->setFixedSlot(BYOBRequestSlot_View, ObjectValue(*view)); + + return request; +} + +// Streams spec, 3.11.3. new ReadableStreamBYOBRequest ( controller, view ) +bool +ReadableStreamBYOBRequest::constructor(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue controllerVal = args.get(0); + HandleValue viewVal = args.get(1); + + if (!ThrowIfNotConstructing(cx, args, "ReadableStreamBYOBRequest")) + return false; + + // TODO: open PR against spec to add these checks. + // They're expected to have happened in code using requests. + if (!Is(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; +} + +// Streams spec, 3.11.4.1 get view +static MOZ_MUST_USE bool +ReadableStreamBYOBRequest_view_impl(JSContext* cx, const CallArgs& args) +{ + // Step 2: Return this.[[view]]. + NativeObject* request = &args.thisv().toObject().as(); + args.rval().set(request->getFixedSlot(BYOBRequestSlot_View)); + return true; +} + +static bool +ReadableStreamBYOBRequest_view(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStreamBYOBRequest(this) is false, throw a TypeError + // exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableStreamBYOBRequest_view_impl>(cx, args); +} + +static MOZ_MUST_USE bool +ReadableByteStreamControllerRespond(JSContext* cx, + Handle controller, + HandleValue bytesWrittenVal); + +// Streams spec, 3.11.4.2. respond ( bytesWritten ) +static MOZ_MUST_USE bool +ReadableStreamBYOBRequest_respond_impl(JSContext* cx, const CallArgs& args) +{ + Rooted request(cx); + request = &args.thisv().toObject().as(); + HandleValue bytesWritten = args.get(0); + + // Step 2: If this.[[associatedReadableByteStreamController]] is undefined, + // throw a TypeError exception. + RootedValue controllerVal(cx, request->getFixedSlot(BYOBRequestSlot_Controller)); + if (controllerVal.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMBYOBREQUEST_NO_CONTROLLER, "respond"); + return false; + } + + // Step 3: Return ? + // ReadableByteStreamControllerRespond(this.[[associatedReadableByteStreamController]], + // bytesWritten). + Rooted controller(cx); + controller = &controllerVal.toObject().as(); + + if (!ReadableByteStreamControllerRespond(cx, controller, bytesWritten)) + return false; + + args.rval().setUndefined(); + return true; +} + +static bool +ReadableStreamBYOBRequest_respond(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStreamBYOBRequest(this) is false, throw a TypeError + // exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableStreamBYOBRequest_respond_impl>(cx, args); +} + +static MOZ_MUST_USE bool +ReadableByteStreamControllerRespondWithNewView(JSContext* cx, + Handle controller, + HandleObject view); + +// Streams spec, 3.11.4.3. respondWithNewView ( view ) +static MOZ_MUST_USE bool +ReadableStreamBYOBRequest_respondWithNewView_impl(JSContext* cx, const CallArgs& args) +{ + Rooted request(cx); + request = &args.thisv().toObject().as(); + HandleValue viewVal = args.get(0); + + // Step 2: If this.[[associatedReadableByteStreamController]] is undefined, + // throw a TypeError exception. + RootedValue controllerVal(cx, request->getFixedSlot(BYOBRequestSlot_Controller)); + if (controllerVal.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMBYOBREQUEST_NO_CONTROLLER, "respondWithNewView"); + return false; + } + + // Step 3: If Type(chunk) is not Object, throw a TypeError exception. + // Step 4: If view does not have a [[ViewedArrayBuffer]] internal slot, throw + // a TypeError exception. + if (!viewVal.isObject() || !JS_IsArrayBufferViewObject(&viewVal.toObject())) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK); + return false; + } + + // Step 5: Return ? + // ReadableByteStreamControllerRespondWithNewView(this.[[associatedReadableByteStreamController]], + // view). + Rooted controller(cx); + controller = &controllerVal.toObject().as(); + RootedObject view(cx, &viewVal.toObject()); + + if (!ReadableByteStreamControllerRespondWithNewView(cx, controller, view)) + return false; + + args.rval().setUndefined(); + return true; +} + +static bool +ReadableStreamBYOBRequest_respondWithNewView(JSContext* cx, unsigned argc, Value* vp) +{ + // Step 1: If ! IsReadableStreamBYOBRequest(this) is false, throw a TypeError + // exception. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod, + ReadableStreamBYOBRequest_respondWithNewView_impl>(cx, args); +} + +static const JSPropertySpec ReadableStreamBYOBRequest_properties[] = { + JS_PSG("view", ReadableStreamBYOBRequest_view, 0), + JS_PS_END +}; + +static const JSFunctionSpec ReadableStreamBYOBRequest_methods[] = { + JS_FN("respond", ReadableStreamBYOBRequest_respond, 1, 0), + JS_FN("respondWithNewView", ReadableStreamBYOBRequest_respondWithNewView, 1, 0), + JS_FS_END +}; + +CLASS_SPEC(ReadableStreamBYOBRequest, 3, 2, ClassSpec::DontDefineConstructor); + +// Streams spec, 3.12.1. IsReadableStreamBYOBRequest ( x ) +// Implemented via is() + +// Streams spec, 3.12.2. IsReadableByteStreamController ( x ) +// Implemented via is() + +// Streams spec, 3.12.3. ReadableByteStreamControllerCallPullIfNeeded ( controller ) +// Unified with 3.9.2 above. + +static void +ReadableByteStreamControllerInvalidateBYOBRequest(NativeObject* controller); + +// Streams spec, 3.12.4. ReadableByteStreamControllerClearPendingPullIntos ( controller ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerClearPendingPullIntos(JSContext* cx, HandleNativeObject controller) +{ + MOZ_ASSERT(controller->is()); + + // Step 1: Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller). + ReadableByteStreamControllerInvalidateBYOBRequest(controller); + + // Step 2: Set controller.[[pendingPullIntos]] to a new empty List. + return SetNewList(cx, controller, ByteControllerSlot_PendingPullIntos); +} + +// Streams spec, 3.12.5. ReadableByteStreamControllerClose ( controller ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerClose(JSContext* cx, Handle controller) +{ + // Step 1: Let stream be controller.[[controlledReadableStream]]. + Rooted stream(cx, StreamFromController(controller)); + + // Step 2: Assert: controller.[[closeRequested]] is false. + MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested)); + + // Step 3: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(stream->readable()); + + // Step 4: If controller.[[queueTotalSize]] > 0, + double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber(); + if (queueTotalSize > 0) { + // Step a: Set controller.[[closeRequested]] to true. + AddControllerFlags(controller, ControllerFlag_CloseRequested); + + // Step b: Return + return true; + } + + // Step 5: If controller.[[pendingPullIntos]] is not empty, + RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos)); + RootedNativeObject pendingPullIntos(cx, &val.toObject().as()); + if (pendingPullIntos->getDenseInitializedLength() != 0) { + // Step a: Let firstPendingPullInto be the first element of + // controller.[[pendingPullIntos]]. + Rooted firstPendingPullInto(cx); + firstPendingPullInto = PeekList(pendingPullIntos); + + // Step b: If firstPendingPullInto.[[bytesFilled]] > 0, + if (firstPendingPullInto->bytesFilled() > 0) { + // Step i: Let e be a new TypeError exception. { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLEBYTESTREAMCONTROLLER_CLOSE_PENDING_PULL); + RootedValue e(cx); + // Not much we can do about uncatchable exceptions, just bail. + if (!cx->getPendingException(&e)) + return false; + // Step ii: Perform ! ReadableByteStreamControllerError(controller, e). + if (!ReadableStreamControllerError(cx, controller, e)) + return false; + + // Step iii: Throw e. + return false; + } + } + + // Step 6: Perform ! ReadableStreamClose(stream). + return ReadableStreamClose(cx, stream); +} + +static MOZ_MUST_USE JSObject* +ReadableByteStreamControllerConvertPullIntoDescriptor(JSContext* cx, + Handle pullIntoDescriptor); + +// Streams spec, 3.12.6. ReadableByteStreamControllerCommitPullIntoDescriptor ( stream, pullIntoDescriptor ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerCommitPullIntoDescriptor(JSContext* cx, Handle stream, + Handle pullIntoDescriptor) +{ + // Step 1: MOZ_ASSERT: stream.[[state]] is not "errored". + MOZ_ASSERT(!stream->errored()); + + // Step 2: Let done be false. + bool done = false; + + // Step 3: If stream.[[state]] is "closed", + if (stream->closed()) { + // Step a: MOZ_ASSERT: pullIntoDescriptor.[[bytesFilled]] is 0. + MOZ_ASSERT(pullIntoDescriptor->bytesFilled() == 0); + + // Step b: Set done to true. + done = true; + } + + // Step 4: Let filledView be + // ! ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor). + RootedObject filledView(cx); + filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(cx, pullIntoDescriptor); + if (!filledView) + return false; + + // Step 5: If pullIntoDescriptor.[[readerType]] is "default", + uint32_t readerType = pullIntoDescriptor->readerType(); + RootedValue filledViewVal(cx, ObjectValue(*filledView)); + if (readerType == ReaderType_Default) { + // Step a: Perform ! ReadableStreamFulfillReadRequest(stream, filledView, done). + if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, filledViewVal, done)) + return false; + } else { + // Step 6: Otherwise, + // Step a: MOZ_ASSERT: pullIntoDescriptor.[[readerType]] is "byob". + MOZ_ASSERT(readerType == ReaderType_BYOB); + + // Step b: Perform ! ReadableStreamFulfillReadIntoRequest(stream, filledView, done). + if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, filledViewVal, done)) + return false; + } + + return true; +} + +// Streams spec, 3.12.7. ReadableByteStreamControllerConvertPullIntoDescriptor ( pullIntoDescriptor ) +static MOZ_MUST_USE JSObject* +ReadableByteStreamControllerConvertPullIntoDescriptor(JSContext* cx, + Handle pullIntoDescriptor) +{ + // Step 1: Let bytesFilled be pullIntoDescriptor.[[bytesFilled]]. + uint32_t bytesFilled = pullIntoDescriptor->bytesFilled(); + + // Step 2: Let elementSize be pullIntoDescriptor.[[elementSize]]. + uint32_t elementSize = pullIntoDescriptor->elementSize(); + + // Step 3: Assert: bytesFilled <= pullIntoDescriptor.[[byteLength]]. + MOZ_ASSERT(bytesFilled <= pullIntoDescriptor->byteLength()); + + // Step 4: Assert: bytesFilled mod elementSize is 0. + MOZ_ASSERT(bytesFilled % elementSize == 0); + + // Step 5: Return ! Construct(pullIntoDescriptor.[[ctor]], + // pullIntoDescriptor.[[buffer]], + // pullIntoDescriptor.[[byteOffset]], + // bytesFilled / elementSize). + RootedObject ctor(cx, pullIntoDescriptor->ctor()); + if (!ctor) { + if (!GetBuiltinConstructor(cx, JSProto_Uint8Array, &ctor)) + return nullptr; + } + RootedObject buffer(cx, pullIntoDescriptor->buffer()); + uint32_t byteOffset = pullIntoDescriptor->byteOffset(); + FixedConstructArgs<3> args(cx); + args[0].setObject(*buffer); + args[1].setInt32(byteOffset); + args[2].setInt32(bytesFilled / elementSize); + return JS_New(cx, ctor, args); +} + +static MOZ_MUST_USE bool +ReadableByteStreamControllerEnqueueChunkToQueue(JSContext* cx, + Handle controller, + HandleObject buffer, uint32_t byteOffset, + uint32_t byteLength); + +static MOZ_MUST_USE ArrayBufferObject* +TransferArrayBuffer(JSContext* cx, HandleObject buffer); + +static MOZ_MUST_USE bool +ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(JSContext* cx, + Handle controller); + +// Streams spec, 3.12.8. ReadableByteStreamControllerEnqueue ( controller, chunk ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerEnqueue(JSContext* cx, + Handle controller, + HandleObject chunk) +{ + // Step 1: Let stream be controller.[[controlledReadableStream]]. + Rooted stream(cx, StreamFromController(controller)); + + // Step 2: Assert: controller.[[closeRequested]] is false. + MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested)); + + // Step 3: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(stream->readable()); + + // Step 4: Let buffer be chunk.[[ViewedArrayBuffer]]. + bool dummy; + RootedObject buffer(cx, JS_GetArrayBufferViewBuffer(cx, chunk, &dummy)); + if (!buffer) + return false; + + // Step 5: Let byteOffset be chunk.[[ByteOffset]]. + uint32_t byteOffset = JS_GetArrayBufferViewByteOffset(chunk); + + // Step 6: Let byteLength be chunk.[[ByteLength]]. + uint32_t byteLength = JS_GetArrayBufferViewByteLength(chunk); + + // Step 7: Let transferredBuffer be ! TransferArrayBuffer(buffer). + RootedArrayBufferObject transferredBuffer(cx, TransferArrayBuffer(cx, buffer)); + if (!transferredBuffer) + return false; + + // Step 8: If ! ReadableStreamHasDefaultReader(stream) is true + if (ReadableStreamHasDefaultReader(stream)) { + // Step a: If ! ReadableStreamGetNumReadRequests(stream) is 0, + if (ReadableStreamGetNumReadRequests(stream) == 0) { + // Step i: Perform + // ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, + // transferredBuffer, + // byteOffset, + // byteLength). + if (!ReadableByteStreamControllerEnqueueChunkToQueue(cx, controller, transferredBuffer, + byteOffset, byteLength)) + { + return false; + } + } else { + // Step b: Otherwise, + // Step i: Assert: controller.[[queue]] is empty. +#if DEBUG + RootedValue val(cx, controller->getFixedSlot(QueueContainerSlot_Queue)); + RootedNativeObject queue(cx, &val.toObject().as()); + MOZ_ASSERT(queue->getDenseInitializedLength() == 0); +#endif // DEBUG + + // Step ii: Let transferredView be + // ! Construct(%Uint8Array%, transferredBuffer, byteOffset, byteLength). + RootedObject transferredView(cx, JS_NewUint8ArrayWithBuffer(cx, transferredBuffer, + byteOffset, byteLength)); + if (!transferredView) + return false; + + // Step iii: Perform ! ReadableStreamFulfillReadRequest(stream, transferredView, false). + RootedValue chunk(cx, ObjectValue(*transferredView)); + if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, chunk, false)) + return false; + } + } else if (ReadableStreamHasBYOBReader(stream)) { + // Step 9: Otherwise, + // Step a: If ! ReadableStreamHasBYOBReader(stream) is true, + // Step i: Perform + // ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, + // transferredBuffer, + // byteOffset, + // byteLength). + if (!ReadableByteStreamControllerEnqueueChunkToQueue(cx, controller, transferredBuffer, + byteOffset, byteLength)) + { + return false; + } + + // Step ii: Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). + if (!ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(cx, controller)) + return false; + } else { + // Step b: Otherwise, + // Step i: Assert: ! IsReadableStreamLocked(stream) is false. + MOZ_ASSERT(!IsReadableStreamLocked(stream)); + + // Step ii: Perform + // ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, + // transferredBuffer, + // byteOffset, + // byteLength). + if (!ReadableByteStreamControllerEnqueueChunkToQueue(cx, controller, transferredBuffer, + byteOffset, byteLength)) + { + return false; + } + } + + return true; +} + +// Streams spec, 3.12.9. +// ReadableByteStreamControllerEnqueueChunkToQueue ( controller, buffer, +// byteOffset, byteLength ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerEnqueueChunkToQueue(JSContext* cx, + Handle controller, + HandleObject buffer, uint32_t byteOffset, + uint32_t byteLength) +{ + MOZ_ASSERT(controller->is(), "must operate on ReadableByteStreamController"); + + // Step 1: Append Record {[[buffer]]: buffer, + // [[byteOffset]]: byteOffset, + // [[byteLength]]: byteLength} + // as the last element of controller.[[queue]]. + RootedValue val(cx, controller->getFixedSlot(QueueContainerSlot_Queue)); + RootedNativeObject queue(cx, &val.toObject().as()); + + Rooted chunk(cx); + chunk = ByteStreamChunk::create(cx, buffer, byteOffset, byteLength); + if (!chunk) + return false; + + RootedValue chunkVal(cx, ObjectValue(*chunk)); + if (!AppendToList(cx, queue, chunkVal)) + return false; + + // Step 2: Add byteLength to controller.[[queueTotalSize]]. + double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber(); + controller->setFixedSlot(QueueContainerSlot_TotalSize, + NumberValue(queueTotalSize + byteLength)); + + return true; +} + +// Streams spec, 3.12.10. ReadableByteStreamControllerError ( controller, e ) +// Unified with 3.9.6 above. + +// Streams spec, 3.12.11. ReadableByteStreamControllerFillHeadPullIntoDescriptor ( controler, size, pullIntoDescriptor ) +static void +ReadableByteStreamControllerFillHeadPullIntoDescriptor(ReadableByteStreamController* controller, uint32_t size, + Handle pullIntoDescriptor) +{ + // Step 1: Assert: either controller.[[pendingPullIntos]] is empty, or the + // first element of controller.[[pendingPullIntos]] is pullIntoDescriptor. +#if DEBUG + Value val = controller->getFixedSlot(ByteControllerSlot_PendingPullIntos); + NativeObject* pendingPullIntos = &val.toObject().as(); + MOZ_ASSERT(pendingPullIntos->getDenseInitializedLength() == 0 || + &pendingPullIntos->getDenseElement(0).toObject() == pullIntoDescriptor); +#endif // DEBUG + + // Step 2: Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller). + ReadableByteStreamControllerInvalidateBYOBRequest(controller); + + // Step 3: Set pullIntoDescriptor.[[bytesFilled]] to pullIntoDescriptor.[[bytesFilled]] + size. + pullIntoDescriptor->setBytesFilled(pullIntoDescriptor->bytesFilled() + size); +} + +// Streams spec, 3.12.12. ReadableByteStreamControllerFillPullIntoDescriptorFromQueue ( controller, pullIntoDescriptor ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(JSContext* cx, + Handle controller, + Handle pullIntoDescriptor, + bool* ready) +{ + *ready = false; + + // Step 1: Let elementSize be pullIntoDescriptor.[[elementSize]]. + uint32_t elementSize = pullIntoDescriptor->elementSize(); + + // Step 2: Let currentAlignedBytes be pullIntoDescriptor.[[bytesFilled]] − + // (pullIntoDescriptor.[[bytesFilled]] mod elementSize). + uint32_t bytesFilled = pullIntoDescriptor->bytesFilled(); + uint32_t currentAlignedBytes = bytesFilled - (bytesFilled % elementSize); + + // Step 3: Let maxBytesToCopy be min(controller.[[queueTotalSize]], + // pullIntoDescriptor.[[byteLength]] − pullIntoDescriptor.[[bytesFilled]]). + uint32_t byteLength = pullIntoDescriptor->byteLength(); + + // The queue size could be negative or overflow uint32_t. We cannot + // validly have a maxBytesToCopy value that'd overflow uint32_t, though, + // so just clamp to that. + Value sizeVal = controller->getFixedSlot(QueueContainerSlot_TotalSize); + uint32_t queueTotalSize = JS::ToUint32(sizeVal.toNumber()); + uint32_t maxBytesToCopy = std::min(queueTotalSize, byteLength - bytesFilled); + + // Step 4: Let maxBytesFilled be pullIntoDescriptor.[[bytesFilled]] + maxBytesToCopy. + uint32_t maxBytesFilled = bytesFilled + maxBytesToCopy; + + // Step 5: Let maxAlignedBytes be maxBytesFilled − (maxBytesFilled mod elementSize). + uint32_t maxAlignedBytes = maxBytesFilled - (maxBytesFilled % elementSize); + + // Step 6: Let totalBytesToCopyRemaining be maxBytesToCopy. + uint32_t totalBytesToCopyRemaining = maxBytesToCopy; + + // Step 7: Let ready be false (implicit). + + // Step 8: If maxAlignedBytes > currentAlignedBytes, + if (maxAlignedBytes > currentAlignedBytes) { + // Step a: Set totalBytesToCopyRemaining to maxAlignedBytes − + // pullIntoDescriptor.[[bytesFilled]]. + totalBytesToCopyRemaining = maxAlignedBytes - bytesFilled; + + // Step b: Let ready be true. + *ready = true; + } + + // Step 9: Let queue be controller.[[queue]]. + RootedValue val(cx, controller->getFixedSlot(QueueContainerSlot_Queue)); + RootedNativeObject queue(cx, &val.toObject().as()); + + // Step 10: Repeat the following steps while totalBytesToCopyRemaining > 0, + Rooted headOfQueue(cx); + while (totalBytesToCopyRemaining > 0) { + MOZ_ASSERT(queue->getDenseInitializedLength() != 0); + + // Step a: Let headOfQueue be the first element of queue. + headOfQueue = PeekList(queue); + + // Step b: Let bytesToCopy be min(totalBytesToCopyRemaining, + // headOfQueue.[[byteLength]]). + uint32_t byteLength = headOfQueue->byteLength(); + uint32_t bytesToCopy = std::min(totalBytesToCopyRemaining, byteLength); + + // Step c: Let destStart be pullIntoDescriptor.[[byteOffset]] + + // pullIntoDescriptor.[[bytesFilled]]. + uint32_t destStart = pullIntoDescriptor->byteOffset() + bytesFilled; + + // Step d: Perform ! CopyDataBlockBytes(pullIntoDescriptor.[[buffer]].[[ArrayBufferData]], + // destStart, + // headOfQueue.[[buffer]].[[ArrayBufferData]], + // headOfQueue.[[byteOffset]], + // bytesToCopy). + RootedArrayBufferObject sourceBuffer(cx, headOfQueue->buffer()); + uint32_t sourceOffset = headOfQueue->byteOffset(); + RootedArrayBufferObject targetBuffer(cx, pullIntoDescriptor->buffer()); + ArrayBufferObject::copyData(targetBuffer, destStart, sourceBuffer, sourceOffset, + bytesToCopy); + + // Step e: If headOfQueue.[[byteLength]] is bytesToCopy, + if (byteLength == bytesToCopy) { + // Step i: Remove the first element of queue, shifting all other elements + // downward (so that the second becomes the first, and so on). + headOfQueue = ShiftFromList(cx, queue); + MOZ_ASSERT(headOfQueue); + } else { + // Step f: Otherwise, + // Step i: Set headOfQueue.[[byteOffset]] to headOfQueue.[[byteOffset]] + + // bytesToCopy. + headOfQueue->SetByteOffset(sourceOffset + bytesToCopy); + + // Step ii: Set headOfQueue.[[byteLength]] to headOfQueue.[[byteLength]] − + // bytesToCopy. + headOfQueue->SetByteLength(byteLength - bytesToCopy); + } + + // Step g: Set controller.[[queueTotalSize]] to + // controller.[[queueTotalSize]] − bytesToCopy. + queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber(); + queueTotalSize -= bytesToCopy; + controller->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(queueTotalSize)); + + // Step h: Perform ! ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, + // bytesToCopy, + // pullIntoDescriptor). + ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesToCopy, + pullIntoDescriptor); + bytesFilled += bytesToCopy; + MOZ_ASSERT(bytesFilled == pullIntoDescriptor->bytesFilled()); + + // Step i: Set totalBytesToCopyRemaining to totalBytesToCopyRemaining − bytesToCopy. + totalBytesToCopyRemaining -= bytesToCopy; + } + + // Step 11: If ready is false, + if (!*ready) { + // Step a: Assert: controller.[[queueTotalSize]] is 0. + MOZ_ASSERT(controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber() == 0); + + // Step b: Assert: pullIntoDescriptor.[[bytesFilled]] > 0. + MOZ_ASSERT(bytesFilled > 0, "should have filled some bytes"); + + // Step c: Assert: pullIntoDescriptor.[[bytesFilled]] < + // pullIntoDescriptor.[[elementSize]]. + MOZ_ASSERT(bytesFilled < elementSize); + } + + // Step 12: Return ready. + return true; +} + +// Streams spec 3.12.13. ReadableByteStreamControllerGetDesiredSize ( controller ) +// Unified with 3.9.8 above. + +// Streams spec, 3.12.14. ReadableByteStreamControllerHandleQueueDrain ( controller ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerHandleQueueDrain(JSContext* cx, HandleNativeObject controller) +{ + MOZ_ASSERT(controller->is()); + + // Step 1: Assert: controller.[[controlledReadableStream]].[[state]] is "readable". + Rooted stream(cx, StreamFromController(controller)); + MOZ_ASSERT(stream->readable()); + + // Step 2: If controller.[[queueTotalSize]] is 0 and + // controller.[[closeRequested]] is true, + double totalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber(); + bool closeRequested = ControllerFlags(controller) & ControllerFlag_CloseRequested; + if (totalSize == 0 && closeRequested) { + // Step a: Perform ! ReadableStreamClose(controller.[[controlledReadableStream]]). + return ReadableStreamClose(cx, stream); + } + + // Step 3: Otherwise, + // Step a: Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + return ReadableStreamControllerCallPullIfNeeded(cx, controller); +} + +// Streams spec 3.12.15. ReadableByteStreamControllerInvalidateBYOBRequest ( controller ) +static void +ReadableByteStreamControllerInvalidateBYOBRequest(NativeObject* controller) +{ + MOZ_ASSERT(controller->is()); + + // Step 1: If controller.[[byobRequest]] is undefined, return. + Value byobRequestVal = controller->getFixedSlot(ByteControllerSlot_BYOBRequest); + if (byobRequestVal.isUndefined()) + return; + + NativeObject* byobRequest = &byobRequestVal.toObject().as(); + // Step 2: Set controller.[[byobRequest]].[[associatedReadableByteStreamController]] + // to undefined. + byobRequest->setFixedSlot(BYOBRequestSlot_Controller, UndefinedValue()); + + // Step 3: Set controller.[[byobRequest]].[[view]] to undefined. + byobRequest->setFixedSlot(BYOBRequestSlot_View, UndefinedValue()); + + // Step 4: Set controller.[[byobRequest]] to undefined. + controller->setFixedSlot(ByteControllerSlot_BYOBRequest, UndefinedValue()); +} + +static MOZ_MUST_USE PullIntoDescriptor* +ReadableByteStreamControllerShiftPendingPullInto(JSContext* cx, HandleNativeObject controller); + +// Streams spec 3.12.16. ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue ( controller ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(JSContext* cx, + Handle controller) +{ + Rooted stream(cx, StreamFromController(controller)); + + // Step 1: Assert: controller.[[closeRequested]] is false. + MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested)); + + // Step 2: Repeat the following steps while controller.[[pendingPullIntos]] + // is not empty, + RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos)); + RootedNativeObject pendingPullIntos(cx, &val.toObject().as()); + Rooted pullIntoDescriptor(cx); + while (pendingPullIntos->getDenseInitializedLength() != 0) { + // Step a: If controller.[[queueTotalSize]] is 0, return. + double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber(); + if (queueTotalSize == 0) + return true; + + // Step b: Let pullIntoDescriptor be the first element of + // controller.[[pendingPullIntos]]. + pullIntoDescriptor = PeekList(pendingPullIntos); + + // Step c: If ! ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) + // is true, + bool ready; + if (!ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(cx, controller, + pullIntoDescriptor, + &ready)) + { + return false; + } + if (ready) { + // Step i: Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). + if (!ReadableByteStreamControllerShiftPendingPullInto(cx, controller)) + return false; + + // Step ii: Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(controller.[[controlledReadableStream]], + // pullIntoDescriptor). + if (!ReadableByteStreamControllerCommitPullIntoDescriptor(cx, stream, + pullIntoDescriptor)) + { + return false; + } + } + } + + return true; +} + +// Streams spec, 3.12.17. ReadableByteStreamControllerPullInto ( controller, view ) +static MOZ_MUST_USE JSObject* +ReadableByteStreamControllerPullInto(JSContext* cx, + Handle controller, + HandleNativeObject view) +{ + MOZ_ASSERT(controller->is()); + MOZ_ASSERT(JS_IsArrayBufferViewObject(view)); + + // Step 1: Let stream be controller.[[controlledReadableStream]]. + Rooted stream(cx, StreamFromController(controller)); + + // Step 2: Let elementSize be 1. + uint32_t elementSize = 1; + + RootedObject ctor(cx); + // Step 4: If view has a [[TypedArrayName]] internal slot (i.e., it is not a + // DataView), + if (view->is()) { + JSProtoKey protoKey = StandardProtoKeyOrNull(view); + MOZ_ASSERT(protoKey); + + if (!GetBuiltinConstructor(cx, protoKey, &ctor)) + return nullptr; + elementSize = 1 << TypedArrayShift(view->as().type()); + } else { + // Step 3: Let ctor be %DataView% (reordered). + if (!GetBuiltinConstructor(cx, JSProto_DataView, &ctor)) + return nullptr; + } + + // Step 5: Let pullIntoDescriptor be Record {[[buffer]]: view.[[ViewedArrayBuffer]], + // [[byteOffset]]: view.[[ByteOffset]], + // [[byteLength]]: view.[[ByteLength]], + // [[bytesFilled]]: 0, + // [[elementSize]]: elementSize, + // [[ctor]]: ctor, + // [[readerType]]: "byob"}. + bool dummy; + RootedArrayBufferObject buffer(cx, &JS_GetArrayBufferViewBuffer(cx, view, &dummy) + ->as()); + if (!buffer) + return nullptr; + + uint32_t byteOffset = JS_GetArrayBufferViewByteOffset(view); + uint32_t byteLength = JS_GetArrayBufferViewByteLength(view); + Rooted pullIntoDescriptor(cx); + pullIntoDescriptor = PullIntoDescriptor::create(cx, buffer, byteOffset, byteLength, 0, + elementSize, ctor, + ReaderType_BYOB); + if (!pullIntoDescriptor) + return nullptr; + + // Step 6: If controller.[[pendingPullIntos]] is not empty, + RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos)); + RootedNativeObject pendingPullIntos(cx, &val.toObject().as()); + if (pendingPullIntos->getDenseInitializedLength() != 0) { + // Step a: Set pullIntoDescriptor.[[buffer]] to + // ! TransferArrayBuffer(pullIntoDescriptor.[[buffer]]). + RootedArrayBufferObject transferredBuffer(cx, TransferArrayBuffer(cx, buffer)); + if (!transferredBuffer) + return nullptr; + pullIntoDescriptor->setBuffer(transferredBuffer); + + // Step b: Append pullIntoDescriptor as the last element of + // controller.[[pendingPullIntos]]. + val = ObjectValue(*pullIntoDescriptor); + if (!AppendToList(cx, pendingPullIntos, val)) + return nullptr; + + // Step c: Return ! ReadableStreamAddReadIntoRequest(stream). + return ReadableStreamAddReadIntoRequest(cx, stream); + } + + // Step 7: If stream.[[state]] is "closed", + if (stream->closed()) { + // Step a: Let emptyView be ! Construct(ctor, pullIntoDescriptor.[[buffer]], + // pullIntoDescriptor.[[byteOffset]], 0). + FixedConstructArgs<3> args(cx); + args[0].setObject(*buffer); + args[1].setInt32(byteOffset); + args[2].setInt32(0); + RootedObject emptyView(cx, JS_New(cx, ctor, args)); + if (!emptyView) + return nullptr; + + // Step b: Return a promise resolved with + // ! CreateIterResultObject(emptyView, true). + RootedValue val(cx, ObjectValue(*emptyView)); + RootedObject iterResult(cx, CreateIterResultObject(cx, val, true)); + if (!iterResult) + return nullptr; + val = ObjectValue(*iterResult); + return PromiseObject::unforgeableResolve(cx, val); + } + + // Step 8: If controller.[[queueTotalSize]] > 0, + double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber(); + if (queueTotalSize > 0) { + // Step a: If ! ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, + // pullIntoDescriptor) + // is true, + bool ready; + if (!ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(cx, controller, + pullIntoDescriptor, &ready)) + { + return nullptr; + } + + if (ready) { + // Step i: Let filledView be + // ! ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor). + RootedObject filledView(cx); + filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(cx, + pullIntoDescriptor); + if (!filledView) + return nullptr; + + // Step ii: Perform ! ReadableByteStreamControllerHandleQueueDrain(controller). + if (!ReadableByteStreamControllerHandleQueueDrain(cx, controller)) + return nullptr; + + // Step iii: Return a promise resolved with + // ! CreateIterResultObject(filledView, false). + val = ObjectValue(*filledView); + RootedObject iterResult(cx, CreateIterResultObject(cx, val, false)); + if (!iterResult) + return nullptr; + val = ObjectValue(*iterResult); + return PromiseObject::unforgeableResolve(cx, val); + } + + // Step b: If controller.[[closeRequested]] is true, + if (ControllerFlags(controller) & ControllerFlag_CloseRequested) { + // Step i: Let e be a TypeError exception. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_CLOSED, "read"); + + // Not much we can do about uncatchable exceptions, just bail. + RootedValue e(cx); + if (!GetAndClearException(cx, &e)) + return nullptr; + + // Step ii: Perform ! ReadableByteStreamControllerError(controller, e). + if (!ReadableStreamControllerError(cx, controller, e)) + return nullptr; + + // Step iii: Return a promise rejected with e. + return PromiseObject::unforgeableReject(cx, e); + } + } + + // Step 9: Set pullIntoDescriptor.[[buffer]] to + // ! TransferArrayBuffer(pullIntoDescriptor.[[buffer]]). + RootedArrayBufferObject transferredBuffer(cx, TransferArrayBuffer(cx, buffer)); + if (!transferredBuffer) + return nullptr; + pullIntoDescriptor->setBuffer(transferredBuffer); + + // Step 10: Append pullIntoDescriptor as the last element of + // controller.[[pendingPullIntos]]. + val = ObjectValue(*pullIntoDescriptor); + if (!AppendToList(cx, pendingPullIntos, val)) + return nullptr; + + // Step 11: Let promise be ! ReadableStreamAddReadIntoRequest(stream). + Rooted promise(cx, ReadableStreamAddReadIntoRequest(cx, stream)); + if (!promise) + return nullptr; + + // Step 12: Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) + return nullptr; + + // Step 13: Return promise. + return promise; +} + +static MOZ_MUST_USE bool +ReadableByteStreamControllerRespondInternal(JSContext* cx, + Handle controller, + double bytesWritten); + +// Streams spec 3.12.18. ReadableByteStreamControllerRespond( controller, bytesWritten ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerRespond(JSContext* cx, + Handle controller, + HandleValue bytesWrittenVal) +{ + MOZ_ASSERT(controller->is()); + + // Step 1: Let bytesWritten be ? ToNumber(bytesWritten). + double bytesWritten; + if (!ToNumber(cx, bytesWrittenVal, &bytesWritten)) + return false; + + // Step 2: If ! IsFiniteNonNegativeNumber(bytesWritten) is false, + if (bytesWritten < 0 || mozilla::IsNaN(bytesWritten) || mozilla::IsInfinite(bytesWritten)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NUMBER_MUST_BE_FINITE_NON_NEGATIVE, "bytesWritten"); + return false; + } + + // Step 3: Assert: controller.[[pendingPullIntos]] is not empty. +#if DEBUG + Value val = controller->getFixedSlot(ByteControllerSlot_PendingPullIntos); + RootedNativeObject pendingPullIntos(cx, &val.toObject().as()); + MOZ_ASSERT(pendingPullIntos->getDenseInitializedLength() != 0); +#endif // DEBUG + + // Step 4: Perform ? ReadableByteStreamControllerRespondInternal(controller, bytesWritten). + return ReadableByteStreamControllerRespondInternal(cx, controller, bytesWritten); +} + +// Streams spec 3.12.19. ReadableByteStreamControllerRespondInClosedState( controller, firstDescriptor ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerRespondInClosedState(JSContext* cx, + Handle controller, + Handle firstDescriptor) +{ + // Step 1: Set firstDescriptor.[[buffer]] to + // ! TransferArrayBuffer(firstDescriptor.[[buffer]]). + RootedArrayBufferObject buffer(cx, firstDescriptor->buffer()); + RootedArrayBufferObject transferredBuffer(cx, TransferArrayBuffer(cx, buffer)); + if (!transferredBuffer) + return false; + firstDescriptor->setBuffer(transferredBuffer); + + // Step 2: Assert: firstDescriptor.[[bytesFilled]] is 0. + MOZ_ASSERT(firstDescriptor->bytesFilled() == 0); + + // Step 3: Let stream be controller.[[controlledReadableStream]]. + Rooted stream(cx, StreamFromController(controller)); + + // Step 4: If ReadableStreamHasBYOBReader(stream) is true, + if (ReadableStreamHasBYOBReader(stream)) { + // Step a: Repeat the following steps while + // ! ReadableStreamGetNumReadIntoRequests(stream) > 0, + Rooted descriptor(cx); + while (ReadableStreamGetNumReadRequests(stream) > 0) { + // Step i: Let pullIntoDescriptor be + // ! ReadableByteStreamControllerShiftPendingPullInto(controller). + descriptor = ReadableByteStreamControllerShiftPendingPullInto(cx, controller); + if (!descriptor) + return false; + + // Step ii: Perform ! + // ReadableByteStreamControllerCommitPullIntoDescriptor(stream, + // pullIntoDescriptor). + if (!ReadableByteStreamControllerCommitPullIntoDescriptor(cx, stream, descriptor)) + return false; + } + } + + return true; +} + +// Streams spec 3.12.20. +// ReadableByteStreamControllerRespondInReadableState( controller, bytesWritten, pullIntoDescriptor ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerRespondInReadableState(JSContext* cx, + Handle controller, + uint32_t bytesWritten, + Handle pullIntoDescriptor) +{ + // Step 1: If pullIntoDescriptor.[[bytesFilled]] + bytesWritten > pullIntoDescriptor.[[byteLength]], + // throw a RangeError exception. + uint32_t bytesFilled = pullIntoDescriptor->bytesFilled(); + uint32_t byteLength = pullIntoDescriptor->byteLength(); + if (bytesFilled + bytesWritten > byteLength) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_BYTESWRITTEN); + return false; + } + + // Step 2: Perform ! ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, + // bytesWritten, + // pullIntoDescriptor). + ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesWritten, + pullIntoDescriptor); + bytesFilled += bytesWritten; + + // Step 3: If pullIntoDescriptor.[[bytesFilled]] < + // pullIntoDescriptor.[[elementSize]], return. + uint32_t elementSize = pullIntoDescriptor->elementSize(); + if (bytesFilled < elementSize) + return true; + + // Step 4: Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). + if (!ReadableByteStreamControllerShiftPendingPullInto(cx, controller)) + return false; + + // Step 5: Let remainderSize be pullIntoDescriptor.[[bytesFilled]] mod + // pullIntoDescriptor.[[elementSize]]. + uint32_t remainderSize = bytesFilled % elementSize; + + // Step 6: If remainderSize > 0, + RootedArrayBufferObject buffer(cx, pullIntoDescriptor->buffer()); + if (remainderSize > 0) { + // Step a: Let end be pullIntoDescriptor.[[byteOffset]] + + // pullIntoDescriptor.[[bytesFilled]]. + uint32_t end = pullIntoDescriptor->byteOffset() + bytesFilled; + + // Step b: Let remainder be ? CloneArrayBuffer(pullIntoDescriptor.[[buffer]], + // end − remainderSize, + // remainderSize, %ArrayBuffer%). + // TODO: this really, really should just use a slot to store the remainder. + RootedObject remainderObj(cx, JS_NewArrayBuffer(cx, remainderSize)); + if (!remainderObj) + return false; + RootedArrayBufferObject remainder(cx, &remainderObj->as()); + ArrayBufferObject::copyData(remainder, 0, buffer, end - remainderSize, remainderSize); + + // Step c: Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, + // remainder, 0, + // remainder.[[ByteLength]]). + // Note: `remainderSize` is equivalent to remainder.[[ByteLength]]. + if (!ReadableByteStreamControllerEnqueueChunkToQueue(cx, controller, remainder, 0, + remainderSize)) + { + return false; + } + } + + // Step 7: Set pullIntoDescriptor.[[buffer]] to + // ! TransferArrayBuffer(pullIntoDescriptor.[[buffer]]). + RootedArrayBufferObject transferredBuffer(cx, TransferArrayBuffer(cx, buffer)); + if (!transferredBuffer) + return false; + pullIntoDescriptor->setBuffer(transferredBuffer); + + // Step 8: Set pullIntoDescriptor.[[bytesFilled]] to pullIntoDescriptor.[[bytesFilled]] − + // remainderSize. + pullIntoDescriptor->setBytesFilled(bytesFilled - remainderSize); + + // Step 9: Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(controller.[[controlledReadableStream]], + // pullIntoDescriptor). + Rooted stream(cx, StreamFromController(controller)); + if (!ReadableByteStreamControllerCommitPullIntoDescriptor(cx, stream, pullIntoDescriptor)) + return false; + + // Step 10: Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). + return ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(cx, controller); +} + +// Streams spec, 3.12.21. ReadableByteStreamControllerRespondInternal ( controller, bytesWritten ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerRespondInternal(JSContext* cx, + Handle controller, + double bytesWritten) +{ + // Step 1: Let firstDescriptor be the first element of controller.[[pendingPullIntos]]. + RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos)); + RootedNativeObject pendingPullIntos(cx, &val.toObject().as()); + Rooted firstDescriptor(cx); + firstDescriptor = PeekList(pendingPullIntos); + + // Step 2: Let stream be controller.[[controlledReadableStream]]. + Rooted stream(cx, StreamFromController(controller)); + + // Step 3: If stream.[[state]] is "closed", + if (stream->closed()) { + // Step a: If bytesWritten is not 0, throw a TypeError exception. + if (bytesWritten != 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMBYOBREQUEST_RESPOND_CLOSED); + return false; + } + + // Step b: Perform + // ! ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor). + return ReadableByteStreamControllerRespondInClosedState(cx, controller, firstDescriptor); + } + + // Step 4: Otherwise, + // Step a: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(stream->readable()); + + // Step b: Perform ? ReadableByteStreamControllerRespondInReadableState(controller, + // bytesWritten, + // firstDescriptor). + return ReadableByteStreamControllerRespondInReadableState(cx, controller, bytesWritten, + firstDescriptor); +} + +// Streams spec, 3.12.22. ReadableByteStreamControllerRespondWithNewView ( controller, view ) +static MOZ_MUST_USE bool +ReadableByteStreamControllerRespondWithNewView(JSContext* cx, + Handle controller, + HandleObject view) +{ + // Step 1: Assert: controller.[[pendingPullIntos]] is not empty. + RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos)); + RootedNativeObject pendingPullIntos(cx, &val.toObject().as()); + MOZ_ASSERT(pendingPullIntos->getDenseInitializedLength() != 0); + + // Step 2: Let firstDescriptor be the first element of controller.[[pendingPullIntos]]. + Rooted firstDescriptor(cx); + firstDescriptor = PeekList(pendingPullIntos); + + // Step 3: If firstDescriptor.[[byteOffset]] + firstDescriptor.[[bytesFilled]] + // is not view.[[ByteOffset]], throw a RangeError exception. + uint32_t byteOffset = uint32_t(JS_GetArrayBufferViewByteOffset(view)); + if (firstDescriptor->byteOffset() + firstDescriptor->bytesFilled() != byteOffset) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_OFFSET); + return false; + } + + // Step 4: If firstDescriptor.[[byteLength]] is not view.[[ByteLength]], + // throw a RangeError exception. + uint32_t byteLength = JS_GetArrayBufferViewByteLength(view); + if (firstDescriptor->byteLength() != byteLength) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_SIZE); + return false; + } + + // Step 5: Set firstDescriptor.[[buffer]] to view.[[ViewedArrayBuffer]]. + bool dummy; + RootedArrayBufferObject buffer(cx, + &AsArrayBuffer(JS_GetArrayBufferViewBuffer(cx, view, &dummy))); + if (!buffer) + return false; + firstDescriptor->setBuffer(buffer); + + // Step 6: Perform ? ReadableByteStreamControllerRespondInternal(controller, + // view.[[ByteLength]]). + return ReadableByteStreamControllerRespondInternal(cx, controller, byteLength); +} + +// Streams spec, 3.12.23. ReadableByteStreamControllerShiftPendingPullInto ( controller ) +static MOZ_MUST_USE PullIntoDescriptor* +ReadableByteStreamControllerShiftPendingPullInto(JSContext* cx, HandleNativeObject controller) +{ + MOZ_ASSERT(controller->is()); + + // Step 1: Let descriptor be the first element of controller.[[pendingPullIntos]]. + // Step 2: Remove descriptor from controller.[[pendingPullIntos]], shifting + // all other elements downward (so that the second becomes the first, + // and so on). + RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos)); + RootedNativeObject pendingPullIntos(cx, &val.toObject().as()); + Rooted descriptor(cx); + descriptor = ShiftFromList(cx, pendingPullIntos); + MOZ_ASSERT(descriptor); + + // Step 3: Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller). + ReadableByteStreamControllerInvalidateBYOBRequest(controller); + + // Step 4: Return descriptor. + return descriptor; +} + +// Streams spec, 3.12.24. ReadableByteStreamControllerShouldCallPull ( controller ) +// Unified with 3.9.3 above. + +// Streams spec, 6.1.2. new ByteLengthQueuingStrategy({ highWaterMark }) +bool +js::ByteLengthQueuingStrategy::constructor(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject strategy(cx, NewBuiltinClassInstance(cx)); + if (!strategy) + return false; + + RootedObject argObj(cx, ToObject(cx, args.get(0))); + if (!argObj) + return false; + + RootedValue highWaterMark(cx); + if (!GetProperty(cx, argObj, argObj, cx->names().highWaterMark, &highWaterMark)) + return false; + + if (!SetProperty(cx, strategy, cx->names().highWaterMark, highWaterMark)) + return false; + + args.rval().setObject(*strategy); + return true; +} + +// Streams spec 6.1.3.1. size ( chunk ) +bool +ByteLengthQueuingStrategy_size(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: Return ? GetV(chunk, "byteLength"). + return GetProperty(cx, args.get(0), cx->names().byteLength, args.rval()); +} + +static const JSPropertySpec ByteLengthQueuingStrategy_properties[] = { + JS_PS_END +}; + +static const JSFunctionSpec ByteLengthQueuingStrategy_methods[] = { + JS_FN("size", ByteLengthQueuingStrategy_size, 1, 0), + JS_FS_END +}; + +CLASS_SPEC(ByteLengthQueuingStrategy, 1, 0, 0); + +// Streams spec, 6.2.2. new CountQueuingStrategy({ highWaterMark }) +bool +js::CountQueuingStrategy::constructor(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted strategy(cx, NewBuiltinClassInstance(cx)); + if (!strategy) + return false; + + RootedObject argObj(cx, ToObject(cx, args.get(0))); + if (!argObj) + return false; + + RootedValue highWaterMark(cx); + if (!GetProperty(cx, argObj, argObj, cx->names().highWaterMark, &highWaterMark)) + return false; + + if (!SetProperty(cx, strategy, cx->names().highWaterMark, highWaterMark)) + return false; + + args.rval().setObject(*strategy); + return true; +} + +// Streams spec 6.2.3.1. size ( chunk ) +bool +CountQueuingStrategy_size(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: Return 1. + args.rval().setInt32(1); + return true; +} + +static const JSPropertySpec CountQueuingStrategy_properties[] = { + JS_PS_END +}; + +static const JSFunctionSpec CountQueuingStrategy_methods[] = { + JS_FN("size", CountQueuingStrategy_size, 0, 0), + JS_FS_END +}; + +CLASS_SPEC(CountQueuingStrategy, 1, 0, 0); + +#undef CLASS_SPEC + +// Streams spec, 6.3.1. DequeueValue ( container ) nothrow +inline static MOZ_MUST_USE bool +DequeueValue(JSContext* cx, HandleNativeObject container, MutableHandleValue chunk) +{ + // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal + // slots. + MOZ_ASSERT(IsReadableStreamController(container)); + + // Step 2: Assert: queue is not empty. + RootedValue val(cx, container->getFixedSlot(QueueContainerSlot_Queue)); + RootedNativeObject queue(cx, &val.toObject().as()); + MOZ_ASSERT(queue->getDenseInitializedLength() > 0); + + // Step 3. Let pair be the first element of queue. + // Step 4. Remove pair from queue, shifting all other elements downward + // (so that the second becomes the first, and so on). + Rooted pair(cx, ShiftFromList(cx, queue)); + MOZ_ASSERT(pair); + + // Step 5: Set container.[[queueTotalSize]] to + // container.[[queueTotalSize]] − pair.[[size]]. + // Step 6: If container.[[queueTotalSize]] < 0, set + // container.[[queueTotalSize]] to 0. + // (This can occur due to rounding errors.) + double totalSize = container->getFixedSlot(QueueContainerSlot_TotalSize).toNumber(); + + totalSize -= pair->size(); + if (totalSize < 0) + totalSize = 0; + container->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(totalSize)); + + // Step 7: Return pair.[[value]]. + chunk.set(pair->value()); + return true; +} + +// Streams spec, 6.3.2. EnqueueValueWithSize ( container, value, size ) throws +static MOZ_MUST_USE bool +EnqueueValueWithSize(JSContext* cx, HandleNativeObject container, HandleValue value, + HandleValue sizeVal) +{ + // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal + // slots. + MOZ_ASSERT(IsReadableStreamController(container)); + + // Step 2: Let size be ? ToNumber(size). + double size; + if (!ToNumber(cx, sizeVal, &size)) + return false; + + // Step 3: If ! IsFiniteNonNegativeNumber(size) is false, throw a RangeError + // exception. + if (size < 0 || mozilla::IsNaN(size) || mozilla::IsInfinite(size)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NUMBER_MUST_BE_FINITE_NON_NEGATIVE, "size"); + return false; + } + + // 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()); + + QueueEntry* entry = QueueEntry::create(cx, value, 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; +} + +// Streams spec, 6.3.3. PeekQueueValue ( container ) nothrow +// Used by WritableStream. +// static MOZ_MUST_USE Value +// PeekQueueValue(NativeObject* container) +// { +// // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal +// // slots. +// MOZ_ASSERT(IsReadableStreamController(container)); + +// // Step 2: Assert: queue is not empty. +// Value val = container->getFixedSlot(QueueContainerSlot_Queue); +// NativeObject* queue = &val.toObject().as(); +// MOZ_ASSERT(queue->getDenseInitializedLength() > 0); + +// // Step 3: Let pair be the first element of container.[[queue]]. +// QueueEntry* pair = PeekList(queue); + +// // Step 4: Return pair.[[value]]. +// return pair->value(); +// } + +/** + * Streams spec, 6.3.4. ResetQueue ( container ) nothrow + */ +inline static MOZ_MUST_USE bool +ResetQueue(JSContext* cx, HandleNativeObject container) +{ + // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal + // slots. + MOZ_ASSERT(IsReadableStreamController(container)); + + // Step 2: Set container.[[queue]] to a new empty List. + if (!SetNewList(cx, container, QueueContainerSlot_Queue)) + return false; + + // Step 3: Set container.[[queueTotalSize]] to 0. + container->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(0)); + + return true; +} + + +/** + * Streams spec, 6.4.1. InvokeOrNoop ( O, P, args ) + */ +inline static MOZ_MUST_USE bool +InvokeOrNoop(JSContext* cx, HandleValue O, HandlePropertyName P, HandleValue arg, + MutableHandleValue rval) +{ + // Step 1: Assert: P is a valid property key (omitted). + // Step 2: If args was not passed, let args be a new empty List (omitted). + // Step 3: Let method be ? GetV(O, P). + RootedValue method(cx); + if (!GetProperty(cx, O, P, &method)) + return false; + + // Step 4: If method is undefined, return. + if (method.isUndefined()) + return true; + + // Step 5: Return ? Call(method, O, args). + return Call(cx, method, O, arg, rval); +} + +/** + * Streams spec, 6.4.3. PromiseInvokeOrNoop ( O, P, args ) + * Specialized to one arg, because that's what all stream related callers use. + */ +static MOZ_MUST_USE JSObject* +PromiseInvokeOrNoop(JSContext* cx, HandleValue O, HandlePropertyName P, HandleValue arg) +{ + // Step 1: Assert: O is not undefined. + MOZ_ASSERT(!O.isUndefined()); + + // Step 2: Assert: ! IsPropertyKey(P) is true (implicit). + // Step 3: Assert: args is a List (omitted). + + // Step 4: Let returnValue be InvokeOrNoop(O, P, args). + // Step 5: If returnValue is an abrupt completion, return a promise + // rejected with returnValue.[[Value]]. + RootedValue returnValue(cx); + if (!InvokeOrNoop(cx, O, P, arg, &returnValue)) + return PromiseRejectedWithPendingError(cx); + + // Step 6: Otherwise, return a promise resolved with returnValue.[[Value]]. + return PromiseObject::unforgeableResolve(cx, returnValue); +} + +/** + * Streams spec, 6.4.4 TransferArrayBuffer ( O ) + */ +static MOZ_MUST_USE ArrayBufferObject* +TransferArrayBuffer(JSContext* cx, HandleObject buffer) +{ + // Step 1 (implicit). + + // Step 2. + MOZ_ASSERT(buffer->is()); + + // Step 3. + MOZ_ASSERT(!JS_IsDetachedArrayBufferObject(buffer)); + + // Step 5 (reordered). + uint32_t size = buffer->as().byteLength(); + + // Steps 4, 6. + void* contents = JS_StealArrayBufferContents(cx, buffer); + if (!contents) + return nullptr; + MOZ_ASSERT(JS_IsDetachedArrayBufferObject(buffer)); + + // Step 7. + RootedObject transferredBuffer(cx, JS_NewArrayBufferWithContents(cx, size, contents)); + if (!transferredBuffer) + return nullptr; + return &transferredBuffer->as(); +} + +// Streams spec, 6.4.5. ValidateAndNormalizeHighWaterMark ( highWaterMark ) +static MOZ_MUST_USE bool +ValidateAndNormalizeHighWaterMark(JSContext* cx, HandleValue highWaterMarkVal, double* highWaterMark) +{ + // Step 1: Set highWaterMark to ? ToNumber(highWaterMark). + if (!ToNumber(cx, highWaterMarkVal, highWaterMark)) + return false; + + // Step 2: If highWaterMark is NaN, throw a TypeError exception. + // Step 3: If highWaterMark < 0, throw a RangeError exception. + if (mozilla::IsNaN(*highWaterMark) || *highWaterMark < 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_STREAM_INVALID_HIGHWATERMARK); + return false; + } + + // Step 4: Return highWaterMark. + return true; +} + +// Streams spec, 6.4.6. ValidateAndNormalizeQueuingStrategy ( size, highWaterMark ) +static MOZ_MUST_USE bool +ValidateAndNormalizeQueuingStrategy(JSContext* cx, HandleValue size, + HandleValue highWaterMarkVal, double* highWaterMark) +{ + // Step 1: If size is not undefined and ! IsCallable(size) is false, throw a + // TypeError exception. + if (!size.isUndefined() && !IsCallable(size)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, + "ReadableStream argument options.size"); + return false; + } + + // Step 2: Let highWaterMark be ? ValidateAndNormalizeHighWaterMark(highWaterMark). + if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal, highWaterMark)) + return false; + + // Step 3: Return Record {[[size]]: size, [[highWaterMark]]: highWaterMark}. + return true; +} diff --git a/js/src/builtin/Stream.h b/js/src/builtin/Stream.h new file mode 100644 index 0000000000..90b61b49af --- /dev/null +++ b/js/src/builtin/Stream.h @@ -0,0 +1,118 @@ +/* -*- 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_Stream_h +#define builtin_Stream_h + +#include "vm/NativeObject.h" + +namespace js { + +class AutoSetNewObjectMetadata; + +class ReadableStream : public NativeObject +{ + public: + static ReadableStream* createDefaultStream(JSContext* cx, HandleValue underlyingSource, + HandleValue size, HandleValue highWaterMark); + static ReadableStream* createByteStream(JSContext* cx, HandleValue underlyingSource, + HandleValue highWaterMark); + + inline bool readable() const; + inline bool closed() const; + inline bool errored() const; + inline bool disturbed() const; + + enum State { + Readable = 1 << 0, + Closed = 1 << 1, + Errored = 1 << 2, + Disturbed = 1 << 3 + }; + + private: + static ReadableStream* createStream(JSContext* cx); + + public: + static bool constructor(JSContext* cx, unsigned argc, Value* vp); + static const ClassSpec classSpec_; + static const Class class_; + static const ClassSpec protoClassSpec_; + static const Class protoClass_; +}; + +class ReadableStreamDefaultReader : public NativeObject +{ + public: + static bool constructor(JSContext* cx, unsigned argc, Value* vp); + static const ClassSpec classSpec_; + static const Class class_; + static const ClassSpec protoClassSpec_; + static const Class protoClass_; +}; + +class ReadableStreamBYOBReader : public NativeObject +{ + public: + static bool constructor(JSContext* cx, unsigned argc, Value* vp); + static const ClassSpec classSpec_; + static const Class class_; + static const ClassSpec protoClassSpec_; + static const Class protoClass_; +}; + +class ReadableStreamDefaultController : public NativeObject +{ + public: + static bool constructor(JSContext* cx, unsigned argc, Value* vp); + static const ClassSpec classSpec_; + static const Class class_; + static const ClassSpec protoClassSpec_; + static const Class protoClass_; +}; + +class ReadableByteStreamController : public NativeObject +{ + public: + static bool constructor(JSContext* cx, unsigned argc, Value* vp); + static const ClassSpec classSpec_; + static const Class class_; + static const ClassSpec protoClassSpec_; + static const Class protoClass_; +}; + +class ReadableStreamBYOBRequest : public NativeObject +{ + public: + static bool constructor(JSContext* cx, unsigned argc, Value* vp); + static const ClassSpec classSpec_; + static const Class class_; + static const ClassSpec protoClassSpec_; + static const Class protoClass_; +}; + +class ByteLengthQueuingStrategy : public NativeObject +{ + public: + static bool constructor(JSContext* cx, unsigned argc, Value* vp); + static const ClassSpec classSpec_; + static const Class class_; + static const ClassSpec protoClassSpec_; + static const Class protoClass_; +}; + +class CountQueuingStrategy : public NativeObject +{ + public: + static bool constructor(JSContext* cx, unsigned argc, Value* vp); + static const ClassSpec classSpec_; + static const Class class_; + static const ClassSpec protoClassSpec_; + static const Class protoClass_; +}; + +} // namespace js + +#endif /* builtin_Stream_h */ diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index 4e962458b0..f8ac4fd22e 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -1441,6 +1441,14 @@ RejectPromise(JSContext* cx, unsigned argc, Value* vp) return result; } +static bool +StreamsAreEnabled(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(cx->options().streams()); + return true; +} + static unsigned finalizeCount = 0; static void @@ -4178,6 +4186,10 @@ JS_FN_HELP("rejectPromise", RejectPromise, 2, 0, "rejectPromise(promise, reason)", " Reject a Promise by calling the JSAPI function JS::RejectPromise."), +JS_FN_HELP("streamsAreEnabled", StreamsAreEnabled, 0, 0, +"streamsAreEnabled()", +" Returns a boolean indicating whether WHATWG Streams are enabled for the current compartment."), + JS_FN_HELP("makeFinalizeObserver", MakeFinalizeObserver, 0, 0, "makeFinalizeObserver()", " Get a special object whose finalization increases the counter returned\n" diff --git a/js/src/js.msg b/js/src/js.msg index e651b10137..3c7f12ae5e 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -613,11 +613,38 @@ MSG_DEF(JSMSG_RETURN_NOT_CALLABLE, 0, JSEXN_TYPEERR, "property 'return' of i MSG_DEF(JSMSG_ITERATOR_NO_THROW, 0, JSEXN_TYPEERR, "iterator does not have a 'throw' method") // Async Iteration -MSG_DEF(JSMSG_FOR_AWAIT_NOT_OF, 0, JSEXN_TYPEERR, "'for await' loop should be used with 'of'") +MSG_DEF(JSMSG_FOR_AWAIT_NOT_OF, 0, JSEXN_SYNTAXERR, "'for await' loop should be used with 'of'") MSG_DEF(JSMSG_NOT_AN_ASYNC_GENERATOR, 0, JSEXN_TYPEERR, "Not an async generator") MSG_DEF(JSMSG_NOT_AN_ASYNC_ITERATOR, 0, JSEXN_TYPEERR, "Not an async from sync iterator") MSG_DEF(JSMSG_GET_ASYNC_ITER_RETURNED_PRIMITIVE, 0, JSEXN_TYPEERR, "[Symbol.asyncIterator]() returned a non-object value") +// ReadableStream +MSG_DEF(JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG,0, JSEXN_RANGEERR,"'underlyingSource.type' must be \"bytes\" or undefined.") +MSG_DEF(JSMSG_READABLESTREAM_INVALID_READER_MODE, 0, JSEXN_RANGEERR,"'mode' must be \"byob\" or undefined.") +MSG_DEF(JSMSG_NUMBER_MUST_BE_FINITE_NON_NEGATIVE, 1, JSEXN_RANGEERR, "'{0}' must be a finite, non-negative number.") +MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_BYTESWRITTEN, 0, JSEXN_RANGEERR, "'bytesWritten' exceeds remaining length.") +MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_SIZE, 0, JSEXN_RANGEERR, "view size does not match requested data.") +MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_OFFSET, 0, JSEXN_RANGEERR, "view offset does not match requested position.") +MSG_DEF(JSMSG_READABLESTREAM_NOT_LOCKED, 1, JSEXN_TYPEERR, "The ReadableStream method '{0}' may only be called on a locked stream.") +MSG_DEF(JSMSG_READABLESTREAM_LOCKED, 0, JSEXN_TYPEERR, "A Reader may only be created for an unlocked ReadableStream.") +MSG_DEF(JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER, 0, JSEXN_TYPEERR, "ReadableStream.getReader('byob') requires a ReadableByteStreamController.") +MSG_DEF(JSMSG_READABLESTREAM_CONTROLLER_SET, 0, JSEXN_TYPEERR, "The ReadableStream already has a controller defined.") +MSG_DEF(JSMSG_READABLESTREAMREADER_NOT_OWNED, 1, JSEXN_TYPEERR, "The ReadableStream reader method '{0}' may only be called on a reader owned by a stream.") +MSG_DEF(JSMSG_READABLESTREAMREADER_NOT_EMPTY, 1, JSEXN_TYPEERR, "The ReadableStream reader method '{0}' may not be called on a reader with read requests.") +MSG_DEF(JSMSG_READABLESTREAMBYOBREADER_READ_EMPTY_VIEW, 0, JSEXN_TYPEERR, "ReadableStreamBYOBReader.read() was passed an empty TypedArrayBuffer view.") +MSG_DEF(JSMSG_READABLESTREAMREADER_RELEASED, 0, JSEXN_TYPEERR, "The ReadableStream reader was released.") +MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_CLOSED, 1, JSEXN_TYPEERR, "The ReadableStream controller method '{0}' called on a stream already closing.") +MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, 1, JSEXN_TYPEERR, "The ReadableStream controller method '{0}' may only be called on a stream in the 'readable' state.") +MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNKSIZE,0, JSEXN_RANGEERR, "ReadableByteStreamController requires a positive integer or undefined for 'autoAllocateChunkSize'.") +MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK, 0, JSEXN_TYPEERR, "ReadableByteStreamController passed a bad chunk.") +MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_CLOSE_PENDING_PULL, 0, JSEXN_TYPEERR, "The ReadableByteStreamController cannot be closed while the buffer is being filled.") +MSG_DEF(JSMSG_READABLESTREAMBYOBREQUEST_NO_CONTROLLER, 1, JSEXN_TYPEERR, "ReadableStreamBYOBRequest method '{0}' called on a request with no controller.") +MSG_DEF(JSMSG_READABLESTREAMBYOBREQUEST_RESPOND_CLOSED, 0, JSEXN_TYPEERR, "ReadableStreamBYOBRequest method 'respond' called with non-zero number of bytes with a closed controller.") +MSG_DEF(JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, 1, JSEXN_TYPEERR, "ReadableStream method {0} not yet implemented") + +// Other Stream-related +MSG_DEF(JSMSG_STREAM_INVALID_HIGHWATERMARK, 0, JSEXN_RANGEERR, "'highWaterMark' must be a non-negative, non-NaN number.") + // BigInt MSG_DEF(JSMSG_BIGINT_TO_NUMBER, 0, JSEXN_TYPEERR, "can't convert BigInt to number") MSG_DEF(JSMSG_NUMBER_TO_BIGINT, 0, JSEXN_RANGEERR, "can't convert non-finite number to BigInt") diff --git a/js/src/jsfriendapi.h b/js/src/jsfriendapi.h index b4c4e15801..76537dc739 100644 --- a/js/src/jsfriendapi.h +++ b/js/src/jsfriendapi.h @@ -2005,6 +2005,12 @@ JS_IsArrayBufferViewObject(JSObject* obj); extern JS_FRIEND_API(uint32_t) JS_GetArrayBufferViewByteLength(JSObject* obj); +/** + * More generic name for JS_GetTypedArrayByteOffset to cover DataViews as well + */ +extern JS_FRIEND_API(uint32_t) +JS_GetArrayBufferViewByteOffset(JSObject* obj); + /* * Return a pointer to the start of the data referenced by a typed array. The * data is still owned by the typed array, and should not be modified on diff --git a/js/src/jsnum.cpp b/js/src/jsnum.cpp index fd23e6ccd5..beff701bd0 100644 --- a/js/src/jsnum.cpp +++ b/js/src/jsnum.cpp @@ -1003,6 +1003,13 @@ static const JSFunctionSpec number_methods[] = { JS_FS_END }; +bool +js::IsInteger(const Value& val) +{ + return val.isInt32() || + (mozilla::IsFinite(val.toDouble()) && JS::ToInteger(val.toDouble()) == val.toDouble()); +} + // ES6 draft ES6 15.7.3.12 static bool Number_isInteger(JSContext* cx, unsigned argc, Value* vp) @@ -1013,9 +1020,7 @@ Number_isInteger(JSContext* cx, unsigned argc, Value* vp) return true; } Value val = args[0]; - args.rval().setBoolean(val.isInt32() || - (mozilla::IsFinite(val.toDouble()) && - JS::ToInteger(val.toDouble()) == val.toDouble())); + args.rval().setBoolean(js::IsInteger(val)); return true; } diff --git a/js/src/jsnum.h b/js/src/jsnum.h index ee07d0a35d..7c96bfb652 100644 --- a/js/src/jsnum.h +++ b/js/src/jsnum.h @@ -72,6 +72,10 @@ Int32ToString(ExclusiveContext* cx, int32_t i); extern JSAtom* Int32ToAtom(ExclusiveContext* cx, int32_t si); +// ES6 15.7.3.12 +extern bool +IsInteger(const Value& val); + /* * Convert an integer or double (contained in the given value) to a string and * append to the given buffer. diff --git a/js/src/jsprototypes.h b/js/src/jsprototypes.h index 52d0d8ea4a..9822a47b6c 100644 --- a/js/src/jsprototypes.h +++ b/js/src/jsprototypes.h @@ -108,6 +108,17 @@ IF_SAB(real,imaginary)(Atomics, InitAtomicsClass, OCLASP(Atomics)) \ imaginary(WasmMemory, dummy, dummy) \ imaginary(WasmTable, dummy, dummy) \ real(Promise, InitViaClassSpec, OCLASP(Promise)) \ + real(ReadableStream, InitViaClassSpec, &js::ReadableStream::class_) \ + real(ReadableStreamDefaultReader, InitViaClassSpec, &js::ReadableStreamDefaultReader::class_) \ + real(ReadableStreamBYOBReader, InitViaClassSpec, &js::ReadableStreamBYOBReader::class_) \ + real(ReadableStreamDefaultController, InitViaClassSpec, &js::ReadableStreamDefaultController::class_) \ + real(ReadableByteStreamController, InitViaClassSpec, &js::ReadableByteStreamController::class_) \ + real(ReadableStreamBYOBRequest, InitViaClassSpec, &js::ReadableStreamBYOBRequest::class_) \ + imaginary(WritableStream, dummy, dummy) \ + imaginary(WritableStreamDefaultWriter, dummy, dummy) \ + imaginary(WritableStreamDefaultController,dummy, dummy) \ + real(ByteLengthQueuingStrategy, InitViaClassSpec, &js::ByteLengthQueuingStrategy::class_) \ + real(CountQueuingStrategy, InitViaClassSpec, &js::CountQueuingStrategy::class_) \ #define JS_FOR_EACH_PROTOTYPE(macro) JS_FOR_PROTOTYPES(macro,macro) diff --git a/js/src/moz.build b/js/src/moz.build index 5429888a2c..adc8aa3e0f 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -384,6 +384,7 @@ main_deunified_sources = [ SOURCES += [ 'builtin/BigInt.cpp', 'builtin/RegExp.cpp', + 'builtin/Stream.cpp', 'frontend/Parser.cpp', 'gc/StoreBuffer.cpp', 'jsarray.cpp', diff --git a/js/src/vm/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index 9e0bf3f4da..6ace034adb 100644 --- a/js/src/vm/ArrayBufferObject.cpp +++ b/js/src/vm/ArrayBufferObject.cpp @@ -1891,6 +1891,17 @@ JS_GetArrayBufferViewByteLength(JSObject* obj) : obj->as().byteLength(); } +JS_FRIEND_API(uint32_t) +JS_GetArrayBufferViewByteOffset(JSObject* obj) +{ + obj = CheckedUnwrap(obj); + if (!obj) + return 0; + return obj->is() + ? obj->as().byteOffset() + : obj->as().byteOffset(); +} + JS_FRIEND_API(JSObject*) JS_GetObjectAsArrayBufferView(JSObject* obj, uint32_t* length, bool* isSharedMemory, uint8_t** data) { diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index f05a4db9be..2a217e357f 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -35,6 +35,7 @@ macro(AsyncGeneratorFunction, AsyncGeneratorFunction, "AsyncGeneratorFunction") \ macro(AsyncWrapped, AsyncWrapped, "AsyncWrapped") \ macro(async, async, "async") \ + macro(autoAllocateChunkSize, autoAllocateChunkSize, "autoAllocateChunkSize") \ macro(await, await, "await") \ macro(bigint64, bigint64, "bigint64") \ macro(biguint64, biguint64, "biguint64") \ @@ -48,6 +49,7 @@ macro(buffer, buffer, "buffer") \ macro(builder, builder, "builder") \ macro(by, by, "by") \ + macro(byob, byob, "byob") \ macro(byteAlignment, byteAlignment, "byteAlignment") \ macro(byteLength, byteLength, "byteLength") \ macro(byteOffset, byteOffset, "byteOffset") \ @@ -59,6 +61,7 @@ macro(callee, callee, "callee") \ macro(caller, caller, "caller") \ macro(callFunction, callFunction, "callFunction") \ + macro(cancel, cancel, "cancel") \ macro(case, case_, "case") \ macro(caseFirst, caseFirst, "caseFirst") \ macro(catch, catch_, "catch") \ @@ -180,6 +183,7 @@ macro(has, has, "has") \ macro(hasOwn, hasOwn, "hasOwn") \ macro(hasOwnProperty, hasOwnProperty, "hasOwnProperty") \ + macro(highWaterMark, highWaterMark, "highWaterMark") \ macro(hour, hour, "hour") \ macro(hourCycle, hourCycle, "hourCycle") \ macro(if, if_, "if") \ @@ -250,6 +254,7 @@ macro(minusSign, minusSign, "minusSign") \ macro(minute, minute, "minute") \ macro(missingArguments, missingArguments, "missingArguments") \ + macro(mode, mode, "mode") \ macro(module, module, "module") \ macro(Module, Module, "Module") \ macro(ModuleInstantiate, ModuleInstantiate, "ModuleInstantiate") \ @@ -312,6 +317,7 @@ macro(prototype, prototype, "prototype") \ macro(proxy, proxy, "proxy") \ macro(public, public_, "public") \ + macro(pull, pull, "pull") \ macro(raw, raw, "raw") \ macro(reason, reason, "reason") \ macro(RegExpFlagsGetter, RegExpFlagsGetter, "RegExpFlagsGetter") \ @@ -347,6 +353,7 @@ macro(StarGeneratorNext, StarGeneratorNext, "StarGeneratorNext") \ macro(StarGeneratorReturn, StarGeneratorReturn, "StarGeneratorReturn") \ macro(StarGeneratorThrow, StarGeneratorThrow, "StarGeneratorThrow") \ + macro(start, start, "start") \ macro(startTimestamp, startTimestamp, "startTimestamp") \ macro(state, state, "state") \ macro(static, static_, "static") \ diff --git a/js/src/vm/GlobalObject.cpp b/js/src/vm/GlobalObject.cpp index b7d3344b3e..1321be9e9b 100644 --- a/js/src/vm/GlobalObject.cpp +++ b/js/src/vm/GlobalObject.cpp @@ -23,6 +23,7 @@ #include "builtin/Promise.h" #include "builtin/RegExp.h" #include "builtin/SelfHostingDefines.h" +#include "builtin/Stream.h" #include "builtin/SymbolObject.h" #include "builtin/TypedObject.h" #include "builtin/WeakMapObject.h" @@ -98,6 +99,16 @@ GlobalObject::skipDeselectedConstructor(JSContext* cx, JSProtoKey key) case JSProto_WebAssembly: return !wasm::HasSupport(cx); + case JSProto_ReadableStream: + case JSProto_ReadableStreamDefaultReader: + case JSProto_ReadableStreamBYOBReader: + case JSProto_ReadableStreamDefaultController: + case JSProto_ReadableByteStreamController: + case JSProto_ReadableStreamBYOBRequest: + case JSProto_ByteLengthQueuingStrategy: + case JSProto_CountQueuingStrategy: + return !cx->options().streams(); + #ifdef ENABLE_SHARED_ARRAY_BUFFER case JSProto_Atomics: case JSProto_SharedArrayBuffer: diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index e8e6ad937a..e006846752 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -36,6 +36,7 @@ #include "builtin/Promise.h" #include "builtin/Reflect.h" #include "builtin/SelfHostingDefines.h" +#include "builtin/Stream.h" #include "builtin/TypedObject.h" #include "builtin/WeakSetObject.h" #include "gc/Marking.h" @@ -71,7 +72,6 @@ using JS::AutoCheckCannotGC; using mozilla::IsInRange; using mozilla::Maybe; using mozilla::PodMove; -using mozilla::Maybe; static void selfHosting_WarningReporter(JSContext* cx, JSErrorReport* report) @@ -2490,6 +2490,9 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FN("CallWeakSetMethodIfWrapped", CallNonGenericSelfhostedMethod>, 2, 0), + JS_FN("IsReadableStreamBYOBRequest", + intrinsic_IsInstanceOfBuiltin, 1, 0), + // See builtin/TypedObject.h for descriptors of the typedobj functions. JS_FN("NewOpaqueTypedObject", js::NewOpaqueTypedObject, 1, 0), JS_FN("NewDerivedTypedObject", js::NewDerivedTypedObject, 3, 0), From 9b9075eef67bbe52a72e8f801847f5b559f45987 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 17:33:34 -0500 Subject: [PATCH 04/31] Issue #1442 - Part 4: Add JSAPI functions for working with ReadableStream. https://bugzilla.mozilla.org/show_bug.cgi?id=1272697 --- js/public/Stream.h | 510 ++++++++++++ js/src/builtin/Stream.cpp | 914 +++++++++++++++++----- js/src/builtin/Stream.h | 66 +- js/src/js.msg | 11 +- js/src/jsapi-tests/moz.build | 5 + js/src/jsapi-tests/testIntTypesABI.cpp | 1 + js/src/jsapi-tests/testReadableStream.cpp | 676 ++++++++++++++++ js/src/jsapi-tests/tests.cpp | 4 + js/src/jsapi.cpp | 366 ++++++++- js/src/jsapi.h | 1 + js/src/moz.build | 1 + js/src/vm/CommonPropertyNames.h | 46 ++ js/src/vm/Runtime.cpp | 6 + js/src/vm/Runtime.h | 7 + 14 files changed, 2409 insertions(+), 205 deletions(-) create mode 100644 js/public/Stream.h create mode 100644 js/src/jsapi-tests/testReadableStream.cpp diff --git a/js/public/Stream.h b/js/public/Stream.h new file mode 100644 index 0000000000..b91239ac9a --- /dev/null +++ b/js/public/Stream.h @@ -0,0 +1,510 @@ +/* -*- 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/. */ + +/* + * JSAPI functions and callbacks related to WHATWG Stream objects. + * + * Much of the API here mirrors the JS API of ReadableStream and associated + * classes, e.g. ReadableStreamDefaultReader, ReadableStreamBYOBReader, + * ReadableStreamDefaultController, ReadableByteStreamController, and + * ReadableStreamBYOBRequest. + * + * There are some crucial differences, though: Functionality that's exposed + * as methods/accessors on controllers in JS is exposed as functions taking + * ReadableStream instances instead. This is because an analysis of how + * the API would be used showed that all functions that'd take controllers + * would do so by first getting the controller from the stream instance it's + * associated with and then call the function taking it. I.e., it would purely + * add boilerplate without any gains in ease of use of the API. + * + * Some functions exposed here deal with ReadableStream instances that have an + * embedding-provided underlying source. These instances are largely similar + * to byte streams as created using |new ReadableStream({type: "bytes"})|: + * They enable users to acquire ReadableStreamBYOBReaders and only vend chunks + * that're typed array instances. + * + * When creating an "external readable stream" using + * JS::NewReadableExternalSourceStreamObject, an underlying source and a set + * of flags can be passed to be stored on the stream. The underlying source is + * treated as an opaque void* pointer by the JS engine: it's purely meant as + * a reference to be used by the embedding to identify whatever actual source + * it uses to supply data for the stream. Similarly, the flags aren't + * interpreted by the JS engine, but are passed to some of the callbacks below + * and can be retrieved using JS::ReadableStreamGetEmbeddingFlags. + * + * External readable streams are optimized to allow the embedding to interact + * with them with a minimum of overhead: chunks aren't enqueued as individual + * typed array instances; instead, the embedding only updates the amount of + * data available using ReadableStreamUpdateDataAvailableFromSource. + * When content requests data by reading from a reader, + * WriteIntoReadRequestBufferCallback is invoked, asking the embedding to + * write data directly into the buffer we're about to hand to content. + * + * Additionally, ReadableStreamGetExternalUnderlyingSource can be used to + * get the void* pointer to the underlying source. This is equivalent to + * acquiring a reader for the stream in that it locks the stream until it + * is released again using JS::ReadableStreamReleaseExternalUnderlyingSource. + * + * Embeddings are expected to detect situations where an API exposed to JS + * takes a ReadableStream to read from that has an external underlying source. + * In those situations, it might be preferable to directly perform data + * transfers from the stream's underlying source to whatever sink the + * embedding uses, assuming that such direct transfers can be performed + * more efficiently. + */ + +#ifndef js_Stream_h +#define js_Stream_h + +#include "jstypes.h" + +#include "js/TypeDecls.h" + +namespace JS { + +/** + * Invoked whenever a reader desires more data from a ReadableStream's + * embedding-provided underlying source. + * + * The given |desiredSize| is the absolute size, not a delta from the previous + * desired size. + */ +typedef void +(* RequestReadableStreamDataCallback)(JSContext* cx, HandleObject stream, + void* underlyingSource, uint8_t flags, size_t desiredSize); + +/** + * Invoked to cause the embedding to fill the given |buffer| with data from + * the given embedding-provided underlying source. + * + * This can only happen after the embedding has updated the amount of data + * available using JS::ReadableStreamUpdateDataAvailableFromSource. If at + * least one read request is pending when + * JS::ReadableStreamUpdateDataAvailableFromSource is called, + * the WriteIntoReadRequestBufferCallback is invoked immediately from under + * the call to JS::WriteIntoReadRequestBufferCallback. If not, it is invoked + * if and when a new read request is made. + * + * Note: This callback *must not cause GC*, because that could potentially + * invalidate the |buffer| pointer. + */ +typedef void +(* WriteIntoReadRequestBufferCallback)(JSContext* cx, HandleObject stream, + void* underlyingSource, uint8_t flags, void* buffer, + size_t length, size_t* bytesWritten); + +/** + * Invoked in reaction to the ReadableStream being canceled to allow the + * embedding to free the underlying source. + * + * This is equivalent to calling |cancel| on non-external underlying sources + * provided to the ReadableStream constructor in JavaScript. + * + * The given |reason| is the JS::Value that was passed as an argument to + * ReadableStream#cancel(). + * + * The returned JS::Value will be used to resolve the Promise returned by + * ReadableStream#cancel(). + */ +typedef Value +(* CancelReadableStreamCallback)(JSContext* cx, HandleObject stream, + void* underlyingSource, uint8_t flags, HandleValue reason); + +/** + * Invoked in reaction to a ReadableStream with an embedding-provided + * underlying source being closed. + */ +typedef void +(* ReadableStreamClosedCallback)(JSContext* cx, HandleObject stream, void* underlyingSource, + uint8_t flags); + +/** + * Invoked in reaction to a ReadableStream with an embedding-provided + * underlying source being errored with the + * given reason. + */ +typedef void +(* ReadableStreamErroredCallback)(JSContext* cx, HandleObject stream, void* underlyingSource, + uint8_t flags, HandleValue reason); + +/** + * Invoked in reaction to a ReadableStream with an embedding-provided + * underlying source being finalized. Only the underlying source is passed + * as an argument, while the ReadableStream itself is not to prevent the + * embedding from operating on a JSObject that might not be in a valid state + * anymore. + * + * Note: the ReadableStream might be finalized on a background thread. That + * means this callback might be invoked from an arbitrary thread, which the + * embedding must be able to handle. + */ +typedef void +(* ReadableStreamFinalizeCallback)(void* underlyingSource, uint8_t flags); + +/** + * Sets runtime-wide callbacks to use for interacting with embedding-provided + * hooks for operating on ReadableStream instances. + * + * See the documentation for the individual callback types for details. + */ +extern JS_PUBLIC_API(void) +SetReadableStreamCallbacks(JSContext* cx, + RequestReadableStreamDataCallback dataRequestCallback, + WriteIntoReadRequestBufferCallback writeIntoReadRequestCallback, + CancelReadableStreamCallback cancelCallback, + ReadableStreamClosedCallback closedCallback, + ReadableStreamErroredCallback erroredCallback, + ReadableStreamFinalizeCallback finalizeCallback); + +extern JS_PUBLIC_API(bool) +HasReadableStreamCallbacks(JSContext* cx); + +/** + * Returns a new instance of the ReadableStream builtin class in the current + * compartment, configured as a default stream. + * If a |proto| is passed, that gets set as the instance's [[Prototype]] + * instead of the original value of |ReadableStream.prototype|. + */ +extern JS_PUBLIC_API(JSObject*) +NewReadableDefaultStreamObject(JSContext* cx, HandleObject underlyingSource = nullptr, + HandleFunction size = nullptr, double highWaterMark = 1, + HandleObject proto = nullptr); + +/** + * Returns a new instance of the ReadableStream builtin class in the current + * compartment, configured as a byte stream. + * If a |proto| is passed, that gets set as the instance's [[Prototype]] + * instead of the original value of |ReadableStream.prototype|. + */ +extern JS_PUBLIC_API(JSObject*) +NewReadableByteStreamObject(JSContext* cx, HandleObject underlyingSource = nullptr, + double highWaterMark = 0, HandleObject proto = nullptr); + +/** + * Returns a new instance of the ReadableStream builtin class in the current + * compartment, with the right slot layout. If a |proto| is passed, that gets + * set as the instance's [[Prototype]] instead of the original value of + * |ReadableStream.prototype|. + * + * The instance is optimized for operating as a byte stream backed by an + * embedding-provided underlying source, using the callbacks set via + * |JS::SetReadableStreamCallbacks|. + * + * The given |flags| will be passed to all applicable callbacks and can be + * used to disambiguate between different types of stream sources the + * embedding might support. + * + * Note: the embedding is responsible for ensuring that the pointer to the + * underlying source stays valid as long as the stream can be read from. + * The underlying source can be freed if the tree is canceled or errored. + * It can also be freed if the stream is destroyed. The embedding is notified + * of that using ReadableStreamFinalizeCallback. + */ +extern JS_PUBLIC_API(JSObject*) +NewReadableExternalSourceStreamObject(JSContext* cx, void* underlyingSource, + uint8_t flags = 0, HandleObject proto = nullptr); + +/** + * Returns the flags that were passed to NewReadableExternalSourceStreamObject + * when creating the given stream. + * + * Asserts that the given stream has an embedding-provided underlying source. + */ +extern JS_PUBLIC_API(uint8_t) +ReadableStreamGetEmbeddingFlags(const JSObject* stream); + +/** + * Returns the embedding-provided underlying source of the given |stream|. + * + * Can be used to optimize operations if both the underlying source and the + * intended sink are embedding-provided. In that case it might be + * preferrable to pipe data directly from source to sink without interacting + * with the stream at all. + * + * Locks the stream until ReadableStreamReleaseExternalUnderlyingSource is + * called. + * + * Throws an exception if the stream is locked, i.e. if a reader has been + * acquired for the stream, or if ReadableStreamGetExternalUnderlyingSource + * has been used previously without releasing the external source again. + * + * Throws an exception if the stream isn't readable, i.e if it is errored or + * closed. This is different from ReadableStreamGetReader because we don't + * have a Promise to resolve/reject, which a reader provides. + * + * Asserts that the stream has an embedding-provided underlying source. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamGetExternalUnderlyingSource(JSContext* cx, HandleObject stream, void** source); + +/** + * Releases the embedding-provided underlying source of the given |stream|, + * returning the stream into an unlocked state. + * + * Asserts that the stream was locked through + * ReadableStreamGetExternalUnderlyingSource. + * + * Asserts that the stream has an embedding-provided underlying source. + */ +extern JS_PUBLIC_API(void) +ReadableStreamReleaseExternalUnderlyingSource(JSObject* stream); + +/** + * Update the amount of data available at the underlying source of the given + * |stream|. + * + * Can only be used for streams with an embedding-provided underlying source. + * The JS engine will use the given value to satisfy read requests for the + * stream by invoking the JS::WriteIntoReadRequestBuffer callback. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamUpdateDataAvailableFromSource(JSContext* cx, HandleObject stream, + uint32_t availableData); + +/** + * Returns true if the given object is an unwrapped ReadableStream object, + * false otherwise. + */ +extern JS_PUBLIC_API(bool) +IsReadableStream(const JSObject* obj); + +/** + * Returns true if the given object is an unwrapped + * ReadableStreamDefaultReader or ReadableStreamBYOBReader object, + * false otherwise. + */ +extern JS_PUBLIC_API(bool) +IsReadableStreamReader(const JSObject* obj); + +/** + * Returns true if the given object is an unwrapped + * ReadableStreamDefaultReader object, false otherwise. + */ +extern JS_PUBLIC_API(bool) +IsReadableStreamDefaultReader(const JSObject* obj); + +/** + * Returns true if the given object is an unwrapped + * ReadableStreamBYOBReader object, false otherwise. + */ +extern JS_PUBLIC_API(bool) +IsReadableStreamBYOBReader(const JSObject* obj); + +enum class ReadableStreamMode { + Default, + Byte, + ExternalSource +}; + +/** + * Returns the stream's ReadableStreamMode. If the mode is |Byte| or + * |ExternalSource|, it's possible to acquire a BYOB reader for more optimized + * operations. + * + * Asserts that |stream| is an unwrapped ReadableStream instance. + */ +extern JS_PUBLIC_API(ReadableStreamMode) +ReadableStreamGetMode(const JSObject* stream); + +enum class ReadableStreamReaderMode { + Default, + BYOB +}; + +/** + * Returns true if the given ReadableStream is readable, false if not. + * + * Asserts that |stream| is an unwrapped ReadableStream instance. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamIsReadable(const JSObject* stream); + +/** + * Returns true if the given ReadableStream is locked, false if not. + * + * Asserts that |stream| is an unwrapped ReadableStream instance. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamIsLocked(const JSObject* stream); + +/** + * Returns true if the given ReadableStream is disturbed, false if not. + * + * Asserts that |stream| is an ReadableStream instance. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamIsDisturbed(const JSObject* stream); + +/** + * Cancels the given ReadableStream with the given reason and returns a + * Promise resolved according to the result. + * + * Asserts that |stream| is an unwrapped ReadableStream instance. + */ +extern JS_PUBLIC_API(JSObject*) +ReadableStreamCancel(JSContext* cx, HandleObject stream, HandleValue reason); + +/** + * Creates a reader of the type specified by the mode option and locks the + * stream to the new reader. + * + * Asserts that |stream| is an unwrapped ReadableStream instance. + */ +extern JS_PUBLIC_API(JSObject*) +ReadableStreamGetReader(JSContext* cx, HandleObject stream, ReadableStreamReaderMode mode); + +/** + * Tees the given ReadableStream and stores the two resulting streams in + * outparams. Returns false if the operation fails, e.g. because the stream is + * locked. + * + * Asserts that |stream| is an unwrapped ReadableStream instance. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamTee(JSContext* cx, HandleObject stream, + MutableHandleObject branch1Stream, MutableHandleObject branch2Stream); + +/** + * Retrieves the desired combined size of additional chunks to fill the given + * ReadableStream's queue. Stores the result in |value| and sets |hasValue| to + * true on success, returns false on failure. + * + * If the stream is errored, the call will succeed but no value will be stored + * in |value| and |hasValue| will be set to false. + * + * Note: This is semantically equivalent to the |desiredSize| getter on + * the stream controller's prototype in JS. We expose it with the stream + * itself as a target for simplicity. + * + * Asserts that |stream| is an unwrapped ReadableStream instance. + */ +extern JS_PUBLIC_API(void) +ReadableStreamGetDesiredSize(JSObject* stream, bool* hasValue, double* value); + +/** + * Closes the given ReadableStream. + * + * Throws a TypeError and returns false if the closing operation fails. + * + * Note: This is semantically equivalent to the |close| method on + * the stream controller's prototype in JS. We expose it with the stream + * itself as a target for simplicity. + * + * Asserts that |stream| is an unwrapped ReadableStream instance. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamClose(JSContext* cx, HandleObject stream); + +/** + * Returns true if the given ReadableStream reader is locked, false otherwise. + * + * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or + * ReadableStreamBYOBReader instance. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamReaderIsClosed(const JSObject* reader); + +/** + * Enqueues the given chunk in the given ReadableStream. + * + * Throws a TypeError and returns false if the enqueing operation fails. + * + * Note: This is semantically equivalent to the |enqueue| method on + * the stream controller's prototype in JS. We expose it with the stream + * itself as a target for simplicity. + * + * If the ReadableStream has an underlying byte source, the given chunk must + * be a typed array or a DataView. Consider using + * ReadableByteStreamEnqueueBuffer. + * + * Asserts that |stream| is an unwrapped ReadableStream instance. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamEnqueue(JSContext* cx, HandleObject stream, HandleValue chunk); + +/** + * Enqueues the given buffer as a chunk in the given ReadableStream. + * + * Throws a TypeError and returns false if the enqueing operation fails. + * + * Note: This is semantically equivalent to the |enqueue| method on + * the stream controller's prototype in JS. We expose it with the stream + * itself as a target for simplicity. Additionally, the JS version only + * takes typed arrays and ArrayBufferView instances as arguments, whereas + * this takes an ArrayBuffer, obviating the need to wrap it into a typed + * array. + * + * Asserts that |stream| is an unwrapped ReadableStream instance and |buffer| + * an unwrapped ArrayBuffer instance. + */ +extern JS_PUBLIC_API(bool) +ReadableByteStreamEnqueueBuffer(JSContext* cx, HandleObject stream, HandleObject buffer); + +/** + * Errors the given ReadableStream, causing all future interactions to fail + * with the given error value. + * + * Throws a TypeError and returns false if the erroring operation fails. + * + * Note: This is semantically equivalent to the |error| method on + * the stream controller's prototype in JS. We expose it with the stream + * itself as a target for simplicity. + * + * Asserts that |stream| is an unwrapped ReadableStream instance. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamError(JSContext* cx, HandleObject stream, HandleValue error); + +/** + * Cancels the given ReadableStream reader's associated stream. + * + * Throws a TypeError and returns false if the given reader isn't active. + * + * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or + * ReadableStreamBYOBReader instance. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamReaderCancel(JSContext* cx, HandleObject reader, HandleValue reason); + +/** + * Cancels the given ReadableStream reader's associated stream. + * + * Throws a TypeError and returns false if the given reader has pending + * read or readInto (for default or byob readers, respectively) requests. + * + * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or + * ReadableStreamBYOBReader instance. + */ +extern JS_PUBLIC_API(bool) +ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject reader); + +/** + * Requests a read from the reader's associated ReadableStream and returns the + * resulting PromiseObject. + * + * Returns a Promise that's resolved with the read result once available or + * rejected immediately if the stream is errored or the operation failed. + * + * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader instance. + */ +extern JS_PUBLIC_API(JSObject*) +ReadableStreamDefaultReaderRead(JSContext* cx, HandleObject reader); + +/** + * Requests a read from the reader's associated ReadableStream into the given + * ArrayBufferView and returns the resulting PromiseObject. + * + * Returns a Promise that's resolved with the read result once available or + * rejected immediately if the stream is errored or the operation failed. + * + * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader and + * |view| an unwrapped typed array or DataView instance. + */ +extern JS_PUBLIC_API(JSObject*) +ReadableStreamBYOBReaderRead(JSContext* cx, HandleObject reader, HandleObject view); + +} // namespace JS + +#endif // js_Realm_h diff --git a/js/src/builtin/Stream.cpp b/js/src/builtin/Stream.cpp index 8eae95bcf7..aa8ce3091e 100644 --- a/js/src/builtin/Stream.cpp +++ b/js/src/builtin/Stream.cpp @@ -5,6 +5,8 @@ #include "builtin/Stream.h" +#include "js/Stream.h" + #include "jscntxt.h" #include "gc/Heap.h" @@ -73,9 +75,14 @@ enum ControllerFlags { ControllerFlag_CloseRequested = 1 << 3, ControllerFlag_TeeBranch = 1 << 4, ControllerFlag_TeeBranch1 = 1 << 5, - ControllerFlag_TeeBranch2 = 1 << 6 + ControllerFlag_TeeBranch2 = 1 << 6, + ControllerFlag_ExternalSource = 1 << 7, + ControllerFlag_SourceLocked = 1 << 8, }; +// Offset at which embedding flags are stored. +constexpr uint8_t ControllerEmbeddingFlagsOffset = 24; + enum BYOBRequestSlots { BYOBRequestSlot_Controller, BYOBRequestSlot_View, @@ -84,7 +91,7 @@ enum BYOBRequestSlots { template MOZ_ALWAYS_INLINE bool -Is(HandleValue v) +Is(const HandleValue v) { return v.isObject() && v.toObject().is(); } @@ -98,13 +105,6 @@ IsReadableStreamController(const JSObject* controller) } #endif // DEBUG -static bool -IsReadableStreamReader(const JSObject* reader) -{ - return reader->is() || - reader->is(); -} - static inline uint32_t ControllerFlags(const NativeObject* controller) { @@ -142,30 +142,43 @@ SetStreamState(ReadableStream* stream, uint32_t state) stream->setFixedSlot(StreamSlot_State, Int32Value(state)); } -inline bool +bool ReadableStream::readable() const { return StreamState(this) & Readable; } -inline bool +bool ReadableStream::closed() const { return StreamState(this) & Closed; } -inline bool +bool ReadableStream::errored() const { return StreamState(this) & Errored; } -inline bool +bool ReadableStream::disturbed() const { return StreamState(this) & Disturbed; } +inline static bool +ReaderHasStream(const NativeObject* reader) +{ + MOZ_ASSERT(JS::IsReadableStreamReader(reader)); + return !reader->getFixedSlot(ReaderSlot_Stream).isUndefined(); +} + +bool +js::ReadableStreamReaderIsClosed(const JSObject* reader) +{ + return !ReaderHasStream(&reader->as()); +} + inline static MOZ_MUST_USE ReadableStream* StreamFromController(const NativeObject* controller) { @@ -174,28 +187,51 @@ StreamFromController(const NativeObject* controller) } inline static MOZ_MUST_USE NativeObject* -ControllerFromStream(ReadableStream* stream) +ControllerFromStream(const ReadableStream* stream) { Value controllerVal = stream->getFixedSlot(StreamSlot_Controller); MOZ_ASSERT(IsReadableStreamController(&controllerVal.toObject())); return &controllerVal.toObject().as(); } +inline static bool +HasController(const ReadableStream* stream) +{ + return !stream->getFixedSlot(StreamSlot_Controller).isUndefined(); +} + +JS::ReadableStreamMode +ReadableStream::mode() const +{ + NativeObject* controller = ControllerFromStream(this); + if (controller->is()) + return JS::ReadableStreamMode::Default; + return controller->as().hasExternalSource() + ? JS::ReadableStreamMode::ExternalSource + : JS::ReadableStreamMode::Byte; +} + inline static MOZ_MUST_USE ReadableStream* StreamFromReader(const NativeObject* reader) { - MOZ_ASSERT(IsReadableStreamReader(reader)); + MOZ_ASSERT(ReaderHasStream(reader)); return &reader->getFixedSlot(ReaderSlot_Stream).toObject().as(); } inline static MOZ_MUST_USE NativeObject* -ReaderFromStream(NativeObject* stream) +ReaderFromStream(const NativeObject* stream) { Value readerVal = stream->getFixedSlot(StreamSlot_Reader); - MOZ_ASSERT(IsReadableStreamReader(&readerVal.toObject())); + MOZ_ASSERT(JS::IsReadableStreamReader(&readerVal.toObject())); return &readerVal.toObject().as(); } +inline static bool +HasReader(const ReadableStream* stream) +{ + return !stream->getFixedSlot(StreamSlot_Reader).isUndefined(); +} + inline static MOZ_MUST_USE JSFunction* NewHandler(JSContext *cx, Native handler, HandleObject target) { @@ -573,7 +609,7 @@ const Class TeeState::class_ = { JSCLASS_HAS_RESERVED_SLOTS(SlotCount) }; -#define CLASS_SPEC(cls, nCtorArgs, nSlots, specFlags) \ +#define CLASS_SPEC(cls, nCtorArgs, nSlots, specFlags, classFlags, classOps) \ const ClassSpec cls::classSpec_ = { \ GenericCreateConstructor, \ GenericCreatePrototype, \ @@ -588,8 +624,9 @@ const ClassSpec cls::classSpec_ = { \ const Class cls::class_ = { \ #cls, \ JSCLASS_HAS_RESERVED_SLOTS(nSlots) | \ - JSCLASS_HAS_CACHED_PROTO(JSProto_##cls), \ - JS_NULL_CLASS_OPS, \ + JSCLASS_HAS_CACHED_PROTO(JSProto_##cls) | \ + classFlags, \ + classOps, \ &cls::classSpec_ \ }; \ \ @@ -602,13 +639,13 @@ const Class cls::protoClass_ = { \ // Streams spec, 3.2.3., steps 1-4. ReadableStream* -ReadableStream::createStream(JSContext* cx) +ReadableStream::createStream(JSContext* cx, HandleObject proto /* = nullptr */) { - Rooted stream(cx, NewBuiltinClassInstance(cx)); + Rooted stream(cx, NewObjectWithClassProto(cx, proto)); if (!stream) return nullptr; - // Step 1 (reordered): Set this.[[state]] to "readable". + // Step 1: Set this.[[state]] to "readable". // Step 2: Set this.[[reader]] and this.[[storedError]] to undefined (implicit). // Step 3: Set this.[[disturbed]] to false (implicit). // Step 4: Set this.[[readableStreamController]] to undefined (implicit). @@ -625,17 +662,18 @@ CreateReadableStreamDefaultController(JSContext* cx, Handle str // Streams spec, 3.2.3., steps 1-4, 8. ReadableStream* ReadableStream::createDefaultStream(JSContext* cx, HandleValue underlyingSource, - HandleValue size, HandleValue highWaterMark) + HandleValue size, HandleValue highWaterMark, + HandleObject proto /* = nullptr */) { - + // Steps 1-4. Rooted stream(cx, createStream(cx)); if (!stream) return nullptr; - // Step b: Set this.[[readableStreamController]] to - // ? Construct(ReadableStreamDefaultController, - // « this, underlyingSource, size, - // highWaterMark »). + // Step 8.b: Set this.[[readableStreamController]] to + // ? Construct(ReadableStreamDefaultController, + // « this, underlyingSource, size, + // highWaterMark »). RootedObject controller(cx, CreateReadableStreamDefaultController(cx, stream, underlyingSource, size, @@ -656,16 +694,16 @@ CreateReadableByteStreamController(JSContext* cx, Handle stream // Streams spec, 3.2.3., steps 1-4, 7. ReadableStream* ReadableStream::createByteStream(JSContext* cx, HandleValue underlyingSource, - HandleValue highWaterMark) + HandleValue highWaterMark, HandleObject proto /* = nullptr */) { - - Rooted stream(cx, createStream(cx)); + // Steps 1-4. + Rooted stream(cx, createStream(cx, proto)); if (!stream) return nullptr; - // Step b: Set this.[[readableStreamController]] to - // ? Construct(ReadableByteStreamController, « this, underlyingSource, - // highWaterMark »). + // Step 7.b: Set this.[[readableStreamController]] to + // ? Construct(ReadableByteStreamController, + // « this, underlyingSource, highWaterMark »). RootedObject controller(cx, CreateReadableByteStreamController(cx, stream, underlyingSource, highWaterMark)); @@ -677,6 +715,29 @@ ReadableStream::createByteStream(JSContext* cx, HandleValue underlyingSource, return stream; } +static MOZ_MUST_USE ReadableByteStreamController* +CreateReadableByteStreamController(JSContext* cx, Handle stream, + void* underlyingSource); + +ReadableStream* +ReadableStream::createExternalSourceStream(JSContext* cx, void* underlyingSource, + uint8_t flags, HandleObject proto /* = nullptr */) +{ + Rooted stream(cx, createStream(cx, proto)); + if (!stream) + return nullptr; + + RootedNativeObject controller(cx, CreateReadableByteStreamController(cx, stream, + underlyingSource)); + if (!controller) + return nullptr; + + stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller)); + AddControllerFlags(controller, flags << ControllerEmbeddingFlagsOffset); + + return stream; +} + // Streams spec, 3.2.3. bool ReadableStream::constructor(JSContext* cx, unsigned argc, Value* vp) @@ -722,8 +783,8 @@ ReadableStream::constructor(JSContext* cx, unsigned argc, Value* vp) if (!CompareStrings(cx, type, cx->names().bytes, ¬ByteStream)) return false; - // Step 7 & 8.a (reordered): If highWaterMark is undefined, let - // highWaterMark be 1 (or 0 for byte streams). + // Step 7.a & 8.a (reordered): If highWaterMark is undefined, let + // highWaterMark be 1 (or 0 for byte streams). if (highWaterMark.isUndefined()) highWaterMark = Int32Value(notByteStream ? 1 : 0); @@ -731,9 +792,15 @@ ReadableStream::constructor(JSContext* cx, unsigned argc, Value* vp) // Step 7: If typeString is "bytes", if (!notByteStream) { + // Step 7.b: Set this.[[readableStreamController]] to + // ? Construct(ReadableByteStreamController, + // « this, underlyingSource, highWaterMark »). stream = createByteStream(cx, underlyingSource, highWaterMark); } else if (typeVal.isUndefined()) { // Step 8: Otherwise, if type is undefined, + // Step 8.b: Set this.[[readableStreamController]] to + // ? Construct(ReadableStreamDefaultController, + // « this, underlyingSource, size, highWaterMark »). stream = createDefaultStream(cx, underlyingSource, size, highWaterMark); } else { // Step 9: Otherwise, throw a RangeError exception. @@ -748,9 +815,6 @@ ReadableStream::constructor(JSContext* cx, unsigned argc, Value* vp) return true; } -static MOZ_ALWAYS_INLINE bool -IsReadableStreamLocked(ReadableStream* stream); - // Streams spec, 3.2.4.1. get locked static MOZ_MUST_USE bool ReadableStream_locked_impl(JSContext* cx, const CallArgs& args) @@ -758,7 +822,7 @@ ReadableStream_locked_impl(JSContext* cx, const CallArgs& args) Rooted stream(cx, &args.thisv().toObject().as()); // Step 2: Return ! IsReadableStreamLocked(this). - args.rval().setBoolean(IsReadableStreamLocked(stream)); + args.rval().setBoolean(stream->locked()); return true; } @@ -770,9 +834,6 @@ ReadableStream_locked(JSContext* cx, unsigned argc, Value* vp) return CallNonGenericMethod, ReadableStream_locked_impl>(cx, args); } -static MOZ_MUST_USE JSObject* -ReadableStreamCancel(JSContext* cx, Handle stream, HandleValue reason); - // Streams spec, 3.2.4.2. cancel ( reason ) static MOZ_MUST_USE bool ReadableStream_cancel(JSContext* cx, unsigned argc, Value* vp) @@ -790,14 +851,14 @@ ReadableStream_cancel(JSContext* cx, unsigned argc, Value* vp) // Step 2: If ! IsReadableStreamLocked(this) is true, return a promise // rejected with a TypeError exception. - if (IsReadableStreamLocked(stream)) { + if (stream->locked()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_NOT_LOCKED, "cancel"); return ReturnPromiseRejectedWithPendingError(cx, args); } // Step 3: Return ! ReadableStreamCancel(this, reason). - RootedObject cancelPromise(cx, ReadableStreamCancel(cx, stream, args.get(0))); + RootedObject cancelPromise(cx, ReadableStream::cancel(cx, stream, args.get(0))); if (!cancelPromise) return false; args.rval().setObject(*cancelPromise); @@ -936,7 +997,7 @@ static const JSPropertySpec ReadableStream_properties[] = { JS_PS_END }; -CLASS_SPEC(ReadableStream, 0, StreamSlotCount, 0); +CLASS_SPEC(ReadableStream, 0, StreamSlotCount, 0, 0, JS_NULL_CLASS_OPS); // Streams spec, 3.3.1. AcquireReadableStreamBYOBReader ( stream ) // Always inlined. @@ -948,22 +1009,26 @@ CLASS_SPEC(ReadableStream, 0, StreamSlotCount, 0); // Using is instead. // Streams spec, 3.3.4. IsReadableStreamDisturbed ( stream ) -static MOZ_ALWAYS_INLINE bool -IsReadableStreamDisturbed(ReadableStream* stream) -{ - // Step 1: Assert: ! IsReadableStream(stream) is true (implicit). - // Step 2: Return stream.[[disturbed]]. - return stream->disturbed(); -} +// Using stream->disturbed() instead. // Streams spec, 3.3.5. IsReadableStreamLocked ( stream ) -static MOZ_ALWAYS_INLINE bool -IsReadableStreamLocked(ReadableStream* stream) +bool +ReadableStream::locked() const { // Step 1: Assert: ! IsReadableStream(stream) is true (implicit). // Step 2: If stream.[[reader]] is undefined, return false. // Step 3: Return true. - return !stream->getFixedSlot(StreamSlot_Reader).isUndefined(); + // Special-casing for streams with external sources. Those can be locked + // explicitly via JSAPI, which is indicated by a controller flag. + // IsReadableStreamLocked is called from the controller's constructor, at + // which point we can't yet call ControllerFromStream(stream), but the + // source also can't be locked yet. + if (HasController(this) && + (ControllerFlags(ControllerFromStream(this)) & ControllerFlag_SourceLocked)) + { + return true; + } + return HasReader(this); } static MOZ_MUST_USE bool @@ -1055,9 +1120,6 @@ TeeReaderReadHandler(JSContext* cx, unsigned argc, Value* vp) return true; } -static MOZ_MUST_USE JSObject* -ReadableStreamDefaultReaderRead(JSContext* cx, HandleNativeObject reader); - static MOZ_MUST_USE JSObject* ReadableStreamTee_Pull(JSContext* cx, Handle teeState, Handle branchStream) @@ -1071,7 +1133,7 @@ ReadableStreamTee_Pull(JSContext* cx, Handle teeState, // handler which takes the argument result and performs the // following steps: Rooted reader(cx, teeState->reader()); - RootedObject readPromise(cx, ReadableStreamDefaultReaderRead(cx, reader)); + RootedObject readPromise(cx, ReadableStreamDefaultReader::read(cx, reader)); if (!readPromise) return nullptr; @@ -1119,7 +1181,7 @@ ReadableStreamTee_Cancel(JSContext* cx, Handle teeState, Rooted promise(cx, teeState->promise()); // Step b: Let cancelResult be ! ReadableStreamCancel(stream, compositeReason). - RootedObject cancelResult(cx, ReadableStreamCancel(cx, stream, compositeReasonVal)); + RootedObject cancelResult(cx, ReadableStream::cancel(cx, stream, compositeReasonVal)); if (!cancelResult) { if (!RejectWithPendingError(cx, promise)) return nullptr; @@ -1314,9 +1376,6 @@ ReadableStreamAddReadRequest(JSContext* cx, Handle stream) return promise; } -static MOZ_MUST_USE bool -ReadableStreamClose(JSContext* cx, Handle stream); - static MOZ_MUST_USE JSObject* ReadableStreamControllerCancelSteps(JSContext* cx, HandleNativeObject controller, HandleValue reason); @@ -1330,9 +1389,12 @@ ReturnUndefined(JSContext* cx, unsigned argc, Value* vp) return true; } +MOZ_MUST_USE bool +ReadableStreamCloseInternal(JSContext* cx, Handle stream); + // Streams spec, 3.4.3. ReadableStreamCancel ( stream, reason ) -static MOZ_MUST_USE JSObject* -ReadableStreamCancel(JSContext* cx, Handle stream, HandleValue reason) +/* static */ MOZ_MUST_USE JSObject* +ReadableStream::cancel(JSContext* cx, Handle stream, HandleValue reason) { // Step 1: Set stream.[[disturbed]] to true. uint32_t state = StreamState(stream) | ReadableStream::Disturbed; @@ -1351,7 +1413,7 @@ ReadableStreamCancel(JSContext* cx, Handle stream, HandleValue } // Step 4: Perform ! ReadableStreamClose(stream). - if (!ReadableStreamClose(cx, stream)) + if (!ReadableStreamCloseInternal(cx, stream)) return nullptr; // Step 5: Let sourceCancelPromise be @@ -1365,16 +1427,15 @@ ReadableStreamCancel(JSContext* cx, Handle stream, HandleValue // Step 6: Return the result of transforming sourceCancelPromise by a // fulfillment handler that returns undefined. RootedAtom funName(cx, cx->names().empty); - RootedFunction returnUndefined(cx, - NewNativeFunction(cx, ReturnUndefined, 0, funName)); + RootedFunction returnUndefined(cx, NewNativeFunction(cx, ReturnUndefined, 0, funName)); if (!returnUndefined) return nullptr; return JS::CallOriginalPromiseThen(cx, sourceCancelPromise, returnUndefined, nullptr); } // Streams spec, 3.4.4. ReadableStreamClose ( stream ) -static MOZ_MUST_USE bool -ReadableStreamClose(JSContext* cx, Handle stream) +MOZ_MUST_USE bool +ReadableStreamCloseInternal(JSContext* cx, Handle stream) { // Step 1: Assert: stream.[[state]] is "readable". MOZ_ASSERT(stream->readable()); @@ -1422,12 +1483,23 @@ ReadableStreamClose(JSContext* cx, Handle stream) // Step 6: Resolve reader.[[closedPromise]] with undefined. // Step 7: Return (implicit). RootedObject closedPromise(cx, &reader->getFixedSlot(ReaderSlot_ClosedPromise).toObject()); - return ResolvePromise(cx, closedPromise, UndefinedHandleValue); + if (!ResolvePromise(cx, closedPromise, UndefinedHandleValue)) + return false; + + if (stream->mode() == JS::ReadableStreamMode::ExternalSource && + cx->runtime()->readableStreamClosedCallback) + { + NativeObject* controller = ControllerFromStream(stream); + void* source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate(); + cx->runtime()->readableStreamClosedCallback(cx, stream, source, stream->embeddingFlags()); + } + + return true; } // Streams spec, 3.4.5. ReadableStreamError ( stream, e ) -static MOZ_MUST_USE bool -ReadableStreamError(JSContext* cx, Handle stream, HandleValue e) +MOZ_MUST_USE bool +ReadableStreamErrorInternal(JSContext* cx, Handle stream, HandleValue e) { // Step 1: Assert: ! IsReadableStream(stream) is true (implicit). @@ -1471,7 +1543,19 @@ ReadableStreamError(JSContext* cx, Handle stream, HandleValue e // Step 9: Reject reader.[[closedPromise]] with e. val = reader->getFixedSlot(ReaderSlot_ClosedPromise); Rooted closedPromise(cx, &val.toObject().as()); - return PromiseObject::reject(cx, closedPromise, e); + if (!PromiseObject::reject(cx, closedPromise, e)) + return false; + + if (stream->mode() == JS::ReadableStreamMode::ExternalSource && + cx->runtime()->readableStreamErroredCallback) + { + NativeObject* controller = ControllerFromStream(stream); + void* source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate(); + cx->runtime()->readableStreamErroredCallback(cx, stream, source, + stream->embeddingFlags(), e); + } + + return true; } // Streams spec, 3.4.6. ReadableStreamFulfillReadIntoRequest( stream, chunk, done ) @@ -1509,16 +1593,13 @@ ReadableStreamFulfillReadOrReadIntoRequest(JSContext* cx, Handleis()); - // Step 1: Return the number of elements in // stream.[[reader]].[[readRequests]]. - Value readerVal = stream->getFixedSlot(StreamSlot_Reader); - NativeObject* reader = &readerVal.toObject().as(); - MOZ_ASSERT(reader->is() || - reader->is()); + if (!HasReader(stream)) + return 0; + NativeObject* reader = ReaderFromStream(stream); Value readRequests = reader->getFixedSlot(ReaderSlot_Requests); return readRequests.toObject().as().getDenseInitializedLength(); } @@ -1564,7 +1645,7 @@ CreateReadableStreamDefaultReader(JSContext* cx, Handle stream) // Step 2: If ! IsReadableStreamLocked(stream) is true, throw a TypeError // exception. - if (IsReadableStreamLocked(stream)) { + if (stream->locked()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED); return nullptr; @@ -1641,7 +1722,7 @@ ReadableStreamDefaultReader_cancel(JSContext* cx, unsigned argc, Value* vp) // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise // rejected with a TypeError exception. RootedNativeObject reader(cx, &args.thisv().toObject().as()); - if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + if (!ReaderHasStream(reader)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAMREADER_NOT_OWNED, "cancel"); return ReturnPromiseRejectedWithPendingError(cx, args); @@ -1668,15 +1749,16 @@ ReadableStreamDefaultReader_read(JSContext* cx, unsigned argc, Value* vp) // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise // rejected with a TypeError exception. - RootedNativeObject reader(cx, &args.thisv().toObject().as()); - if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + Rooted reader(cx); + reader = &args.thisv().toObject().as(); + if (!ReaderHasStream(reader)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAMREADER_NOT_OWNED, "read"); return ReturnPromiseRejectedWithPendingError(cx, args); } // Step 3: Return ! ReadableStreamDefaultReaderRead(this). - JSObject* readPromise = ReadableStreamDefaultReaderRead(cx, reader); + JSObject* readPromise = ReadableStreamDefaultReader::read(cx, reader); if (!readPromise) return false; args.rval().setObject(*readPromise); @@ -1694,7 +1776,7 @@ ReadableStreamDefaultReader_releaseLock_impl(JSContext* cx, const CallArgs& args reader = &args.thisv().toObject().as(); // Step 2: If this.[[ownerReadableStream]] is undefined, return. - if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + if (!ReaderHasStream(reader)) { args.rval().setUndefined(); return true; } @@ -1738,7 +1820,8 @@ static const JSPropertySpec ReadableStreamDefaultReader_properties[] = { JS_PS_END }; -CLASS_SPEC(ReadableStreamDefaultReader, 1, ReaderSlotCount, ClassSpec::DontDefineConstructor); +CLASS_SPEC(ReadableStreamDefaultReader, 1, ReaderSlotCount, ClassSpec::DontDefineConstructor, 0, + JS_NULL_CLASS_OPS); // Streams spec, 3.6.3 new ReadableStreamBYOBReader ( stream ) @@ -1750,13 +1833,14 @@ CreateReadableStreamBYOBReader(JSContext* cx, Handle stream) // is false, throw a TypeError exception. if (!ControllerFromStream(stream)->is()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER); + JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER, + "ReadableStream.getReader('byob')"); return nullptr; } // Step 3: If ! IsReadableStreamLocked(stream) is true, throw a TypeError // exception. - if (IsReadableStreamLocked(stream)) { + if (stream->locked()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED); return nullptr; } @@ -1832,7 +1916,7 @@ ReadableStreamBYOBReader_cancel(JSContext* cx, unsigned argc, Value* vp) // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise // rejected with a TypeError exception. RootedNativeObject reader(cx, &args.thisv().toObject().as()); - if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + if (!ReaderHasStream(reader)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAMREADER_NOT_OWNED, "cancel"); return ReturnPromiseRejectedWithPendingError(cx, args); @@ -1846,10 +1930,6 @@ ReadableStreamBYOBReader_cancel(JSContext* cx, unsigned argc, Value* vp) return true; } -static MOZ_MUST_USE JSObject* -ReadableStreamBYOBReaderRead(JSContext* cx, HandleNativeObject reader, - Handle view); - // Streams spec, 3.6.4.3 read ( ) static MOZ_MUST_USE bool ReadableStreamBYOBReader_read(JSContext* cx, unsigned argc, Value* vp) @@ -1864,8 +1944,9 @@ ReadableStreamBYOBReader_read(JSContext* cx, unsigned argc, Value* vp) // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise // rejected with a TypeError exception. - RootedNativeObject reader(cx, &args.thisv().toObject().as()); - if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + Rooted reader(cx); + reader = &args.thisv().toObject().as(); + if (!ReaderHasStream(reader)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAMREADER_NOT_OWNED, "read"); return ReturnPromiseRejectedWithPendingError(cx, args); @@ -1880,20 +1961,20 @@ ReadableStreamBYOBReader_read(JSContext* cx, unsigned argc, Value* vp) return ReturnPromiseRejectedWithPendingError(cx, args); } - Rooted view(cx, &viewVal.toObject().as()); + Rooted view(cx, &viewVal.toObject().as()); // Step 5: If view.[[ByteLength]] is 0, return a promise rejected with a // TypeError exception. // Note: It's ok to use the length in number of elements here because all we // want to know is whether it's < 0. - if (view->length() == 0) { + if (JS_GetArrayBufferViewByteLength(view) == 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAMBYOBREADER_READ_EMPTY_VIEW); return ReturnPromiseRejectedWithPendingError(cx, args); } // Step 6: Return ! ReadableStreamBYOBReaderRead(this, view). - JSObject* readPromise = ReadableStreamBYOBReaderRead(cx, reader, view); + JSObject* readPromise = ReadableStreamBYOBReader::read(cx, reader, view); if (!readPromise) return false; args.rval().setObject(*readPromise); @@ -1911,7 +1992,7 @@ ReadableStreamBYOBReader_releaseLock_impl(JSContext* cx, const CallArgs& args) reader = &args.thisv().toObject().as(); // Step 2: If this.[[ownerReadableStream]] is undefined, return. - if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) { + if (!ReaderHasStream(reader)) { args.rval().setUndefined(); return true; } @@ -1954,7 +2035,7 @@ static const JSFunctionSpec ReadableStreamBYOBReader_methods[] = { JS_FS_END }; -CLASS_SPEC(ReadableStreamBYOBReader, 1, 3, ClassSpec::DontDefineConstructor); +CLASS_SPEC(ReadableStreamBYOBReader, 1, 3, ClassSpec::DontDefineConstructor, 0, JS_NULL_CLASS_OPS); inline static MOZ_MUST_USE bool ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject controller); @@ -1975,7 +2056,7 @@ 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); + return &ReadableStreamCancel(cx, stream, reason)->as(); } // Streams spec, 3.7.4. ReadableStreamReaderGenericInitialize ( reader, stream ) @@ -2064,12 +2145,12 @@ ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader) static MOZ_MUST_USE JSObject* ReadableByteStreamControllerPullInto(JSContext* cx, Handle controller, - HandleNativeObject view); + Handle view); // Streams spec, 3.7.6. ReadableStreamBYOBReaderRead ( reader, view ) -static MOZ_MUST_USE JSObject* -ReadableStreamBYOBReaderRead(JSContext* cx, HandleNativeObject reader, - Handle view) +/* static */ MOZ_MUST_USE JSObject* +ReadableStreamBYOBReader::read(JSContext* cx, Handle reader, + Handle view) { MOZ_ASSERT(reader->is()); @@ -2097,11 +2178,9 @@ static MOZ_MUST_USE JSObject* ReadableStreamControllerPullSteps(JSContext* cx, HandleNativeObject controller); // Streams spec, 3.7.7. ReadableStreamDefaultReaderRead ( reader ) -static MOZ_MUST_USE JSObject* -ReadableStreamDefaultReaderRead(JSContext* cx, HandleNativeObject reader) +MOZ_MUST_USE JSObject* +ReadableStreamDefaultReader::read(JSContext* cx, Handle reader) { - MOZ_ASSERT(reader->is()); - // Step 1: Let stream be reader.[[ownerReadableStream]]. // Step 2: Assert: stream is not undefined. Rooted stream(cx, StreamFromReader(reader)); @@ -2294,12 +2373,13 @@ ReadableStreamDefaultController::constructor(JSContext* cx, unsigned argc, Value // Step 2: If stream.[[readableStreamController]] is not undefined, throw a // TypeError exception. - if (!stream->getFixedSlot(StreamSlot_Controller).isUndefined()) { + 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) @@ -2357,13 +2437,10 @@ static MOZ_MUST_USE bool ReadableStreamDefaultControllerClose(JSContext* cx, Handle controller); -// Streams spec, 3.8.4.2 close() +// Unified implementation of steps 2-3 of 3.8.4.2 and 3.10.4.3. static MOZ_MUST_USE bool -ReadableStreamDefaultController_close_impl(JSContext* cx, const CallArgs& args) +VerifyControllerStateForClosing(JSContext* cx, HandleNativeObject controller) { - Rooted controller(cx); - controller = &args.thisv().toObject().as(); - // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception. if (ControllerFlags(controller) & ControllerFlag_CloseRequested) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, @@ -2380,6 +2457,20 @@ ReadableStreamDefaultController_close_impl(JSContext* cx, const CallArgs& args) return false; } + return true; +} + +// Streams spec, 3.8.4.2 close() +static MOZ_MUST_USE bool +ReadableStreamDefaultController_close_impl(JSContext* cx, const CallArgs& args) +{ + Rooted controller(cx); + controller = &args.thisv().toObject().as(); + + // Steps 2-3. + if (!VerifyControllerStateForClosing(cx, controller)) + return false; + // Step 4: Perform ! ReadableStreamDefaultControllerClose(this). if (!ReadableStreamDefaultControllerClose(cx, controller)) return false; @@ -2452,12 +2543,10 @@ ReadableStreamDefaultController_error_impl(JSContext* cx, const CallArgs& args) controller = &args.thisv().toObject().as(); // Step 2: Let stream be this.[[controlledReadableStream]]. - ReadableStream* stream = StreamFromController(controller); - // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception. - if (!stream->readable()) { + if (!StreamFromController(controller)->readable()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close"); + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "error"); return false; } @@ -2491,7 +2580,8 @@ static const JSFunctionSpec ReadableStreamDefaultController_methods[] = { JS_FS_END }; -CLASS_SPEC(ReadableStreamDefaultController, 4, 7, ClassSpec::DontDefineConstructor); +CLASS_SPEC(ReadableStreamDefaultController, 4, 7, ClassSpec::DontDefineConstructor, 0, + JS_NULL_CLASS_OPS); /** * Unified implementation of ReadableStream controllers' [[CancelSteps]] internal @@ -2538,6 +2628,15 @@ ReadableStreamControllerCancelSteps(JSContext* cx, HandleNativeObject controller return ReadableStreamTee_Cancel(cx, teeState, defaultController, reason); } + if (ControllerFlags(controller) & ControllerFlag_ExternalSource) { + void* source = underlyingSource.toPrivate(); + Rooted stream(cx, StreamFromController(controller)); + RootedValue rval(cx); + rval = cx->runtime()->readableStreamCancelCallback(cx, stream, source, + stream->embeddingFlags(), reason); + return PromiseObject::unforgeableResolve(cx, rval); + } + return PromiseInvokeOrNoop(cx, underlyingSource, cx->names().cancel, reason); } @@ -2569,8 +2668,8 @@ ReadableStreamDefaultControllerPullSteps(JSContext* cx, HandleNativeObject contr // perform ! ReadableStreamClose(stream). bool closeRequested = ControllerFlags(controller) & ControllerFlag_CloseRequested; if (closeRequested && queue->getDenseInitializedLength() == 0) { - if (!ReadableStreamClose(cx, stream)) - return nullptr; + if (!ReadableStreamCloseInternal(cx, stream)) + return nullptr; } // Step c: Otherwise, perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this). @@ -2647,6 +2746,9 @@ ControllerPullFailedHandler(JSContext* cx, unsigned argc, Value* vp) static bool ReadableStreamControllerShouldCallPull(NativeObject* controller); +static MOZ_MUST_USE double +ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller); + // Streams spec, 3.9.2 ReadableStreamDefaultControllerCallPullIfNeeded ( controller ) // and // Streams spec, 3.12.3. ReadableByteStreamControllerCallPullIfNeeded ( controller ) @@ -2687,6 +2789,13 @@ ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject contr Rooted teeState(cx, &underlyingSource.toObject().as()); Rooted stream(cx, StreamFromController(controller)); pullPromise = ReadableStreamTee_Pull(cx, teeState, stream); + } else if (ControllerFlags(controller) & ControllerFlag_ExternalSource) { + void* source = underlyingSource.toPrivate(); + Rooted stream(cx, StreamFromController(controller)); + double desiredSize = ReadableStreamControllerGetDesiredSizeUnchecked(controller); + cx->runtime()->readableStreamDataRequestCallback(cx, stream, source, + stream->embeddingFlags(), desiredSize); + pullPromise = PromiseObject::unforgeableResolve(cx, UndefinedHandleValue); } else { pullPromise = PromiseInvokeOrNoop(cx, underlyingSource, cx->names().pull, controllerVal); } @@ -2734,7 +2843,7 @@ ReadableStreamControllerShouldCallPull(NativeObject* controller) // Step 5: If ! IsReadableStreamLocked(stream) is true and // ! ReadableStreamGetNumReadRequests(stream) > 0, return true. // Steps 5-6 of 3.12.24 are equivalent in our implementation. - if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0) + if (stream->locked() && ReadableStreamGetNumReadRequests(stream) > 0) return true; // Step 6: Let desiredSize be ReadableStreamDefaultControllerGetDesiredSize(controller). @@ -2767,15 +2876,11 @@ ReadableStreamDefaultControllerClose(JSContext* cx, RootedNativeObject queue(cx); queue = &controller->getFixedSlot(QueueContainerSlot_Queue).toObject().as(); if (queue->getDenseInitializedLength() == 0) - return ReadableStreamClose(cx, stream); + return ReadableStreamCloseInternal(cx, stream); return true; } -static MOZ_MUST_USE bool -ReadableStreamFulfillReadOrReadIntoRequest(JSContext* cx, Handle stream, - HandleValue chunk, bool done); - static MOZ_MUST_USE bool EnqueueValueWithSize(JSContext* cx, HandleNativeObject container, HandleValue value, HandleValue sizeVal); @@ -2798,7 +2903,7 @@ ReadableStreamDefaultControllerEnqueue(JSContext* cx, // Step 4: If ! IsReadableStreamLocked(stream) is true and // ! ReadableStreamGetNumReadRequests(stream) > 0, perform // ! ReadableStreamFulfillReadRequest(stream, chunk, false). - if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0) { + if (stream->locked() && ReadableStreamGetNumReadRequests(stream) > 0) { if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, chunk, false)) return false; } else { @@ -2847,9 +2952,6 @@ ReadableStreamDefaultControllerEnqueue(JSContext* cx, return true; } -static MOZ_MUST_USE bool -ReadableStreamError(JSContext* cx, Handle stream, HandleValue e); - static MOZ_MUST_USE bool ReadableByteStreamControllerClearPendingPullIntos(JSContext* cx, HandleNativeObject controller); @@ -2881,7 +2983,7 @@ ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, Hand return false; // Step 4 (or 5): Perform ! ReadableStreamError(stream, e). - return ReadableStreamError(cx, stream, e); + return ReadableStreamErrorInternal(cx, stream, e); } // Streams spec, 3.9.7. ReadableStreamDefaultControllerErrorIfNeeded ( controller, e ) nothrow @@ -3012,6 +3114,11 @@ CreateReadableByteStreamController(JSContext* cx, Handle stream return controller; } +bool +ReadableByteStreamController::hasExternalSource() { + return ControllerFlags(this) & ControllerFlag_ExternalSource; +} + // Streams spec, 3.10.3. // new ReadableByteStreamController ( stream, underlyingByteSource, // highWaterMark ) @@ -3035,7 +3142,7 @@ ReadableByteStreamController::constructor(JSContext* cx, unsigned argc, Value* v // Step 2: If stream.[[readableStreamController]] is not undefined, throw a // TypeError exception. - if (!stream->getFixedSlot(StreamSlot_Controller).isUndefined()) { + if (HasController(stream)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_CONTROLLER_SET); return false; @@ -3050,6 +3157,71 @@ ReadableByteStreamController::constructor(JSContext* cx, unsigned argc, Value* v return true; } +// Version of the ReadableByteStreamConstructor that's specialized for +// handling external, embedding-provided, underlying sources. +static MOZ_MUST_USE ReadableByteStreamController* +CreateReadableByteStreamController(JSContext* cx, Handle stream, + void* underlyingSource) +{ + Rooted controller(cx); + controller = NewBuiltinClassInstance(cx); + if (!controller) + return nullptr; + + // Step 3: Set this.[[controlledReadableStream]] to stream. + controller->setFixedSlot(ControllerSlot_Stream, ObjectValue(*stream)); + + // Step 4: Set this.[[underlyingByteSource]] to underlyingByteSource. + controller->setFixedSlot(ControllerSlot_UnderlyingSource, PrivateValue(underlyingSource)); + + // Step 5: Set this.[[pullAgain]], and this.[[pulling]] to false. + controller->setFixedSlot(ControllerSlot_Flags, Int32Value(ControllerFlag_ExternalSource)); + + // Step 6: Perform ! ReadableByteStreamControllerClearPendingPullIntos(this). + // Omitted. + + // Step 7: Perform ! ResetQueue(this). + controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(0)); + + // Step 8: Set this.[[started]] and this.[[closeRequested]] to false. + // Step 9: Set this.[[strategyHWM]] to + // ? ValidateAndNormalizeHighWaterMark(highWaterMark). + controller->setFixedSlot(ControllerSlot_StrategyHWM, Int32Value(0)); + + // Step 10: Let autoAllocateChunkSize be + // ? GetV(underlyingByteSource, "autoAllocateChunkSize"). + // Step 11: If autoAllocateChunkSize is not undefined, + // Step 12: Set this.[[autoAllocateChunkSize]] to autoAllocateChunkSize. + // Omitted. + + // Step 13: Set this.[[pendingPullIntos]] to a new empty List. + if (!SetNewList(cx, controller, ByteControllerSlot_PendingPullIntos)) + return nullptr; + + // Step 14: Let controller be this (implicit). + // Step 15: Let startResult be + // ? InvokeOrNoop(underlyingSource, "start", « this »). + // Omitted. + + // Step 16: Let startPromise be a promise resolved with startResult: + RootedObject startPromise(cx, PromiseObject::unforgeableResolve(cx, UndefinedHandleValue)); + if (!startPromise) + return nullptr; + + RootedObject onStartFulfilled(cx, NewHandler(cx, ControllerStartHandler, controller)); + if (!onStartFulfilled) + return nullptr; + + RootedObject onStartRejected(cx, NewHandler(cx, ControllerStartFailedHandler, controller)); + if (!onStartRejected) + return nullptr; + + if (!JS::AddPromiseReactions(cx, startPromise, onStartFulfilled, onStartRejected)) + return nullptr; + + return controller; +} + static MOZ_MUST_USE ReadableStreamBYOBRequest* CreateReadableStreamBYOBRequest(JSContext* cx, Handle controller, HandleObject view); @@ -3132,20 +3304,9 @@ ReadableByteStreamController_close_impl(JSContext* cx, const CallArgs& args) Rooted controller(cx); controller = &args.thisv().toObject().as(); - // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception. - if (ControllerFlags(controller) & ControllerFlag_CloseRequested) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_READABLESTREAMCONTROLLER_CLOSED, "close"); + // Steps 2-3. + if (!VerifyControllerStateForClosing(cx, controller)) return false; - } - - // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable", - // throw a TypeError exception. - if (!StreamFromController(controller)->readable()) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close"); - return false; - } // Step 4: Perform ? ReadableByteStreamControllerClose(this). if (!ReadableByteStreamControllerClose(cx, controller)) @@ -3197,7 +3358,8 @@ ReadableByteStreamController_enqueue_impl(JSContext* cx, const CallArgs& args) // throw a TypeError exception. if (!chunkVal.isObject() || !JS_IsArrayBufferViewObject(&chunkVal.toObject())) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK); + JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK, + "ReadableByteStreamController#enqueue"); return false; } RootedObject chunk(cx, &chunkVal.toObject()); @@ -3265,7 +3427,41 @@ static const JSFunctionSpec ReadableByteStreamController_methods[] = { JS_FS_END }; -CLASS_SPEC(ReadableByteStreamController, 3, 9, ClassSpec::DontDefineConstructor); +static void +ReadableByteStreamControllerFinalize(FreeOp* fop, JSObject* obj) +{ + ReadableByteStreamController& controller = obj->as(); + + if (controller.getFixedSlot(ControllerSlot_Flags).isUndefined()) + return; + + uint32_t flags = ControllerFlags(&controller); + if (!(flags & ControllerFlag_ExternalSource)) + return; + + uint8_t embeddingFlags = flags >> ControllerEmbeddingFlagsOffset; + + void* underlyingSource = controller.getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate(); + obj->runtimeFromAnyThread()->readableStreamFinalizeCallback(underlyingSource, embeddingFlags); +} + +static const ClassOps ReadableByteStreamControllerClassOps = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + ReadableByteStreamControllerFinalize, + nullptr, /* call */ + nullptr, /* hasInstance */ + nullptr, /* construct */ + nullptr, /* trace */ +}; + +CLASS_SPEC(ReadableByteStreamController, 3, 9, ClassSpec::DontDefineConstructor, + JSCLASS_BACKGROUND_FINALIZE, &ReadableByteStreamControllerClassOps); // Streams spec, 3.10.5.1. [[PullSteps]] () // Unified with 3.8.5.1 above. @@ -3290,32 +3486,61 @@ ReadableByteStreamControllerPullSteps(JSContext* cx, HandleNativeObject controll // Step 3.a: MOZ_ASSERT: ! ReadableStreamGetNumReadRequests(_stream_) is 0. MOZ_ASSERT(ReadableStreamGetNumReadRequests(stream) == 0); - // Step 3.b: Let entry be the first element of this.[[queue]]. - // Step 3.c: Remove entry from this.[[queue]], shifting all other elements - // downward (so that the second becomes the first, and so on). - val = controller->getFixedSlot(QueueContainerSlot_Queue); - RootedNativeObject queue(cx, &val.toObject().as()); - Rooted entry(cx, ShiftFromList(cx, queue)); - MOZ_ASSERT(entry); + RootedObject view(cx); - // Step 3.d: Set this.[[queueTotalSize]] to this.[[queueTotalSize]] − entry.[[byteLength]]. - uint32_t byteLength = entry->byteLength(); - queueTotalSize = queueTotalSize - byteLength; - controller->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(queueTotalSize)); + if (stream->mode() == JS::ReadableStreamMode::ExternalSource) { + val = controller->getFixedSlot(ControllerSlot_UnderlyingSource); + void* underlyingSource = val.toPrivate(); + + view = JS_NewUint8Array(cx, queueTotalSize); + if (!view) + return nullptr; + + size_t bytesWritten; + { + JS::AutoCheckCannotGC noGC(cx); + bool dummy; + void* buffer = JS_GetArrayBufferViewData(view, &dummy, noGC); + auto cb = cx->runtime()->readableStreamWriteIntoReadRequestCallback; + MOZ_ASSERT(cb); + // TODO: use bytesWritten to correctly update the request's state. + cb(cx, stream, underlyingSource, stream->embeddingFlags(), buffer, + queueTotalSize, &bytesWritten); + } + + queueTotalSize = queueTotalSize - bytesWritten; + } else { + // Step 3.b: Let entry be the first element of this.[[queue]]. + // Step 3.c: Remove entry from this.[[queue]], shifting all other elements + // downward (so that the second becomes the first, and so on). + val = controller->getFixedSlot(QueueContainerSlot_Queue); + RootedNativeObject queue(cx, &val.toObject().as()); + Rooted entry(cx, ShiftFromList(cx, queue)); + MOZ_ASSERT(entry); + + queueTotalSize = queueTotalSize - entry->byteLength(); + + // Step 3.f: Let view be ! Construct(%Uint8Array%, « entry.[[buffer]], + // entry.[[byteOffset]], entry.[[byteLength]] »). + // (reordered) + RootedObject buffer(cx, entry->buffer()); + + uint32_t byteOffset = entry->byteOffset(); + view = JS_NewUint8ArrayWithBuffer(cx, buffer, byteOffset, entry->byteLength()); + if (!view) + return nullptr; + } + + // Step 3.d: Set this.[[queueTotalSize]] to + // this.[[queueTotalSize]] − entry.[[byteLength]]. + // (reordered) + controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(queueTotalSize)); // Step 3.e: Perform ! ReadableByteStreamControllerHandleQueueDrain(this). + // (reordered) if (!ReadableByteStreamControllerHandleQueueDrain(cx, controller)) return nullptr; - // Step 3.f: Let view be ! Construct(%Uint8Array%, « entry.[[buffer]], - // entry.[[byteOffset]], entry.[[byteLength]] »). - RootedObject buffer(cx, entry->buffer()); - - uint32_t byteOffset = entry->byteOffset(); - RootedObject view(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, byteOffset, byteLength)); - if (!view) - return nullptr; - // Step 3.g: Return a promise resolved with ! CreateIterResultObject(view, false). val.setObject(*view); RootedObject iterResult(cx, CreateIterResultObject(cx, val, false)); @@ -3548,7 +3773,8 @@ ReadableStreamBYOBRequest_respondWithNewView_impl(JSContext* cx, const CallArgs& // a TypeError exception. if (!viewVal.isObject() || !JS_IsArrayBufferViewObject(&viewVal.toObject())) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK); + JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK, + "ReadableStreamBYOBRequest#respondWithNewView"); return false; } @@ -3587,7 +3813,8 @@ static const JSFunctionSpec ReadableStreamBYOBRequest_methods[] = { JS_FS_END }; -CLASS_SPEC(ReadableStreamBYOBRequest, 3, 2, ClassSpec::DontDefineConstructor); +CLASS_SPEC(ReadableStreamBYOBRequest, 3, 2, ClassSpec::DontDefineConstructor, 0, + JS_NULL_CLASS_OPS); // Streams spec, 3.12.1. IsReadableStreamBYOBRequest ( x ) // Implemented via is() @@ -3665,7 +3892,7 @@ ReadableByteStreamControllerClose(JSContext* cx, Handlereadable()); - // Step 4: Let buffer be chunk.[[ViewedArrayBuffer]]. - bool dummy; - RootedObject buffer(cx, JS_GetArrayBufferViewBuffer(cx, chunk, &dummy)); - if (!buffer) - return false; + // To make enqueuing chunks via JSAPI nicer, we want to be able to deal + // with ArrayBuffer objects in addition to ArrayBuffer views here. + // This cannot happen when enqueuing happens via + // ReadableByteStreamController_enqueue because that throws if invoked + // with anything but an ArrayBuffer view. - // Step 5: Let byteOffset be chunk.[[ByteOffset]]. - uint32_t byteOffset = JS_GetArrayBufferViewByteOffset(chunk); + Rooted buffer(cx); + uint32_t byteOffset; + uint32_t byteLength; - // Step 6: Let byteLength be chunk.[[ByteLength]]. - uint32_t byteLength = JS_GetArrayBufferViewByteLength(chunk); + if (chunk->is()) { + // Steps 4-6 for ArrayBuffer objects. + buffer = &chunk->as(); + byteOffset = 0; + byteLength = buffer->byteLength(); + } else { + // Step 4: Let buffer be chunk.[[ViewedArrayBuffer]]. + bool dummy; + JSObject* bufferObj = JS_GetArrayBufferViewBuffer(cx, chunk, &dummy); + if (!bufferObj) + return false; + buffer = &bufferObj->as(); + + // Step 5: Let byteOffset be chunk.[[ByteOffset]]. + byteOffset = JS_GetArrayBufferViewByteOffset(chunk); + + // Step 6: Let byteLength be chunk.[[ByteLength]]. + byteLength = JS_GetArrayBufferViewByteLength(chunk); + } // Step 7: Let transferredBuffer be ! TransferArrayBuffer(buffer). RootedArrayBufferObject transferredBuffer(cx, TransferArrayBuffer(cx, buffer)); @@ -3854,7 +4099,7 @@ ReadableByteStreamControllerEnqueue(JSContext* cx, } else { // Step b: Otherwise, // Step i: Assert: ! IsReadableStreamLocked(stream) is false. - MOZ_ASSERT(!IsReadableStreamLocked(stream)); + MOZ_ASSERT(!stream->locked()); // Step ii: Perform // ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, @@ -3979,6 +4224,39 @@ ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(JSContext* cx, *ready = true; } + if (ControllerFlags(controller) & ControllerFlag_ExternalSource) { + // TODO: it probably makes sense to eagerly drain the underlying source. + // We have a buffer lying around anyway, whereas the source might be + // able to free or reuse buffers once their content is copied into + // our buffer. + if (!ready) + return true; + + Value val = controller->getFixedSlot(ControllerSlot_UnderlyingSource); + void* underlyingSource = val.toPrivate(); + + RootedArrayBufferObject targetBuffer(cx, pullIntoDescriptor->buffer()); + Rooted stream(cx, StreamFromController(controller)); + + size_t bytesWritten; + { + JS::AutoCheckCannotGC noGC(cx); + bool dummy; + uint8_t* buffer = JS_GetArrayBufferData(targetBuffer, &dummy, noGC); + buffer += bytesFilled; + auto cb = cx->runtime()->readableStreamWriteIntoReadRequestCallback; + MOZ_ASSERT(cb); + cb(cx, stream, underlyingSource, stream->embeddingFlags(), buffer, + totalBytesToCopyRemaining, &bytesWritten); + pullIntoDescriptor->setBytesFilled(bytesFilled + bytesWritten); + } + + queueTotalSize -= bytesWritten; + controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(queueTotalSize)); + + return true; + } + // Step 9: Let queue be controller.[[queue]]. RootedValue val(cx, controller->getFixedSlot(QueueContainerSlot_Queue)); RootedNativeObject queue(cx, &val.toObject().as()); @@ -4082,7 +4360,7 @@ ReadableByteStreamControllerHandleQueueDrain(JSContext* cx, HandleNativeObject c bool closeRequested = ControllerFlags(controller) & ControllerFlag_CloseRequested; if (totalSize == 0 && closeRequested) { // Step a: Perform ! ReadableStreamClose(controller.[[controlledReadableStream]]). - return ReadableStreamClose(cx, stream); + return ReadableStreamCloseInternal(cx, stream); } // Step 3: Otherwise, @@ -4172,10 +4450,9 @@ ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(JSContext* cx, static MOZ_MUST_USE JSObject* ReadableByteStreamControllerPullInto(JSContext* cx, Handle controller, - HandleNativeObject view) + Handle view) { MOZ_ASSERT(controller->is()); - MOZ_ASSERT(JS_IsArrayBufferViewObject(view)); // Step 1: Let stream be controller.[[controlledReadableStream]]. Rooted stream(cx, StreamFromController(controller)); @@ -4669,7 +4946,7 @@ static const JSFunctionSpec ByteLengthQueuingStrategy_methods[] = { JS_FS_END }; -CLASS_SPEC(ByteLengthQueuingStrategy, 1, 0, 0); +CLASS_SPEC(ByteLengthQueuingStrategy, 1, 0, 0, 0, JS_NULL_CLASS_OPS); // Streams spec, 6.2.2. new CountQueuingStrategy({ highWaterMark }) bool @@ -4716,7 +4993,7 @@ static const JSFunctionSpec CountQueuingStrategy_methods[] = { JS_FS_END }; -CLASS_SPEC(CountQueuingStrategy, 1, 0, 0); +CLASS_SPEC(CountQueuingStrategy, 1, 0, 0, 0, JS_NULL_CLASS_OPS); #undef CLASS_SPEC @@ -4955,3 +5232,258 @@ ValidateAndNormalizeQueuingStrategy(JSContext* cx, HandleValue size, // Step 3: Return Record {[[size]]: size, [[highWaterMark]]: highWaterMark}. return true; } + +MOZ_MUST_USE bool +js::ReadableStreamReaderCancel(JSContext* cx, HandleObject readerObj, HandleValue reason) +{ + MOZ_ASSERT(IsReadableStreamReader(readerObj)); + RootedNativeObject reader(cx, &readerObj->as()); + MOZ_ASSERT(StreamFromReader(reader)); + return ReadableStreamReaderGenericCancel(cx, reader, reason); +} + +MOZ_MUST_USE bool +js::ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject readerObj) +{ + MOZ_ASSERT(IsReadableStreamReader(readerObj)); + RootedNativeObject reader(cx, &readerObj->as()); + MOZ_ASSERT(ReadableStreamGetNumReadRequests(StreamFromReader(reader)) == 0); + return ReadableStreamReaderGenericRelease(cx, reader); +} + +MOZ_MUST_USE bool +ReadableStream::enqueue(JSContext* cx, Handle stream, HandleValue chunk) +{ + Rooted controller(cx); + controller = &ControllerFromStream(stream)->as(); + + MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested)); + MOZ_ASSERT(stream->readable()); + + return ReadableStreamDefaultControllerEnqueue(cx, controller, chunk); +} + +MOZ_MUST_USE bool +ReadableStream::enqueueBuffer(JSContext* cx, Handle stream, + Handle chunk) +{ + Rooted controller(cx); + controller = &ControllerFromStream(stream)->as(); + + MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested)); + MOZ_ASSERT(stream->readable()); + + return ReadableByteStreamControllerEnqueue(cx, controller, chunk); +} + +void +ReadableStream::desiredSize(bool* hasSize, double* size) const +{ + if (errored()) { + *hasSize = false; + return; + } + + *hasSize = true; + + if (closed()) { + *size = 0; + return; + } + + NativeObject* controller = ControllerFromStream(this); + *size = ReadableStreamControllerGetDesiredSizeUnchecked(controller); +} + +/*static */ bool +ReadableStream::getExternalSource(JSContext* cx, Handle stream, void** source) +{ + MOZ_ASSERT(stream->mode() == JS::ReadableStreamMode::ExternalSource); + if (stream->locked()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED); + return false; + } + if (!stream->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, + "ReadableStreamGetExternalUnderlyingSource"); + return false; + } + + auto controller = &ControllerFromStream(stream)->as(); + AddControllerFlags(controller, ControllerFlag_SourceLocked); + *source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate(); + return true; +} + +void +ReadableStream::releaseExternalSource() +{ + MOZ_ASSERT(mode() == JS::ReadableStreamMode::ExternalSource); + MOZ_ASSERT(locked()); + auto controller = ControllerFromStream(this); + MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_SourceLocked); + RemoveControllerFlags(controller, ControllerFlag_SourceLocked); +} + +uint8_t +ReadableStream::embeddingFlags() const +{ + uint8_t flags = ControllerFlags(ControllerFromStream(this)) >> ControllerEmbeddingFlagsOffset; + MOZ_ASSERT_IF(flags, mode() == JS::ReadableStreamMode::ExternalSource); + return flags; +} + +// Streams spec, 3.10.4.4. steps 1-3 +// and +// Streams spec, 3.12.8. steps 8-9 +// +// Adapted to handling updates signaled by the embedding for streams with +// external underlying sources. +// +// The remaining steps of those two functions perform checks and asserts that +// don't apply to streams with external underlying sources. +MOZ_MUST_USE bool +ReadableStream::updateDataAvailableFromSource(JSContext* cx, Handle stream, + uint32_t availableData) +{ + Rooted controller(cx); + controller = &ControllerFromStream(stream)->as(); + + // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception. + if (ControllerFlags(controller) & ControllerFlag_CloseRequested) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_CLOSED, "enqueue"); + return false; + } + + // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable", + // throw a TypeError exception. + if (!StreamFromController(controller)->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "enqueue"); + return false; + } + + RemoveControllerFlags(controller, ControllerFlag_Pulling | ControllerFlag_PullAgain); + +#if DEBUG + uint32_t oldAvailableData = controller->getFixedSlot(QueueContainerSlot_TotalSize).toInt32(); +#endif // DEBUG + controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(availableData)); + + // Step 8.a: If ! ReadableStreamGetNumReadRequests(stream) is 0, + // Reordered because for externally-sourced streams it applies regardless + // of reader type. + if (ReadableStreamGetNumReadRequests(stream) == 0) + return true; + + // Step 8: If ! ReadableStreamHasDefaultReader(stream) is true + if (ReadableStreamHasDefaultReader(stream)) { + // Step b: Otherwise, + // Step i: Assert: controller.[[queue]] is empty. + MOZ_ASSERT(oldAvailableData == 0); + + // Step ii: Let transferredView be + // ! Construct(%Uint8Array%, transferredBuffer, byteOffset, byteLength). + JSObject* viewObj = JS_NewUint8Array(cx, availableData); + Rooted transferredView(cx, &viewObj->as()); + if (!transferredView) + return false; + + Value val = controller->getFixedSlot(ControllerSlot_UnderlyingSource); + void* underlyingSource = val.toPrivate(); + + size_t bytesWritten; + { + JS::AutoCheckCannotGC noGC(cx); + bool dummy; + void* buffer = JS_GetArrayBufferViewData(transferredView, &dummy, noGC); + auto cb = cx->runtime()->readableStreamWriteIntoReadRequestCallback; + MOZ_ASSERT(cb); + // TODO: use bytesWritten to correctly update the request's state. + cb(cx, stream, underlyingSource, stream->embeddingFlags(), buffer, + availableData, &bytesWritten); + } + + // Step iii: Perform ! ReadableStreamFulfillReadRequest(stream, transferredView, false). + RootedValue chunk(cx, ObjectValue(*transferredView)); + if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, chunk, false)) + return false; + + controller->setFixedSlot(QueueContainerSlot_TotalSize, + Int32Value(availableData - bytesWritten)); + } else if (ReadableStreamHasBYOBReader(stream)) { + // Step 9: Otherwise, + // Step a: If ! ReadableStreamHasBYOBReader(stream) is true, + // Step i: Perform + // (Not needed for external underlying sources.) + + // Step ii: Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). + if (!ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(cx, controller)) + return false; + } else { + // Step b: Otherwise, + // Step i: Assert: ! IsReadableStreamLocked(stream) is false. + MOZ_ASSERT(!stream->locked()); + + // Step ii: Perform + // ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, + // transferredBuffer, + // byteOffset, + // byteLength). + // (Not needed for external underlying sources.) + } + + return true; +} + +MOZ_MUST_USE bool +ReadableStream::close(JSContext* cx, Handle stream) +{ + RootedNativeObject controllerObj(cx, ControllerFromStream(stream)); + if (!VerifyControllerStateForClosing(cx, controllerObj)) + return false; + + if (controllerObj->is()) { + Rooted controller(cx); + controller = &controllerObj->as(); + return ReadableStreamDefaultControllerClose(cx, controller); + } + + Rooted controller(cx); + controller = &controllerObj->as(); + return ReadableByteStreamControllerClose(cx, controller); +} + +MOZ_MUST_USE bool +ReadableStream::error(JSContext* cx, Handle stream, HandleValue reason) +{ + // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception. + if (!stream->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "error"); + return false; + } + + // Step 4: Perform ! ReadableStreamDefaultControllerError(this, e). + RootedNativeObject controller(cx, ControllerFromStream(stream)); + return ReadableStreamControllerError(cx, controller, reason); +} + +MOZ_MUST_USE bool +ReadableStream::tee(JSContext* cx, Handle stream, bool cloneForBranch2, + MutableHandle branch1Stream, + MutableHandle branch2Stream) +{ + return ReadableStreamTee(cx, stream, false, branch1Stream, branch2Stream); +} + +MOZ_MUST_USE NativeObject* +ReadableStream::getReader(JSContext* cx, Handle stream, + JS::ReadableStreamReaderMode mode) +{ + if (mode == JS::ReadableStreamReaderMode::Default) + return CreateReadableStreamDefaultReader(cx, stream); + return CreateReadableStreamBYOBReader(cx, stream); +} diff --git a/js/src/builtin/Stream.h b/js/src/builtin/Stream.h index 90b61b49af..0726e78361 100644 --- a/js/src/builtin/Stream.h +++ b/js/src/builtin/Stream.h @@ -6,8 +6,10 @@ #ifndef builtin_Stream_h #define builtin_Stream_h +#include "builtin/Promise.h" #include "vm/NativeObject.h" + namespace js { class AutoSetNewObjectMetadata; @@ -16,14 +18,50 @@ class ReadableStream : public NativeObject { public: static ReadableStream* createDefaultStream(JSContext* cx, HandleValue underlyingSource, - HandleValue size, HandleValue highWaterMark); + HandleValue size, HandleValue highWaterMark, + HandleObject proto = nullptr); static ReadableStream* createByteStream(JSContext* cx, HandleValue underlyingSource, - HandleValue highWaterMark); + HandleValue highWaterMark, + HandleObject proto = nullptr); + static ReadableStream* createExternalSourceStream(JSContext* cx, void* underlyingSource, + uint8_t flags, HandleObject proto = nullptr); - inline bool readable() const; - inline bool closed() const; - inline bool errored() const; - inline bool disturbed() const; + bool readable() const; + bool closed() const; + bool errored() const; + bool disturbed() const; + + bool locked() const; + + void desiredSize(bool* hasSize, double* size) const; + + JS::ReadableStreamMode mode() const; + + static MOZ_MUST_USE bool close(JSContext* cx, Handle stream); + static MOZ_MUST_USE JSObject* cancel(JSContext* cx, Handle stream, + HandleValue reason); + static MOZ_MUST_USE bool error(JSContext* cx, Handle stream, + HandleValue error); + + static MOZ_MUST_USE NativeObject* getReader(JSContext* cx, Handle stream, + JS::ReadableStreamReaderMode mode); + + static MOZ_MUST_USE bool tee(JSContext* cx, + Handle stream, bool cloneForBranch2, + MutableHandle branch1Stream, + MutableHandle branch2Stream); + + static MOZ_MUST_USE bool enqueue(JSContext* cx, Handle stream, + HandleValue chunk); + static MOZ_MUST_USE bool enqueueBuffer(JSContext* cx, Handle stream, + Handle chunk); + static MOZ_MUST_USE bool getExternalSource(JSContext* cx, Handle stream, + void** source); + void releaseExternalSource(); + uint8_t embeddingFlags() const; + static MOZ_MUST_USE bool updateDataAvailableFromSource(JSContext* cx, + Handle stream, + uint32_t availableData); enum State { Readable = 1 << 0, @@ -33,7 +71,7 @@ class ReadableStream : public NativeObject }; private: - static ReadableStream* createStream(JSContext* cx); + static MOZ_MUST_USE ReadableStream* createStream(JSContext* cx, HandleObject proto = nullptr); public: static bool constructor(JSContext* cx, unsigned argc, Value* vp); @@ -46,6 +84,8 @@ class ReadableStream : public NativeObject class ReadableStreamDefaultReader : public NativeObject { public: + static MOZ_MUST_USE JSObject* read(JSContext* cx, Handle reader); + static bool constructor(JSContext* cx, unsigned argc, Value* vp); static const ClassSpec classSpec_; static const Class class_; @@ -56,6 +96,9 @@ class ReadableStreamDefaultReader : public NativeObject class ReadableStreamBYOBReader : public NativeObject { public: + static MOZ_MUST_USE JSObject* read(JSContext* cx, Handle reader, + Handle view); + static bool constructor(JSContext* cx, unsigned argc, Value* vp); static const ClassSpec classSpec_; static const Class class_; @@ -63,6 +106,13 @@ class ReadableStreamBYOBReader : public NativeObject static const Class protoClass_; }; +bool ReadableStreamReaderIsClosed(const JSObject* reader); + +MOZ_MUST_USE bool ReadableStreamReaderCancel(JSContext* cx, HandleObject reader, + HandleValue reason); + +MOZ_MUST_USE bool ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject reader); + class ReadableStreamDefaultController : public NativeObject { public: @@ -76,6 +126,8 @@ class ReadableStreamDefaultController : public NativeObject class ReadableByteStreamController : public NativeObject { public: + bool hasExternalSource(); + static bool constructor(JSContext* cx, unsigned argc, Value* vp); static const ClassSpec classSpec_; static const Class class_; diff --git a/js/src/js.msg b/js/src/js.msg index 3c7f12ae5e..b8aa77902d 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -625,18 +625,19 @@ MSG_DEF(JSMSG_NUMBER_MUST_BE_FINITE_NON_NEGATIVE, 1, JSEXN_RANGEERR, "'{0}' must MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_BYTESWRITTEN, 0, JSEXN_RANGEERR, "'bytesWritten' exceeds remaining length.") MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_SIZE, 0, JSEXN_RANGEERR, "view size does not match requested data.") MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_OFFSET, 0, JSEXN_RANGEERR, "view offset does not match requested position.") -MSG_DEF(JSMSG_READABLESTREAM_NOT_LOCKED, 1, JSEXN_TYPEERR, "The ReadableStream method '{0}' may only be called on a locked stream.") +MSG_DEF(JSMSG_READABLESTREAM_NOT_LOCKED, 1, JSEXN_TYPEERR, "'{0}' may only be called on a locked stream.") MSG_DEF(JSMSG_READABLESTREAM_LOCKED, 0, JSEXN_TYPEERR, "A Reader may only be created for an unlocked ReadableStream.") -MSG_DEF(JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER, 0, JSEXN_TYPEERR, "ReadableStream.getReader('byob') requires a ReadableByteStreamController.") +MSG_DEF(JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER, 1, JSEXN_TYPEERR, "{0} requires a ReadableByteStreamController.") +MSG_DEF(JSMSG_READABLESTREAM_NOT_DEFAULT_CONTROLLER, 1, JSEXN_TYPEERR, "{0} requires a ReadableStreamDefaultController.") MSG_DEF(JSMSG_READABLESTREAM_CONTROLLER_SET, 0, JSEXN_TYPEERR, "The ReadableStream already has a controller defined.") MSG_DEF(JSMSG_READABLESTREAMREADER_NOT_OWNED, 1, JSEXN_TYPEERR, "The ReadableStream reader method '{0}' may only be called on a reader owned by a stream.") MSG_DEF(JSMSG_READABLESTREAMREADER_NOT_EMPTY, 1, JSEXN_TYPEERR, "The ReadableStream reader method '{0}' may not be called on a reader with read requests.") MSG_DEF(JSMSG_READABLESTREAMBYOBREADER_READ_EMPTY_VIEW, 0, JSEXN_TYPEERR, "ReadableStreamBYOBReader.read() was passed an empty TypedArrayBuffer view.") MSG_DEF(JSMSG_READABLESTREAMREADER_RELEASED, 0, JSEXN_TYPEERR, "The ReadableStream reader was released.") -MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_CLOSED, 1, JSEXN_TYPEERR, "The ReadableStream controller method '{0}' called on a stream already closing.") -MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, 1, JSEXN_TYPEERR, "The ReadableStream controller method '{0}' may only be called on a stream in the 'readable' state.") +MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_CLOSED, 1, JSEXN_TYPEERR, "'{0}' called on a stream already closing.") +MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, 1, JSEXN_TYPEERR, "'{0}' may only be called on a stream in the 'readable' state.") MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNKSIZE,0, JSEXN_RANGEERR, "ReadableByteStreamController requires a positive integer or undefined for 'autoAllocateChunkSize'.") -MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK, 0, JSEXN_TYPEERR, "ReadableByteStreamController passed a bad chunk.") +MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK, 1, JSEXN_TYPEERR, "{0} passed a bad chunk.") MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_CLOSE_PENDING_PULL, 0, JSEXN_TYPEERR, "The ReadableByteStreamController cannot be closed while the buffer is being filled.") MSG_DEF(JSMSG_READABLESTREAMBYOBREQUEST_NO_CONTROLLER, 1, JSEXN_TYPEERR, "ReadableStreamBYOBRequest method '{0}' called on a request with no controller.") MSG_DEF(JSMSG_READABLESTREAMBYOBREQUEST_RESPOND_CLOSED, 0, JSEXN_TYPEERR, "ReadableStreamBYOBRequest method 'respond' called with non-zero number of bytes with a closed controller.") diff --git a/js/src/jsapi-tests/moz.build b/js/src/jsapi-tests/moz.build index 35bc69b85b..1e5f43aa5a 100644 --- a/js/src/jsapi-tests/moz.build +++ b/js/src/jsapi-tests/moz.build @@ -126,6 +126,11 @@ if CONFIG['ENABLE_ION']: 'testJitRValueAlloc.cpp', ] +if CONFIG['ENABLE_STREAMS']: + UNIFIED_SOURCES += [ + 'testReadableStream.cpp', + ] + DEFINES['EXPORT_JS_API'] = True LOCAL_INCLUDES += [ diff --git a/js/src/jsapi-tests/testIntTypesABI.cpp b/js/src/jsapi-tests/testIntTypesABI.cpp index 066e7ad1b8..420a07c7f8 100644 --- a/js/src/jsapi-tests/testIntTypesABI.cpp +++ b/js/src/jsapi-tests/testIntTypesABI.cpp @@ -29,6 +29,7 @@ #include "js/RequiredDefines.h" #include "js/RootingAPI.h" #include "js/SliceBudget.h" +#include "js/Stream.h" #include "js/StructuredClone.h" #include "js/TracingAPI.h" #include "js/TrackedOptimizationInfo.h" diff --git a/js/src/jsapi-tests/testReadableStream.cpp b/js/src/jsapi-tests/testReadableStream.cpp new file mode 100644 index 0000000000..f1d373d5d2 --- /dev/null +++ b/js/src/jsapi-tests/testReadableStream.cpp @@ -0,0 +1,676 @@ +/* -*- 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 "jsapi.h" + +#include "jsapi-tests/tests.h" + +using namespace JS; + +char test_buffer_data[] = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +static JSObject* +NewDefaultStream(JSContext* cx, HandleObject source = nullptr, HandleFunction size = nullptr, + double highWaterMark = 1, HandleObject proto = nullptr) +{ + RootedObject stream(cx, NewReadableDefaultStreamObject(cx, source, size, highWaterMark, + proto)); + MOZ_ASSERT_IF(stream, IsReadableStream(stream)); + return stream; +} + +static JSObject* +NewByteStream(JSContext* cx, double highWaterMark = 0, HandleObject proto = nullptr) +{ + RootedObject source(cx, JS_NewPlainObject(cx)); + MOZ_ASSERT(source); + + RootedObject stream(cx, NewReadableByteStreamObject(cx, source, highWaterMark, proto)); + MOZ_ASSERT_IF(stream, IsReadableStream(stream)); + return stream; +} + +static bool dataRequestCBCalled = false; +static void +DataRequestCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags, + size_t desiredSize) +{ + MOZ_ASSERT(!dataRequestCBCalled, "Invalid test setup"); + dataRequestCBCalled = true; +} + +static bool writeIntoRequestBufferCBCalled = false; +static void +WriteIntoRequestBufferCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags, + void* buffer, size_t length, size_t* bytesWritten) +{ + MOZ_ASSERT(!writeIntoRequestBufferCBCalled, "Invalid test setup"); + MOZ_ASSERT(length <= sizeof(test_buffer_data)); + memcpy(buffer, test_buffer_data, length); + writeIntoRequestBufferCBCalled = true; + *bytesWritten = length; +} + +static bool cancelStreamCBCalled = false; +static Value cancelStreamReason; +static Value +CancelStreamCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags, + HandleValue reason) +{ + MOZ_ASSERT(!cancelStreamCBCalled, "Invalid test setup"); + cancelStreamCBCalled = true; + cancelStreamReason = reason; + return reason; +} + +static bool streamClosedCBCalled = false; +static Value streamClosedReason; +static void +StreamClosedCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags) +{ + MOZ_ASSERT(!streamClosedCBCalled, "Invalid test setup"); + streamClosedCBCalled = true; +} + +static bool streamErroredCBCalled = false; +static Value streamErroredReason; +static void +StreamErroredCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags, + HandleValue reason) +{ + MOZ_ASSERT(!streamErroredCBCalled, "Invalid test setup"); + streamErroredCBCalled = true; + streamErroredReason = reason; +} + +static bool finalizeStreamCBCalled = false; +static void* finalizedStreamUnderlyingSource; +static void +FinalizeStreamCB(void* underlyingSource, uint8_t flags) +{ + MOZ_ASSERT(!finalizeStreamCBCalled, "Invalid test setup"); + finalizeStreamCBCalled = true; + finalizedStreamUnderlyingSource = underlyingSource; +} + +static void +ResetCallbacks() +{ + dataRequestCBCalled = false; + writeIntoRequestBufferCBCalled = false; + cancelStreamReason = UndefinedValue(); + cancelStreamCBCalled = false; + streamClosedCBCalled = false; + streamErroredCBCalled = false; + finalizeStreamCBCalled = false; +} + +static bool +GetIterResult(JSContext* cx, HandleObject promise, MutableHandleValue value, bool* done) +{ + RootedObject iterResult(cx, &GetPromiseResult(promise).toObject()); + + bool found; + if (!JS_HasProperty(cx, iterResult, "value", &found)) + return false; + MOZ_ASSERT(found); + if (!JS_HasProperty(cx, iterResult, "done", &found)) + return false; + MOZ_ASSERT(found); + + RootedValue doneVal(cx); + if (!JS_GetProperty(cx, iterResult, "value", value)) + return false; + if (!JS_GetProperty(cx, iterResult, "done", &doneVal)) + return false; + + *done = doneVal.toBoolean(); + MOZ_ASSERT_IF(*done, value.isUndefined()); + + return true; +} + +static JSObject* +GetReadChunk(JSContext* cx, HandleObject readRequest) +{ + MOZ_ASSERT(GetPromiseState(readRequest) == PromiseState::Fulfilled); + RootedValue resultVal(cx, GetPromiseResult(readRequest)); + MOZ_ASSERT(resultVal.isObject()); + RootedObject result(cx, &resultVal.toObject()); + RootedValue chunkVal(cx); + JS_GetProperty(cx, result, "value", &chunkVal); + return &chunkVal.toObject(); +} + +BEGIN_TEST(testReadableStream_NewReadableStream) +{ + RootedObject stream(cx, NewDefaultStream(cx)); + CHECK(stream); + CHECK(ReadableStreamGetMode(stream) == ReadableStreamMode::Default); + return true; +} +END_TEST(testReadableStream_NewReadableStream) + +BEGIN_TEST(testReadableStream_NewReadableByteStream) +{ + RootedObject stream(cx, NewByteStream(cx)); + CHECK(stream); + CHECK(ReadableStreamGetMode(stream) == ReadableStreamMode::Byte); + return true; +} +END_TEST(testReadableStream_NewReadableByteStream) + +BEGIN_TEST(testReadableStream_ReadableStreamGetReaderDefault) +{ + RootedObject stream(cx, NewDefaultStream(cx)); + CHECK(stream); + + RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default)); + CHECK(reader); + CHECK(IsReadableStreamDefaultReader(reader)); + CHECK(ReadableStreamIsLocked(stream)); + CHECK(!ReadableStreamReaderIsClosed(reader)); + + return true; +} +END_TEST(testReadableStream_ReadableStreamGetReaderDefault) + +BEGIN_TEST(testReadableStream_ReadableStreamGetReaderBYOB) +{ + RootedObject stream(cx, NewByteStream(cx)); + CHECK(stream); + + RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::BYOB)); + CHECK(reader); + CHECK(IsReadableStreamBYOBReader(reader)); + CHECK(ReadableStreamIsLocked(stream)); + CHECK(!ReadableStreamReaderIsClosed(reader)); + + return true; +} +END_TEST(testReadableStream_ReadableStreamGetReaderBYOB) + +BEGIN_TEST(testReadableStream_ReadableStreamTee) +{ + RootedObject stream(cx, NewDefaultStream(cx)); + CHECK(stream); + + RootedObject leftStream(cx); + RootedObject rightStream(cx); + CHECK(ReadableStreamTee(cx, stream, &leftStream, &rightStream)); + CHECK(ReadableStreamIsLocked(stream)); + CHECK(leftStream); + CHECK(IsReadableStream(leftStream)); + CHECK(rightStream); + CHECK(IsReadableStream(rightStream)); + + return true; +} +END_TEST(testReadableStream_ReadableStreamTee) + +BEGIN_TEST(testReadableStream_ReadableStreamEnqueue) +{ + RootedObject stream(cx, NewDefaultStream(cx)); + CHECK(stream); + + RootedObject chunk(cx, JS_NewPlainObject(cx)); + CHECK(chunk); + RootedValue chunkVal(cx, ObjectValue(*chunk)); + CHECK(ReadableStreamEnqueue(cx, stream, chunkVal)); + + return true; +} +END_TEST(testReadableStream_ReadableStreamEnqueue) + +BEGIN_TEST(testReadableStream_ReadableByteStreamEnqueue) +{ + RootedObject stream(cx, NewDefaultStream(cx)); + CHECK(stream); + + RootedObject chunk(cx, JS_NewUint8Array(cx, 42)); + CHECK(chunk); + CHECK(!ReadableByteStreamEnqueueBuffer(cx, stream, chunk)); + CHECK(JS_IsExceptionPending(cx)); + + return true; +} +END_TEST(testReadableStream_ReadableByteStreamEnqueue) + +BEGIN_TEST(testReadableStream_ReadableStreamDefaultReaderRead) +{ + RootedObject stream(cx, NewDefaultStream(cx)); + CHECK(stream); + RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default)); + CHECK(reader); + + RootedObject request(cx, ReadableStreamDefaultReaderRead(cx, reader)); + CHECK(request); + CHECK(IsPromiseObject(request)); + CHECK(GetPromiseState(request) == PromiseState::Pending); + + RootedObject chunk(cx, JS_NewPlainObject(cx)); + CHECK(chunk); + RootedValue chunkVal(cx, ObjectValue(*chunk)); + CHECK(ReadableStreamEnqueue(cx, stream, chunkVal)); + + CHECK(GetReadChunk(cx, request) == chunk); + + return true; +} +END_TEST(testReadableStream_ReadableStreamDefaultReaderRead) + +BEGIN_TEST(testReadableStream_ReadableByteStreamDefaultReaderRead) +{ + RootedObject stream(cx, NewByteStream(cx)); + CHECK(stream); + + RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default)); + CHECK(reader); + + RootedObject request(cx, ReadableStreamDefaultReaderRead(cx, reader)); + CHECK(request); + CHECK(IsPromiseObject(request)); + CHECK(GetPromiseState(request) == PromiseState::Pending); + + size_t length = sizeof(test_buffer_data); + RootedObject buffer(cx, JS_NewArrayBufferWithExternalContents(cx, length, test_buffer_data)); + CHECK(buffer); + RootedObject chunk(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, 0, length)); + CHECK(chunk); + bool isShared; + CHECK(!JS_IsDetachedArrayBufferObject(buffer)); + + CHECK(ReadableByteStreamEnqueueBuffer(cx, stream, chunk)); + + CHECK(JS_IsDetachedArrayBufferObject(buffer)); + RootedObject readChunk(cx, GetReadChunk(cx, request)); + CHECK(JS_IsUint8Array(readChunk)); + void* readBufferData; + { + JS::AutoCheckCannotGC autoNoGC(cx); + readBufferData = JS_GetArrayBufferViewData(readChunk, &isShared, autoNoGC); + } + CHECK(readBufferData); + CHECK(!memcmp(test_buffer_data, readBufferData, length)); + + return true; +} +END_TEST(testReadableStream_ReadableByteStreamDefaultReaderRead) + +BEGIN_TEST(testReadableStream_ReadableByteStreamBYOBReaderRead) +{ + RootedObject stream(cx, NewByteStream(cx)); + CHECK(stream); + + RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::BYOB)); + CHECK(reader); + + size_t length = sizeof(test_buffer_data); + RootedObject targetArray(cx, JS_NewUint8Array(cx, length)); + bool isShared; + + RootedObject request(cx, ReadableStreamBYOBReaderRead(cx, reader, targetArray)); + CHECK(request); + CHECK(IsPromiseObject(request)); + CHECK(GetPromiseState(request) == PromiseState::Pending); + CHECK(JS_IsDetachedArrayBufferObject(JS_GetArrayBufferViewBuffer(cx, targetArray, &isShared))); + + RootedObject buffer(cx, JS_NewArrayBufferWithExternalContents(cx, length, test_buffer_data)); + CHECK(buffer); + CHECK(!JS_IsDetachedArrayBufferObject(buffer)); + + CHECK(ReadableByteStreamEnqueueBuffer(cx, stream, buffer)); + + CHECK(JS_IsDetachedArrayBufferObject(buffer)); + RootedObject readChunk(cx, GetReadChunk(cx, request)); + CHECK(JS_IsUint8Array(readChunk)); + void* readBufferData; + { + JS::AutoCheckCannotGC autoNoGC(cx); + readBufferData = JS_GetArrayBufferViewData(readChunk, &isShared, autoNoGC); + } + CHECK(readBufferData); + CHECK(!memcmp(test_buffer_data, readBufferData, length)); + // TODO: eliminate the memcpy that happens here. +// CHECK(readBufferData == test_buffer_data); + + return true; +} +END_TEST(testReadableStream_ReadableByteStreamBYOBReaderRead) + +BEGIN_TEST(testReadableStream_ReadableStreamDefaultReaderClose) +{ + SetReadableStreamCallbacks(cx, &DataRequestCB, &WriteIntoRequestBufferCB, + &CancelStreamCB, &StreamClosedCB, &StreamErroredCB, + &FinalizeStreamCB); + RootedObject stream(cx, NewDefaultStream(cx)); + CHECK(stream); + RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default)); + CHECK(reader); + + RootedObject request(cx, ReadableStreamDefaultReaderRead(cx, reader)); + CHECK(request); + CHECK(IsPromiseObject(request)); + CHECK(GetPromiseState(request) == PromiseState::Pending); + + CHECK(ReadableStreamClose(cx, stream)); + + bool done; + RootedValue value(cx); + CHECK(GetPromiseState(request) == PromiseState::Fulfilled); + CHECK(GetIterResult(cx, request, &value, &done)); + CHECK(value.isUndefined()); + CHECK(done); + + // The callbacks are only invoked for external streams. + CHECK(!streamClosedCBCalled); + + return true; +} +END_TEST(testReadableStream_ReadableStreamDefaultReaderClose) + +BEGIN_TEST(testReadableStream_ReadableStreamDefaultReaderError) +{ + ResetCallbacks(); + SetReadableStreamCallbacks(cx, &DataRequestCB, &WriteIntoRequestBufferCB, + &CancelStreamCB, &StreamClosedCB, &StreamErroredCB, + &FinalizeStreamCB); + RootedObject stream(cx, NewDefaultStream(cx)); + CHECK(stream); + RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default)); + CHECK(reader); + + RootedObject request(cx, ReadableStreamDefaultReaderRead(cx, reader)); + CHECK(request); + CHECK(IsPromiseObject(request)); + CHECK(GetPromiseState(request) == PromiseState::Pending); + + CHECK(ReadableStreamIsLocked(stream)); + CHECK(ReadableStreamIsReadable(stream)); + RootedValue error(cx, Int32Value(42)); + CHECK(ReadableStreamError(cx, stream, error)); + + CHECK(GetPromiseState(request) == PromiseState::Rejected); + RootedValue reason(cx, GetPromiseResult(request)); + CHECK(reason.isInt32()); + CHECK(reason.toInt32() == 42); + + // The callbacks are only invoked for external streams. + CHECK(!streamErroredCBCalled); + + return true; +} +END_TEST(testReadableStream_ReadableStreamDefaultReaderError) + +static JSObject* +NewExternalSourceStream(JSContext* cx, void* underlyingSource, + RequestReadableStreamDataCallback dataRequestCallback, + WriteIntoReadRequestBufferCallback writeIntoReadRequestCallback, + CancelReadableStreamCallback cancelCallback, + ReadableStreamClosedCallback closedCallback, + ReadableStreamErroredCallback erroredCallback, + ReadableStreamFinalizeCallback finalizeCallback) +{ + SetReadableStreamCallbacks(cx, dataRequestCallback, writeIntoReadRequestCallback, + cancelCallback, closedCallback, erroredCallback, + finalizeCallback); + RootedObject stream(cx, NewReadableExternalSourceStreamObject(cx, underlyingSource)); + MOZ_ASSERT_IF(stream, IsReadableStream(stream)); + return stream; +} + +BEGIN_TEST(testReadableStream_CreateReadableByteStreamWithExternalSource) +{ + ResetCallbacks(); + + RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB, + &WriteIntoRequestBufferCB, &CancelStreamCB, + &StreamClosedCB, &StreamErroredCB, + &FinalizeStreamCB)); + CHECK(stream); + CHECK(ReadableStreamGetMode(stream) == JS::ReadableStreamMode::ExternalSource); + void* underlyingSource; + CHECK(ReadableStreamGetExternalUnderlyingSource(cx, stream, &underlyingSource)); + CHECK(underlyingSource == &test_buffer_data); + CHECK(ReadableStreamIsLocked(stream)); + ReadableStreamReleaseExternalUnderlyingSource(stream); + + return true; +} +END_TEST(testReadableStream_CreateReadableByteStreamWithExternalSource) + +BEGIN_TEST(testReadableStream_ExternalSourceCancel) +{ + ResetCallbacks(); + + RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB, + &WriteIntoRequestBufferCB, &CancelStreamCB, + &StreamClosedCB, &StreamErroredCB, + &FinalizeStreamCB)); + CHECK(stream); + RootedValue reason(cx, Int32Value(42)); + CHECK(ReadableStreamCancel(cx, stream, reason)); + CHECK(cancelStreamCBCalled); + CHECK(cancelStreamReason == reason); + + return true; +} +END_TEST(testReadableStream_ExternalSourceCancel) + +BEGIN_TEST(testReadableStream_ExternalSourceGetReader) +{ + ResetCallbacks(); + + RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB, + &WriteIntoRequestBufferCB, &CancelStreamCB, + &StreamClosedCB, &StreamErroredCB, + &FinalizeStreamCB)); + CHECK(stream); + + RootedValue streamVal(cx, ObjectValue(*stream)); + CHECK(JS_SetProperty(cx, global, "stream", streamVal)); + RootedValue rval(cx); + EVAL("stream.getReader()", &rval); + CHECK(rval.isObject()); + RootedObject reader(cx, &rval.toObject()); + CHECK(IsReadableStreamDefaultReader(reader)); + + return true; +} +END_TEST(testReadableStream_ExternalSourceGetReader) + +BEGIN_TEST(testReadableStream_ExternalSourceUpdateAvailableData) +{ + ResetCallbacks(); + + RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB, + &WriteIntoRequestBufferCB, &CancelStreamCB, + &StreamClosedCB, &StreamErroredCB, + &FinalizeStreamCB)); + CHECK(stream); + + ReadableStreamUpdateDataAvailableFromSource(cx, stream, 1024); + + return true; +} +END_TEST(testReadableStream_ExternalSourceUpdateAvailableData) + +struct ReadFromExternalSourceFixture : public JSAPITest +{ + virtual ~ReadFromExternalSourceFixture() {} + + bool readWithoutDataAvailable(const char* evalSrc, const char* evalSrc2, + uint32_t writtenLength) + { + ResetCallbacks(); + definePrint(); + + RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB, + &WriteIntoRequestBufferCB, + &CancelStreamCB, + &StreamClosedCB, &StreamErroredCB, + &FinalizeStreamCB)); + CHECK(stream); + js::RunJobs(cx); + void* underlyingSource; + CHECK(ReadableStreamGetExternalUnderlyingSource(cx, stream, &underlyingSource)); + CHECK(underlyingSource == &test_buffer_data); + CHECK(ReadableStreamIsLocked(stream)); + ReadableStreamReleaseExternalUnderlyingSource(stream); + + RootedValue streamVal(cx, ObjectValue(*stream)); + CHECK(JS_SetProperty(cx, global, "stream", streamVal)); + + RootedValue rval(cx); + EVAL(evalSrc, &rval); + CHECK(dataRequestCBCalled); + CHECK(!writeIntoRequestBufferCBCalled); + CHECK(rval.isObject()); + RootedObject promise(cx, &rval.toObject()); + CHECK(IsPromiseObject(promise)); + CHECK(GetPromiseState(promise) == PromiseState::Pending); + + size_t length = sizeof(test_buffer_data); + ReadableStreamUpdateDataAvailableFromSource(cx, stream, length); + + CHECK(writeIntoRequestBufferCBCalled); + CHECK(GetPromiseState(promise) == PromiseState::Fulfilled); + RootedValue iterVal(cx); + bool done; + if (!GetIterResult(cx, promise, &iterVal, &done)) + return false; + + CHECK(!done); + RootedObject chunk(cx, &iterVal.toObject()); + CHECK(JS_IsUint8Array(chunk)); + + { + JS::AutoCheckCannotGC noGC(cx); + bool dummy; + void* buffer = JS_GetArrayBufferViewData(chunk, &dummy, noGC); + CHECK(!memcmp(buffer, test_buffer_data, writtenLength)); + } + + dataRequestCBCalled = false; + writeIntoRequestBufferCBCalled = false; + EVAL(evalSrc2, &rval); + CHECK(dataRequestCBCalled); + CHECK(!writeIntoRequestBufferCBCalled); + + return true; + } + + bool readWithDataAvailable(const char* evalSrc, uint32_t writtenLength) { + ResetCallbacks(); + definePrint(); + + RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB, + &WriteIntoRequestBufferCB, &CancelStreamCB, + &StreamClosedCB, &StreamErroredCB, + &FinalizeStreamCB)); + CHECK(stream); + void* underlyingSource; + CHECK(ReadableStreamGetExternalUnderlyingSource(cx, stream, &underlyingSource)); + CHECK(underlyingSource == &test_buffer_data); + CHECK(ReadableStreamIsLocked(stream)); + ReadableStreamReleaseExternalUnderlyingSource(stream); + + size_t length = sizeof(test_buffer_data); + ReadableStreamUpdateDataAvailableFromSource(cx, stream, length); + + RootedValue streamVal(cx, ObjectValue(*stream)); + CHECK(JS_SetProperty(cx, global, "stream", streamVal)); + + RootedValue rval(cx); + EVAL(evalSrc, &rval); + CHECK(writeIntoRequestBufferCBCalled); + CHECK(rval.isObject()); + RootedObject promise(cx, &rval.toObject()); + CHECK(IsPromiseObject(promise)); + CHECK(GetPromiseState(promise) == PromiseState::Fulfilled); + RootedValue iterVal(cx); + bool done; + if (!GetIterResult(cx, promise, &iterVal, &done)) + return false; + + CHECK(!done); + RootedObject chunk(cx, &iterVal.toObject()); + CHECK(JS_IsUint8Array(chunk)); + + { + JS::AutoCheckCannotGC noGC(cx); + bool dummy; + void* buffer = JS_GetArrayBufferViewData(chunk, &dummy, noGC); + CHECK(!memcmp(buffer, test_buffer_data, writtenLength)); + } + + return true; + } +}; + +BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture, + testReadableStream_ExternalSourceReadDefaultWithoutDataAvailable) +{ + return readWithoutDataAvailable("r = stream.getReader(); r.read()", "r.read()", sizeof(test_buffer_data)); +} +END_FIXTURE_TEST(ReadFromExternalSourceFixture, + testReadableStream_ExternalSourceReadDefaultWithoutDataAvailable) + +BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture, + testReadableStream_ExternalSourceCloseWithPendingRead) +{ + CHECK(readWithoutDataAvailable("r = stream.getReader(); request0 = r.read(); " + "request1 = r.read(); request0", "r.read()", + sizeof(test_buffer_data))); + + RootedValue val(cx); + CHECK(JS_GetProperty(cx, global, "request1", &val)); + CHECK(val.isObject()); + RootedObject request(cx, &val.toObject()); + CHECK(IsPromiseObject(request)); + CHECK(GetPromiseState(request) == PromiseState::Pending); + + CHECK(JS_GetProperty(cx, global, "stream", &val)); + RootedObject stream(cx, &val.toObject()); + ReadableStreamClose(cx, stream); + + val = GetPromiseResult(request); + MOZ_ASSERT(val.isObject()); + RootedObject result(cx, &val.toObject()); + + JS_GetProperty(cx, result, "done", &val); + CHECK(val.isBoolean()); + CHECK(val.toBoolean() == true); + + JS_GetProperty(cx, result, "value", &val); + CHECK(val.isUndefined()); + return true; +} +END_FIXTURE_TEST(ReadFromExternalSourceFixture, + testReadableStream_ExternalSourceCloseWithPendingRead) + +BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture, + testReadableStream_ExternalSourceReadDefaultWithDataAvailable) +{ + return readWithDataAvailable("r = stream.getReader(); r.read()", sizeof(test_buffer_data)); +} +END_FIXTURE_TEST(ReadFromExternalSourceFixture, + testReadableStream_ExternalSourceReadDefaultWithDataAvailable) + +BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture, + testReadableStream_ExternalSourceReadBYOBWithoutDataAvailable) +{ + return readWithoutDataAvailable("r = stream.getReader({mode: 'byob'}); r.read(new Uint8Array(63))", "r.read(new Uint8Array(10))", 10); +} +END_FIXTURE_TEST(ReadFromExternalSourceFixture, + testReadableStream_ExternalSourceReadBYOBWithoutDataAvailable) + +BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture, + testReadableStream_ExternalSourceReadBYOBWithDataAvailable) +{ + return readWithDataAvailable("r = stream.getReader({mode: 'byob'}); r.read(new Uint8Array(10))", 10); +} +END_FIXTURE_TEST(ReadFromExternalSourceFixture, + testReadableStream_ExternalSourceReadBYOBWithDataAvailable) diff --git a/js/src/jsapi-tests/tests.cpp b/js/src/jsapi-tests/tests.cpp index b75fa6602d..61e79eab6d 100644 --- a/js/src/jsapi-tests/tests.cpp +++ b/js/src/jsapi-tests/tests.cpp @@ -81,6 +81,10 @@ JSObject* JSAPITest::createGlobal(JSPrincipals* principals) /* Create the global object. */ JS::RootedObject newGlobal(cx); JS::CompartmentOptions options; +#ifdef ENABLE_STREAMS + options.creationOptions().setStreamsEnabled(true); +#endif + printf("enabled\n"); options.behaviors().setVersion(JSVERSION_LATEST); newGlobal = JS_NewGlobalObject(cx, getGlobalClass(), principals, JS::FireOnNewGlobalHook, options); diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 0fd82f7c2d..9067ca3d3d 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -46,6 +46,7 @@ #include "builtin/MapObject.h" #include "builtin/Promise.h" #include "builtin/RegExp.h" +#include "builtin/Stream.h" #include "builtin/SymbolObject.h" #ifdef ENABLE_BINARYDATA # include "builtin/TypedObject.h" @@ -5095,7 +5096,6 @@ CallOriginalPromiseThenImpl(JSContext* cx, JS::HandleObject promiseObj, return false; } return true; - } JS_PUBLIC_API(JSObject*) @@ -5126,7 +5126,7 @@ JS::AddPromiseReactions(JSContext* cx, JS::HandleObject promiseObj, * Unforgeable version of Promise.all for internal use. * * Takes a dense array of Promise objects and returns a promise that's - * resolved with an array of resolution values when all those promises ahve + * resolved with an array of resolution values when all those promises have * been resolved, or rejected with the rejection value of the first rejected * promise. * @@ -5141,6 +5141,368 @@ JS::GetWaitForAllPromise(JSContext* cx, const JS::AutoObjectVector& promises) return js::GetWaitForAllPromise(cx, promises); } +JS_PUBLIC_API(JSObject*) +JS::NewReadableDefaultStreamObject(JSContext* cx, + JS::HandleObject underlyingSource /* = nullptr */, + JS::HandleFunction size /* = nullptr */, + double highWaterMark /* = 1 */, + JS::HandleObject proto /* = nullptr */) +{ + MOZ_ASSERT(!cx->runtime()->isAtomsCompartment(cx->compartment())); + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + + RootedObject source(cx, underlyingSource); + if (!source) { + source = NewBuiltinClassInstance(cx); + if (!source) + return nullptr; + } + RootedValue sourceVal(cx, ObjectValue(*source)); + RootedValue sizeVal(cx, size ? ObjectValue(*size) : UndefinedValue()); + RootedValue highWaterMarkVal(cx, NumberValue(highWaterMark)); + return ReadableStream::createDefaultStream(cx, sourceVal, sizeVal, highWaterMarkVal, proto); +} + +JS_PUBLIC_API(JSObject*) +JS::NewReadableByteStreamObject(JSContext* cx, + JS::HandleObject underlyingSource /* = nullptr */, + double highWaterMark /* = 1 */, + JS::HandleObject proto /* = nullptr */) +{ + MOZ_ASSERT(!cx->runtime()->isAtomsCompartment(cx->compartment())); + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + + RootedObject source(cx, underlyingSource); + if (!source) { + source = NewBuiltinClassInstance(cx); + if (!source) + return nullptr; + } + RootedValue sourceVal(cx, ObjectValue(*source)); + RootedValue highWaterMarkVal(cx, NumberValue(highWaterMark)); + return ReadableStream::createByteStream(cx, sourceVal, highWaterMarkVal, proto); +} + +extern JS_PUBLIC_API(void) +JS::SetReadableStreamCallbacks(JSContext* cx, + JS::RequestReadableStreamDataCallback dataRequestCallback, + JS::WriteIntoReadRequestBufferCallback writeIntoReadRequestCallback, + JS::CancelReadableStreamCallback cancelCallback, + JS::ReadableStreamClosedCallback closedCallback, + JS::ReadableStreamErroredCallback erroredCallback, + JS::ReadableStreamFinalizeCallback finalizeCallback) +{ + MOZ_ASSERT(dataRequestCallback); + MOZ_ASSERT(writeIntoReadRequestCallback); + MOZ_ASSERT(cancelCallback); + MOZ_ASSERT(closedCallback); + MOZ_ASSERT(erroredCallback); + MOZ_ASSERT(finalizeCallback); + + JSRuntime* rt = cx->runtime(); + + MOZ_ASSERT(!rt->readableStreamDataRequestCallback); + MOZ_ASSERT(!rt->readableStreamWriteIntoReadRequestCallback); + MOZ_ASSERT(!rt->readableStreamCancelCallback); + MOZ_ASSERT(!rt->readableStreamClosedCallback); + MOZ_ASSERT(!rt->readableStreamErroredCallback); + MOZ_ASSERT(!rt->readableStreamFinalizeCallback); + + rt->readableStreamDataRequestCallback = dataRequestCallback; + rt->readableStreamWriteIntoReadRequestCallback = writeIntoReadRequestCallback; + rt->readableStreamCancelCallback = cancelCallback; + rt->readableStreamClosedCallback = closedCallback; + rt->readableStreamErroredCallback = erroredCallback; + rt->readableStreamFinalizeCallback = finalizeCallback; +} + +JS_PUBLIC_API(bool) +JS::HasReadableStreamCallbacks(JSContext* cx) +{ + return cx->runtime()->readableStreamDataRequestCallback; +} + +JS_PUBLIC_API(JSObject*) +JS::NewReadableExternalSourceStreamObject(JSContext* cx, void* underlyingSource, + uint8_t flags /* = 0 */, + HandleObject proto /* = nullptr */) +{ + MOZ_ASSERT(!cx->runtime()->isAtomsCompartment(cx->compartment())); + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + +#ifdef DEBUG + JSRuntime* rt = cx->runtime(); + MOZ_ASSERT(rt->readableStreamDataRequestCallback); + MOZ_ASSERT(rt->readableStreamWriteIntoReadRequestCallback); + MOZ_ASSERT(rt->readableStreamCancelCallback); + MOZ_ASSERT(rt->readableStreamClosedCallback); + MOZ_ASSERT(rt->readableStreamErroredCallback); + MOZ_ASSERT(rt->readableStreamFinalizeCallback); +#endif // DEBUG + + return ReadableStream::createExternalSourceStream(cx, underlyingSource, flags, proto); +} + +JS_PUBLIC_API(uint8_t) +JS::ReadableStreamGetEmbeddingFlags(const JSObject* stream) +{ + return stream->as().embeddingFlags(); +} + +JS_PUBLIC_API(bool) +JS::IsReadableStream(const JSObject* obj) +{ + return obj->is(); +} + +JS_PUBLIC_API(bool) +JS::IsReadableStreamReader(const JSObject* obj) +{ + return obj->is() || obj->is(); +} + +JS_PUBLIC_API(bool) +JS::IsReadableStreamDefaultReader(const JSObject* obj) +{ + return obj->is(); +} + +JS_PUBLIC_API(bool) +JS::IsReadableStreamBYOBReader(const JSObject* obj) +{ + return obj->is(); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamIsReadable(const JSObject* stream) +{ + return stream->as().readable(); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamIsLocked(const JSObject* stream) +{ + return stream->as().locked(); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamIsDisturbed(const JSObject* stream) +{ + return stream->as().disturbed(); +} + +JS_PUBLIC_API(JSObject*) +JS::ReadableStreamCancel(JSContext* cx, HandleObject streamObj, HandleValue reason) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, streamObj); + assertSameCompartment(cx, reason); + + Rooted stream(cx, &streamObj->as()); + return ReadableStream::cancel(cx, stream, reason); +} + +JS_PUBLIC_API(JS::ReadableStreamMode) +JS::ReadableStreamGetMode(const JSObject* stream) +{ + return stream->as().mode(); +} + +JS_PUBLIC_API(JSObject*) +JS::ReadableStreamGetReader(JSContext* cx, HandleObject streamObj, ReadableStreamReaderMode mode) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, streamObj); + + Rooted stream(cx, &streamObj->as()); + return ReadableStream::getReader(cx, stream, mode); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamGetExternalUnderlyingSource(JSContext* cx, HandleObject streamObj, void** source) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, streamObj); + + Rooted stream(cx, &streamObj->as()); + return ReadableStream::getExternalSource(cx, stream, source); +} + +JS_PUBLIC_API(void) +JS::ReadableStreamReleaseExternalUnderlyingSource(JSObject* stream) +{ + stream->as().releaseExternalSource(); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamUpdateDataAvailableFromSource(JSContext* cx, JS::HandleObject streamObj, + uint32_t availableData) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, streamObj); + + Rooted stream(cx, &streamObj->as()); + return ReadableStream::updateDataAvailableFromSource(cx, stream, availableData); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamTee(JSContext* cx, HandleObject streamObj, + MutableHandleObject branch1Obj, MutableHandleObject branch2Obj) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, streamObj); + + Rooted stream(cx, &streamObj->as()); + Rooted branch1Stream(cx); + Rooted branch2Stream(cx); + + if (!ReadableStream::tee(cx, stream, false, &branch1Stream, &branch2Stream)) + return false; + + branch1Obj.set(branch1Stream); + branch2Obj.set(branch2Stream); + + return true; +} + +JS_PUBLIC_API(void) +JS::ReadableStreamGetDesiredSize(JSObject* streamObj, bool* hasValue, double* value) +{ + streamObj->as().desiredSize(hasValue, value); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamClose(JSContext* cx, HandleObject streamObj) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, streamObj); + + Rooted stream(cx, &streamObj->as()); + return ReadableStream::close(cx, stream); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamEnqueue(JSContext* cx, HandleObject streamObj, HandleValue chunk) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, streamObj); + assertSameCompartment(cx, chunk); + + Rooted stream(cx, &streamObj->as()); + if (stream->mode() != JS::ReadableStreamMode::Default) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_NOT_DEFAULT_CONTROLLER, + "JS::ReadableStreamEnqueue"); + return false; + } + return ReadableStream::enqueue(cx, stream, chunk); +} + +JS_PUBLIC_API(bool) +JS::ReadableByteStreamEnqueueBuffer(JSContext* cx, HandleObject streamObj, HandleObject chunkObj) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, streamObj); + assertSameCompartment(cx, chunkObj); + + Rooted stream(cx, &streamObj->as()); + if (stream->mode() != JS::ReadableStreamMode::Byte) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER, + "JS::ReadableByteStreamEnqueueBuffer"); + return false; + } + + Rooted buffer(cx); + if (chunkObj->is()) { + bool dummy; + buffer = &JS_GetArrayBufferViewBuffer(cx, chunkObj, &dummy)->as(); + } else if (chunkObj->is()) { + buffer = &chunkObj->as(); + } else { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK, + "JS::ReadableByteStreamEnqueueBuffer"); + return false; + } + + return ReadableStream::enqueueBuffer(cx, stream, buffer); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamError(JSContext* cx, HandleObject streamObj, HandleValue error) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, streamObj); + assertSameCompartment(cx, error); + + Rooted stream(cx, &streamObj->as()); + return js::ReadableStream::error(cx, stream, error); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamReaderIsClosed(const JSObject* reader) +{ + return js::ReadableStreamReaderIsClosed(reader); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamReaderCancel(JSContext* cx, HandleObject reader, HandleValue reason) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, reader); + assertSameCompartment(cx, reason); + + return js::ReadableStreamReaderCancel(cx, reader, reason); +} + +JS_PUBLIC_API(bool) +JS::ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject reader) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, reader); + + return js::ReadableStreamReaderReleaseLock(cx, reader); +} + +JS_PUBLIC_API(JSObject*) +JS::ReadableStreamDefaultReaderRead(JSContext* cx, HandleObject readerObj) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, readerObj); + + Rooted reader(cx, &readerObj->as()); + return js::ReadableStreamDefaultReader::read(cx, reader); +} + +JS_PUBLIC_API(JSObject*) +JS::ReadableStreamBYOBReaderRead(JSContext* cx, HandleObject readerObj, HandleObject viewObj) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, readerObj); + assertSameCompartment(cx, viewObj); + + Rooted reader(cx, &readerObj->as()); + Rooted view(cx, &viewObj->as()); + return js::ReadableStreamBYOBReader::read(cx, reader, view); +} + JS_PUBLIC_API(void) JS::SetAsyncTaskCallbacks(JSContext* cx, JS::StartAsyncTaskCallback start, JS::FinishAsyncTaskCallback finish) diff --git a/js/src/jsapi.h b/js/src/jsapi.h index a8e28a587b..951b612502 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -35,6 +35,7 @@ #include "js/Principals.h" #include "js/Realm.h" #include "js/RootingAPI.h" +#include "js/Stream.h" #include "js/TracingAPI.h" #include "js/UniquePtr.h" #include "js/Utility.h" diff --git a/js/src/moz.build b/js/src/moz.build index adc8aa3e0f..d42cf59730 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -92,6 +92,7 @@ EXPORTS.js += [ '../public/Result.h', '../public/RootingAPI.h', '../public/SliceBudget.h', + '../public/Stream.h', '../public/StructuredClone.h', '../public/SweepingAPI.h', '../public/TraceKind.h', diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index 2a217e357f..ebfad89fa0 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -18,6 +18,8 @@ macro(apply, apply, "apply") \ macro(args, args, "args") \ macro(arguments, arguments, "arguments") \ + macro(AcquireReadableStreamBYOBReader, AcquireReadableStreamBYOBReader, "AcquireReadableStreamBYOBReader") \ + macro(AcquireReadableStreamDefaultReader, AcquireReadableStreamDefaultReader, "AcquireReadableStreamDefaultReader") \ macro(ArrayBufferSpecies, ArrayBufferSpecies, "ArrayBufferSpecies") \ macro(ArrayIterator, ArrayIterator, "Array Iterator") \ macro(ArrayIteratorNext, ArrayIteratorNext, "ArrayIteratorNext") \ @@ -319,6 +321,50 @@ macro(public, public_, "public") \ macro(pull, pull, "pull") \ macro(raw, raw, "raw") \ + macro(ReadableByteStreamControllerGetDesiredSize, \ + ReadableByteStreamControllerGetDesiredSize, \ + "ReadableByteStreamControllerGetDesiredSize") \ + macro(ReadableByteStreamController_close, \ + ReadableByteStreamController_close, \ + "ReadableByteStreamController_close") \ + macro(ReadableByteStreamController_enqueue, \ + ReadableByteStreamController_enqueue, \ + "ReadableByteStreamController_enqueue") \ + macro(ReadableByteStreamController_error, \ + ReadableByteStreamController_error, \ + "ReadableByteStreamController_error") \ + macro(ReadableStreamBYOBReader_cancel, \ + ReadableStreamBYOBReader_cancel, \ + "ReadableStreamBYOBReader_cancel") \ + macro(ReadableStreamBYOBReader_read, \ + ReadableStreamBYOBReader_read, \ + "ReadableStreamBYOBReader_read") \ + macro(ReadableStreamBYOBReader_releaseLock, \ + ReadableStreamBYOBReader_releaseLock, \ + "ReadableStreamBYOBReader_releaseLock") \ + macro(ReadableStream_cancel, ReadableStream_cancel, "ReadableStream_cancel") \ + macro(ReadableStreamDefaultControllerGetDesiredSize, \ + ReadableStreamDefaultControllerGetDesiredSize, \ + "ReadableStreamDefaultControllerGetDesiredSize") \ + macro(ReadableStreamDefaultController_close, \ + ReadableStreamDefaultController_close, \ + "ReadableStreamDefaultController_close") \ + macro(ReadableStreamDefaultController_enqueue, \ + ReadableStreamDefaultController_enqueue, \ + "ReadableStreamDefaultController_enqueue") \ + macro(ReadableStreamDefaultController_error, \ + ReadableStreamDefaultController_error, \ + "ReadableStreamDefaultController_error") \ + macro(ReadableStreamDefaultReader_cancel, \ + ReadableStreamDefaultReader_cancel, \ + "ReadableStreamDefaultReader_cancel") \ + macro(ReadableStreamDefaultReader_read, \ + ReadableStreamDefaultReader_read, \ + "ReadableStreamDefaultReader_read") \ + macro(ReadableStreamDefaultReader_releaseLock, \ + ReadableStreamDefaultReader_releaseLock, \ + "ReadableStreamDefaultReader_releaseLock") \ + macro(ReadableStreamTee, ReadableStreamTee, "ReadableStreamTee") \ macro(reason, reason, "reason") \ macro(RegExpFlagsGetter, RegExpFlagsGetter, "RegExpFlagsGetter") \ macro(RegExpStringIterator, RegExpStringIterator, "RegExp String Iterator") \ diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp index 053b7c44b0..c5deff940c 100644 --- a/js/src/vm/Runtime.cpp +++ b/js/src/vm/Runtime.cpp @@ -156,6 +156,12 @@ JSRuntime::JSRuntime(JSRuntime* parentRuntime) startAsyncTaskCallback(nullptr), finishAsyncTaskCallback(nullptr), promiseTasksToDestroy(mutexid::PromiseTaskPtrVector), + readableStreamDataRequestCallback(nullptr), + readableStreamWriteIntoReadRequestCallback(nullptr), + readableStreamCancelCallback(nullptr), + readableStreamClosedCallback(nullptr), + readableStreamErroredCallback(nullptr), + readableStreamFinalizeCallback(nullptr), exclusiveAccessLock(mutexid::RuntimeExclusiveAccess), #ifdef DEBUG mainThreadHasExclusiveAccess(false), diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h index 962f0e1a1e..13523844c1 100644 --- a/js/src/vm/Runtime.h +++ b/js/src/vm/Runtime.h @@ -655,6 +655,13 @@ struct JSRuntime : public JS::shadow::Runtime, JS::FinishAsyncTaskCallback finishAsyncTaskCallback; js::ExclusiveData promiseTasksToDestroy; + JS::RequestReadableStreamDataCallback readableStreamDataRequestCallback; + JS::WriteIntoReadRequestBufferCallback readableStreamWriteIntoReadRequestCallback; + JS::CancelReadableStreamCallback readableStreamCancelCallback; + JS::ReadableStreamClosedCallback readableStreamClosedCallback; + JS::ReadableStreamErroredCallback readableStreamErroredCallback; + JS::ReadableStreamFinalizeCallback readableStreamFinalizeCallback; + private: /* * Lock taken when using per-runtime or per-zone data that could otherwise From 0701dccacab59a2ff9ff8805bf390484aec8aee5 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 18:25:48 -0500 Subject: [PATCH 05/31] Issue #1442 - Part 5: Implement WebIDL bindings for Streams. https://bugzilla.mozilla.org/show_bug.cgi?id=1128959 --- dom/bindings/Codegen.py | 32 ++++-- dom/bindings/ReadableStream.h | 28 +++++ dom/bindings/SpiderMonkeyInterface.h | 144 ++++++++++++++++++++++++ dom/bindings/TypedArray.h | 134 ++-------------------- dom/bindings/moz.build | 2 + dom/bindings/parser/WebIDL.py | 39 +++++-- dom/canvas/CanvasRenderingContext2D.cpp | 8 +- dom/canvas/ImageBitmap.cpp | 2 +- dom/canvas/WebGLTextureUpload.cpp | 4 +- dom/crypto/WebCryptoTask.cpp | 4 +- dom/network/TCPSocketParent.cpp | 2 +- dom/xhr/XMLHttpRequestMainThread.cpp | 2 +- js/src/builtin/Stream.cpp | 8 ++ js/src/jsfriendapi.h | 3 + 14 files changed, 255 insertions(+), 157 deletions(-) create mode 100644 dom/bindings/ReadableStream.h create mode 100644 dom/bindings/SpiderMonkeyInterface.h diff --git a/dom/bindings/Codegen.py b/dom/bindings/Codegen.py index a70253fc3a..28f6d2a9a9 100644 --- a/dom/bindings/Codegen.py +++ b/dom/bindings/Codegen.py @@ -1156,7 +1156,10 @@ class CGHeaders(CGWrapper): # just include their header if we need to have functions # taking references to them declared in that header. headerSet = declareIncludes - headerSet.add("mozilla/dom/TypedArray.h") + if unrolled.isReadableStream(): + headerSet.add("mozilla/dom/ReadableStream.h") + else: + headerSet.add("mozilla/dom/TypedArray.h") else: try: typeDesc = config.getDescriptor(unrolled.inner.identifier.name) @@ -1371,7 +1374,10 @@ def UnionTypes(unionTypes, config): elif f.isInterface(): if f.isSpiderMonkeyInterface(): headers.add("jsfriendapi.h") - headers.add("mozilla/dom/TypedArray.h") + if f.isReadableStream(): + headers.add("mozilla/dom/ReadableStream.h") + else: + headers.add("mozilla/dom/TypedArray.h") else: try: typeDesc = config.getDescriptor(f.inner.identifier.name) @@ -1457,7 +1463,10 @@ def UnionConversions(unionTypes, config): elif f.isInterface(): if f.isSpiderMonkeyInterface(): headers.add("jsfriendapi.h") - headers.add("mozilla/dom/TypedArray.h") + if f.isReadableStream(): + headers.add("mozilla/dom/ReadableStream.h") + else: + headers.add("mozilla/dom/TypedArray.h") elif f.inner.isExternal(): try: typeDesc = config.getDescriptor(f.inner.identifier.name) @@ -5582,8 +5591,8 @@ def getJSToNativeConversionInfo(type, descriptorProvider, failureCode=None, if type.isSpiderMonkeyInterface(): assert not isEnforceRange and not isClamp name = type.unroll().name # unroll() because it may be nullable - arrayType = CGGeneric(name) - declType = arrayType + interfaceType = CGGeneric(name) + declType = interfaceType if type.nullable(): declType = CGTemplatedType("Nullable", declType) objRef = "${declName}.SetValue()" @@ -5607,10 +5616,11 @@ def getJSToNativeConversionInfo(type, descriptorProvider, failureCode=None, # This is a bit annoying. In a union we don't want to have a # holder, since unions don't support that. But if we're optional we # want to have a holder, so that the callee doesn't see - # Optional >. So do a holder if we're - # optional and use a RootedTypedArray otherwise. + # Optional>. So do a + # holder if we're optional and use a RootedSpiderMonkeyInterface + # otherwise. if isOptional: - holderType = CGTemplatedType("TypedArrayRooter", arrayType) + holderType = CGTemplatedType("SpiderMonkeyInterfaceRooter", interfaceType) # If our typed array is nullable, this will set the Nullable to # be not-null, but that's ok because we make an explicit # SetNull() call on it as needed if our JS value is actually @@ -5622,7 +5632,7 @@ def getJSToNativeConversionInfo(type, descriptorProvider, failureCode=None, else: holderType = None holderArgs = None - declType = CGTemplatedType("RootedTypedArray", declType) + declType = CGTemplatedType("RootedSpiderMonkeyInterface", declType) declArgs = "cx" else: holderType = None @@ -6371,7 +6381,7 @@ def getMaybeWrapValueFuncForType(type): if type.nullable(): return "MaybeWrapObjectOrNullValue" return "MaybeWrapObjectValue" - # Spidermonkey interfaces are never DOM objects. Neither are sequences or + # SpiderMonkey interfaces are never DOM objects. Neither are sequences or # dictionaries, since those are always plain JS objects. if type.isSpiderMonkeyInterface() or type.isDictionary() or type.isSequence(): if type.nullable(): @@ -13679,7 +13689,7 @@ class ForwardDeclarationBuilder: except NoSuchDescriptorError: pass - # Note: Spidermonkey interfaces are typedefs, so can't be + # Note: SpiderMonkey interfaces are typedefs, so can't be # forward-declared elif t.isPromise(): self.addInMozillaDom("Promise") diff --git a/dom/bindings/ReadableStream.h b/dom/bindings/ReadableStream.h new file mode 100644 index 0000000000..1ea7ac4d88 --- /dev/null +++ b/dom/bindings/ReadableStream.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadableStream_h +#define mozilla_dom_ReadableStream_h + +#include "mozilla/dom/SpiderMonkeyInterface.h" + +namespace mozilla { +namespace dom { + +struct ReadableStream : public SpiderMonkeyInterfaceObjectStorage +{ + inline bool Init(JSObject* obj) + { + MOZ_ASSERT(!inited()); + mImplObj = mWrappedObj = js::UnwrapReadableStream(obj); + return inited(); + } +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_ReadableStream_h */ diff --git a/dom/bindings/SpiderMonkeyInterface.h b/dom/bindings/SpiderMonkeyInterface.h new file mode 100644 index 0000000000..f852afddaf --- /dev/null +++ b/dom/bindings/SpiderMonkeyInterface.h @@ -0,0 +1,144 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SpiderMonkeyInterface_h +#define mozilla_dom_SpiderMonkeyInterface_h + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/TracingAPI.h" + +namespace mozilla { +namespace dom { + +/* + * Class that just handles the JSObject storage and tracing for spidermonkey + * interfaces + */ +struct SpiderMonkeyInterfaceObjectStorage +{ +protected: + JSObject* mImplObj; + JSObject* mWrappedObj; + + SpiderMonkeyInterfaceObjectStorage() + : mImplObj(nullptr), + mWrappedObj(nullptr) + { + } + + SpiderMonkeyInterfaceObjectStorage(SpiderMonkeyInterfaceObjectStorage&& aOther) + : mImplObj(aOther.mImplObj), + mWrappedObj(aOther.mWrappedObj) + { + aOther.mImplObj = nullptr; + aOther.mWrappedObj = nullptr; + } + +public: + inline void TraceSelf(JSTracer* trc) + { + JS::UnsafeTraceRoot(trc, &mImplObj, "SpiderMonkeyInterfaceObjectStorage.mImplObj"); + JS::UnsafeTraceRoot(trc, &mWrappedObj, "SpiderMonkeyInterfaceObjectStorage.mWrappedObj"); + } + + inline bool inited() const + { + return !!mImplObj; + } + + inline bool WrapIntoNewCompartment(JSContext* cx) + { + return JS_WrapObject(cx, + JS::MutableHandle::fromMarkedLocation(&mWrappedObj)); + } + + inline JSObject *Obj() const + { + MOZ_ASSERT(inited()); + return mWrappedObj; + } + +private: + SpiderMonkeyInterfaceObjectStorage(const SpiderMonkeyInterfaceObjectStorage&) = delete; +}; + +// A class for rooting an existing SpiderMonkey Interface struct +template +class MOZ_RAII SpiderMonkeyInterfaceRooter : private JS::CustomAutoRooter +{ +public: + template + SpiderMonkeyInterfaceRooter(const CX& cx, + InterfaceType* aInterface MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : JS::CustomAutoRooter(cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM_TO_PARENT), + mInterface(aInterface) + { + } + + virtual void trace(JSTracer* trc) override + { + mInterface->TraceSelf(trc); + } + +private: + SpiderMonkeyInterfaceObjectStorage* const mInterface; +}; + +// And a specialization for dealing with nullable SpiderMonkey interfaces +template struct Nullable; +template +class MOZ_RAII SpiderMonkeyInterfaceRooter> : + private JS::CustomAutoRooter +{ +public: + template + SpiderMonkeyInterfaceRooter(const CX& cx, + Nullable* aInterface MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : JS::CustomAutoRooter(cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM_TO_PARENT), + mInterface(aInterface) + { + } + + virtual void trace(JSTracer* trc) override + { + if (!mInterface->IsNull()) { + mInterface->Value().TraceSelf(trc); + } + } + +private: + Nullable* const mInterface; +}; + +// Class for easily setting up a rooted SpiderMonkey interface object on the +// stack +template +class MOZ_RAII RootedSpiderMonkeyInterface final : public InterfaceType, + private SpiderMonkeyInterfaceRooter +{ +public: + template + explicit RootedSpiderMonkeyInterface(const CX& cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : InterfaceType(), + SpiderMonkeyInterfaceRooter(cx, this + MOZ_GUARD_OBJECT_NOTIFIER_PARAM_TO_PARENT) + { + } + + template + RootedSpiderMonkeyInterface(const CX& cx, JSObject* obj MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : InterfaceType(obj), + SpiderMonkeyInterfaceRooter(cx, this + MOZ_GUARD_OBJECT_NOTIFIER_PARAM_TO_PARENT) + { + } +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_SpiderMonkeyInterface_h */ diff --git a/dom/bindings/TypedArray.h b/dom/bindings/TypedArray.h index 631707579b..8de0621d46 100644 --- a/dom/bindings/TypedArray.h +++ b/dom/bindings/TypedArray.h @@ -6,51 +6,15 @@ #ifndef mozilla_dom_TypedArray_h #define mozilla_dom_TypedArray_h -#include "jsapi.h" -#include "jsfriendapi.h" -#include "js/RootingAPI.h" -#include "js/TracingAPI.h" #include "mozilla/Attributes.h" #include "mozilla/Move.h" #include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/SpiderMonkeyInterface.h" #include "nsWrapperCache.h" namespace mozilla { namespace dom { -/* - * Class that just handles the JSObject storage and tracing for typed arrays - */ -struct TypedArrayObjectStorage : AllTypedArraysBase { -protected: - JSObject* mTypedObj; - JSObject* mWrappedObj; - - TypedArrayObjectStorage() - : mTypedObj(nullptr), - mWrappedObj(nullptr) - { - } - - TypedArrayObjectStorage(TypedArrayObjectStorage&& aOther) - : mTypedObj(aOther.mTypedObj), - mWrappedObj(aOther.mWrappedObj) - { - aOther.mTypedObj = nullptr; - aOther.mWrappedObj = nullptr; - } - -public: - inline void TraceSelf(JSTracer* trc) - { - JS::UnsafeTraceRoot(trc, &mTypedObj, "TypedArray.mTypedObj"); - JS::UnsafeTraceRoot(trc, &mWrappedObj, "TypedArray.mWrappedObj"); - } - -private: - TypedArrayObjectStorage(const TypedArrayObjectStorage&) = delete; -}; - /* * Various typed array classes for argument conversion. We have a base class * that has a way of initializing a TypedArray from an existing typed array, and @@ -60,7 +24,9 @@ private: template -struct TypedArray_base : public TypedArrayObjectStorage { +struct TypedArray_base : public SpiderMonkeyInterfaceObjectStorage, + AllTypedArraysBase +{ typedef T element_type; TypedArray_base() @@ -72,7 +38,7 @@ struct TypedArray_base : public TypedArrayObjectStorage { } TypedArray_base(TypedArray_base&& aOther) - : TypedArrayObjectStorage(Move(aOther)), + : SpiderMonkeyInterfaceObjectStorage(Move(aOther)), mData(aOther.mData), mLength(aOther.mLength), mShared(aOther.mShared), @@ -94,14 +60,10 @@ public: inline bool Init(JSObject* obj) { MOZ_ASSERT(!inited()); - mTypedObj = mWrappedObj = UnwrapArray(obj); + mImplObj = mWrappedObj = UnwrapArray(obj); return inited(); } - inline bool inited() const { - return !!mTypedObj; - } - // About shared memory: // // Any DOM TypedArray as well as any DOM ArrayBufferView that does @@ -173,22 +135,11 @@ public: return mLength; } - inline JSObject *Obj() const { - MOZ_ASSERT(inited()); - return mWrappedObj; - } - - inline bool WrapIntoNewCompartment(JSContext* cx) - { - return JS_WrapObject(cx, - JS::MutableHandle::fromMarkedLocation(&mWrappedObj)); - } - inline void ComputeLengthAndData() const { MOZ_ASSERT(inited()); MOZ_ASSERT(!mComputed); - GetLengthAndDataAndSharedness(mTypedObj, &mLength, &mShared, &mData); + GetLengthAndDataAndSharedness(mImplObj, &mLength, &mShared, &mData); mComputed = true; } @@ -363,77 +314,6 @@ class TypedArrayCreator const ArrayType& mArray; }; -// A class for rooting an existing TypedArray struct -template -class MOZ_RAII TypedArrayRooter : private JS::CustomAutoRooter -{ -public: - template - TypedArrayRooter(const CX& cx, - ArrayType* aArray MOZ_GUARD_OBJECT_NOTIFIER_PARAM) : - JS::CustomAutoRooter(cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM_TO_PARENT), - mArray(aArray) - { - } - - virtual void trace(JSTracer* trc) override - { - mArray->TraceSelf(trc); - } - -private: - TypedArrayObjectStorage* const mArray; -}; - -// And a specialization for dealing with nullable typed arrays -template struct Nullable; -template -class MOZ_RAII TypedArrayRooter > : - private JS::CustomAutoRooter -{ -public: - template - TypedArrayRooter(const CX& cx, - Nullable* aArray MOZ_GUARD_OBJECT_NOTIFIER_PARAM) : - JS::CustomAutoRooter(cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM_TO_PARENT), - mArray(aArray) - { - } - - virtual void trace(JSTracer* trc) override - { - if (!mArray->IsNull()) { - mArray->Value().TraceSelf(trc); - } - } - -private: - Nullable* const mArray; -}; - -// Class for easily setting up a rooted typed array object on the stack -template -class MOZ_RAII RootedTypedArray final : public ArrayType, - private TypedArrayRooter -{ -public: - template - explicit RootedTypedArray(const CX& cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM) : - ArrayType(), - TypedArrayRooter(cx, this - MOZ_GUARD_OBJECT_NOTIFIER_PARAM_TO_PARENT) - { - } - - template - RootedTypedArray(const CX& cx, JSObject* obj MOZ_GUARD_OBJECT_NOTIFIER_PARAM) : - ArrayType(obj), - TypedArrayRooter(cx, this - MOZ_GUARD_OBJECT_NOTIFIER_PARAM_TO_PARENT) - { - } -}; - } // namespace dom } // namespace mozilla diff --git a/dom/bindings/moz.build b/dom/bindings/moz.build index 47303c0957..22e2c491ef 100644 --- a/dom/bindings/moz.build +++ b/dom/bindings/moz.build @@ -38,9 +38,11 @@ EXPORTS.mozilla.dom += [ 'NonRefcountedDOMObject.h', 'Nullable.h', 'PrimitiveConversions.h', + 'ReadableStream.h', 'Record.h', 'RootedDictionary.h', 'SimpleGlobalObject.h', + 'SpiderMonkeyInterface.h', 'StructuredClone.h', 'ToJSValue.h', 'TypedArray.h', diff --git a/dom/bindings/parser/WebIDL.py b/dom/bindings/parser/WebIDL.py index 59db43f6bd..0aa3afa4e8 100644 --- a/dom/bindings/parser/WebIDL.py +++ b/dom/bindings/parser/WebIDL.py @@ -2054,6 +2054,9 @@ class IDLType(IDLObject): def isRecord(self): return False + def isReadableStream(self): + return False + def isArrayBuffer(self): return False @@ -2081,12 +2084,12 @@ class IDLType(IDLObject): def isSpiderMonkeyInterface(self): """ Returns a boolean indicating whether this type is an 'interface' - type that is implemented in Spidermonkey. At the moment, this - only returns true for the types from the TypedArray spec. """ + type that is implemented in SpiderMonkey. """ return self.isInterface() and (self.isArrayBuffer() or self.isArrayBufferView() or self.isSharedArrayBuffer() or - self.isTypedArray()) + self.isTypedArray() or + self.isReadableStream()) def isDictionary(self): return False @@ -2275,6 +2278,9 @@ class IDLNullableType(IDLParameterizedType): def isRecord(self): return self.inner.isRecord() + def isReadableStream(self): + return self.inner.isReadableStream() + def isArrayBuffer(self): return self.inner.isArrayBuffer() @@ -2634,6 +2640,9 @@ class IDLTypedefType(IDLType): def isRecord(self): return self.inner.isRecord() + def isReadableStream(self): + return self.inner.isReadableStream() + def isDictionary(self): return self.inner.isDictionary() @@ -2945,7 +2954,8 @@ class IDLBuiltinType(IDLType): 'Int32Array', 'Uint32Array', 'Float32Array', - 'Float64Array' + 'Float64Array', + 'ReadableStream', ) TagLookup = { @@ -2980,7 +2990,8 @@ class IDLBuiltinType(IDLType): Types.Int32Array: IDLType.Tags.interface, Types.Uint32Array: IDLType.Tags.interface, Types.Float32Array: IDLType.Tags.interface, - Types.Float64Array: IDLType.Tags.interface + Types.Float64Array: IDLType.Tags.interface, + Types.ReadableStream: IDLType.Tags.interface, } def __init__(self, location, name, type): @@ -3027,6 +3038,9 @@ class IDLBuiltinType(IDLType): return (self._typeTag >= IDLBuiltinType.Types.Int8Array and self._typeTag <= IDLBuiltinType.Types.Float64Array) + def isReadableStream(self): + return self._typeTag == IDLBuiltinType.Types.ReadableStream + def isInterface(self): # TypedArray things are interface types per the TypedArray spec, # but we handle them as builtins because SpiderMonkey implements @@ -3034,7 +3048,8 @@ class IDLBuiltinType(IDLType): return (self.isArrayBuffer() or self.isArrayBufferView() or self.isSharedArrayBuffer() or - self.isTypedArray()) + self.isTypedArray() or + self.isReadableStream()) def isNonCallbackInterface(self): # All the interfaces we can be are non-callback @@ -3104,6 +3119,7 @@ class IDLBuiltinType(IDLType): # that's not an ArrayBuffer or a callback interface (self.isArrayBuffer() and not other.isArrayBuffer()) or (self.isSharedArrayBuffer() and not other.isSharedArrayBuffer()) or + (self.isReadableStream() and not other.isReadableStream()) or # ArrayBufferView is distinguishable from everything # that's not an ArrayBufferView or typed array. (self.isArrayBufferView() and not other.isArrayBufferView() and @@ -3213,7 +3229,10 @@ BuiltinTypes = { IDLBuiltinType.Types.Float32Array), IDLBuiltinType.Types.Float64Array: IDLBuiltinType(BuiltinLocation(""), "Float64Array", - IDLBuiltinType.Types.Float64Array) + IDLBuiltinType.Types.Float64Array), + IDLBuiltinType.Types.ReadableStream: + IDLBuiltinType(BuiltinLocation(""), "ReadableStream", + IDLBuiltinType.Types.ReadableStream), } @@ -5232,7 +5251,8 @@ class Tokenizer(object): "maplike": "MAPLIKE", "setlike": "SETLIKE", "iterable": "ITERABLE", - "namespace": "NAMESPACE" + "namespace": "NAMESPACE", + "ReadableStream": "READABLESTREAM", } tokens.extend(keywords.values()) @@ -6420,6 +6440,7 @@ class Parser(Tokenizer): NonAnyType : PrimitiveType Null | ARRAYBUFFER Null | SHAREDARRAYBUFFER Null + | READABLESTREAM Null | OBJECT Null """ if p[1] == "object": @@ -6428,6 +6449,8 @@ class Parser(Tokenizer): type = BuiltinTypes[IDLBuiltinType.Types.ArrayBuffer] elif p[1] == "SharedArrayBuffer": type = BuiltinTypes[IDLBuiltinType.Types.SharedArrayBuffer] + elif p[1] == "ReadableStream": + type = BuiltinTypes[IDLBuiltinType.Types.ReadableStream] else: type = BuiltinTypes[p[1]] diff --git a/dom/canvas/CanvasRenderingContext2D.cpp b/dom/canvas/CanvasRenderingContext2D.cpp index f56f2bdf53..d8c3c4e135 100644 --- a/dom/canvas/CanvasRenderingContext2D.cpp +++ b/dom/canvas/CanvasRenderingContext2D.cpp @@ -2974,8 +2974,8 @@ ValidateRect(double& aX, double& aY, double& aWidth, double& aHeight, bool aIsZe // The values of canvas API input are in double precision, but Moz2D APIs are // using float precision. Bypass canvas API calls when the input is out of // float precision to avoid precision problem - if (!std::isfinite((float)aX) | !std::isfinite((float)aY) | - !std::isfinite((float)aWidth) | !std::isfinite((float)aHeight)) { + if (!std::isfinite((float)aX) || !std::isfinite((float)aY) || + !std::isfinite((float)aWidth) || !std::isfinite((float)aHeight)) { return false; } @@ -5961,7 +5961,7 @@ void CanvasRenderingContext2D::PutImageData(ImageData& aImageData, double aDx, double aDy, ErrorResult& aError) { - RootedTypedArray arr(RootingCx()); + RootedSpiderMonkeyInterface arr(RootingCx()); DebugOnly inited = arr.Init(aImageData.GetDataObject()); MOZ_ASSERT(inited); @@ -5977,7 +5977,7 @@ CanvasRenderingContext2D::PutImageData(ImageData& aImageData, double aDx, double aDirtyHeight, ErrorResult& aError) { - RootedTypedArray arr(RootingCx()); + RootedSpiderMonkeyInterface arr(RootingCx()); DebugOnly inited = arr.Init(aImageData.GetDataObject()); MOZ_ASSERT(inited); diff --git a/dom/canvas/ImageBitmap.cpp b/dom/canvas/ImageBitmap.cpp index 042c0fa11a..7aaf4e74d8 100644 --- a/dom/canvas/ImageBitmap.cpp +++ b/dom/canvas/ImageBitmap.cpp @@ -929,7 +929,7 @@ ImageBitmap::CreateInternal(nsIGlobalObject* aGlobal, ImageData& aImageData, const Maybe& aCropRect, ErrorResult& aRv) { // Copy data into SourceSurface. - RootedTypedArray array(RootingCx()); + RootedSpiderMonkeyInterface array(RootingCx()); DebugOnly inited = array.Init(aImageData.GetDataObject()); MOZ_ASSERT(inited); diff --git a/dom/canvas/WebGLTextureUpload.cpp b/dom/canvas/WebGLTextureUpload.cpp index ed199cfb43..661fb91ce2 100644 --- a/dom/canvas/WebGLTextureUpload.cpp +++ b/dom/canvas/WebGLTextureUpload.cpp @@ -475,7 +475,7 @@ WebGLTexture::TexImage(const char* funcName, TexImageTarget target, GLint level, GLsizei depth, GLint border, const webgl::PackingInfo& pi, const TexImageSource& src) { - dom::RootedTypedArray scopedArr(dom::RootingCx()); + dom::RootedSpiderMonkeyInterface scopedArr(dom::RootingCx()); const auto blob = ValidateTexOrSubImage(mContext, funcName, target, width, height, depth, border, pi, src, &scopedArr); if (!blob) @@ -491,7 +491,7 @@ WebGLTexture::TexSubImage(const char* funcName, TexImageTarget target, GLint lev const webgl::PackingInfo& pi, const TexImageSource& src) { const GLint border = 0; - dom::RootedTypedArray scopedArr(dom::RootingCx()); + dom::RootedSpiderMonkeyInterface scopedArr(dom::RootingCx()); const auto blob = ValidateTexOrSubImage(mContext, funcName, target, width, height, depth, border, pi, src, &scopedArr); if (!blob) diff --git a/dom/crypto/WebCryptoTask.cpp b/dom/crypto/WebCryptoTask.cpp index ed47325f8d..34a4c877d7 100644 --- a/dom/crypto/WebCryptoTask.cpp +++ b/dom/crypto/WebCryptoTask.cpp @@ -1366,7 +1366,7 @@ public: mDataIsJwk = false; // Try ArrayBuffer - RootedTypedArray ab(aCx); + RootedSpiderMonkeyInterface ab(aCx); if (ab.Init(aKeyData)) { if (!mKeyData.Assign(ab)) { mEarlyRv = NS_ERROR_DOM_OPERATION_ERR; @@ -1375,7 +1375,7 @@ public: } // Try ArrayBufferView - RootedTypedArray abv(aCx); + RootedSpiderMonkeyInterface abv(aCx); if (abv.Init(aKeyData)) { if (!mKeyData.Assign(abv)) { mEarlyRv = NS_ERROR_DOM_OPERATION_ERR; diff --git a/dom/network/TCPSocketParent.cpp b/dom/network/TCPSocketParent.cpp index 96eab44510..0e7f0e17ae 100644 --- a/dom/network/TCPSocketParent.cpp +++ b/dom/network/TCPSocketParent.cpp @@ -310,7 +310,7 @@ TCPSocketParent::RecvData(const SendableData& aData, const nsTArray& buffer = aData.get_ArrayOfuint8_t(); bool ok = IPC::DeserializeArrayBuffer(autoCx, buffer, &val); NS_ENSURE_TRUE(ok, true); - RootedTypedArray data(autoCx); + RootedSpiderMonkeyInterface data(autoCx); data.Init(&val.toObject()); Optional byteLength(buffer.Length()); mSocket->SendWithTrackingNumber(autoCx, data, 0, byteLength, aTrackingNumber, rv); diff --git a/dom/xhr/XMLHttpRequestMainThread.cpp b/dom/xhr/XMLHttpRequestMainThread.cpp index aa1fc4e908..c7a4f6e6aa 100644 --- a/dom/xhr/XMLHttpRequestMainThread.cpp +++ b/dom/xhr/XMLHttpRequestMainThread.cpp @@ -2827,7 +2827,7 @@ XMLHttpRequestMainThread::Send(nsIVariant* aVariant) nsresult rv = aVariant->GetAsJSVal(&realVal); if (NS_SUCCEEDED(rv) && !realVal.isPrimitive()) { JS::Rooted obj(rootingCx, realVal.toObjectOrNull()); - RootedTypedArray buf(rootingCx); + RootedSpiderMonkeyInterface buf(rootingCx); if (buf.Init(obj)) { RequestBody body(&buf); return SendInternal(&body); diff --git a/js/src/builtin/Stream.cpp b/js/src/builtin/Stream.cpp index aa8ce3091e..de2414d351 100644 --- a/js/src/builtin/Stream.cpp +++ b/js/src/builtin/Stream.cpp @@ -5487,3 +5487,11 @@ ReadableStream::getReader(JSContext* cx, Handle stream, return CreateReadableStreamDefaultReader(cx, stream); return CreateReadableStreamBYOBReader(cx, stream); } + +JS_FRIEND_API(JSObject*) +js::UnwrapReadableStream(JSObject* obj) +{ + if (JSObject* unwrapped = CheckedUnwrap(obj)) + return unwrapped->is() ? unwrapped : nullptr; + return nullptr; +} diff --git a/js/src/jsfriendapi.h b/js/src/jsfriendapi.h index 76537dc739..c463019dac 100644 --- a/js/src/jsfriendapi.h +++ b/js/src/jsfriendapi.h @@ -1787,6 +1787,9 @@ UnwrapArrayBufferView(JSObject* obj); extern JS_FRIEND_API(JSObject*) UnwrapSharedArrayBuffer(JSObject* obj); +extern JS_FRIEND_API(JSObject*) +UnwrapReadableStream(JSObject* obj); + namespace detail { From 4c5775b62142a9ec95171917fedf221fe1edf123 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 19:04:02 -0500 Subject: [PATCH 06/31] Issue #1442 - Part 6: binding codegen naming alignment for Streams. https://bugzilla.mozilla.org/show_bug.cgi?id=1128959 --- dom/bindings/Codegen.py | 132 +++++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 63 deletions(-) diff --git a/dom/bindings/Codegen.py b/dom/bindings/Codegen.py index 28f6d2a9a9..b7caaad7bb 100644 --- a/dom/bindings/Codegen.py +++ b/dom/bindings/Codegen.py @@ -5621,12 +5621,12 @@ def getJSToNativeConversionInfo(type, descriptorProvider, failureCode=None, # otherwise. if isOptional: holderType = CGTemplatedType("SpiderMonkeyInterfaceRooter", interfaceType) - # If our typed array is nullable, this will set the Nullable to - # be not-null, but that's ok because we make an explicit - # SetNull() call on it as needed if our JS value is actually - # null. XXXbz Because "Maybe" takes const refs for constructor - # arguments, we can't pass a reference here; have to pass a - # pointer. + # If our SpiderMonkey interface is nullable, this will set the + # Nullable to be not-null, but that's ok because we make an + # explicit SetNull() call on it as needed if our JS value is + # actually null. XXXbz Because "Maybe" takes const refs for + # constructor arguments, we can't pass a reference here; have + # to pass a pointer. holderArgs = "cx, &%s" % objRef declArgs = None else: @@ -6401,7 +6401,8 @@ recordWrapLevel = 0 def getWrapTemplateForType(type, descriptorProvider, result, successCode, - returnsNewObject, exceptionCode, typedArraysAreStructs, + returnsNewObject, exceptionCode, + spiderMonkeyInterfacesAreStructs, isConstructorRetval=False): """ Reflect a C++ value stored in "result", of IDL type "type" into JS. The @@ -6411,8 +6412,9 @@ def getWrapTemplateForType(type, descriptorProvider, result, successCode, doing a 'break' if the entire conversion template is inside a block that the 'break' will exit). - If typedArraysAreStructs is true, then if the type is a typed array, - "result" is one of the dom::TypedArray subclasses, not a JSObject*. + If spiderMonkeyInterfacesAreStructs is true, then if the type is a + SpiderMonkey interface, "result" is one of the + dom::SpiderMonkeyInterfaceObjectStorage subclasses, not a JSObject*. The resulting string should be used with string.Template. It needs the following keys when substituting: @@ -6507,7 +6509,7 @@ def getWrapTemplateForType(type, descriptorProvider, result, successCode, recTemplate, recInfall = getWrapTemplateForType(type.inner, descriptorProvider, "%s.Value()" % result, successCode, returnsNewObject, exceptionCode, - typedArraysAreStructs) + spiderMonkeyInterfacesAreStructs) code = fill( """ @@ -6539,7 +6541,7 @@ def getWrapTemplateForType(type, descriptorProvider, result, successCode, 'returnsNewObject': returnsNewObject, 'exceptionCode': exceptionCode, 'obj': "returnArray", - 'typedArraysAreStructs': typedArraysAreStructs + 'spiderMonkeyInterfacesAreStructs': spiderMonkeyInterfacesAreStructs }) sequenceWrapLevel -= 1 code = fill( @@ -6593,7 +6595,7 @@ def getWrapTemplateForType(type, descriptorProvider, result, successCode, 'returnsNewObject': returnsNewObject, 'exceptionCode': exceptionCode, 'obj': "returnObj", - 'typedArraysAreStructs': typedArraysAreStructs + 'spiderMonkeyInterfacesAreStructs': spiderMonkeyInterfacesAreStructs }) recordWrapLevel -= 1 if type.keyType.isByteString(): @@ -6759,7 +6761,7 @@ def getWrapTemplateForType(type, descriptorProvider, result, successCode, return (head + _setValue(result, wrapAsType=type), False) if (type.isObject() or (type.isSpiderMonkeyInterface() and - not typedArraysAreStructs)): + not spiderMonkeyInterfacesAreStructs)): # See comments in GetOrCreateDOMReflector explaining why we need # to wrap here. if type.nullable(): @@ -6778,21 +6780,21 @@ def getWrapTemplateForType(type, descriptorProvider, result, successCode, if not (type.isUnion() or type.isPrimitive() or type.isDictionary() or type.isDate() or - (type.isSpiderMonkeyInterface() and typedArraysAreStructs)): + (type.isSpiderMonkeyInterface() and spiderMonkeyInterfacesAreStructs)): raise TypeError("Need to learn to wrap %s" % type) if type.nullable(): recTemplate, recInfal = getWrapTemplateForType(type.inner, descriptorProvider, "%s.Value()" % result, successCode, returnsNewObject, exceptionCode, - typedArraysAreStructs) + spiderMonkeyInterfacesAreStructs) return ("if (%s.IsNull()) {\n" % result + indent(setNull()) + "}\n" + recTemplate, recInfal) if type.isSpiderMonkeyInterface(): - assert typedArraysAreStructs + assert spiderMonkeyInterfacesAreStructs # See comments in GetOrCreateDOMReflector explaining why we need # to wrap here. # NB: setObject(..., some-object-type) calls JS_WrapValue(), so is fallible @@ -6874,7 +6876,7 @@ def wrapForType(type, descriptorProvider, templateValues): templateValues.get('successCode', None), templateValues.get('returnsNewObject', False), templateValues.get('exceptionCode', "return false;\n"), - templateValues.get('typedArraysAreStructs', False), + templateValues.get('spiderMonkeyInterfacesAreStructs', False), isConstructorRetval=templateValues.get('isConstructorRetval', False))[0] defaultValues = {'obj': 'obj'} @@ -8203,10 +8205,10 @@ class CGMethodCall(CGThing): # "object" arg. # First grab all the overloads that have a non-callback interface - # (which includes typed arrays and arraybuffers) at the - # distinguishing index. We can also include the ones that have an - # "object" here, since if those are present no other object-typed - # argument will be. + # (which includes SpiderMonkey interfaces) at the distinguishing + # index. We can also include the ones that have an "object" here, + # since if those are present no other object-typed argument will + # be. objectSigs = [ s for s in possibleSignatures if (distinguishingType(s).isObject() or @@ -8237,16 +8239,17 @@ class CGMethodCall(CGThing): # There might be more than one thing in objectSigs; we need to check # which ones we unwrap to. if len(objectSigs) > 0: - # Here it's enough to guard on our argument being an object. The - # code for unwrapping non-callback interfaces, typed arrays, - # sequences, and Dates will just bail out and move on to - # the next overload if the object fails to unwrap correctly, - # while "object" accepts any object anyway. We could even not - # do the isObject() check up front here, but in cases where we - # have multiple object overloads it makes sense to do it only - # once instead of for each overload. That will also allow the - # unwrapping test to skip having to do codegen for the - # null-or-undefined case, which we already handled above. + # Here it's enough to guard on our argument being an object. + # The code for unwrapping non-callback interfaces, spiderMonkey + # interfaces, sequences, and Dates will just bail out and move + # on to the next overload if the object fails to unwrap + # correctly, while "object" accepts any object anyway. We + # could even not do the isObject() check up front here, but in + # cases where we have multiple object overloads it makes sense + # to do it only once instead of for each overload. That will + # also allow the unwrapping test to skip having to do codegen + # for the null-or-undefined case, which we already handled + # above. caseBody.append(CGGeneric("if (%s.isObject()) {\n" % distinguishingArg)) for sig in objectSigs: @@ -10249,7 +10252,7 @@ class CGUnionStruct(CGThing): "jsvalHandle": "rval", "obj": "scopeObj", "result": val, - "typedArraysAreStructs": True + "spiderMonkeyInterfacesAreStructs": True }) return CGGeneric(wrapCode) @@ -13229,7 +13232,7 @@ class CGDictionary(CGThing): # 'obj' can just be allowed to be the string "obj", since that # will be our dictionary object, which is presumably itself in # the right scope. - 'typedArraysAreStructs': True + 'spiderMonkeyInterfacesAreStructs': True }) conversion = CGGeneric(innerTemplate) conversion = CGWrapper(conversion, @@ -13998,7 +14001,7 @@ class CGBindingRoot(CGThing): return {desc.getDescriptor(desc.interface.parent.identifier.name)} for x in dependencySortObjects(jsImplemented, getParentDescriptor, lambda d: d.interface.identifier.name): - cgthings.append(CGCallbackInterface(x, typedArraysAreStructs=True)) + cgthings.append(CGCallbackInterface(x, spiderMonkeyInterfacesAreStructs=True)) cgthings.append(CGJSImplClass(x)) # And make sure we have the right number of newlines at the end @@ -14060,14 +14063,13 @@ class CGBindingRoot(CGThing): class CGNativeMember(ClassMethod): def __init__(self, descriptorProvider, member, name, signature, extendedAttrs, breakAfter=True, passJSBitsAsNeeded=True, visibility="public", - typedArraysAreStructs=True, variadicIsSequence=False, - resultNotAddRefed=False, - virtual=False, - override=False): + spiderMonkeyInterfacesAreStructs=True, + variadicIsSequence=False, resultNotAddRefed=False, + virtual=False, override=False): """ - If typedArraysAreStructs is false, typed arrays will be passed as - JS::Handle. If it's true they will be passed as one of the - dom::TypedArray subclasses. + If spiderMonkeyInterfacesAreStructs is false, SpiderMonkey interfaces + will be passed as JS::Handle. If it's true they will be + passed as one of the dom::SpiderMonkeyInterfaceObjectStorage subclasses. If passJSBitsAsNeeded is false, we don't automatically pass in a JSContext* or a JSObject* based on the return and argument types. We @@ -14078,7 +14080,7 @@ class CGNativeMember(ClassMethod): self.extendedAttrs = extendedAttrs self.resultAlreadyAddRefed = not resultNotAddRefed self.passJSBitsAsNeeded = passJSBitsAsNeeded - self.typedArraysAreStructs = typedArraysAreStructs + self.spiderMonkeyInterfacesAreStructs = spiderMonkeyInterfacesAreStructs self.variadicIsSequence = variadicIsSequence breakAfterSelf = "\n" if breakAfter else "" ClassMethod.__init__(self, name, @@ -14385,7 +14387,7 @@ class CGNativeMember(ClassMethod): False, False) if type.isSpiderMonkeyInterface(): - if not self.typedArraysAreStructs: + if not self.spiderMonkeyInterfacesAreStructs: return "JS::Handle", False, False # Unroll for the name, in case we're nullable. @@ -15557,16 +15559,16 @@ class CGFastCallback(CGClass): class CGCallbackInterface(CGCallback): - def __init__(self, descriptor, typedArraysAreStructs=False): + def __init__(self, descriptor, spiderMonkeyInterfacesAreStructs=False): iface = descriptor.interface attrs = [m for m in iface.members if m.isAttr() and not m.isStatic()] - getters = [CallbackGetter(a, descriptor, typedArraysAreStructs) + getters = [CallbackGetter(a, descriptor, spiderMonkeyInterfacesAreStructs) for a in attrs] - setters = [CallbackSetter(a, descriptor, typedArraysAreStructs) + setters = [CallbackSetter(a, descriptor, spiderMonkeyInterfacesAreStructs) for a in attrs if not a.readonly] methods = [m for m in iface.members if m.isMethod() and not m.isStatic() and not m.isIdentifierLess()] - methods = [CallbackOperation(m, sig, descriptor, typedArraysAreStructs) + methods = [CallbackOperation(m, sig, descriptor, spiderMonkeyInterfacesAreStructs) for m in methods for sig in m.signatures()] if iface.isJSImplemented() and iface.ctor(): sigs = descriptor.interface.ctor().signatures() @@ -15611,7 +15613,8 @@ class CallbackMember(CGNativeMember): # CallSetup already handled the unmark-gray bits for us. we don't have # anything better to use for 'obj', really... def __init__(self, sig, name, descriptorProvider, needThisHandling, - rethrowContentException=False, typedArraysAreStructs=False, + rethrowContentException=False, + spiderMonkeyInterfacesAreStructs=False, wrapScope='CallbackKnownNotGray()'): """ needThisHandling is True if we need to be able to accept a specified @@ -15646,7 +15649,7 @@ class CallbackMember(CGNativeMember): extendedAttrs={}, passJSBitsAsNeeded=False, visibility=visibility, - typedArraysAreStructs=typedArraysAreStructs) + spiderMonkeyInterfacesAreStructs=spiderMonkeyInterfacesAreStructs) # We have to do all the generation of our body now, because # the caller relies on us throwing if we can't manage it. self.exceptionCode = ("aRv.Throw(NS_ERROR_UNEXPECTED);\n" @@ -15763,7 +15766,7 @@ class CallbackMember(CGNativeMember): 'obj': self.wrapScope, 'returnsNewObject': False, 'exceptionCode': self.exceptionCode, - 'typedArraysAreStructs': self.typedArraysAreStructs + 'spiderMonkeyInterfacesAreStructs': self.spiderMonkeyInterfacesAreStructs }) except MethodNotNewObjectError as err: raise TypeError("%s being passed as an argument to %s but is not " @@ -15867,10 +15870,11 @@ class CallbackMember(CGNativeMember): class CallbackMethod(CallbackMember): def __init__(self, sig, name, descriptorProvider, needThisHandling, - rethrowContentException=False, typedArraysAreStructs=False): + rethrowContentException=False, + spiderMonkeyInterfacesAreStructs=False): CallbackMember.__init__(self, sig, name, descriptorProvider, needThisHandling, rethrowContentException, - typedArraysAreStructs=typedArraysAreStructs) + spiderMonkeyInterfacesAreStructs=spiderMonkeyInterfacesAreStructs) def getRvalDecl(self): return "JS::Rooted rval(cx, JS::UndefinedValue());\n" @@ -15929,12 +15933,12 @@ class CallbackOperationBase(CallbackMethod): """ def __init__(self, signature, jsName, nativeName, descriptor, singleOperation, rethrowContentException=False, - typedArraysAreStructs=False): + spiderMonkeyInterfacesAreStructs=False): self.singleOperation = singleOperation self.methodName = descriptor.binaryNameFor(jsName) CallbackMethod.__init__(self, signature, nativeName, descriptor, singleOperation, rethrowContentException, - typedArraysAreStructs=typedArraysAreStructs) + spiderMonkeyInterfacesAreStructs=spiderMonkeyInterfacesAreStructs) def getThisDecl(self): if not self.singleOperation: @@ -15985,7 +15989,8 @@ class CallbackOperation(CallbackOperationBase): """ Codegen actual WebIDL operations on callback interfaces. """ - def __init__(self, method, signature, descriptor, typedArraysAreStructs): + def __init__(self, method, signature, descriptor, + spiderMonkeyInterfacesAreStructs): self.ensureASCIIName(method) self.method = method jsName = method.identifier.name @@ -15994,7 +15999,7 @@ class CallbackOperation(CallbackOperationBase): MakeNativeName(descriptor.binaryNameFor(jsName)), descriptor, descriptor.interface.isSingleOperationInterface(), rethrowContentException=descriptor.interface.isJSImplemented(), - typedArraysAreStructs=typedArraysAreStructs) + spiderMonkeyInterfacesAreStructs=spiderMonkeyInterfacesAreStructs) def getPrettyName(self): return "%s.%s" % (self.descriptorProvider.interface.identifier.name, @@ -16005,13 +16010,14 @@ class CallbackAccessor(CallbackMember): """ Shared superclass for CallbackGetter and CallbackSetter. """ - def __init__(self, attr, sig, name, descriptor, typedArraysAreStructs): + def __init__(self, attr, sig, name, descriptor, + spiderMonkeyInterfacesAreStructs): self.ensureASCIIName(attr) self.attrName = attr.identifier.name CallbackMember.__init__(self, sig, name, descriptor, needThisHandling=False, rethrowContentException=descriptor.interface.isJSImplemented(), - typedArraysAreStructs=typedArraysAreStructs) + spiderMonkeyInterfacesAreStructs=spiderMonkeyInterfacesAreStructs) def getPrettyName(self): return "%s.%s" % (self.descriptorProvider.interface.identifier.name, @@ -16019,12 +16025,12 @@ class CallbackAccessor(CallbackMember): class CallbackGetter(CallbackAccessor): - def __init__(self, attr, descriptor, typedArraysAreStructs): + def __init__(self, attr, descriptor, spiderMonkeyInterfacesAreStructs): CallbackAccessor.__init__(self, attr, (attr.type, []), callbackGetterName(attr, descriptor), descriptor, - typedArraysAreStructs) + spiderMonkeyInterfacesAreStructs) def getRvalDecl(self): return "JS::Rooted rval(cx, JS::UndefinedValue());\n" @@ -16046,12 +16052,12 @@ class CallbackGetter(CallbackAccessor): class CallbackSetter(CallbackAccessor): - def __init__(self, attr, descriptor, typedArraysAreStructs): + def __init__(self, attr, descriptor, spiderMonkeyInterfacesAreStructs): CallbackAccessor.__init__(self, attr, (BuiltinTypes[IDLBuiltinType.Types.void], [FakeArgument(attr.type, attr)]), callbackSetterName(attr, descriptor), - descriptor, typedArraysAreStructs) + descriptor, spiderMonkeyInterfacesAreStructs) def getRvalDecl(self): # We don't need an rval @@ -16086,7 +16092,7 @@ class CGJSImplInitOperation(CallbackOperationBase): "__init", "__Init", descriptor, singleOperation=False, rethrowContentException=True, - typedArraysAreStructs=True) + spiderMonkeyInterfacesAreStructs=True) def getPrettyName(self): return "__init" From dd2c9eb41877eacac3d16fc6f3cc6dcfbd381e4b Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 19:08:24 -0500 Subject: [PATCH 07/31] Issue #1442 - Part 7: Use of ReadableStream in WebIDL files. https://bugzilla.mozilla.org/show_bug.cgi?id=1128959 --- dom/fetch/Fetch.cpp | 19 +++++++++++++++++++ dom/fetch/Fetch.h | 4 ++++ dom/fetch/Request.h | 2 ++ dom/fetch/Response.h | 2 ++ dom/webidl/Response.webidl | 5 +++++ 5 files changed, 32 insertions(+) diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index 6a6b4faaf8..42c9fc74f9 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -1000,5 +1000,24 @@ template void FetchBody::SetMimeType(); +template +void +FetchBody::GetBody(JSContext* aCx, + JS::MutableHandle aMessage) +{ + // TODO +} + +template +void +FetchBody::GetBody(JSContext* aCx, + JS::MutableHandle aMessage); + +template +void +FetchBody::GetBody(JSContext* aCx, + JS::MutableHandle aMessage); + + } // namespace dom } // namespace mozilla diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index 73d5c15199..bc2df62689 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -147,6 +147,10 @@ public: return ConsumeBody(CONSUME_TEXT, aRv); } + void + GetBody(JSContext* aCx, + JS::MutableHandle aMessage); + // Utility public methods accessed by various runnables. void diff --git a/dom/fetch/Request.h b/dom/fetch/Request.h index 8c01cbd799..8a22f1340b 100644 --- a/dom/fetch/Request.h +++ b/dom/fetch/Request.h @@ -128,6 +128,8 @@ public: void GetBody(nsIInputStream** aStream) { return mRequest->GetBody(aStream); } + using FetchBody::GetBody; + void SetBody(nsIInputStream* aStream) { return mRequest->SetBody(aStream); } diff --git a/dom/fetch/Response.h b/dom/fetch/Response.h index fa2ced87d8..61f1427f93 100644 --- a/dom/fetch/Response.h +++ b/dom/fetch/Response.h @@ -105,6 +105,8 @@ public: void GetBody(nsIInputStream** aStream) { return mInternalResponse->GetBody(aStream); } + using FetchBody::GetBody; + static already_AddRefed Error(const GlobalObject& aGlobal); diff --git a/dom/webidl/Response.webidl b/dom/webidl/Response.webidl index 08f31fe29a..adaa70d66d 100644 --- a/dom/webidl/Response.webidl +++ b/dom/webidl/Response.webidl @@ -30,6 +30,11 @@ interface Response { }; Response implements Body; +// This should be part of Body but we don't want to expose body to request yet. +partial interface Response { + readonly attribute ReadableStream? body; +}; + dictionary ResponseInit { unsigned short status = 200; ByteString statusText = "OK"; From bff1f3bc60b28001e1710617c9fb1eac03610da6 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 19:20:08 -0500 Subject: [PATCH 08/31] Issue #1442 - Part 8: Fetch implementation of streams. https://bugzilla.mozilla.org/show_bug.cgi?id=1128959 +worker-friendly pref checking for the DOM API. --- dom/base/nsContentUtils.cpp | 19 ++ dom/base/nsContentUtils.h | 2 + dom/bindings/Bindings.conf | 2 + dom/fetch/Fetch.cpp | 97 ++++++++++- dom/fetch/Fetch.h | 30 ++-- dom/fetch/FetchStream.cpp | 336 ++++++++++++++++++++++++++++++++++++ dom/fetch/FetchStream.h | 107 ++++++++++++ dom/fetch/Request.cpp | 19 +- dom/fetch/Response.cpp | 26 ++- dom/fetch/moz.build | 1 + dom/webidl/Response.webidl | 1 + dom/workers/WorkerPrefs.h | 1 + modules/libpref/init/all.js | 3 + 13 files changed, 620 insertions(+), 24 deletions(-) create mode 100644 dom/fetch/FetchStream.cpp create mode 100644 dom/fetch/FetchStream.h diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 50cc781efc..130188ef98 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -8735,6 +8735,25 @@ nsContentUtils::PushEnabled(JSContext* aCx, JSObject* aObj) return workerPrivate->PushEnabled(); } +// static +bool +nsContentUtils::StreamsEnabled(JSContext* aCx, JSObject* aObj) +{ + if (NS_IsMainThread()) { + return Preferences::GetBool("dom.streams.enabled", false); + } + + using namespace workers; + + // Otherwise, check the pref via the WorkerPrivate + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + if (!workerPrivate) { + return false; + } + + return workerPrivate->StreamsEnabled(); +} + // static bool nsContentUtils::IsNonSubresourceRequest(nsIChannel* aChannel) diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h index c0e44b3be4..c1f8885f73 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h @@ -2679,6 +2679,8 @@ public: static mozilla::net::ReferrerPolicy GetReferrerPolicyFromHeader(const nsAString& aHeader); static bool PushEnabled(JSContext* aCx, JSObject* aObj); + + static bool StreamsEnabled(JSContext* aCx, JSObject* aObj); static bool IsNonSubresourceRequest(nsIChannel* aChannel); diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index e1560bfa3f..39fa0d7dc9 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -722,6 +722,7 @@ DOMInterfaces = { 'headers': 'headers_', 'referrerPolicy': 'referrerPolicy_' }, + 'implicitJSContext': [ 'arrayBuffer', 'blob', 'formData', 'json', 'text' ], }, 'ResizeObservation': { @@ -746,6 +747,7 @@ DOMInterfaces = { 'Response': { 'binaryNames': { 'headers': 'headers_' }, + 'implicitJSContext': [ 'arrayBuffer', 'blob', 'formData', 'json', 'text' ], }, 'RGBColor': { diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index 42c9fc74f9..63e2c83eaa 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -5,6 +5,7 @@ #include "Fetch.h" #include "FetchConsumer.h" +#include "FetchStream.h" #include "nsIDocument.h" #include "nsIGlobalObject.h" @@ -22,6 +23,7 @@ #include "mozilla/ErrorResult.h" #include "mozilla/dom/BodyUtil.h" +#include "mozilla/dom/DOMError.h" #include "mozilla/dom/EncodingUtils.h" #include "mozilla/dom/Exceptions.h" #include "mozilla/dom/FetchDriver.h" @@ -35,6 +37,7 @@ #include "mozilla/dom/Response.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/URLSearchParams.h" +#include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/workers/ServiceWorkerManager.h" #include "FetchObserver.h" @@ -918,6 +921,7 @@ ExtractByteStreamFromBody(const ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUS template FetchBody::FetchBody() : mWorkerPrivate(nullptr) + , mReadableStreamBody(nullptr) , mBodyUsed(false) { if (!NS_IsMainThread()) { @@ -937,9 +941,45 @@ FetchBody::~FetchBody() { } +template +bool +FetchBody::BodyUsed() const +{ + if (mBodyUsed) { + return true; + } + + // If this object is disturbed or locked, return false. + if (mReadableStreamBody) { + AutoJSAPI jsapi; + // If we start tracking ownership, we need something like this. + //if (!jsapi.Init(mOwner)) { + // return true; + //} + + JSContext* cx = jsapi.cx(); + + JS::Rooted body(cx, mReadableStreamBody); + if (JS::ReadableStreamIsDisturbed(body) || + JS::ReadableStreamIsLocked(body)) { + return true; + } + } + + return false; +} + +template +bool +FetchBody::BodyUsed() const; + +template +bool +FetchBody::BodyUsed() const; + template already_AddRefed -FetchBody::ConsumeBody(FetchConsumeType aType, ErrorResult& aRv) +FetchBody::ConsumeBody(JSContext* aCx, FetchConsumeType aType, ErrorResult& aRv) { RefPtr signal = DerivedClass()->GetSignal(); if (signal && signal->Aborted()) { @@ -954,9 +994,16 @@ FetchBody::ConsumeBody(FetchConsumeType aType, ErrorResult& aRv) SetBodyUsed(); + nsCOMPtr global = DerivedClass()->GetParentObject(); + + // If we already created a ReadableStreamBody we have to close it now. + if (mReadableStreamBody) { + JS::Rooted body(aCx, mReadableStreamBody); + JS::ReadableStreamClose(aCx, body); + } + RefPtr promise = - FetchBodyConsumer::Create(DerivedClass()->GetParentObject(), - this, signal, aType, aRv); + FetchBodyConsumer::Create(global, this, signal, aType, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } @@ -966,11 +1013,11 @@ FetchBody::ConsumeBody(FetchConsumeType aType, ErrorResult& aRv) template already_AddRefed -FetchBody::ConsumeBody(FetchConsumeType aType, ErrorResult& aRv); +FetchBody::ConsumeBody(JSContext* aCx, FetchConsumeType aType, ErrorResult& aRv); template already_AddRefed -FetchBody::ConsumeBody(FetchConsumeType aType, ErrorResult& aRv); +FetchBody::ConsumeBody(JSContext* aCx, FetchConsumeType aType, ErrorResult& aRv); template void @@ -1003,20 +1050,52 @@ FetchBody::SetMimeType(); template void FetchBody::GetBody(JSContext* aCx, - JS::MutableHandle aMessage) + JS::MutableHandle aBodyOut, + ErrorResult& aRv) { - // TODO + nsCOMPtr inputStream; + DerivedClass()->GetBody(getter_AddRefs(inputStream)); + + if (!inputStream) { + aBodyOut.set(nullptr); + return; + } + + if (!mReadableStreamBody) { + JS::Rooted body(aCx, + FetchStream::Create(aCx, + DerivedClass()->GetParentObject(), + inputStream, + aRv)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + MOZ_ASSERT(body); + + // If the body has been already consumed, we close the stream. + if (BodyUsed() && !JS::ReadableStreamClose(aCx, body)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + mReadableStreamBody = body; + } + + aBodyOut.set(mReadableStreamBody); } template void FetchBody::GetBody(JSContext* aCx, - JS::MutableHandle aMessage); + JS::MutableHandle aMessage, + ErrorResult& aRv); template void FetchBody::GetBody(JSContext* aCx, - JS::MutableHandle aMessage); + JS::MutableHandle aMessage, + ErrorResult& aRv); } // namespace dom diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index bc2df62689..821bc3643f 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -115,41 +115,42 @@ public: NS_IMETHOD_(MozExternalRefCountType) Release(void) = 0; bool - BodyUsed() const { return mBodyUsed; } + BodyUsed() const; already_AddRefed - ArrayBuffer(ErrorResult& aRv) + ArrayBuffer(JSContext* aCx, ErrorResult& aRv) { - return ConsumeBody(CONSUME_ARRAYBUFFER, aRv); + return ConsumeBody(aCx, CONSUME_ARRAYBUFFER, aRv); } already_AddRefed - Blob(ErrorResult& aRv) + Blob(JSContext* aCx, ErrorResult& aRv) { - return ConsumeBody(CONSUME_BLOB, aRv); + return ConsumeBody(aCx, CONSUME_BLOB, aRv); } already_AddRefed - FormData(ErrorResult& aRv) + FormData(JSContext* aCx, ErrorResult& aRv) { - return ConsumeBody(CONSUME_FORMDATA, aRv); + return ConsumeBody(aCx, CONSUME_FORMDATA, aRv); } already_AddRefed - Json(ErrorResult& aRv) + Json(JSContext* aCx, ErrorResult& aRv) { - return ConsumeBody(CONSUME_JSON, aRv); + return ConsumeBody(aCx, CONSUME_JSON, aRv); } already_AddRefed - Text(ErrorResult& aRv) + Text(JSContext* aCx, ErrorResult& aRv) { - return ConsumeBody(CONSUME_TEXT, aRv); + return ConsumeBody(aCx, CONSUME_TEXT, aRv); } void GetBody(JSContext* aCx, - JS::MutableHandle aMessage); + JS::MutableHandle aBodyOut, + ErrorResult& aRv); // Utility public methods accessed by various runnables. @@ -174,6 +175,9 @@ protected: // Always set whenever the FetchBody is created on the worker thread. workers::WorkerPrivate* mWorkerPrivate; + // This is the ReadableStream exposed to content. Its underlying source is a FetchStream object. + JS::Heap mReadableStreamBody; + virtual ~FetchBody(); void @@ -186,7 +190,7 @@ private: } already_AddRefed - ConsumeBody(FetchConsumeType aType, ErrorResult& aRv); + ConsumeBody(JSContext* aCx, FetchConsumeType aType, ErrorResult& aRv); bool IsOnTargetThread() diff --git a/dom/fetch/FetchStream.cpp b/dom/fetch/FetchStream.cpp new file mode 100644 index 0000000000..7b165b2a9d --- /dev/null +++ b/dom/fetch/FetchStream.cpp @@ -0,0 +1,336 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "FetchStream.h" +#include "nsNetCID.h" +#include "nsITransport.h" +#include "nsIStreamTransportService.h" +#include "nsProxyRelease.h" + +#include "mozilla/dom/DOMError.h" + +#define FETCH_STREAM_FLAG 0 + +static NS_DEFINE_CID(kStreamTransportServiceCID, + NS_STREAMTRANSPORTSERVICE_CID); + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS(FetchStream, nsIInputStreamCallback) + +/* static */ JSObject* +FetchStream::Create(JSContext* aCx, nsIGlobalObject* aGlobal, + nsIInputStream* aInputStream, ErrorResult& aRv) +{ + MOZ_DIAGNOSTIC_ASSERT(aCx); + MOZ_DIAGNOSTIC_ASSERT(aInputStream); + + RefPtr stream = new FetchStream(aGlobal, aInputStream); + + if (!JS::HasReadableStreamCallbacks(aCx)) { + JS::SetReadableStreamCallbacks(aCx, + &FetchStream::RequestDataCallback, + &FetchStream::WriteIntoReadRequestCallback, + &FetchStream::CancelCallback, + &FetchStream::ClosedCallback, + &FetchStream::ErroredCallback, + &FetchStream::FinalizeCallback); + } + + JS::Rooted body(aCx, + JS::NewReadableExternalSourceStreamObject(aCx, stream, FETCH_STREAM_FLAG)); + if (!body) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + stream->mReadableStream = body; + + // JS engine will call the finalize callback. + NS_ADDREF(stream.get()); + return body; +} + +/* static */ void +FetchStream::RequestDataCallback(JSContext* aCx, + JS::HandleObject aStream, + void* aUnderlyingSource, + uint8_t aFlags, + size_t aDesiredSize) +{ + MOZ_DIAGNOSTIC_ASSERT(aUnderlyingSource); + MOZ_DIAGNOSTIC_ASSERT(aFlags == FETCH_STREAM_FLAG); + MOZ_DIAGNOSTIC_ASSERT(JS::ReadableStreamIsDisturbed(aStream)); + + RefPtr stream = static_cast(aUnderlyingSource); + + MOZ_DIAGNOSTIC_ASSERT(stream->mState == eWaiting || + stream->mState == eChecking); + + if (stream->mState == eChecking) { + // If we are looking for more data, there is nothing else we should do: + // let's move this checking operation in a reading. + MOZ_ASSERT(stream->mInputStream); + stream->mState = eReading; + return; + } + + stream->mState = eReading; + + if (!stream->mInputStream) { + // This is the first use of the stream. Let's convert the + // mOriginalInputStream into an nsIAsyncInputStream. + MOZ_ASSERT(stream->mOriginalInputStream); + + bool nonBlocking = false; + nsresult rv = stream->mOriginalInputStream->IsNonBlocking(&nonBlocking); + if (NS_WARN_IF(NS_FAILED(rv))) { + stream->ErrorPropagation(aCx, aStream, rv); + return; + } + + nsCOMPtr asyncStream = + do_QueryInterface(stream->mOriginalInputStream); + if (!nonBlocking || !asyncStream) { + nsCOMPtr sts = + do_GetService(kStreamTransportServiceCID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + stream->ErrorPropagation(aCx, aStream, rv); + return; + } + + nsCOMPtr transport; + rv = sts->CreateInputTransport(stream->mOriginalInputStream, + /* aStartOffset */ 0, + /* aReadLimit */ -1, + /* aCloseWhenDone */ true, + getter_AddRefs(transport)); + if (NS_WARN_IF(NS_FAILED(rv))) { + stream->ErrorPropagation(aCx, aStream, rv); + return; + } + + nsCOMPtr wrapper; + rv = transport->OpenInputStream(/* aFlags */ 0, + /* aSegmentSize */ 0, + /* aSegmentCount */ 0, + getter_AddRefs(wrapper)); + if (NS_WARN_IF(NS_FAILED(rv))) { + stream->ErrorPropagation(aCx, aStream, rv); + return; + } + + asyncStream = do_QueryInterface(wrapper); + } + + stream->mInputStream = asyncStream; + stream->mOriginalInputStream = nullptr; + } + + MOZ_DIAGNOSTIC_ASSERT(stream->mInputStream); + MOZ_DIAGNOSTIC_ASSERT(!stream->mOriginalInputStream); + + nsresult rv = + stream->mInputStream->AsyncWait(stream, 0, 0, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + stream->ErrorPropagation(aCx, aStream, rv); + return; + } + + // All good. +} + +/* static */ void +FetchStream::WriteIntoReadRequestCallback(JSContext* aCx, + JS::HandleObject aStream, + void* aUnderlyingSource, + uint8_t aFlags, void* aBuffer, + size_t aLength, size_t* aByteWritten) +{ + MOZ_DIAGNOSTIC_ASSERT(aUnderlyingSource); + MOZ_DIAGNOSTIC_ASSERT(aFlags == FETCH_STREAM_FLAG); + MOZ_DIAGNOSTIC_ASSERT(aBuffer); + MOZ_DIAGNOSTIC_ASSERT(aByteWritten); + + RefPtr stream = static_cast(aUnderlyingSource); + + MOZ_DIAGNOSTIC_ASSERT(stream->mInputStream); + MOZ_DIAGNOSTIC_ASSERT(stream->mState == eWriting); + stream->mState = eChecking; + + uint32_t written; + nsresult rv = + stream->mInputStream->Read(static_cast(aBuffer), aLength, &written); + if (NS_WARN_IF(NS_FAILED(rv))) { + stream->ErrorPropagation(aCx, aStream, rv); + return; + } + + *aByteWritten = written; + + if (written == 0) { + stream->mState = eClosed; + JS::ReadableStreamClose(aCx, aStream); + return; + } + + rv = stream->mInputStream->AsyncWait(stream, 0, 0, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + stream->ErrorPropagation(aCx, aStream, rv); + return; + } + + // All good. +} + +/* static */ JS::Value +FetchStream::CancelCallback(JSContext* aCx, JS::HandleObject aStream, + void* aUnderlyingSource, uint8_t aFlags, + JS::HandleValue aReason) +{ + MOZ_DIAGNOSTIC_ASSERT(aUnderlyingSource); + MOZ_DIAGNOSTIC_ASSERT(aFlags == FETCH_STREAM_FLAG); + + RefPtr stream = static_cast(aUnderlyingSource); + + if (stream->mInputStream) { + stream->mInputStream->CloseWithStatus(NS_BASE_STREAM_CLOSED); + } + + stream->mState = eClosed; + + return JS::UndefinedValue(); +} + +/* static */ void +FetchStream::ClosedCallback(JSContext* aCx, JS::HandleObject aStream, + void* aUnderlyingSource, uint8_t aFlags) +{ + MOZ_DIAGNOSTIC_ASSERT(aUnderlyingSource); + MOZ_DIAGNOSTIC_ASSERT(aFlags == FETCH_STREAM_FLAG); +} + +/* static */ void +FetchStream::ErroredCallback(JSContext* aCx, JS::HandleObject aStream, + void* aUnderlyingSource, uint8_t aFlags, + JS::HandleValue aReason) +{ + MOZ_DIAGNOSTIC_ASSERT(aUnderlyingSource); + MOZ_DIAGNOSTIC_ASSERT(aFlags == FETCH_STREAM_FLAG); +} + +void +FetchStream::FinalizeCallback(void* aUnderlyingSource, uint8_t aFlags) +{ + MOZ_DIAGNOSTIC_ASSERT(aUnderlyingSource); + MOZ_DIAGNOSTIC_ASSERT(aFlags == FETCH_STREAM_FLAG); + + RefPtr stream = + dont_AddRef(static_cast(aUnderlyingSource)); + + stream->mState = eClosed; + stream->mReadableStream = nullptr; +} + +FetchStream::FetchStream(nsIGlobalObject* aGlobal, + nsIInputStream* aInputStream) + : mState(eWaiting) + , mGlobal(aGlobal) + , mOriginalInputStream(aInputStream) + , mOwningEventTarget(nullptr) + , mReadableStream(nullptr) +{ + MOZ_DIAGNOSTIC_ASSERT(aInputStream); +} + +FetchStream::~FetchStream() +{ + NS_ProxyRelease(mOwningEventTarget, mGlobal.forget()); +} + +void +FetchStream::ErrorPropagation(JSContext* aCx, JS::HandleObject aStream, + nsresult aError) +{ + // Nothing to do. + if (mState == eClosed) { + return; + } + + // We cannot continue with any other operation. + mState = eClosed; + + // Let's close the stream. + if (aError == NS_BASE_STREAM_CLOSED) { + JS::ReadableStreamClose(aCx, aStream); + return; + } + + nsCOMPtr window = do_QueryInterface(mGlobal); + + // Let's use a generic error. + RefPtr error = new DOMError(window, NS_ERROR_DOM_TYPE_ERR); + + JS::Rooted errorValue(aCx); + if (ToJSValue(aCx, error, &errorValue)) { + JS::ReadableStreamError(aCx, aStream, errorValue); + } +} + +NS_IMETHODIMP +FetchStream::OnInputStreamReady(nsIAsyncInputStream* aStream) +{ + MOZ_DIAGNOSTIC_ASSERT(aStream); + + // Already closed. We have nothing else to do here. + if (mState == eClosed) { + return NS_OK; + } + + MOZ_DIAGNOSTIC_ASSERT(mInputStream); + MOZ_DIAGNOSTIC_ASSERT(mState == eReading || mState == eChecking); + + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(mGlobal))) { + // Without JSContext we are not able to close the stream or to propagate the + // error. + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted stream(cx, mReadableStream); + + uint64_t size = 0; + nsresult rv = mInputStream->Available(&size); + if (NS_SUCCEEDED(rv) && size == 0) { + // In theory this should not happen. If size is 0, the stream should be + // considered closed. + rv = NS_BASE_STREAM_CLOSED; + } + + // No warning for stream closed. + if (rv == NS_BASE_STREAM_CLOSED || NS_WARN_IF(NS_FAILED(rv))) { + ErrorPropagation(cx, stream, rv); + return NS_OK; + } + + // This extra checking is completed. Let's wait for the next read request. + if (mState == eChecking) { + mState = eWaiting; + return NS_OK; + } + + mState = eWriting; + JS::ReadableStreamUpdateDataAvailableFromSource(cx, stream, size); + + // The WriteInto callback changes mState to eChecking. + MOZ_DIAGNOSTIC_ASSERT(mState == eChecking); + + return NS_OK; +} + +} // dom namespace +} // mozilla namespace diff --git a/dom/fetch/FetchStream.h b/dom/fetch/FetchStream.h new file mode 100644 index 0000000000..ebe94e205b --- /dev/null +++ b/dom/fetch/FetchStream.h @@ -0,0 +1,107 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 mozilla_dom_FetchStream_h +#define mozilla_dom_FetchStream_h + +#include "Fetch.h" +#include "jsapi.h" +#include "nsIAsyncInputStream.h" +#include "nsISupportsImpl.h" + +class nsIGlobalObject; + +class nsIInputStream; + +namespace mozilla { +namespace dom { + +class FetchStream final : public nsIInputStreamCallback +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAMCALLBACK + + static JSObject* + Create(JSContext* aCx, nsIGlobalObject* aGlobal, + nsIInputStream* aInputStream, ErrorResult& aRv); + +private: + FetchStream(nsIGlobalObject* aGlobal, nsIInputStream* aInputStream); + ~FetchStream(); + + static void + RequestDataCallback(JSContext* aCx, JS::HandleObject aStream, + void* aUnderlyingSource, uint8_t aFlags, + size_t aDesiredSize); + + static void + WriteIntoReadRequestCallback(JSContext* aCx, JS::HandleObject aStream, + void* aUnderlyingSource, uint8_t aFlags, + void* aBuffer, size_t aLength, + size_t* aByteWritten); + + static JS::Value + CancelCallback(JSContext* aCx, JS::HandleObject aStream, + void* aUnderlyingSource, uint8_t aFlags, + JS::HandleValue aReason); + + static void + ClosedCallback(JSContext* aCx, JS::HandleObject aStream, + void* aUnderlyingSource, uint8_t aFlags); + + static void + ErroredCallback(JSContext* aCx, JS::HandleObject aStream, + void* aUnderlyingSource, uint8_t aFlags, + JS::HandleValue reason); + + static void + FinalizeCallback(void* aUnderlyingSource, uint8_t aFlags); + + void + ErrorPropagation(JSContext* aCx, JS::HandleObject aStream, nsresult aRv); + + // Common methods + + enum State { + // RequestDataCallback has not been called yet. We haven't started to read + // data from the stream yet. + eWaiting, + + // We are reading data in a separate I/O thread. + eReading, + + // We are ready to write something in the JS Buffer. + eWriting, + + // After a writing, we want to check if the stream is closed. After the + // check, we go back to eWaiting. If a reading request happens in the + // meantime, we move to eReading state. + eChecking, + + // Operation completed. + eClosed, + }; + + // Touched only on the target thread. + State mState; + + nsCOMPtr mGlobal; + + // This is the original inputStream received during the CTOR. It will be + // converted into an nsIAsyncInputStream and stored into mInputStream at the + // first use. + nsCOMPtr mOriginalInputStream; + nsCOMPtr mInputStream; + + nsCOMPtr mOwningEventTarget; + + JS::Heap mReadableStream; +}; + +} // dom namespace +} // mozilla namespace + +#endif // mozilla_dom_FetchStream_h diff --git a/dom/fetch/Request.cpp b/dom/fetch/Request.cpp index ab87a3215a..47fb62e885 100644 --- a/dom/fetch/Request.cpp +++ b/dom/fetch/Request.cpp @@ -29,7 +29,24 @@ using namespace workers; NS_IMPL_CYCLE_COLLECTING_ADDREF(Request) NS_IMPL_CYCLE_COLLECTING_RELEASE(Request) -NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Request, mOwner, mHeaders) + +NS_IMPL_CYCLE_COLLECTION_CLASS(Request) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Request) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mHeaders) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Request) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHeaders) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Request) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamBody) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Request) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY diff --git a/dom/fetch/Response.cpp b/dom/fetch/Response.cpp index 42b25ae1d9..20487906ba 100644 --- a/dom/fetch/Response.cpp +++ b/dom/fetch/Response.cpp @@ -26,7 +26,27 @@ namespace dom { NS_IMPL_CYCLE_COLLECTING_ADDREF(Response) NS_IMPL_CYCLE_COLLECTING_RELEASE(Response) -NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Response, mOwner, mHeaders) + +NS_IMPL_CYCLE_COLLECTION_CLASS(Response) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Response) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mHeaders) + + tmp->mReadableStreamBody = nullptr; + + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Response) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHeaders) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Response) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamBody) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Response) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY @@ -42,10 +62,14 @@ Response::Response(nsIGlobalObject* aGlobal, InternalResponse* aInternalResponse MOZ_ASSERT(aInternalResponse->Headers()->Guard() == HeadersGuardEnum::Immutable || aInternalResponse->Headers()->Guard() == HeadersGuardEnum::Response); SetMimeType(); + + // Keep a firm grip! + mozilla::HoldJSObjects(this); } Response::~Response() { + mozilla::DropJSObjects(this); } /* static */ already_AddRefed diff --git a/dom/fetch/moz.build b/dom/fetch/moz.build index d1a8a49323..cd41620f93 100644 --- a/dom/fetch/moz.build +++ b/dom/fetch/moz.build @@ -24,6 +24,7 @@ UNIFIED_SOURCES += [ 'FetchConsumer.cpp', 'FetchDriver.cpp', 'FetchObserver.cpp', + 'FetchStream.cpp', 'FetchUtil.cpp', 'Headers.cpp', 'InternalHeaders.cpp', diff --git a/dom/webidl/Response.webidl b/dom/webidl/Response.webidl index adaa70d66d..d02b714f7d 100644 --- a/dom/webidl/Response.webidl +++ b/dom/webidl/Response.webidl @@ -32,6 +32,7 @@ Response implements Body; // This should be part of Body but we don't want to expose body to request yet. partial interface Response { + [GetterThrows, Func="nsContentUtils::StreamsEnabled"] readonly attribute ReadableStream? body; }; diff --git a/dom/workers/WorkerPrefs.h b/dom/workers/WorkerPrefs.h index 215b375ddb..415435cf06 100644 --- a/dom/workers/WorkerPrefs.h +++ b/dom/workers/WorkerPrefs.h @@ -35,6 +35,7 @@ WORKER_SIMPLE_PREF("dom.serviceWorkers.testing.enabled", ServiceWorkersTestingEn WORKER_SIMPLE_PREF("dom.serviceWorkers.openWindow.enabled", OpenWindowEnabled, OPEN_WINDOW_ENABLED) 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("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/modules/libpref/init/all.js b/modules/libpref/init/all.js index d1e2ddf1e2..3d6325655e 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -4585,6 +4585,9 @@ pref("layout.animated-image-layers.enabled", false); // Abort API pref("dom.abortController.enabled", true); +// Streams API +pref("dom.streams.enabled", false); + // Push pref("dom.push.enabled", false); pref("dom.push.loglevel", "error"); From 3979e4847c88a1d5ad92114a31f4dfcbc32802b7 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 19:24:08 -0500 Subject: [PATCH 09/31] Issue #1442 - Part 9: Stream shutdown handling. https://bugzilla.mozilla.org/show_bug.cgi?id=1128959 --- dom/fetch/FetchStream.cpp | 169 +++++++++++++++++++++++++++++++++++++- dom/fetch/FetchStream.h | 17 ++++ 2 files changed, 182 insertions(+), 4 deletions(-) diff --git a/dom/fetch/FetchStream.cpp b/dom/fetch/FetchStream.cpp index 7b165b2a9d..e57991fcc2 100644 --- a/dom/fetch/FetchStream.cpp +++ b/dom/fetch/FetchStream.cpp @@ -8,6 +8,9 @@ #include "nsITransport.h" #include "nsIStreamTransportService.h" #include "nsProxyRelease.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "Workers.h" #include "mozilla/dom/DOMError.h" @@ -19,7 +22,67 @@ static NS_DEFINE_CID(kStreamTransportServiceCID, namespace mozilla { namespace dom { -NS_IMPL_ISUPPORTS(FetchStream, nsIInputStreamCallback) +using namespace workers; + +namespace { + +class FetchStreamWorkerHolder final : public WorkerHolder +{ +public: + explicit FetchStreamWorkerHolder(FetchStream* aStream) + : WorkerHolder() + , mStream(aStream) + , mWasNotified(false) + {} + + bool Notify(Status aStatus) override + { + if (!mWasNotified) { + mWasNotified = true; + mStream->Close(); + } + + return true; + } + + WorkerPrivate* GetWorkerPrivate() const + { + return mWorkerPrivate; + } + +private: + RefPtr mStream; + bool mWasNotified; +}; + +class FetchStreamWorkerHolderShutdown final : public WorkerControlRunnable +{ +public: + FetchStreamWorkerHolderShutdown(WorkerPrivate* aWorkerPrivate, + UniquePtr&& aHolder, + nsCOMPtr&& aGlobal) + : WorkerControlRunnable(aWorkerPrivate) + , mHolder(Move(aHolder)) + , mGlobal(Move(aGlobal)) + {} + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + mHolder = nullptr; + mGlobal = nullptr; + return true; + } + +private: + UniquePtr mHolder; + nsCOMPtr mGlobal; +}; + +} // anonymous + +NS_IMPL_ISUPPORTS(FetchStream, nsIInputStreamCallback, nsIObserver, + nsISupportsWeakReference) /* static */ JSObject* FetchStream::Create(JSContext* aCx, nsIGlobalObject* aGlobal, @@ -30,6 +93,35 @@ FetchStream::Create(JSContext* aCx, nsIGlobalObject* aGlobal, RefPtr stream = new FetchStream(aGlobal, aInputStream); + if (NS_IsMainThread()) { + nsCOMPtr os = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!os)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + aRv = os->AddObserver(stream, DOM_WINDOW_DESTROYED_TOPIC, true); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + } else { + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + MOZ_ASSERT(workerPrivate); + + UniquePtr holder( + new FetchStreamWorkerHolder(stream)); + if (NS_WARN_IF(!holder->HoldWorker(workerPrivate, Closing))) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + // Note, this will create a ref-cycle between the holder and the stream. + // The cycle is broken when the stream is closed or the worker begins + // shutting down. + stream->mWorkerHolder = Move(holder); + } + if (!JS::HasReadableStreamCallbacks(aCx)) { JS::SetReadableStreamCallbacks(aCx, &FetchStream::RequestDataCallback, @@ -228,11 +320,16 @@ FetchStream::FinalizeCallback(void* aUnderlyingSource, uint8_t aFlags) MOZ_DIAGNOSTIC_ASSERT(aUnderlyingSource); MOZ_DIAGNOSTIC_ASSERT(aFlags == FETCH_STREAM_FLAG); + // This can be called in any thread. + RefPtr stream = dont_AddRef(static_cast(aUnderlyingSource)); - stream->mState = eClosed; - stream->mReadableStream = nullptr; + if (stream->mState == eClosed) { + return; + } + + stream->CloseAndReleaseObjects(); } FetchStream::FetchStream(nsIGlobalObject* aGlobal, @@ -248,7 +345,6 @@ FetchStream::FetchStream(nsIGlobalObject* aGlobal, FetchStream::~FetchStream() { - NS_ProxyRelease(mOwningEventTarget, mGlobal.forget()); } void @@ -332,5 +428,70 @@ FetchStream::OnInputStreamReady(nsIAsyncInputStream* aStream) return NS_OK; } +void +FetchStream::Close() +{ + if (mState == eClosed) { + return; + } + + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(mGlobal))) { + return; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted stream(cx, mReadableStream); + JS::ReadableStreamClose(cx, stream); + + CloseAndReleaseObjects(); +} + +void +FetchStream::CloseAndReleaseObjects() +{ + MOZ_DIAGNOSTIC_ASSERT(mState != eClosed); + mState = eClosed; + + if (mWorkerHolder) { + RefPtr r = + new FetchStreamWorkerHolderShutdown( + static_cast(mWorkerHolder.get())->GetWorkerPrivate(), + Move(mWorkerHolder), Move(mGlobal)); + r->Dispatch(); + } else { + RefPtr self = this; + RefPtr r = NS_NewRunnableFunction( + [self] () { + nsCOMPtr os = mozilla::services::GetObserverService(); + if (os) { + os->RemoveObserver(self, DOM_WINDOW_DESTROYED_TOPIC); + } + self->mGlobal = nullptr; + }); + + NS_DispatchToMainThread(r); + } +} + +// nsIObserver +// ----------- + +NS_IMETHODIMP +FetchStream::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) +{ + AssertIsOnMainThread(); + + MOZ_ASSERT(strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) == 0); + + nsCOMPtr window = do_QueryInterface(mGlobal); + if (SameCOMIdentity(aSubject, window)) { + Close(); + } + + return NS_OK; +} + } // dom namespace } // mozilla namespace diff --git a/dom/fetch/FetchStream.h b/dom/fetch/FetchStream.h index ebe94e205b..b18b7356ff 100644 --- a/dom/fetch/FetchStream.h +++ b/dom/fetch/FetchStream.h @@ -9,7 +9,9 @@ #include "Fetch.h" #include "jsapi.h" #include "nsIAsyncInputStream.h" +#include "nsIObserver.h" #include "nsISupportsImpl.h" +#include "nsWeakReference.h" class nsIGlobalObject; @@ -18,16 +20,26 @@ class nsIInputStream; namespace mozilla { namespace dom { +namespace workers { +class WorkerHolder; +} + class FetchStream final : public nsIInputStreamCallback + , public nsIObserver + , public nsSupportsWeakReference { public: NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIOBSERVER static JSObject* Create(JSContext* aCx, nsIGlobalObject* aGlobal, nsIInputStream* aInputStream, ErrorResult& aRv); + void + Close(); + private: FetchStream(nsIGlobalObject* aGlobal, nsIInputStream* aInputStream); ~FetchStream(); @@ -63,6 +75,9 @@ private: void ErrorPropagation(JSContext* aCx, JS::HandleObject aStream, nsresult aRv); + void + CloseAndReleaseObjects(); + // Common methods enum State { @@ -98,6 +113,8 @@ private: nsCOMPtr mOwningEventTarget; + UniquePtr mWorkerHolder; + JS::Heap mReadableStream; }; From a4146b60a46e92390a9d3a1fffecd53ebae2fa7b Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 22:58:24 -0500 Subject: [PATCH 10/31] Issue #1442 - Part 10a - Unify body extraction in Fetch/Beacon/XHR. https://bugzilla.mozilla.org/show_bug.cgi?id=1329298 Pre-requisite for Part 11. --- dom/base/FormData.cpp | 4 +- dom/base/Navigator.cpp | 124 +++++---- dom/base/Navigator.h | 10 +- dom/fetch/BodyExtractor.cpp | 236 ++++++++++++++++++ dom/fetch/BodyExtractor.h | 46 ++++ dom/fetch/Fetch.cpp | 196 ++++----------- dom/fetch/Request.cpp | 8 +- dom/fetch/Response.cpp | 9 +- dom/fetch/moz.build | 2 + dom/tests/mochitest/beacon/beacon-handler.sjs | 3 +- dom/url/URLSearchParams.cpp | 5 +- dom/webidl/Navigator.webidl | 2 +- dom/xhr/XMLHttpRequestMainThread.cpp | 190 +------------- dom/xhr/XMLHttpRequestMainThread.h | 46 +--- dom/xhr/nsIXMLHttpRequest.idl | 5 +- 15 files changed, 439 insertions(+), 447 deletions(-) create mode 100644 dom/fetch/BodyExtractor.cpp create mode 100644 dom/fetch/BodyExtractor.h diff --git a/dom/base/FormData.cpp b/dom/base/FormData.cpp index de4d71a676..d47a970fc8 100644 --- a/dom/base/FormData.cpp +++ b/dom/base/FormData.cpp @@ -398,7 +398,7 @@ FormData::Constructor(const GlobalObject& aGlobal, NS_IMETHODIMP FormData::GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) + nsACString& aContentTypeWithCharset, nsACString& aCharset) { FSMultipartFormData fs(NS_LITERAL_CSTRING("UTF-8"), nullptr); @@ -419,7 +419,7 @@ FormData::GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength, } } - fs.GetContentType(aContentType); + fs.GetContentType(aContentTypeWithCharset); aCharset.Truncate(); *aContentLength = 0; NS_ADDREF(*aBody = fs.GetSubmissionBody(aContentLength)); diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp index ef8a453742..894e0a162e 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp @@ -11,7 +11,9 @@ #include "nsPluginArray.h" #include "nsMimeTypeArray.h" #include "mozilla/MemoryReporting.h" +#include "mozilla/dom/BodyExtractor.h" #include "mozilla/dom/DesktopNotification.h" +#include "mozilla/dom/FetchBinding.h" #include "mozilla/dom/File.h" #include "nsGeolocation.h" #include "nsIClassOfService.h" @@ -857,8 +859,52 @@ BeaconStreamListener::OnDataAvailable(nsIRequest *aRequest, bool Navigator::SendBeacon(const nsAString& aUrl, - const Nullable& aData, + const Nullable& aData, ErrorResult& aRv) +{ + if (aData.IsNull()) { + return SendBeaconInternal(aUrl, nullptr, /* isBlob */ false, aRv); + } + + if (aData.Value().IsArrayBuffer()) { + BodyExtractor body(&aData.Value().GetAsArrayBuffer()); + return SendBeaconInternal(aUrl, &body, /* isBlob*/ false, aRv); + } + + if (aData.Value().IsArrayBufferView()) { + BodyExtractor body(&aData.Value().GetAsArrayBufferView()); + return SendBeaconInternal(aUrl, &body, /* isBlob*/ false, aRv); + } + + if (aData.Value().IsBlob()) { + BodyExtractor body(&aData.Value().GetAsBlob()); + return SendBeaconInternal(aUrl, &body, /* isBlob */ true, aRv); + } + + if (aData.Value().IsFormData()) { + BodyExtractor body(&aData.Value().GetAsFormData()); + return SendBeaconInternal(aUrl, &body, /* isBlob */ false, aRv); + } + + if (aData.Value().IsUSVString()) { + BodyExtractor body(&aData.Value().GetAsUSVString()); + return SendBeaconInternal(aUrl, &body, /* isBlob */ false, aRv); + } + + if (aData.Value().IsURLSearchParams()) { + BodyExtractor body(&aData.Value().GetAsURLSearchParams()); + return SendBeaconInternal(aUrl, &body, /* isBlob */ false, aRv); + } + + MOZ_CRASH("Invalid data type."); + return false; +} + +bool +Navigator::SendBeaconInternal(const nsAString& aUrl, + BodyExtractorBase* aBody, + bool aIsBlob, + ErrorResult& aRv) { if (!mWindow) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); @@ -900,9 +946,9 @@ Navigator::SendBeacon(const nsAString& aUrl, nsIChannel::LOAD_CLASSIFY_URI; // No need to use CORS for sendBeacon unless it's a BLOB - nsSecurityFlags securityFlags = (!aData.IsNull() && aData.Value().IsBlob()) - ? nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS - : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS; + nsSecurityFlags securityFlags = aIsBlob + ? nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS + : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS; securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; nsCOMPtr channel; @@ -928,67 +974,15 @@ Navigator::SendBeacon(const nsAString& aUrl, } httpChannel->SetReferrer(documentURI); - nsCString mimeType; - if (!aData.IsNull()) { - nsCOMPtr in; + nsCOMPtr in; + nsAutoCString contentTypeWithCharset; + nsAutoCString charset; + uint64_t length = 0; - if (aData.Value().IsString()) { - nsCString stringData = NS_ConvertUTF16toUTF8(aData.Value().GetAsString()); - nsCOMPtr strStream = do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID, &rv); - if (NS_FAILED(rv)) { - aRv.Throw(NS_ERROR_FAILURE); - return false; - } - rv = strStream->SetData(stringData.BeginReading(), stringData.Length()); - if (NS_FAILED(rv)) { - aRv.Throw(NS_ERROR_FAILURE); - return false; - } - mimeType.AssignLiteral("text/plain;charset=UTF-8"); - in = strStream; - - } else if (aData.Value().IsArrayBufferView()) { - - nsCOMPtr strStream = do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID, &rv); - if (NS_FAILED(rv)) { - aRv.Throw(NS_ERROR_FAILURE); - return false; - } - - const ArrayBufferView& view = aData.Value().GetAsArrayBufferView(); - view.ComputeLengthAndData(); - rv = strStream->SetData(reinterpret_cast(view.Data()), - view.Length()); - - if (NS_FAILED(rv)) { - aRv.Throw(NS_ERROR_FAILURE); - return false; - } - mimeType.AssignLiteral("application/octet-stream"); - in = strStream; - - } else if (aData.Value().IsBlob()) { - Blob& blob = aData.Value().GetAsBlob(); - blob.GetInternalStream(getter_AddRefs(in), aRv); - if (NS_WARN_IF(aRv.Failed())) { - return false; - } - - nsAutoString type; - blob.GetType(type); - mimeType = NS_ConvertUTF16toUTF8(type); - - } else if (aData.Value().IsFormData()) { - FormData& form = aData.Value().GetAsFormData(); - uint64_t len; - nsAutoCString charset; - form.GetSendInfo(getter_AddRefs(in), - &len, - mimeType, - charset); - } else { - MOZ_ASSERT(false, "switch statements not in sync"); - aRv.Throw(NS_ERROR_FAILURE); + if (aBody) { + aRv = aBody->GetAsStream(getter_AddRefs(in), &length, + contentTypeWithCharset, charset); + if (NS_WARN_IF(aRv.Failed())) { return false; } @@ -997,7 +991,7 @@ Navigator::SendBeacon(const nsAString& aUrl, aRv.Throw(NS_ERROR_FAILURE); return false; } - uploadChannel->ExplicitSetUploadStream(in, mimeType, -1, + uploadChannel->ExplicitSetUploadStream(in, contentTypeWithCharset, length, NS_LITERAL_CSTRING("POST"), false); } else { diff --git a/dom/base/Navigator.h b/dom/base/Navigator.h index df99844ca9..16fcd4d440 100644 --- a/dom/base/Navigator.h +++ b/dom/base/Navigator.h @@ -30,12 +30,13 @@ class nsIURI; namespace mozilla { namespace dom { +class BodyExtractorBase; class Geolocation; class systemMessageCallback; class MediaDevices; struct MediaStreamConstraints; class WakeLock; -class ArrayBufferViewOrBlobOrStringOrFormData; +class ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams; class ServiceWorkerContainer; class DOMRequest; } // namespace dom @@ -196,7 +197,7 @@ public: #endif // MOZ_AUDIO_CHANNEL_MANAGER bool SendBeacon(const nsAString& aUrl, - const Nullable& aData, + const Nullable& aData, ErrorResult& aRv); void MozGetUserMedia(const MediaStreamConstraints& aConstraints, @@ -255,6 +256,11 @@ private: bool CheckPermission(const char* type); static bool CheckPermission(nsPIDOMWindowInner* aWindow, const char* aType); + bool SendBeaconInternal(const nsAString& aUrl, + BodyExtractorBase* aBody, + bool aIsBlob, + ErrorResult& aRv); + RefPtr mMimeTypes; RefPtr mPlugins; RefPtr mPermissions; diff --git a/dom/fetch/BodyExtractor.cpp b/dom/fetch/BodyExtractor.cpp new file mode 100644 index 0000000000..889526314e --- /dev/null +++ b/dom/fetch/BodyExtractor.cpp @@ -0,0 +1,236 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BodyExtractor.h" +#include "mozilla/dom/EncodingUtils.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/URLSearchParams.h" +#include "mozilla/dom/XMLHttpRequest.h" +#include "nsContentUtils.h" +#include "nsIDOMDocument.h" +#include "nsIDOMSerializer.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsIStorageStream.h" +#include "nsStringStream.h" +#include "nsIUnicodeEncoder.h" + +namespace mozilla { +namespace dom { + +static nsresult +GetBufferDataAsStream(const uint8_t* aData, uint32_t aDataLength, + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) +{ + aContentType.SetIsVoid(true); + aCharset.Truncate(); + + *aContentLength = aDataLength; + const char* data = reinterpret_cast(aData); + + nsCOMPtr stream; + nsresult rv = NS_NewByteInputStream(getter_AddRefs(stream), data, aDataLength, + NS_ASSIGNMENT_COPY); + NS_ENSURE_SUCCESS(rv, rv); + + stream.forget(aResult); + + return NS_OK; +} + +template<> nsresult +BodyExtractor::GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const +{ + mBody->ComputeLengthAndData(); + return GetBufferDataAsStream(mBody->Data(), mBody->Length(), + aResult, aContentLength, aContentTypeWithCharset, + aCharset); +} + +template<> nsresult +BodyExtractor::GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const +{ + mBody->ComputeLengthAndData(); + return GetBufferDataAsStream(mBody->Data(), mBody->Length(), + aResult, aContentLength, aContentTypeWithCharset, + aCharset); +} + +template<> nsresult +BodyExtractor::GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const +{ + nsCOMPtr domdoc(do_QueryInterface(mBody)); + NS_ENSURE_STATE(domdoc); + aCharset.AssignLiteral("UTF-8"); + + nsresult rv; + nsCOMPtr storStream; + rv = NS_NewStorageStream(4096, UINT32_MAX, getter_AddRefs(storStream)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr output; + rv = storStream->GetOutputStream(0, getter_AddRefs(output)); + NS_ENSURE_SUCCESS(rv, rv); + + if (mBody->IsHTMLDocument()) { + aContentTypeWithCharset.AssignLiteral("text/html;charset=UTF-8"); + + nsString serialized; + if (!nsContentUtils::SerializeNodeToMarkup(mBody, true, serialized)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsAutoCString utf8Serialized; + if (!AppendUTF16toUTF8(serialized, utf8Serialized, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t written; + rv = output->Write(utf8Serialized.get(), utf8Serialized.Length(), &written); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(written == utf8Serialized.Length()); + } else { + aContentTypeWithCharset.AssignLiteral("application/xml;charset=UTF-8"); + + nsCOMPtr serializer = + do_CreateInstance(NS_XMLSERIALIZER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Make sure to use the encoding we'll send + rv = serializer->SerializeToStream(domdoc, output, aCharset); + NS_ENSURE_SUCCESS(rv, rv); + } + + output->Close(); + + uint32_t length; + rv = storStream->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + *aContentLength = length; + + rv = storStream->NewInputStream(0, aResult); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +template<> nsresult +BodyExtractor::GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const +{ + nsCOMPtr encoder = + EncodingUtils::EncoderForEncoding("UTF-8"); + if (!encoder) { + return NS_ERROR_OUT_OF_MEMORY; + } + + int32_t destBufferLen; + nsresult rv = encoder->GetMaxLength(mBody->BeginReading(), mBody->Length(), + &destBufferLen); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString encoded; + if (!encoded.SetCapacity(destBufferLen, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + char* destBuffer = encoded.BeginWriting(); + int32_t srcLen = (int32_t) mBody->Length(); + int32_t outLen = destBufferLen; + rv = encoder->Convert(mBody->BeginReading(), &srcLen, destBuffer, &outLen); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(outLen <= destBufferLen); + encoded.SetLength(outLen); + + rv = NS_NewCStringInputStream(aResult, encoded); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aContentLength = outLen; + aContentTypeWithCharset.AssignLiteral("text/plain;charset=UTF-8"); + aCharset.AssignLiteral("UTF-8"); + return NS_OK; +} + +template<> nsresult +BodyExtractor::GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const +{ + aContentTypeWithCharset.AssignLiteral("text/plain"); + aCharset.Truncate(); + + nsresult rv = mBody->Available(aContentLength); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr stream(mBody); + stream.forget(aResult); + return NS_OK; +} + +template<> nsresult +BodyExtractor::GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const +{ + return mBody->GetSendInfo(aResult, aContentLength, aContentTypeWithCharset, + aCharset); +} + +template<> nsresult +BodyExtractor::GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const +{ + return mBody->GetSendInfo(aResult, aContentLength, aContentTypeWithCharset, + aCharset); +} + +template<> nsresult +BodyExtractor::GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const +{ + return mBody->GetSendInfo(aResult, aContentLength, aContentTypeWithCharset, + aCharset); +} + +template<> nsresult +BodyExtractor::GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const +{ + return mBody->GetSendInfo(aResult, aContentLength, aContentTypeWithCharset, + aCharset); +} + +} // dom namespace +} // mozilla namespace diff --git a/dom/fetch/BodyExtractor.h b/dom/fetch/BodyExtractor.h new file mode 100644 index 0000000000..7d4bfd068e --- /dev/null +++ b/dom/fetch/BodyExtractor.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_BodyExtractor_h +#define mozilla_dom_BodyExtractor_h + +#include "nsString.h" + +class nsIInputStream; + +namespace mozilla { +namespace dom { + +class BodyExtractorBase +{ +public: + virtual nsresult GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const = 0; +}; + +// The implementation versions of this template are: +// ArrayBuffer, ArrayBufferView, Blob, FormData, nsAString, nsIDocument, +// nsIInputStream, nsIXHRSendable, URLSearchParams +template +class BodyExtractor final : public BodyExtractorBase +{ + Type* mBody; +public: + explicit BodyExtractor(Type* aBody) : mBody(aBody) + {} + + nsresult GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const override; +}; + +} // dom namespace +} // mozilla namespace + +#endif // mozilla_dom_BodyExtractor_h diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index 63e2c83eaa..de54b6094e 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -12,7 +12,6 @@ #include "nsIStreamLoader.h" #include "nsIThreadRetargetableRequest.h" #include "nsIUnicodeDecoder.h" -#include "nsIUnicodeEncoder.h" #include "nsDOMString.h" #include "nsNetUtil.h" @@ -24,7 +23,6 @@ #include "mozilla/ErrorResult.h" #include "mozilla/dom/BodyUtil.h" #include "mozilla/dom/DOMError.h" -#include "mozilla/dom/EncodingUtils.h" #include "mozilla/dom/Exceptions.h" #include "mozilla/dom/FetchDriver.h" #include "mozilla/dom/File.h" @@ -40,6 +38,7 @@ #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/workers/ServiceWorkerManager.h" +#include "BodyExtractor.h" #include "FetchObserver.h" #include "InternalRequest.h" #include "InternalResponse.h" @@ -725,154 +724,48 @@ WorkerFetchResolver::FlushConsoleReport() mReporter->FlushConsoleReports(worker->GetDocument()); } -namespace { - -nsresult -ExtractFromArrayBuffer(const ArrayBuffer& aBuffer, - nsIInputStream** aStream, - uint64_t& aContentLength) -{ - aBuffer.ComputeLengthAndData(); - aContentLength = aBuffer.Length(); - //XXXnsm reinterpret_cast<> is used in DOMParser, should be ok. - return NS_NewByteInputStream(aStream, - reinterpret_cast(aBuffer.Data()), - aBuffer.Length(), NS_ASSIGNMENT_COPY); -} - -nsresult -ExtractFromArrayBufferView(const ArrayBufferView& aBuffer, - nsIInputStream** aStream, - uint64_t& aContentLength) -{ - aBuffer.ComputeLengthAndData(); - aContentLength = aBuffer.Length(); - //XXXnsm reinterpret_cast<> is used in DOMParser, should be ok. - return NS_NewByteInputStream(aStream, - reinterpret_cast(aBuffer.Data()), - aBuffer.Length(), NS_ASSIGNMENT_COPY); -} - -nsresult -ExtractFromBlob(const Blob& aBlob, - nsIInputStream** aStream, - nsCString& aContentType, - uint64_t& aContentLength) -{ - RefPtr impl = aBlob.Impl(); - ErrorResult rv; - aContentLength = impl->GetSize(rv); - if (NS_WARN_IF(rv.Failed())) { - return rv.StealNSResult(); - } - - impl->GetInternalStream(aStream, rv); - if (NS_WARN_IF(rv.Failed())) { - return rv.StealNSResult(); - } - - nsAutoString type; - impl->GetType(type); - aContentType = NS_ConvertUTF16toUTF8(type); - return NS_OK; -} - -nsresult -ExtractFromFormData(FormData& aFormData, - nsIInputStream** aStream, - nsCString& aContentType, - uint64_t& aContentLength) -{ - nsAutoCString unusedCharset; - return aFormData.GetSendInfo(aStream, &aContentLength, - aContentType, unusedCharset); -} - -nsresult -ExtractFromUSVString(const nsString& aStr, - nsIInputStream** aStream, - nsCString& aContentType, - uint64_t& aContentLength) -{ - nsCOMPtr encoder = EncodingUtils::EncoderForEncoding("UTF-8"); - if (!encoder) { - return NS_ERROR_OUT_OF_MEMORY; - } - - int32_t destBufferLen; - nsresult rv = encoder->GetMaxLength(aStr.get(), aStr.Length(), &destBufferLen); - if (NS_WARN_IF(NS_FAILED(rv))) { - return rv; - } - - nsCString encoded; - if (!encoded.SetCapacity(destBufferLen, fallible)) { - return NS_ERROR_OUT_OF_MEMORY; - } - - char* destBuffer = encoded.BeginWriting(); - int32_t srcLen = (int32_t) aStr.Length(); - int32_t outLen = destBufferLen; - rv = encoder->Convert(aStr.get(), &srcLen, destBuffer, &outLen); - if (NS_WARN_IF(NS_FAILED(rv))) { - return rv; - } - - MOZ_ASSERT(outLen <= destBufferLen); - encoded.SetLength(outLen); - - aContentType = NS_LITERAL_CSTRING("text/plain;charset=UTF-8"); - aContentLength = outLen; - - return NS_NewCStringInputStream(aStream, encoded); -} - -nsresult -ExtractFromURLSearchParams(const URLSearchParams& aParams, - nsIInputStream** aStream, - nsCString& aContentType, - uint64_t& aContentLength) -{ - nsAutoString serialized; - aParams.Stringify(serialized); - aContentType = NS_LITERAL_CSTRING("application/x-www-form-urlencoded;charset=UTF-8"); - aContentLength = serialized.Length(); - return NS_NewCStringInputStream(aStream, NS_ConvertUTF16toUTF8(serialized)); -} -} // namespace - nsresult ExtractByteStreamFromBody(const OwningArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams& aBodyInit, nsIInputStream** aStream, - nsCString& aContentType, + nsCString& aContentTypeWithCharset, uint64_t& aContentLength) { MOZ_ASSERT(aStream); + nsAutoCString charset; + aContentTypeWithCharset.SetIsVoid(true); if (aBodyInit.IsArrayBuffer()) { - const ArrayBuffer& buf = aBodyInit.GetAsArrayBuffer(); - return ExtractFromArrayBuffer(buf, aStream, aContentLength); + BodyExtractor body(&aBodyInit.GetAsArrayBuffer()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } if (aBodyInit.IsArrayBufferView()) { - const ArrayBufferView& buf = aBodyInit.GetAsArrayBufferView(); - return ExtractFromArrayBufferView(buf, aStream, aContentLength); + BodyExtractor body(&aBodyInit.GetAsArrayBufferView()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } if (aBodyInit.IsBlob()) { - const Blob& blob = aBodyInit.GetAsBlob(); - return ExtractFromBlob(blob, aStream, aContentType, aContentLength); + Blob& blob = aBodyInit.GetAsBlob(); + BodyExtractor body(&blob); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } if (aBodyInit.IsFormData()) { - FormData& form = aBodyInit.GetAsFormData(); - return ExtractFromFormData(form, aStream, aContentType, aContentLength); + FormData& formData = aBodyInit.GetAsFormData(); + BodyExtractor body(&formData); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } if (aBodyInit.IsUSVString()) { - nsAutoString str; - str.Assign(aBodyInit.GetAsUSVString()); - return ExtractFromUSVString(str, aStream, aContentType, aContentLength); + BodyExtractor body(&aBodyInit.GetAsUSVString()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } if (aBodyInit.IsURLSearchParams()) { - URLSearchParams& params = aBodyInit.GetAsURLSearchParams(); - return ExtractFromURLSearchParams(params, aStream, aContentType, aContentLength); + URLSearchParams& usp = aBodyInit.GetAsURLSearchParams(); + BodyExtractor body(&usp); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } NS_NOTREACHED("Should never reach here"); @@ -882,36 +775,47 @@ ExtractByteStreamFromBody(const OwningArrayBufferOrArrayBufferViewOrBlobOrFormDa nsresult ExtractByteStreamFromBody(const ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams& aBodyInit, nsIInputStream** aStream, - nsCString& aContentType, + nsCString& aContentTypeWithCharset, uint64_t& aContentLength) { MOZ_ASSERT(aStream); MOZ_ASSERT(!*aStream); + nsAutoCString charset; + aContentTypeWithCharset.SetIsVoid(true); + if (aBodyInit.IsArrayBuffer()) { - const ArrayBuffer& buf = aBodyInit.GetAsArrayBuffer(); - return ExtractFromArrayBuffer(buf, aStream, aContentLength); + BodyExtractor body(&aBodyInit.GetAsArrayBuffer()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } if (aBodyInit.IsArrayBufferView()) { - const ArrayBufferView& buf = aBodyInit.GetAsArrayBufferView(); - return ExtractFromArrayBufferView(buf, aStream, aContentLength); + BodyExtractor body(&aBodyInit.GetAsArrayBufferView()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } if (aBodyInit.IsBlob()) { - const Blob& blob = aBodyInit.GetAsBlob(); - return ExtractFromBlob(blob, aStream, aContentType, aContentLength); + Blob& blob = aBodyInit.GetAsBlob(); + BodyExtractor body(&blob); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } if (aBodyInit.IsFormData()) { - FormData& form = aBodyInit.GetAsFormData(); - return ExtractFromFormData(form, aStream, aContentType, aContentLength); + FormData& formData = aBodyInit.GetAsFormData(); + BodyExtractor body(&formData); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } if (aBodyInit.IsUSVString()) { - nsAutoString str; - str.Assign(aBodyInit.GetAsUSVString()); - return ExtractFromUSVString(str, aStream, aContentType, aContentLength); + BodyExtractor body(&aBodyInit.GetAsUSVString()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } if (aBodyInit.IsURLSearchParams()) { - URLSearchParams& params = aBodyInit.GetAsURLSearchParams(); - return ExtractFromURLSearchParams(params, aStream, aContentType, aContentLength); + URLSearchParams& usp = aBodyInit.GetAsURLSearchParams(); + BodyExtractor body(&usp); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); } NS_NOTREACHED("Should never reach here"); diff --git a/dom/fetch/Request.cpp b/dom/fetch/Request.cpp index 47fb62e885..4d9f6fc342 100644 --- a/dom/fetch/Request.cpp +++ b/dom/fetch/Request.cpp @@ -580,11 +580,11 @@ Request::Constructor(const GlobalObject& aGlobal, const OwningArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams& bodyInit = bodyInitNullable.Value(); nsCOMPtr stream; - nsAutoCString contentType; + nsAutoCString contentTypeWithCharset; uint64_t contentLengthUnused; aRv = ExtractByteStreamFromBody(bodyInit, getter_AddRefs(stream), - contentType, + contentTypeWithCharset, contentLengthUnused); if (NS_WARN_IF(aRv.Failed())) { return nullptr; @@ -592,10 +592,10 @@ Request::Constructor(const GlobalObject& aGlobal, temporaryBody = stream; - if (!contentType.IsVoid() && + if (!contentTypeWithCharset.IsVoid() && !requestHeaders->Has(NS_LITERAL_CSTRING("Content-Type"), aRv)) { requestHeaders->Append(NS_LITERAL_CSTRING("Content-Type"), - contentType, aRv); + contentTypeWithCharset, aRv); } if (NS_WARN_IF(aRv.Failed())) { diff --git a/dom/fetch/Response.cpp b/dom/fetch/Response.cpp index 20487906ba..337139e3d2 100644 --- a/dom/fetch/Response.cpp +++ b/dom/fetch/Response.cpp @@ -224,22 +224,23 @@ Response::Constructor(const GlobalObject& aGlobal, } nsCOMPtr bodyStream; - nsCString contentType; + nsCString contentTypeWithCharset; uint64_t bodySize = 0; aRv = ExtractByteStreamFromBody(aBody.Value().Value(), getter_AddRefs(bodyStream), - contentType, + contentTypeWithCharset, bodySize); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } internalResponse->SetBody(bodyStream, bodySize); - if (!contentType.IsVoid() && + if (!contentTypeWithCharset.IsVoid() && !internalResponse->Headers()->Has(NS_LITERAL_CSTRING("Content-Type"), aRv)) { // Ignore Append() failing here. ErrorResult error; - internalResponse->Headers()->Append(NS_LITERAL_CSTRING("Content-Type"), contentType, error); + internalResponse->Headers()->Append(NS_LITERAL_CSTRING("Content-Type"), + contentTypeWithCharset, error); error.SuppressException(); } diff --git a/dom/fetch/moz.build b/dom/fetch/moz.build index cd41620f93..d08bdf0cf2 100644 --- a/dom/fetch/moz.build +++ b/dom/fetch/moz.build @@ -4,6 +4,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXPORTS.mozilla.dom += [ + 'BodyExtractor.h', 'ChannelInfo.h', 'Fetch.h', 'FetchDriver.h', @@ -19,6 +20,7 @@ EXPORTS.mozilla.dom += [ ] UNIFIED_SOURCES += [ + 'BodyExtractor.cpp', 'ChannelInfo.cpp', 'Fetch.cpp', 'FetchConsumer.cpp', diff --git a/dom/tests/mochitest/beacon/beacon-handler.sjs b/dom/tests/mochitest/beacon/beacon-handler.sjs index 4c51d371d8..e4a98f7202 100644 --- a/dom/tests/mochitest/beacon/beacon-handler.sjs +++ b/dom/tests/mochitest/beacon/beacon-handler.sjs @@ -88,7 +88,8 @@ function handleRequest(request, response) { data += charcode; } - var mimetype = request.getHeader("Content-Type"); + var mimetype = request.hasHeader("Content-Type") + ? request.getHeader("Content-Type") : "application/octet-stream"; // check to see if this is form data. if (mimetype.indexOf("multipart/form-data") != -1) { diff --git a/dom/url/URLSearchParams.cpp b/dom/url/URLSearchParams.cpp index 3a0311aabd..19cc3cda6b 100644 --- a/dom/url/URLSearchParams.cpp +++ b/dom/url/URLSearchParams.cpp @@ -575,9 +575,10 @@ URLSearchParams::ReadStructuredClone(JSStructuredCloneReader* aReader) NS_IMETHODIMP URLSearchParams::GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) + nsACString& aContentTypeWithCharset, + nsACString& aCharset) { - aContentType.AssignLiteral("application/x-www-form-urlencoded"); + aContentTypeWithCharset.AssignLiteral("application/x-www-form-urlencoded;charset=UTF-8"); aCharset.AssignLiteral("UTF-8"); nsAutoString serialized; diff --git a/dom/webidl/Navigator.webidl b/dom/webidl/Navigator.webidl index 43d53cbac0..37c853da94 100644 --- a/dom/webidl/Navigator.webidl +++ b/dom/webidl/Navigator.webidl @@ -294,7 +294,7 @@ partial interface Navigator { partial interface Navigator { [Throws, Pref="beacon.enabled"] boolean sendBeacon(DOMString url, - optional (ArrayBufferView or Blob or DOMString or FormData)? data = null); + optional BodyInit? data = null); }; partial interface Navigator { diff --git a/dom/xhr/XMLHttpRequestMainThread.cpp b/dom/xhr/XMLHttpRequestMainThread.cpp index c7a4f6e6aa..48eb494747 100644 --- a/dom/xhr/XMLHttpRequestMainThread.cpp +++ b/dom/xhr/XMLHttpRequestMainThread.cpp @@ -2298,175 +2298,6 @@ XMLHttpRequestMainThread::ChangeStateToDone() } } -template<> nsresult -XMLHttpRequestMainThread::RequestBody::GetAsStream( - nsIInputStream** aResult, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) const -{ - nsCOMPtr domdoc(do_QueryInterface(mBody)); - NS_ENSURE_STATE(domdoc); - aCharset.AssignLiteral("UTF-8"); - - nsresult rv; - nsCOMPtr storStream; - rv = NS_NewStorageStream(4096, UINT32_MAX, getter_AddRefs(storStream)); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr output; - rv = storStream->GetOutputStream(0, getter_AddRefs(output)); - NS_ENSURE_SUCCESS(rv, rv); - - if (mBody->IsHTMLDocument()) { - aContentType.AssignLiteral("text/html"); - - nsString serialized; - if (!nsContentUtils::SerializeNodeToMarkup(mBody, true, serialized)) { - return NS_ERROR_OUT_OF_MEMORY; - } - - nsAutoCString utf8Serialized; - if (!AppendUTF16toUTF8(serialized, utf8Serialized, fallible)) { - return NS_ERROR_OUT_OF_MEMORY; - } - - uint32_t written; - rv = output->Write(utf8Serialized.get(), utf8Serialized.Length(), &written); - NS_ENSURE_SUCCESS(rv, rv); - - MOZ_ASSERT(written == utf8Serialized.Length()); - } else { - aContentType.AssignLiteral("application/xml"); - - nsCOMPtr serializer = - do_CreateInstance(NS_XMLSERIALIZER_CONTRACTID, &rv); - NS_ENSURE_SUCCESS(rv, rv); - - // Make sure to use the encoding we'll send - rv = serializer->SerializeToStream(domdoc, output, aCharset); - NS_ENSURE_SUCCESS(rv, rv); - } - - output->Close(); - - uint32_t length; - rv = storStream->GetLength(&length); - NS_ENSURE_SUCCESS(rv, rv); - *aContentLength = length; - - rv = storStream->NewInputStream(0, aResult); - NS_ENSURE_SUCCESS(rv, rv); - return NS_OK; -} - -template<> nsresult -XMLHttpRequestMainThread::RequestBody::GetAsStream( - nsIInputStream** aResult, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) const -{ - aContentType.AssignLiteral("text/plain"); - aCharset.AssignLiteral("UTF-8"); - - nsAutoCString converted; - if (!AppendUTF16toUTF8(*mBody, converted, fallible)) { - return NS_ERROR_OUT_OF_MEMORY; - } - - *aContentLength = converted.Length(); - nsresult rv = NS_NewCStringInputStream(aResult, converted); - NS_ENSURE_SUCCESS(rv, rv); - return NS_OK; -} - -template<> nsresult -XMLHttpRequestMainThread::RequestBody::GetAsStream( - nsIInputStream** aResult, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) const -{ - aContentType.AssignLiteral("text/plain"); - aCharset.Truncate(); - - nsresult rv = mBody->Available(aContentLength); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr stream(mBody); - stream.forget(aResult); - return NS_OK; -} - -template<> nsresult -XMLHttpRequestMainThread::RequestBody::GetAsStream( - nsIInputStream** aResult, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) const -{ - return mBody->GetSendInfo(aResult, aContentLength, aContentType, aCharset); -} - -template<> nsresult -XMLHttpRequestMainThread::RequestBody::GetAsStream( - nsIInputStream** aResult, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) const -{ - return mBody->GetSendInfo(aResult, aContentLength, aContentType, aCharset); -} - -template<> nsresult -XMLHttpRequestMainThread::RequestBody::GetAsStream( - nsIInputStream** aResult, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) const -{ - return mBody->GetSendInfo(aResult, aContentLength, aContentType, aCharset); -} - -template<> nsresult -XMLHttpRequestMainThread::RequestBody::GetAsStream( - nsIInputStream** aResult, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) const -{ - return mBody->GetSendInfo(aResult, aContentLength, aContentType, aCharset); -} - -static nsresult -GetBufferDataAsStream(const uint8_t* aData, uint32_t aDataLength, - nsIInputStream** aResult, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) -{ - aContentType.SetIsVoid(true); - aCharset.Truncate(); - - *aContentLength = aDataLength; - const char* data = reinterpret_cast(aData); - - nsCOMPtr stream; - nsresult rv = NS_NewByteInputStream(getter_AddRefs(stream), data, aDataLength, - NS_ASSIGNMENT_COPY); - NS_ENSURE_SUCCESS(rv, rv); - - stream.forget(aResult); - - return NS_OK; -} - -template<> nsresult -XMLHttpRequestMainThread::RequestBody::GetAsStream( - nsIInputStream** aResult, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) const -{ - mBody->ComputeLengthAndData(); - return GetBufferDataAsStream(mBody->Data(), mBody->Length(), - aResult, aContentLength, aContentType, aCharset); -} - -template<> nsresult -XMLHttpRequestMainThread::RequestBody::GetAsStream( - nsIInputStream** aResult, uint64_t* aContentLength, - nsACString& aContentType, nsACString& aCharset) const -{ - mBody->ComputeLengthAndData(); - return GetBufferDataAsStream(mBody->Data(), mBody->Length(), - aResult, aContentLength, aContentType, aCharset); -} - - nsresult XMLHttpRequestMainThread::CreateChannel() { @@ -2793,7 +2624,7 @@ XMLHttpRequestMainThread::Send(nsIVariant* aVariant) // document? nsCOMPtr doc = do_QueryInterface(supports); if (doc) { - RequestBody body(doc); + BodyExtractor body(doc); return SendInternal(&body); } @@ -2802,21 +2633,21 @@ XMLHttpRequestMainThread::Send(nsIVariant* aVariant) if (wstr) { nsAutoString string; wstr->GetData(string); - RequestBody body(&string); + BodyExtractor body(&string); return SendInternal(&body); } // nsIInputStream? nsCOMPtr stream = do_QueryInterface(supports); if (stream) { - RequestBody body(stream); + BodyExtractor body(stream); return SendInternal(&body); } // nsIXHRSendable? nsCOMPtr sendable = do_QueryInterface(supports); if (sendable) { - RequestBody body(sendable); + BodyExtractor body(sendable); return SendInternal(&body); } @@ -2829,7 +2660,7 @@ XMLHttpRequestMainThread::Send(nsIVariant* aVariant) JS::Rooted obj(rootingCx, realVal.toObjectOrNull()); RootedSpiderMonkeyInterface buf(rootingCx); if (buf.Init(obj)) { - RequestBody body(&buf); + BodyExtractor body(&buf); return SendInternal(&body); } } @@ -2846,12 +2677,12 @@ XMLHttpRequestMainThread::Send(nsIVariant* aVariant) nsString string; string.Adopt(data, len); - RequestBody body(&string); + BodyExtractor body(&string); return SendInternal(&body); } nsresult -XMLHttpRequestMainThread::SendInternal(const RequestBodyBase* aBody) +XMLHttpRequestMainThread::SendInternal(const BodyExtractorBase* aBody) { NS_ENSURE_TRUE(mPrincipal, NS_ERROR_NOT_INITIALIZED); @@ -2917,13 +2748,6 @@ XMLHttpRequestMainThread::SendInternal(const RequestBodyBase* aBody) mAuthorRequestHeaders.Get("content-type", uploadContentType); if (uploadContentType.IsVoid()) { uploadContentType = defaultContentType; - - if (!charset.IsEmpty()) { - // If we are providing the default content type, then we also need to - // provide a charset declaration. - uploadContentType.Append(NS_LITERAL_CSTRING(";charset=")); - uploadContentType.Append(charset); - } } // We don't want to set a charset for streams. diff --git a/dom/xhr/XMLHttpRequestMainThread.h b/dom/xhr/XMLHttpRequestMainThread.h index 4a6eeec2ef..ed02fd1da4 100644 --- a/dom/xhr/XMLHttpRequestMainThread.h +++ b/dom/xhr/XMLHttpRequestMainThread.h @@ -34,6 +34,7 @@ #include "mozilla/MemoryReporting.h" #include "mozilla/NotNull.h" #include "mozilla/dom/MutableBlobStorage.h" +#include "mozilla/dom/BodyExtractor.h" #include "mozilla/dom/TypedArray.h" #include "mozilla/dom/XMLHttpRequest.h" #include "mozilla/dom/XMLHttpRequestBinding.h" @@ -288,34 +289,7 @@ public: private: virtual ~XMLHttpRequestMainThread(); - class RequestBodyBase - { - public: - virtual nsresult GetAsStream(nsIInputStream** aResult, - uint64_t* aContentLength, - nsACString& aContentType, - nsACString& aCharset) const - { - NS_ASSERTION(false, "RequestBodyBase should not be used directly."); - return NS_ERROR_FAILURE; - } - }; - - template - class RequestBody final : public RequestBodyBase - { - Type* mBody; - public: - explicit RequestBody(Type* aBody) : mBody(aBody) - { - } - nsresult GetAsStream(nsIInputStream** aResult, - uint64_t* aContentLength, - nsACString& aContentType, - nsACString& aCharset) const override; - }; - - nsresult SendInternal(const RequestBodyBase* aBody); + nsresult SendInternal(const BodyExtractorBase* aBody); bool IsCrossSiteCORSRequest() const; bool IsDeniedCrossSiteCORSRequest(); @@ -336,7 +310,7 @@ public: Send(JSContext* /*aCx*/, const ArrayBuffer& aArrayBuffer, ErrorResult& aRv) override { - RequestBody body(&aArrayBuffer); + BodyExtractor body(&aArrayBuffer); aRv = SendInternal(&body); } @@ -344,28 +318,28 @@ public: Send(JSContext* /*aCx*/, const ArrayBufferView& aArrayBufferView, ErrorResult& aRv) override { - RequestBody body(&aArrayBufferView); + BodyExtractor body(&aArrayBufferView); aRv = SendInternal(&body); } virtual void Send(JSContext* /*aCx*/, Blob& aBlob, ErrorResult& aRv) override { - RequestBody body(&aBlob); + BodyExtractor body(&aBlob); aRv = SendInternal(&body); } virtual void Send(JSContext* /*aCx*/, URLSearchParams& aURLSearchParams, ErrorResult& aRv) override { - RequestBody body(&aURLSearchParams); + BodyExtractor body(&aURLSearchParams); aRv = SendInternal(&body); } virtual void Send(JSContext* /*aCx*/, nsIDocument& aDoc, ErrorResult& aRv) override { - RequestBody body(&aDoc); + BodyExtractor body(&aDoc); aRv = SendInternal(&body); } @@ -375,7 +349,7 @@ public: if (DOMStringIsNull(aString)) { Send(aCx, aRv); } else { - RequestBody body(&aString); + BodyExtractor body(&aString); aRv = SendInternal(&body); } } @@ -383,7 +357,7 @@ public: virtual void Send(JSContext* /*aCx*/, FormData& aFormData, ErrorResult& aRv) override { - RequestBody body(&aFormData); + BodyExtractor body(&aFormData); aRv = SendInternal(&body); } @@ -391,7 +365,7 @@ public: Send(JSContext* aCx, nsIInputStream* aStream, ErrorResult& aRv) override { NS_ASSERTION(aStream, "Null should go to string version"); - RequestBody body(aStream); + BodyExtractor body(aStream); aRv = SendInternal(&body); } diff --git a/dom/xhr/nsIXMLHttpRequest.idl b/dom/xhr/nsIXMLHttpRequest.idl index 53e80bab70..5505bd47ee 100644 --- a/dom/xhr/nsIXMLHttpRequest.idl +++ b/dom/xhr/nsIXMLHttpRequest.idl @@ -326,9 +326,12 @@ interface nsIXMLHttpRequest : nsISupports [uuid(840d0d00-e83e-4a29-b3c7-67e96e90a499)] interface nsIXHRSendable : nsISupports { + // contentTypeWithCharset can be set to the contentType or + // contentType+charset based on what the spec says. + // See: https://fetch.spec.whatwg.org/#concept-bodyinit-extract void getSendInfo(out nsIInputStream body, out uint64_t contentLength, - out ACString contentType, + out ACString contentTypeWithCharset, out ACString charset); }; From a9520b2a7a067624e13e1672a4336f222754000a Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 23:22:06 -0500 Subject: [PATCH 11/31] Issue #1442 - Part 10b - Use nsIXHRSendable instead Blob/FormData/URLSearchParams. https://bugzilla.mozilla.org/show_bug.cgi?id=1329298 Pre-requisite for Part 11. --- dom/base/Navigator.cpp | 7 ++++--- dom/fetch/BodyExtractor.cpp | 30 ------------------------------ dom/fetch/BodyExtractor.h | 4 ++-- dom/fetch/Fetch.cpp | 15 ++++++--------- dom/xhr/XMLHttpRequestMainThread.h | 12 ++++++------ 5 files changed, 18 insertions(+), 50 deletions(-) diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp index 894e0a162e..d1af772ba3 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp @@ -40,6 +40,7 @@ #include "mozilla/dom/ServiceWorkerContainer.h" #include "mozilla/dom/StorageManager.h" #include "mozilla/dom/TCPSocket.h" +#include "mozilla/dom/URLSearchParams.h" #include "mozilla/dom/workers/RuntimeService.h" #include "mozilla/Hal.h" #include "nsISiteSpecificUserAgent.h" @@ -877,12 +878,12 @@ Navigator::SendBeacon(const nsAString& aUrl, } if (aData.Value().IsBlob()) { - BodyExtractor body(&aData.Value().GetAsBlob()); + BodyExtractor body(&aData.Value().GetAsBlob()); return SendBeaconInternal(aUrl, &body, /* isBlob */ true, aRv); } if (aData.Value().IsFormData()) { - BodyExtractor body(&aData.Value().GetAsFormData()); + BodyExtractor body(&aData.Value().GetAsFormData()); return SendBeaconInternal(aUrl, &body, /* isBlob */ false, aRv); } @@ -892,7 +893,7 @@ Navigator::SendBeacon(const nsAString& aUrl, } if (aData.Value().IsURLSearchParams()) { - BodyExtractor body(&aData.Value().GetAsURLSearchParams()); + BodyExtractor body(&aData.Value().GetAsURLSearchParams()); return SendBeaconInternal(aUrl, &body, /* isBlob */ false, aRv); } diff --git a/dom/fetch/BodyExtractor.cpp b/dom/fetch/BodyExtractor.cpp index 889526314e..89c9c20ddf 100644 --- a/dom/fetch/BodyExtractor.cpp +++ b/dom/fetch/BodyExtractor.cpp @@ -192,36 +192,6 @@ BodyExtractor::GetAsStream(nsIInputStream** aResult, return NS_OK; } -template<> nsresult -BodyExtractor::GetAsStream(nsIInputStream** aResult, - uint64_t* aContentLength, - nsACString& aContentTypeWithCharset, - nsACString& aCharset) const -{ - return mBody->GetSendInfo(aResult, aContentLength, aContentTypeWithCharset, - aCharset); -} - -template<> nsresult -BodyExtractor::GetAsStream(nsIInputStream** aResult, - uint64_t* aContentLength, - nsACString& aContentTypeWithCharset, - nsACString& aCharset) const -{ - return mBody->GetSendInfo(aResult, aContentLength, aContentTypeWithCharset, - aCharset); -} - -template<> nsresult -BodyExtractor::GetAsStream(nsIInputStream** aResult, - uint64_t* aContentLength, - nsACString& aContentTypeWithCharset, - nsACString& aCharset) const -{ - return mBody->GetSendInfo(aResult, aContentLength, aContentTypeWithCharset, - aCharset); -} - template<> nsresult BodyExtractor::GetAsStream(nsIInputStream** aResult, uint64_t* aContentLength, diff --git a/dom/fetch/BodyExtractor.h b/dom/fetch/BodyExtractor.h index 7d4bfd068e..d6b79b0349 100644 --- a/dom/fetch/BodyExtractor.h +++ b/dom/fetch/BodyExtractor.h @@ -24,8 +24,8 @@ public: }; // The implementation versions of this template are: -// ArrayBuffer, ArrayBufferView, Blob, FormData, nsAString, nsIDocument, -// nsIInputStream, nsIXHRSendable, URLSearchParams +// ArrayBuffer, ArrayBufferView, nsIXHRSendable (Blob, FormData, +// URLSearchParams), nsAString, nsIDocument, nsIInputStream template class BodyExtractor final : public BodyExtractorBase { diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index de54b6094e..a831843a96 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -746,13 +746,13 @@ ExtractByteStreamFromBody(const OwningArrayBufferOrArrayBufferViewOrBlobOrFormDa } if (aBodyInit.IsBlob()) { Blob& blob = aBodyInit.GetAsBlob(); - BodyExtractor body(&blob); + BodyExtractor body(&blob); return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, charset); } if (aBodyInit.IsFormData()) { FormData& formData = aBodyInit.GetAsFormData(); - BodyExtractor body(&formData); + BodyExtractor body(&formData); return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, charset); } @@ -763,7 +763,7 @@ ExtractByteStreamFromBody(const OwningArrayBufferOrArrayBufferViewOrBlobOrFormDa } if (aBodyInit.IsURLSearchParams()) { URLSearchParams& usp = aBodyInit.GetAsURLSearchParams(); - BodyExtractor body(&usp); + BodyExtractor body(&usp); return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, charset); } @@ -795,14 +795,12 @@ ExtractByteStreamFromBody(const ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUS charset); } if (aBodyInit.IsBlob()) { - Blob& blob = aBodyInit.GetAsBlob(); - BodyExtractor body(&blob); + BodyExtractor body(&aBodyInit.GetAsBlob()); return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, charset); } if (aBodyInit.IsFormData()) { - FormData& formData = aBodyInit.GetAsFormData(); - BodyExtractor body(&formData); + BodyExtractor body(&aBodyInit.GetAsFormData()); return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, charset); } @@ -812,8 +810,7 @@ ExtractByteStreamFromBody(const ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUS charset); } if (aBodyInit.IsURLSearchParams()) { - URLSearchParams& usp = aBodyInit.GetAsURLSearchParams(); - BodyExtractor body(&usp); + BodyExtractor body(&aBodyInit.GetAsURLSearchParams()); return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, charset); } diff --git a/dom/xhr/XMLHttpRequestMainThread.h b/dom/xhr/XMLHttpRequestMainThread.h index ed02fd1da4..f2145605ef 100644 --- a/dom/xhr/XMLHttpRequestMainThread.h +++ b/dom/xhr/XMLHttpRequestMainThread.h @@ -36,6 +36,9 @@ #include "mozilla/dom/MutableBlobStorage.h" #include "mozilla/dom/BodyExtractor.h" #include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/URLSearchParams.h" #include "mozilla/dom/XMLHttpRequest.h" #include "mozilla/dom/XMLHttpRequestBinding.h" #include "mozilla/dom/XMLHttpRequestEventTarget.h" @@ -55,11 +58,8 @@ class nsIJSID; namespace mozilla { namespace dom { -class Blob; class BlobSet; class DOMString; -class FormData; -class URLSearchParams; class XMLHttpRequestUpload; struct OriginAttributesDictionary; @@ -325,14 +325,14 @@ public: virtual void Send(JSContext* /*aCx*/, Blob& aBlob, ErrorResult& aRv) override { - BodyExtractor body(&aBlob); + BodyExtractor body(&aBlob); aRv = SendInternal(&body); } virtual void Send(JSContext* /*aCx*/, URLSearchParams& aURLSearchParams, ErrorResult& aRv) override { - BodyExtractor body(&aURLSearchParams); + BodyExtractor body(&aURLSearchParams); aRv = SendInternal(&body); } @@ -357,7 +357,7 @@ public: virtual void Send(JSContext* /*aCx*/, FormData& aFormData, ErrorResult& aRv) override { - BodyExtractor body(&aFormData); + BodyExtractor body(&aFormData); aRv = SendInternal(&body); } From 772ab8ac41de484258324599fd5d45909e41ac8a Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 27 Sep 2023 23:40:02 -0500 Subject: [PATCH 12/31] Issue #1442 - Part 10c - Use application/octet-stream for arrayBuffer in sendBeacon. https://bugzilla.mozilla.org/show_bug.cgi?id=1329298 Pre-requisite for Part 11. --- dom/base/Navigator.cpp | 24 ++++++++++++------- dom/base/Navigator.h | 10 +++++++- dom/tests/mochitest/beacon/beacon-handler.sjs | 3 +-- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp index d1af772ba3..6509d8231f 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp @@ -864,37 +864,37 @@ Navigator::SendBeacon(const nsAString& aUrl, ErrorResult& aRv) { if (aData.IsNull()) { - return SendBeaconInternal(aUrl, nullptr, /* isBlob */ false, aRv); + return SendBeaconInternal(aUrl, nullptr, eBeaconTypeOther, aRv); } if (aData.Value().IsArrayBuffer()) { BodyExtractor body(&aData.Value().GetAsArrayBuffer()); - return SendBeaconInternal(aUrl, &body, /* isBlob*/ false, aRv); + return SendBeaconInternal(aUrl, &body, eBeaconTypeOther, aRv); } if (aData.Value().IsArrayBufferView()) { BodyExtractor body(&aData.Value().GetAsArrayBufferView()); - return SendBeaconInternal(aUrl, &body, /* isBlob*/ false, aRv); + return SendBeaconInternal(aUrl, &body, eBeaconTypeOther, aRv); } if (aData.Value().IsBlob()) { BodyExtractor body(&aData.Value().GetAsBlob()); - return SendBeaconInternal(aUrl, &body, /* isBlob */ true, aRv); + return SendBeaconInternal(aUrl, &body, eBeaconTypeOther, aRv); } if (aData.Value().IsFormData()) { BodyExtractor body(&aData.Value().GetAsFormData()); - return SendBeaconInternal(aUrl, &body, /* isBlob */ false, aRv); + return SendBeaconInternal(aUrl, &body, eBeaconTypeOther, aRv); } if (aData.Value().IsUSVString()) { BodyExtractor body(&aData.Value().GetAsUSVString()); - return SendBeaconInternal(aUrl, &body, /* isBlob */ false, aRv); + return SendBeaconInternal(aUrl, &body, eBeaconTypeOther, aRv); } if (aData.Value().IsURLSearchParams()) { BodyExtractor body(&aData.Value().GetAsURLSearchParams()); - return SendBeaconInternal(aUrl, &body, /* isBlob */ false, aRv); + return SendBeaconInternal(aUrl, &body, eBeaconTypeOther, aRv); } MOZ_CRASH("Invalid data type."); @@ -904,7 +904,7 @@ Navigator::SendBeacon(const nsAString& aUrl, bool Navigator::SendBeaconInternal(const nsAString& aUrl, BodyExtractorBase* aBody, - bool aIsBlob, + BeaconType aType, ErrorResult& aRv) { if (!mWindow) { @@ -947,7 +947,7 @@ Navigator::SendBeaconInternal(const nsAString& aUrl, nsIChannel::LOAD_CLASSIFY_URI; // No need to use CORS for sendBeacon unless it's a BLOB - nsSecurityFlags securityFlags = aIsBlob + nsSecurityFlags securityFlags = aType == eBeaconTypeBlob ? nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS; securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; @@ -987,6 +987,12 @@ Navigator::SendBeaconInternal(const nsAString& aUrl, return false; } + if (aType == eBeaconTypeArrayBuffer) { + MOZ_ASSERT(contentTypeWithCharset.IsEmpty()); + MOZ_ASSERT(charset.IsEmpty()); + contentTypeWithCharset.Assign("application/octet-stream"); + } + nsCOMPtr uploadChannel = do_QueryInterface(channel); if (!uploadChannel) { aRv.Throw(NS_ERROR_FAILURE); diff --git a/dom/base/Navigator.h b/dom/base/Navigator.h index 16fcd4d440..f121af383c 100644 --- a/dom/base/Navigator.h +++ b/dom/base/Navigator.h @@ -256,9 +256,17 @@ private: bool CheckPermission(const char* type); static bool CheckPermission(nsPIDOMWindowInner* aWindow, const char* aType); + // This enum helps SendBeaconInternal to apply different behaviors to body + // types. + enum BeaconType { + eBeaconTypeBlob, + eBeaconTypeArrayBuffer, + eBeaconTypeOther + }; + bool SendBeaconInternal(const nsAString& aUrl, BodyExtractorBase* aBody, - bool aIsBlob, + BeaconType aType, ErrorResult& aRv); RefPtr mMimeTypes; diff --git a/dom/tests/mochitest/beacon/beacon-handler.sjs b/dom/tests/mochitest/beacon/beacon-handler.sjs index e4a98f7202..4c51d371d8 100644 --- a/dom/tests/mochitest/beacon/beacon-handler.sjs +++ b/dom/tests/mochitest/beacon/beacon-handler.sjs @@ -88,8 +88,7 @@ function handleRequest(request, response) { data += charcode; } - var mimetype = request.hasHeader("Content-Type") - ? request.getHeader("Content-Type") : "application/octet-stream"; + var mimetype = request.getHeader("Content-Type"); // check to see if this is form data. if (mimetype.indexOf("multipart/form-data") != -1) { From a8a75090c09a26ae2abe97a2acad775d47b7bae5 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 28 Sep 2023 14:05:55 -0500 Subject: [PATCH 13/31] Issue #1442 - Part 11 - Response.body handling. https://bugzilla.mozilla.org/show_bug.cgi?id=1329298 Use BufferSource in webIDL. https://bugzilla.mozilla.org/show_bug.cgi?id=1337722 --- dom/base/Navigator.cpp | 2 +- dom/base/Navigator.h | 3 +- dom/bindings/Bindings.conf | 3 +- dom/fetch/BodyExtractor.cpp | 1 + dom/fetch/BodyExtractor.h | 4 +- dom/fetch/Fetch.cpp | 168 +++++++++++++++++++++++++++++++++--- dom/fetch/Fetch.h | 61 +++++++++++-- dom/fetch/FetchStream.cpp | 102 +++++++++++++++++----- dom/fetch/FetchStream.h | 23 +++-- dom/fetch/Request.cpp | 6 +- dom/fetch/Response.cpp | 100 ++++++++++++++++++--- dom/fetch/Response.h | 6 +- dom/webidl/Fetch.webidl | 2 +- dom/webidl/Response.webidl | 4 +- 14 files changed, 413 insertions(+), 72 deletions(-) diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp index 6509d8231f..febe1f1a3a 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp @@ -860,7 +860,7 @@ BeaconStreamListener::OnDataAvailable(nsIRequest *aRequest, bool Navigator::SendBeacon(const nsAString& aUrl, - const Nullable& aData, + const Nullable& aData, ErrorResult& aRv) { if (aData.IsNull()) { diff --git a/dom/base/Navigator.h b/dom/base/Navigator.h index f121af383c..5738cd2b5a 100644 --- a/dom/base/Navigator.h +++ b/dom/base/Navigator.h @@ -7,6 +7,7 @@ #define mozilla_dom_Navigator_h #include "mozilla/MemoryReporting.h" +#include "mozilla/dom/Fetch.h" #include "mozilla/dom/Nullable.h" #include "mozilla/ErrorResult.h" #include "nsIDOMNavigator.h" @@ -197,7 +198,7 @@ public: #endif // MOZ_AUDIO_CHANNEL_MANAGER bool SendBeacon(const nsAString& aUrl, - const Nullable& aData, + const Nullable& aData, ErrorResult& aRv); void MozGetUserMedia(const MediaStreamConstraints& aConstraints, diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index 39fa0d7dc9..0907a0faf7 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -747,7 +747,8 @@ DOMInterfaces = { 'Response': { 'binaryNames': { 'headers': 'headers_' }, - 'implicitJSContext': [ 'arrayBuffer', 'blob', 'formData', 'json', 'text' ], + 'implicitJSContext': [ 'arrayBuffer', 'blob', 'formData', 'json', 'text', + 'clone', 'cloneUnfiltered' ], }, 'RGBColor': { diff --git a/dom/fetch/BodyExtractor.cpp b/dom/fetch/BodyExtractor.cpp index 89c9c20ddf..b840641214 100644 --- a/dom/fetch/BodyExtractor.cpp +++ b/dom/fetch/BodyExtractor.cpp @@ -14,6 +14,7 @@ #include "nsContentUtils.h" #include "nsIDOMDocument.h" #include "nsIDOMSerializer.h" +#include "nsIGlobalObject.h" #include "nsIInputStream.h" #include "nsIOutputStream.h" #include "nsIStorageStream.h" diff --git a/dom/fetch/BodyExtractor.h b/dom/fetch/BodyExtractor.h index d6b79b0349..73dfef6aff 100644 --- a/dom/fetch/BodyExtractor.h +++ b/dom/fetch/BodyExtractor.h @@ -7,9 +7,11 @@ #ifndef mozilla_dom_BodyExtractor_h #define mozilla_dom_BodyExtractor_h +#include "jsapi.h" #include "nsString.h" class nsIInputStream; +class nsIGlobalObject; namespace mozilla { namespace dom { @@ -25,7 +27,7 @@ public: // The implementation versions of this template are: // ArrayBuffer, ArrayBufferView, nsIXHRSendable (Blob, FormData, -// URLSearchParams), nsAString, nsIDocument, nsIInputStream +// URLSearchParams), nsAString, nsIDocument, nsIInputStream. template class BodyExtractor final : public BodyExtractorBase { diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index a831843a96..141e7c3043 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -725,7 +725,7 @@ WorkerFetchResolver::FlushConsoleReport() } nsresult -ExtractByteStreamFromBody(const OwningArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams& aBodyInit, +ExtractByteStreamFromBody(const fetch::OwningBodyInit& aBodyInit, nsIInputStream** aStream, nsCString& aContentTypeWithCharset, uint64_t& aContentLength) @@ -773,7 +773,7 @@ ExtractByteStreamFromBody(const OwningArrayBufferOrArrayBufferViewOrBlobOrFormDa } nsresult -ExtractByteStreamFromBody(const ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams& aBodyInit, +ExtractByteStreamFromBody(const fetch::BodyInit& aBodyInit, nsIInputStream** aStream, nsCString& aContentTypeWithCharset, uint64_t& aContentLength) @@ -819,6 +819,63 @@ ExtractByteStreamFromBody(const ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUS return NS_ERROR_FAILURE; } +nsresult +ExtractByteStreamFromBody(const fetch::ResponseBodyInit& aBodyInit, + nsIInputStream** aStream, + nsCString& aContentTypeWithCharset, + uint64_t& aContentLength) +{ + MOZ_ASSERT(aStream); + MOZ_ASSERT(!*aStream); + + // ReadableStreams should be handled by + // BodyExtractorReadableStream::GetAsStream. + MOZ_ASSERT(!aBodyInit.IsReadableStream()); + + nsAutoCString charset; + aContentTypeWithCharset.SetIsVoid(true); + + if (aBodyInit.IsArrayBuffer()) { + BodyExtractor body(&aBodyInit.GetAsArrayBuffer()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsArrayBufferView()) { + BodyExtractor body(&aBodyInit.GetAsArrayBufferView()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsBlob()) { + BodyExtractor body(&aBodyInit.GetAsBlob()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsFormData()) { + BodyExtractor body(&aBodyInit.GetAsFormData()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsUSVString()) { + BodyExtractor body(&aBodyInit.GetAsUSVString()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsURLSearchParams()) { + BodyExtractor body(&aBodyInit.GetAsURLSearchParams()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + NS_NOTREACHED("Should never reach here"); + return NS_ERROR_FAILURE; +} + + template FetchBody::FetchBody() : mWorkerPrivate(nullptr) @@ -862,7 +919,8 @@ FetchBody::BodyUsed() const JS::Rooted body(cx, mReadableStreamBody); if (JS::ReadableStreamIsDisturbed(body) || - JS::ReadableStreamIsLocked(body)) { + JS::ReadableStreamIsLocked(body) || + !JS::ReadableStreamIsReadable(body)) { return true; } } @@ -895,14 +953,18 @@ FetchBody::ConsumeBody(JSContext* aCx, FetchConsumeType aType, ErrorRes SetBodyUsed(); - nsCOMPtr global = DerivedClass()->GetParentObject(); - - // If we already created a ReadableStreamBody we have to close it now. + // If we already created a ReadableStreamBody we have to lock it now because + // it may have been shared with other objects.. if (mReadableStreamBody) { JS::Rooted body(aCx, mReadableStreamBody); - JS::ReadableStreamClose(aCx, body); + LockStream(aCx, body, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } } + nsCOMPtr global = DerivedClass()->GetParentObject(); + RefPtr promise = FetchBodyConsumer::Create(global, this, signal, aType, aRv); if (NS_WARN_IF(aRv.Failed())) { @@ -948,6 +1010,23 @@ template void FetchBody::SetMimeType(); +template +void +FetchBody::SetReadableStreamBody(JSObject* aBody) +{ + MOZ_ASSERT(!mReadableStreamBody); + MOZ_ASSERT(aBody); + mReadableStreamBody = aBody; +} + +template +void +FetchBody::SetReadableStreamBody(JSObject* aBody); + +template +void +FetchBody::SetReadableStreamBody(JSObject* aBody); + template void FetchBody::GetBody(JSContext* aCx, @@ -965,6 +1044,7 @@ FetchBody::GetBody(JSContext* aCx, if (!mReadableStreamBody) { JS::Rooted body(aCx, FetchStream::Create(aCx, + this, DerivedClass()->GetParentObject(), inputStream, aRv)); @@ -975,9 +1055,11 @@ FetchBody::GetBody(JSContext* aCx, MOZ_ASSERT(body); // If the body has been already consumed, we close the stream. - if (BodyUsed() && !JS::ReadableStreamClose(aCx, body)) { - aRv.StealExceptionFromJSContext(aCx); - return; + if (BodyUsed()) { + LockStream(aCx, body, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } } mReadableStreamBody = body; @@ -998,6 +1080,72 @@ FetchBody::GetBody(JSContext* aCx, JS::MutableHandle aMessage, ErrorResult& aRv); +template +void +FetchBody::LockStream(JSContext* aCx, + JS::HandleObject aStream, + ErrorResult& aRv) +{ + // XXXMC: TODO +} + +template +void +FetchBody::LockStream(JSContext* aCx, + JS::HandleObject aStream, + ErrorResult& aRv); + +template +void +FetchBody::LockStream(JSContext* aCx, + JS::HandleObject aStream, + ErrorResult& aRv); + +template +void +FetchBody::MaybeTeeReadableStreamBody(JSContext* aCx, + JS::MutableHandle aBodyOut, + ErrorResult& aRv) +{ + MOZ_DIAGNOSTIC_ASSERT(!BodyUsed()); + + if (!mReadableStreamBody) { + return; + } + + JS::Rooted stream(aCx, mReadableStreamBody); + + // If this is a ReadableStream with an external source, this has been + // generated by a Fetch. In this case, Fetch will be able to recreate it + // again when GetBody() is called. + if (JS::ReadableStreamGetMode(stream) == JS::ReadableStreamMode::ExternalSource) { + aBodyOut.set(nullptr); + return; + } + + JS::Rooted branch1(aCx); + JS::Rooted branch2(aCx); + + if (!JS::ReadableStreamTee(aCx, stream, &branch1, &branch2)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + mReadableStreamBody = branch1; + aBodyOut.set(branch2); +} + +template +void +FetchBody::MaybeTeeReadableStreamBody(JSContext* aCx, + JS::MutableHandle aMessage, + ErrorResult& aRv); + +template +void +FetchBody::MaybeTeeReadableStreamBody(JSContext* aCx, + JS::MutableHandle aMessage, + ErrorResult& aRv); } // namespace dom } // namespace mozilla diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index 821bc3643f..d3a7ac598e 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -24,10 +24,12 @@ class nsIGlobalObject; namespace mozilla { namespace dom { -class ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams; +class BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString; +class BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrReadableStreamOrUSVString; class BlobImpl; class InternalRequest; -class OwningArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams; +class OwningBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString; +struct ReadableStream; class RequestOrUSVString; namespace workers { @@ -41,13 +43,20 @@ FetchRequest(nsIGlobalObject* aGlobal, const RequestOrUSVString& aInput, nsresult UpdateRequestReferrer(nsIGlobalObject* aGlobal, InternalRequest* aRequest); +/* Deal with unwieldy long webIDL-generated type names */ +namespace fetch { + typedef BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString BodyInit; + typedef BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrReadableStreamOrUSVString ResponseBodyInit; + typedef OwningBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString OwningBodyInit; +}; + /* * Creates an nsIInputStream based on the fetch specifications 'extract a byte * stream algorithm' - http://fetch.spec.whatwg.org/#concept-bodyinit-extract. * Stores content type in out param aContentType. */ nsresult -ExtractByteStreamFromBody(const OwningArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams& aBodyInit, +ExtractByteStreamFromBody(const fetch::OwningBodyInit& aBodyInit, nsIInputStream** aStream, nsCString& aContentType, uint64_t& aContentLength); @@ -56,7 +65,17 @@ ExtractByteStreamFromBody(const OwningArrayBufferOrArrayBufferViewOrBlobOrFormDa * Non-owning version. */ nsresult -ExtractByteStreamFromBody(const ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams& aBodyInit, +ExtractByteStreamFromBody(const fetch::BodyInit& aBodyInit, + nsIInputStream** aStream, + nsCString& aContentType, + uint64_t& aContentLength); + +/* + * Non-owning version. This method should go away when BodyInit will contain + * ReadableStream. + */ +nsresult +ExtractByteStreamFromBody(const fetch::ResponseBodyInit& aBodyInit, nsIInputStream** aStream, nsCString& aContentType, uint64_t& aContentLength); @@ -72,6 +91,15 @@ enum FetchConsumeType CONSUME_TEXT, }; +class FetchStreamHolder +{ +public: + NS_IMETHOD_(MozExternalRefCountType) AddRef(void) = 0; + NS_IMETHOD_(MozExternalRefCountType) Release(void) = 0; + + virtual void NullifyStream() = 0; +}; + /* * FetchBody's body consumption uses nsIInputStreamPump to read from the * underlying stream to a block of memory, which is then adopted by @@ -106,14 +134,11 @@ enum FetchConsumeType * The pump is always released on the main thread. */ template -class FetchBody +class FetchBody : public FetchStreamHolder { public: friend class FetchBodyConsumer; - NS_IMETHOD_(MozExternalRefCountType) AddRef(void) = 0; - NS_IMETHOD_(MozExternalRefCountType) Release(void) = 0; - bool BodyUsed() const; @@ -152,6 +177,13 @@ public: JS::MutableHandle aBodyOut, ErrorResult& aRv); + // If the body contains a ReadableStream body object, this method produces a + // tee() of it. + void + MaybeTeeReadableStreamBody(JSContext* aCx, + JS::MutableHandle aBodyOut, + ErrorResult& aRv); + // Utility public methods accessed by various runnables. void @@ -166,6 +198,12 @@ public: return mMimeType; } + void + NullifyStream() override + { + mReadableStreamBody = nullptr; + } + virtual AbortSignal* GetSignal() const = 0; @@ -182,6 +220,10 @@ protected: void SetMimeType(); + + void + SetReadableStreamBody(JSObject* aBody); + private: Derived* DerivedClass() const @@ -192,6 +234,9 @@ private: already_AddRefed ConsumeBody(JSContext* aCx, FetchConsumeType aType, ErrorResult& aRv); + void + LockStream(JSContext* aCx, JS::HandleObject aStream, ErrorResult& aRv); + bool IsOnTargetThread() { diff --git a/dom/fetch/FetchStream.cpp b/dom/fetch/FetchStream.cpp index e57991fcc2..94837504f1 100644 --- a/dom/fetch/FetchStream.cpp +++ b/dom/fetch/FetchStream.cpp @@ -60,10 +60,12 @@ class FetchStreamWorkerHolderShutdown final : public WorkerControlRunnable public: FetchStreamWorkerHolderShutdown(WorkerPrivate* aWorkerPrivate, UniquePtr&& aHolder, - nsCOMPtr&& aGlobal) - : WorkerControlRunnable(aWorkerPrivate) + nsCOMPtr&& aGlobal, + RefPtr&& aStreamHolder) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) , mHolder(Move(aHolder)) , mGlobal(Move(aGlobal)) + , mStreamHolder(Move(aStreamHolder)) {} bool @@ -71,12 +73,30 @@ public: { mHolder = nullptr; mGlobal = nullptr; + + mStreamHolder->NullifyStream(); + mStreamHolder = nullptr; + return true; } + // This runnable starts from a JS Thread. We need to disable a couple of + // assertions by overriding the following methods. + + bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + return true; + } + + void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + {} + private: UniquePtr mHolder; nsCOMPtr mGlobal; + RefPtr mStreamHolder; }; } // anonymous @@ -85,13 +105,15 @@ NS_IMPL_ISUPPORTS(FetchStream, nsIInputStreamCallback, nsIObserver, nsISupportsWeakReference) /* static */ JSObject* -FetchStream::Create(JSContext* aCx, nsIGlobalObject* aGlobal, - nsIInputStream* aInputStream, ErrorResult& aRv) +FetchStream::Create(JSContext* aCx, FetchStreamHolder* aStreamHolder, + nsIGlobalObject* aGlobal, nsIInputStream* aInputStream, + ErrorResult& aRv) { MOZ_DIAGNOSTIC_ASSERT(aCx); MOZ_DIAGNOSTIC_ASSERT(aInputStream); + MOZ_DIAGNOSTIC_ASSERT(aStreamHolder); - RefPtr stream = new FetchStream(aGlobal, aInputStream); + RefPtr stream = new FetchStream(aGlobal, aStreamHolder, aInputStream); if (NS_IsMainThread()) { nsCOMPtr os = mozilla::services::GetObserverService(); @@ -226,7 +248,7 @@ FetchStream::RequestDataCallback(JSContext* aCx, MOZ_DIAGNOSTIC_ASSERT(!stream->mOriginalInputStream); nsresult rv = - stream->mInputStream->AsyncWait(stream, 0, 0, nullptr); + stream->mInputStream->AsyncWait(stream, 0, 0, stream->mOwningEventTarget); if (NS_WARN_IF(NS_FAILED(rv))) { stream->ErrorPropagation(aCx, aStream, rv); return; @@ -264,12 +286,11 @@ FetchStream::WriteIntoReadRequestCallback(JSContext* aCx, *aByteWritten = written; if (written == 0) { - stream->mState = eClosed; - JS::ReadableStreamClose(aCx, aStream); + stream->CloseAndReleaseObjects(aCx, aStream); return; } - rv = stream->mInputStream->AsyncWait(stream, 0, 0, nullptr); + rv = stream->mInputStream->AsyncWait(stream, 0, 0, stream->mOwningEventTarget); if (NS_WARN_IF(NS_FAILED(rv))) { stream->ErrorPropagation(aCx, aStream, rv); return; @@ -292,7 +313,7 @@ FetchStream::CancelCallback(JSContext* aCx, JS::HandleObject aStream, stream->mInputStream->CloseWithStatus(NS_BASE_STREAM_CLOSED); } - stream->mState = eClosed; + stream->ReleaseObjects(); return JS::UndefinedValue(); } @@ -325,22 +346,21 @@ FetchStream::FinalizeCallback(void* aUnderlyingSource, uint8_t aFlags) RefPtr stream = dont_AddRef(static_cast(aUnderlyingSource)); - if (stream->mState == eClosed) { - return; - } - - stream->CloseAndReleaseObjects(); + stream->ReleaseObjects(); } FetchStream::FetchStream(nsIGlobalObject* aGlobal, + FetchStreamHolder* aStreamHolder, nsIInputStream* aInputStream) : mState(eWaiting) , mGlobal(aGlobal) + , mStreamHolder(aStreamHolder) , mOriginalInputStream(aInputStream) , mOwningEventTarget(nullptr) , mReadableStream(nullptr) { MOZ_DIAGNOSTIC_ASSERT(aInputStream); + MOZ_DIAGNOSTIC_ASSERT(aStreamHolder); } FetchStream::~FetchStream() @@ -356,12 +376,9 @@ FetchStream::ErrorPropagation(JSContext* aCx, JS::HandleObject aStream, return; } - // We cannot continue with any other operation. - mState = eClosed; - // Let's close the stream. if (aError == NS_BASE_STREAM_CLOSED) { - JS::ReadableStreamClose(aCx, aStream); + CloseAndReleaseObjects(aCx, aStream); return; } @@ -428,6 +445,27 @@ FetchStream::OnInputStreamReady(nsIAsyncInputStream* aStream) return NS_OK; } +/* static */ nsresult +FetchStream::RetrieveInputStream(void* aUnderlyingReadableStreamSource, + nsIInputStream** aInputStream) +{ + MOZ_ASSERT(aUnderlyingReadableStreamSource); + MOZ_ASSERT(aInputStream); + + RefPtr stream = + static_cast(aUnderlyingReadableStreamSource); + + // if mOriginalInputStream is null, the reading already started. We don't want + // to expose the internal inputStream. + if (NS_WARN_IF(!stream->mOriginalInputStream)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + nsCOMPtr inputStream = stream->mOriginalInputStream; + inputStream.forget(aInputStream); + return NS_OK; +} + void FetchStream::Close() { @@ -437,27 +475,42 @@ FetchStream::Close() AutoJSAPI jsapi; if (NS_WARN_IF(!jsapi.Init(mGlobal))) { + ReleaseObjects(); return; } JSContext* cx = jsapi.cx(); JS::Rooted stream(cx, mReadableStream); - JS::ReadableStreamClose(cx, stream); - CloseAndReleaseObjects(); + CloseAndReleaseObjects(cx, stream); } void -FetchStream::CloseAndReleaseObjects() +FetchStream::CloseAndReleaseObjects(JSContext* aCx, JS::HandleObject aStream) { MOZ_DIAGNOSTIC_ASSERT(mState != eClosed); + + ReleaseObjects(); + + if (JS::ReadableStreamIsReadable(aStream)) { + JS::ReadableStreamClose(aCx, aStream); + } +} + +void +FetchStream::ReleaseObjects() +{ + if (mState == eClosed) { + return; + } + mState = eClosed; if (mWorkerHolder) { RefPtr r = new FetchStreamWorkerHolderShutdown( static_cast(mWorkerHolder.get())->GetWorkerPrivate(), - Move(mWorkerHolder), Move(mGlobal)); + Move(mWorkerHolder), Move(mGlobal), Move(mStreamHolder)); r->Dispatch(); } else { RefPtr self = this; @@ -468,6 +521,9 @@ FetchStream::CloseAndReleaseObjects() os->RemoveObserver(self, DOM_WINDOW_DESTROYED_TOPIC); } self->mGlobal = nullptr; + + self->mStreamHolder->NullifyStream(); + self->mStreamHolder = nullptr; }); NS_DispatchToMainThread(r); diff --git a/dom/fetch/FetchStream.h b/dom/fetch/FetchStream.h index b18b7356ff..b27588eda0 100644 --- a/dom/fetch/FetchStream.h +++ b/dom/fetch/FetchStream.h @@ -24,6 +24,8 @@ namespace workers { class WorkerHolder; } +class FetchStreamHolder; + class FetchStream final : public nsIInputStreamCallback , public nsIObserver , public nsSupportsWeakReference @@ -34,14 +36,20 @@ public: NS_DECL_NSIOBSERVER static JSObject* - Create(JSContext* aCx, nsIGlobalObject* aGlobal, - nsIInputStream* aInputStream, ErrorResult& aRv); + Create(JSContext* aCx, FetchStreamHolder* aStreamHolder, + nsIGlobalObject* aGlobal, nsIInputStream* aInputStream, + ErrorResult& aRv); void Close(); + static nsresult + RetrieveInputStream(void* aUnderlyingReadableStreamSource, + nsIInputStream** aInputStream); private: - FetchStream(nsIGlobalObject* aGlobal, nsIInputStream* aInputStream); + FetchStream(nsIGlobalObject* aGlobal, + FetchStreamHolder* aStreamHolder, + nsIInputStream* aInputStream); ~FetchStream(); static void @@ -76,7 +84,10 @@ private: ErrorPropagation(JSContext* aCx, JS::HandleObject aStream, nsresult aRv); void - CloseAndReleaseObjects(); + CloseAndReleaseObjects(JSContext* aCx, JS::HandleObject aStream); + + void + ReleaseObjects(); // Common methods @@ -104,6 +115,8 @@ private: State mState; nsCOMPtr mGlobal; + RefPtr mStreamHolder; + nsCOMPtr mOwningEventTarget; // This is the original inputStream received during the CTOR. It will be // converted into an nsIAsyncInputStream and stored into mInputStream at the @@ -111,8 +124,6 @@ private: nsCOMPtr mOriginalInputStream; nsCOMPtr mInputStream; - nsCOMPtr mOwningEventTarget; - UniquePtr mWorkerHolder; JS::Heap mReadableStream; diff --git a/dom/fetch/Request.cpp b/dom/fetch/Request.cpp index 4d9f6fc342..03df3021c5 100644 --- a/dom/fetch/Request.cpp +++ b/dom/fetch/Request.cpp @@ -574,11 +574,9 @@ Request::Constructor(const GlobalObject& aGlobal, } if (aInit.mBody.WasPassed()) { - const Nullable& bodyInitNullable = - aInit.mBody.Value(); + const Nullable& bodyInitNullable = aInit.mBody.Value(); if (!bodyInitNullable.IsNull()) { - const OwningArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams& bodyInit = - bodyInitNullable.Value(); + const fetch::OwningBodyInit& bodyInit = bodyInitNullable.Value(); nsCOMPtr stream; nsAutoCString contentTypeWithCharset; uint64_t contentLengthUnused; diff --git a/dom/fetch/Response.cpp b/dom/fetch/Response.cpp index 337139e3d2..d69a199076 100644 --- a/dom/fetch/Response.cpp +++ b/dom/fetch/Response.cpp @@ -12,12 +12,15 @@ #include "mozilla/ErrorResult.h" #include "mozilla/dom/FetchBinding.h" +#include "mozilla/dom/ResponseBinding.h" #include "mozilla/dom/Headers.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/URL.h" #include "nsDOMString.h" +#include "BodyExtractor.h" +#include "FetchStream.h" #include "InternalResponse.h" #include "WorkerPrivate.h" @@ -130,7 +133,7 @@ Response::Redirect(const GlobalObject& aGlobal, const nsAString& aUrl, return nullptr; } - Optional> body; + Optional body; ResponseInit init; init.mStatus = aStatus; RefPtr r = Response::Constructor(aGlobal, body, init, aRv); @@ -151,7 +154,7 @@ Response::Redirect(const GlobalObject& aGlobal, const nsAString& aUrl, /*static*/ already_AddRefed Response::Constructor(const GlobalObject& aGlobal, - const Optional>& aBody, + const Optional& aBody, const ResponseInit& aInit, ErrorResult& aRv) { nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); @@ -217,22 +220,65 @@ Response::Constructor(const GlobalObject& aGlobal, } } - if (aBody.WasPassed() && !aBody.Value().IsNull()) { + if (aBody.WasPassed()) { if (aInit.mStatus == 204 || aInit.mStatus == 205 || aInit.mStatus == 304) { aRv.ThrowTypeError(); return nullptr; } - nsCOMPtr bodyStream; nsCString contentTypeWithCharset; + nsCOMPtr bodyStream; uint64_t bodySize = 0; - aRv = ExtractByteStreamFromBody(aBody.Value().Value(), - getter_AddRefs(bodyStream), - contentTypeWithCharset, - bodySize); - if (NS_WARN_IF(aRv.Failed())) { - return nullptr; + + if (aBody.Value().IsReadableStream()) { + const ReadableStream& readableStream = + aBody.Value().GetAsReadableStream(); + + JS::Rooted readableStreamObj(aGlobal.Context(), + readableStream.Obj()); + + if (JS::ReadableStreamIsDisturbed(readableStreamObj) || + JS::ReadableStreamIsLocked(readableStreamObj) || + !JS::ReadableStreamIsReadable(readableStreamObj)) { + aRv.ThrowTypeError(); + return nullptr; + } + + r->SetReadableStreamBody(readableStreamObj); + + // XXXMC: TODO + MOZ_ASSERT(JS::ReadableStreamGetMode(readableStreamObj) != + JS::ReadableStreamMode::ExternalSource); + + void* underlyingSource = nullptr; + if (!JS::ReadableStreamGetExternalUnderlyingSource(aGlobal.Context(), + readableStreamObj, + &underlyingSource)) { + aRv.StealExceptionFromJSContext(aGlobal.Context()); + return nullptr; + } + + bodySize = InternalResponse::UNKNOWN_BODY_SIZE; + + MOZ_ASSERT(underlyingSource); + aRv = FetchStream::RetrieveInputStream(underlyingSource, + getter_AddRefs(bodyStream)); + + // Releasing of the external source is needed in order to avoid an extra stream lock. + JS::ReadableStreamReleaseExternalUnderlyingSource(readableStreamObj); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } else { + aRv = ExtractByteStreamFromBody(aBody.Value(), + getter_AddRefs(bodyStream), + contentTypeWithCharset, + bodySize); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } } + internalResponse->SetBody(bodyStream, bodySize); if (!contentTypeWithCharset.IsVoid() && @@ -254,7 +300,7 @@ Response::Constructor(const GlobalObject& aGlobal, } already_AddRefed -Response::Clone(ErrorResult& aRv) const +Response::Clone(JSContext* aCx, ErrorResult& aRv) { if (BodyUsed()) { aRv.ThrowTypeError(); @@ -263,11 +309,26 @@ Response::Clone(ErrorResult& aRv) const RefPtr ir = mInternalResponse->Clone(); RefPtr response = new Response(mOwner, ir, mSignal); + + JS::Rooted body(aCx); + MaybeTeeReadableStreamBody(aCx, &body, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (body) { + // Maybe we have a body, but we receive null from MaybeTeeReadableStreamBody + // if this body is a native stream. In this case, the InternalResponse will + // have a clone of the native body and the ReadableStream will be created + // lazily if needed. + response->SetReadableStreamBody(body); + } + return response.forget(); } already_AddRefed -Response::CloneUnfiltered(ErrorResult& aRv) const +Response::CloneUnfiltered(JSContext* aCx, ErrorResult& aRv) { if (BodyUsed()) { aRv.ThrowTypeError(); @@ -277,6 +338,21 @@ Response::CloneUnfiltered(ErrorResult& aRv) const RefPtr clone = mInternalResponse->Clone(); RefPtr ir = clone->Unfiltered(); RefPtr ref = new Response(mOwner, ir, mSignal); + + JS::Rooted body(aCx); + MaybeTeeReadableStreamBody(aCx, &body, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (body) { + // Maybe we have a body, but we receive null from MaybeTeeReadableStreamBody + // if this body is a native stream. In this case, the InternalResponse will + // have a clone of the native body and the ReadableStream will be created + // lazily if needed. + ref->SetReadableStreamBody(body); + } + return ref.forget(); } diff --git a/dom/fetch/Response.h b/dom/fetch/Response.h index 61f1427f93..9b29dff998 100644 --- a/dom/fetch/Response.h +++ b/dom/fetch/Response.h @@ -115,7 +115,7 @@ public: static already_AddRefed Constructor(const GlobalObject& aGlobal, - const Optional>& aBody, + const Optional& aBody, const ResponseInit& aInit, ErrorResult& rv); nsIGlobalObject* GetParentObject() const @@ -124,10 +124,10 @@ public: } already_AddRefed - Clone(ErrorResult& aRv) const; + Clone(JSContext* aCx, ErrorResult& aRv); already_AddRefed - CloneUnfiltered(ErrorResult& aRv) const; + CloneUnfiltered(JSContext* aCx, ErrorResult& aRv); void SetBody(nsIInputStream* aBody, int64_t aBodySize); diff --git a/dom/webidl/Fetch.webidl b/dom/webidl/Fetch.webidl index 4b2a0af7d0..137cbde354 100644 --- a/dom/webidl/Fetch.webidl +++ b/dom/webidl/Fetch.webidl @@ -8,7 +8,7 @@ */ typedef object JSON; -typedef (ArrayBuffer or ArrayBufferView or Blob or FormData or USVString or URLSearchParams) BodyInit; +typedef (Blob or BufferSource or FormData or URLSearchParams or USVString) BodyInit; [NoInterfaceObject, Exposed=(Window,Worker)] interface Body { diff --git a/dom/webidl/Response.webidl b/dom/webidl/Response.webidl index d02b714f7d..0f4c99dbac 100644 --- a/dom/webidl/Response.webidl +++ b/dom/webidl/Response.webidl @@ -7,7 +7,9 @@ * https://fetch.spec.whatwg.org/#response-class */ -[Constructor(optional BodyInit? body, optional ResponseInit init), +// This should be Constructor(optional BodyInit... but BodyInit doesn't include +// ReadableStream yet because we don't want to expose the Streams API to Request. +[Constructor(optional (Blob or BufferSource or FormData or URLSearchParams or ReadableStream or USVString) body, optional ResponseInit init), Exposed=(Window,Worker)] interface Response { [NewObject] static Response error(); From 346e8a87d7c61b4d0e22be7116653603d11b6f4e Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 28 Sep 2023 16:54:19 -0500 Subject: [PATCH 14/31] Issue #1442 - Part 12 - Label FetchBody. https://bugzilla.mozilla.org/show_bug.cgi?id=1363318 FetchSignal is not in our tree so this is a partial patch to get mOwner changes. Pre-requisite for Part 13. --- dom/fetch/Fetch.cpp | 11 ++++++++--- dom/fetch/Fetch.h | 8 +++++++- dom/fetch/Request.cpp | 3 +-- dom/fetch/Request.h | 1 - dom/fetch/Response.cpp | 3 +-- dom/fetch/Response.h | 1 - 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index 141e7c3043..99e26e8511 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -877,22 +877,27 @@ ExtractByteStreamFromBody(const fetch::ResponseBodyInit& aBodyInit, template -FetchBody::FetchBody() +FetchBody::FetchBody(nsIGlobalObject* aOwner) : mWorkerPrivate(nullptr) + , mOwner(aOwner) , mReadableStreamBody(nullptr) , mBodyUsed(false) { + MOZ_ASSERT(aOwner); + if (!NS_IsMainThread()) { mWorkerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(mWorkerPrivate); + } else { + mWorkerPrivate = nullptr; } } template -FetchBody::FetchBody(); +FetchBody::FetchBody(nsIGlobalObject* aOwner); template -FetchBody::FetchBody(); +FetchBody::FetchBody(nsIGlobalObject* aOwner); template FetchBody::~FetchBody() diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index d3a7ac598e..b16dac3bf1 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -20,6 +20,7 @@ #include "mozilla/dom/RequestBinding.h" class nsIGlobalObject; +class nsIEventTarget; namespace mozilla { namespace dom { @@ -208,7 +209,9 @@ public: GetSignal() const = 0; protected: - FetchBody(); + nsCOMPtr mOwner; + + explicit FetchBody(nsIGlobalObject* aOwner); // Always set whenever the FetchBody is created on the worker thread. workers::WorkerPrivate* mWorkerPrivate; @@ -252,6 +255,9 @@ private: // Only ever set once, always on target thread. bool mBodyUsed; nsCString mMimeType; + + // The main-thread event target for runnable dispatching. + nsCOMPtr mMainThreadEventTarget; }; } // namespace dom diff --git a/dom/fetch/Request.cpp b/dom/fetch/Request.cpp index 03df3021c5..eec4828131 100644 --- a/dom/fetch/Request.cpp +++ b/dom/fetch/Request.cpp @@ -54,8 +54,7 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Request) NS_INTERFACE_MAP_END Request::Request(nsIGlobalObject* aOwner, InternalRequest* aRequest, AbortSignal* aSignal) - : FetchBody() - , mOwner(aOwner) + : FetchBody(aOwner) , mRequest(aRequest) , mSignal(aSignal) { diff --git a/dom/fetch/Request.h b/dom/fetch/Request.h index 8a22f1340b..d5fc09fe25 100644 --- a/dom/fetch/Request.h +++ b/dom/fetch/Request.h @@ -164,7 +164,6 @@ public: private: ~Request(); - nsCOMPtr mOwner; RefPtr mRequest; // Lazily created. diff --git a/dom/fetch/Response.cpp b/dom/fetch/Response.cpp index d69a199076..b92e918e7c 100644 --- a/dom/fetch/Response.cpp +++ b/dom/fetch/Response.cpp @@ -57,8 +57,7 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Response) NS_INTERFACE_MAP_END Response::Response(nsIGlobalObject* aGlobal, InternalResponse* aInternalResponse, AbortSignal* aSignal) - : FetchBody() - , mOwner(aGlobal) + : FetchBody(aGlobal) , mInternalResponse(aInternalResponse) , mSignal(aSignal) { diff --git a/dom/fetch/Response.h b/dom/fetch/Response.h index 9b29dff998..d2f885fbc7 100644 --- a/dom/fetch/Response.h +++ b/dom/fetch/Response.h @@ -144,7 +144,6 @@ public: private: ~Response(); - nsCOMPtr mOwner; RefPtr mInternalResponse; // Lazily created From 41216b88f6cb0ecd7681eac19be1d34f8169b17b Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 28 Sep 2023 17:34:54 -0500 Subject: [PATCH 15/31] Issue #1442 - Part 12a - Allow idle worker shutdown to begin for an opt-in WorkerHolder mode. https://bugzilla.mozilla.org/show_bug.cgi?id=1362444 Pre-requisite for Part 13. --- dom/workers/WorkerHolder.cpp | 9 ++++++++- dom/workers/WorkerHolder.h | 11 ++++++++++- dom/workers/WorkerPrivate.cpp | 15 +++++++++++---- dom/workers/WorkerPrivate.h | 1 + 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/dom/workers/WorkerHolder.cpp b/dom/workers/WorkerHolder.cpp index 5a8c5c4d8f..dcdcd29fa3 100644 --- a/dom/workers/WorkerHolder.cpp +++ b/dom/workers/WorkerHolder.cpp @@ -8,8 +8,9 @@ BEGIN_WORKERS_NAMESPACE -WorkerHolder::WorkerHolder() +WorkerHolder::WorkerHolder(Behavior aBehavior) : mWorkerPrivate(nullptr) + , mBehavior(aBehavior) { } @@ -44,6 +45,12 @@ WorkerHolder::ReleaseWorker() ReleaseWorkerInternal(); } +WorkerHolder::Behavior +WorkerHolder::GetBehavior() const +{ + return mBehavior; +} + void WorkerHolder::ReleaseWorkerInternal() { diff --git a/dom/workers/WorkerHolder.h b/dom/workers/WorkerHolder.h index 050c6f8e20..d8f3927af7 100644 --- a/dom/workers/WorkerHolder.h +++ b/dom/workers/WorkerHolder.h @@ -73,7 +73,12 @@ class WorkerHolder public: NS_DECL_OWNINGTHREAD - WorkerHolder(); + enum Behavior { + AllowIdleShutdownStart, + PreventIdleShutdownStart, + }; + + explicit WorkerHolder(Behavior aBehavior = PreventIdleShutdownStart); virtual ~WorkerHolder(); bool HoldWorker(WorkerPrivate* aWorkerPrivate, Status aFailStatus); @@ -81,6 +86,8 @@ public: virtual bool Notify(Status aStatus) = 0; + Behavior GetBehavior() const; + protected: void ReleaseWorkerInternal(); @@ -88,6 +95,8 @@ protected: private: void AssertIsOwningThread() const; + + const Behavior mBehavior; }; END_WORKERS_NAMESPACE diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp index 622a882a65..3b3de7e3b5 100644 --- a/dom/workers/WorkerPrivate.cpp +++ b/dom/workers/WorkerPrivate.cpp @@ -4154,6 +4154,7 @@ WorkerPrivate::WorkerPrivate(WorkerPrivate* aParent, , mDebugger(nullptr) , mJSContext(nullptr) , mPRThread(nullptr) + , mNumHoldersPreventingShutdownStart(0) , mDebuggerEventLoopLevel(0) , mMainThreadEventTarget(do_GetMainThread()) , mErrorHandlerRecursionCount(0) @@ -5382,8 +5383,11 @@ WorkerPrivate::AddHolder(WorkerHolder* aHolder, Status aFailStatus) MOZ_ASSERT(!mHolders.Contains(aHolder), "Already know about this one!"); - if (mHolders.IsEmpty() && !ModifyBusyCountFromWorker(true)) { - return false; + if (aHolder->GetBehavior() == WorkerHolder::PreventIdleShutdownStart) { + if (!mNumHoldersPreventingShutdownStart && !ModifyBusyCountFromWorker(true)) { + return false; + } + mNumHoldersPreventingShutdownStart += 1; } mHolders.AppendElement(aHolder); @@ -5398,8 +5402,11 @@ WorkerPrivate::RemoveHolder(WorkerHolder* aHolder) MOZ_ASSERT(mHolders.Contains(aHolder), "Didn't know about this one!"); mHolders.RemoveElement(aHolder); - if (mHolders.IsEmpty() && !ModifyBusyCountFromWorker(false)) { - NS_WARNING("Failed to modify busy count!"); + if (aHolder->GetBehavior() == WorkerHolder::PreventIdleShutdownStart) { + mNumHoldersPreventingShutdownStart -= 1; + if (!mNumHoldersPreventingShutdownStart && !ModifyBusyCountFromWorker(false)) { + NS_WARNING("Failed to modify busy count!"); + } } } diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h index 9effdccc9a..26afecb69b 100644 --- a/dom/workers/WorkerPrivate.h +++ b/dom/workers/WorkerPrivate.h @@ -980,6 +980,7 @@ class WorkerPrivate : public WorkerPrivateParent RefPtr mDebuggerScope; nsTArray mChildWorkers; nsTObserverArray mHolders; + uint32_t mNumHoldersPreventingShutdownStart; nsTArray> mTimeouts; uint32_t mDebuggerEventLoopLevel; RefPtr mMainThreadThrottledEventQueue; From 0b450a3def17997a82d66370378cf18870553f3d Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 28 Sep 2023 17:35:26 -0500 Subject: [PATCH 16/31] Issue #1442 - Part 12b - Allow idle worker shutdown while Cache/CacheStorage DOM objects exist, but block it during Cache operation. https://bugzilla.mozilla.org/show_bug.cgi?id=1362444 Pre-requisite for Part 13. --- dom/cache/CacheOpChild.cpp | 12 ++++++++++-- dom/cache/CacheStorage.cpp | 3 ++- dom/cache/CacheWorkerHolder.cpp | 31 +++++++++++++++++++++++++++---- dom/cache/CacheWorkerHolder.h | 8 ++++++-- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/dom/cache/CacheOpChild.cpp b/dom/cache/CacheOpChild.cpp index 492c205ffc..9dc8997c26 100644 --- a/dom/cache/CacheOpChild.cpp +++ b/dom/cache/CacheOpChild.cpp @@ -74,7 +74,11 @@ CacheOpChild::CacheOpChild(CacheWorkerHolder* aWorkerHolder, MOZ_DIAGNOSTIC_ASSERT(mPromise); MOZ_ASSERT_IF(!NS_IsMainThread(), aWorkerHolder); - SetWorkerHolder(aWorkerHolder); + RefPtr workerHolder = + CacheWorkerHolder::PreferBehavior(aWorkerHolder, + CacheWorkerHolder::PreventIdleShutdownStart); + + SetWorkerHolder(workerHolder); } CacheOpChild::~CacheOpChild() @@ -165,7 +169,11 @@ CacheOpChild::Recv__delete__(const ErrorResult& aRv, break; } - actor->SetWorkerHolder(GetWorkerHolder()); + RefPtr workerHolder = + CacheWorkerHolder::PreferBehavior(GetWorkerHolder(), + CacheWorkerHolder::AllowIdleShutdownStart); + + actor->SetWorkerHolder(workerHolder); RefPtr cache = new Cache(mGlobal, actor); mPromise->MaybeResolve(cache); break; diff --git a/dom/cache/CacheStorage.cpp b/dom/cache/CacheStorage.cpp index 2dae0b157d..3ace28c156 100644 --- a/dom/cache/CacheStorage.cpp +++ b/dom/cache/CacheStorage.cpp @@ -204,7 +204,8 @@ CacheStorage::CreateOnWorker(Namespace aNamespace, nsIGlobalObject* aGlobal, } RefPtr workerHolder = - CacheWorkerHolder::Create(aWorkerPrivate); + CacheWorkerHolder::Create(aWorkerPrivate, + CacheWorkerHolder::AllowIdleShutdownStart); if (!workerHolder) { NS_WARNING("Worker thread is shutting down."); aRv.Throw(NS_ERROR_FAILURE); diff --git a/dom/cache/CacheWorkerHolder.cpp b/dom/cache/CacheWorkerHolder.cpp index 4ac97cbcab..1fd78553ee 100644 --- a/dom/cache/CacheWorkerHolder.cpp +++ b/dom/cache/CacheWorkerHolder.cpp @@ -18,11 +18,11 @@ using mozilla::dom::workers::WorkerPrivate; // static already_AddRefed -CacheWorkerHolder::Create(WorkerPrivate* aWorkerPrivate) +CacheWorkerHolder::Create(WorkerPrivate* aWorkerPrivate, Behavior aBehavior) { MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate); - RefPtr workerHolder = new CacheWorkerHolder(); + RefPtr workerHolder = new CacheWorkerHolder(aBehavior); if (NS_WARN_IF(!workerHolder->HoldWorker(aWorkerPrivate, Terminating))) { return nullptr; } @@ -30,6 +30,28 @@ CacheWorkerHolder::Create(WorkerPrivate* aWorkerPrivate) return workerHolder.forget(); } +// static +already_AddRefed +CacheWorkerHolder::PreferBehavior(CacheWorkerHolder* aCurrentHolder, + Behavior aBehavior) +{ + if (!aCurrentHolder) { + return nullptr; + } + + RefPtr orig = aCurrentHolder; + if (orig->GetBehavior() == aBehavior) { + return orig.forget(); + } + + RefPtr replace = Create(orig->mWorkerPrivate, aBehavior); + if (!replace) { + return orig.forget(); + } + + return replace.forget(); +} + void CacheWorkerHolder::AddActor(ActorChild* aActor) { @@ -88,8 +110,9 @@ CacheWorkerHolder::Notify(Status aStatus) return true; } -CacheWorkerHolder::CacheWorkerHolder() - : mNotified(false) +CacheWorkerHolder::CacheWorkerHolder(Behavior aBehavior) + : WorkerHolder(aBehavior) + , mNotified(false) { } diff --git a/dom/cache/CacheWorkerHolder.h b/dom/cache/CacheWorkerHolder.h index 9eed7e2b6a..7e4c55f839 100644 --- a/dom/cache/CacheWorkerHolder.h +++ b/dom/cache/CacheWorkerHolder.h @@ -25,7 +25,11 @@ class CacheWorkerHolder final : public workers::WorkerHolder { public: static already_AddRefed - Create(workers::WorkerPrivate* aWorkerPrivate); + Create(workers::WorkerPrivate* aWorkerPrivate, + Behavior aBehavior); + + static already_AddRefed + PreferBehavior(CacheWorkerHolder* aCurrentHolder, Behavior aBehavior); void AddActor(ActorChild* aActor); void RemoveActor(ActorChild* aActor); @@ -36,7 +40,7 @@ public: virtual bool Notify(workers::Status aStatus) override; private: - CacheWorkerHolder(); + explicit CacheWorkerHolder(Behavior aBehavior); ~CacheWorkerHolder(); nsTArray mActorList; From ef550b2579ab27f7f557b351d4ce2593fbac1530 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 28 Sep 2023 17:36:52 -0500 Subject: [PATCH 17/31] Issue #1442 - Part 13 - Implement FetchStreamReader. https://bugzilla.mozilla.org/show_bug.cgi?id=1329298 --- dom/fetch/Fetch.cpp | 100 ++++++++--- dom/fetch/Fetch.h | 10 ++ dom/fetch/FetchStreamReader.cpp | 307 ++++++++++++++++++++++++++++++++ dom/fetch/FetchStreamReader.h | 77 ++++++++ dom/fetch/InternalResponse.cpp | 6 +- dom/fetch/InternalResponse.h | 8 +- dom/fetch/Request.cpp | 3 +- dom/fetch/Response.cpp | 104 +++++++---- dom/fetch/moz.build | 2 + dom/webidl/Fetch.webidl | 12 ++ 10 files changed, 568 insertions(+), 61 deletions(-) create mode 100644 dom/fetch/FetchStreamReader.cpp create mode 100644 dom/fetch/FetchStreamReader.h diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index 99e26e8511..ce21cd391a 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -881,6 +881,7 @@ FetchBody::FetchBody(nsIGlobalObject* aOwner) : mWorkerPrivate(nullptr) , mOwner(aOwner) , mReadableStreamBody(nullptr) + , mReadableStreamReader(nullptr) , mBodyUsed(false) { MOZ_ASSERT(aOwner); @@ -904,6 +905,12 @@ FetchBody::~FetchBody() { } +template +FetchBody::~FetchBody(); + +template +FetchBody::~FetchBody(); + template bool FetchBody::BodyUsed() const @@ -958,13 +965,27 @@ FetchBody::ConsumeBody(JSContext* aCx, FetchConsumeType aType, ErrorRes SetBodyUsed(); - // If we already created a ReadableStreamBody we have to lock it now because - // it may have been shared with other objects.. + // If we already have a ReadableStreamBody and it has been created by DOM, we + // have to lock it now because it can have been shared with other objects. if (mReadableStreamBody) { - JS::Rooted body(aCx, mReadableStreamBody); - LockStream(aCx, body, aRv); - if (NS_WARN_IF(aRv.Failed())) { - return nullptr; + JS::Rooted readableStreamObj(aCx, mReadableStreamBody); + if (JS::ReadableStreamGetMode(readableStreamObj) == + JS::ReadableStreamMode::ExternalSource) { + LockStream(aCx, readableStreamObj, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } else { + // If this is not a native ReadableStream, let's activate the + // FetchStreamReader. + MOZ_ASSERT(mFetchStreamReader); + JS::Rooted reader(aCx); + mFetchStreamReader->StartConsuming(aCx, readableStreamObj, &reader, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + mReadableStreamReader = reader; } } @@ -1038,6 +1059,11 @@ FetchBody::GetBody(JSContext* aCx, JS::MutableHandle aBodyOut, ErrorResult& aRv) { + if (mReadableStreamBody) { + aBodyOut.set(mReadableStreamBody); + return; + } + nsCOMPtr inputStream; DerivedClass()->GetBody(getter_AddRefs(inputStream)); @@ -1046,30 +1072,27 @@ FetchBody::GetBody(JSContext* aCx, return; } - if (!mReadableStreamBody) { - JS::Rooted body(aCx, - FetchStream::Create(aCx, - this, - DerivedClass()->GetParentObject(), - inputStream, - aRv)); + JS::Rooted body(aCx, + FetchStream::Create(aCx, + this, + DerivedClass()->GetParentObject(), + inputStream, + aRv)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + MOZ_ASSERT(body); + + // If the body has been already consumed, we lock the stream. + if (BodyUsed()) { + LockStream(aCx, body, aRv); if (NS_WARN_IF(aRv.Failed())) { return; } - - MOZ_ASSERT(body); - - // If the body has been already consumed, we close the stream. - if (BodyUsed()) { - LockStream(aCx, body, aRv); - if (NS_WARN_IF(aRv.Failed())) { - return; - } - } - - mReadableStreamBody = body; } + mReadableStreamBody = body; aBodyOut.set(mReadableStreamBody); } @@ -1091,7 +1114,15 @@ FetchBody::LockStream(JSContext* aCx, JS::HandleObject aStream, ErrorResult& aRv) { - // XXXMC: TODO + JS::Rooted reader(aCx, + JS::ReadableStreamGetReader(aCx, aStream, + JS::ReadableStreamReaderMode::Default)); + if (!reader) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + mReadableStreamReader = reader; } template @@ -1110,10 +1141,18 @@ template void FetchBody::MaybeTeeReadableStreamBody(JSContext* aCx, JS::MutableHandle aBodyOut, + FetchStreamReader** aStreamReader, + nsIInputStream** aInputStream, ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(aStreamReader); + MOZ_DIAGNOSTIC_ASSERT(aInputStream); MOZ_DIAGNOSTIC_ASSERT(!BodyUsed()); + aBodyOut.set(nullptr); + *aStreamReader = nullptr; + *aInputStream = nullptr; + if (!mReadableStreamBody) { return; } @@ -1138,18 +1177,27 @@ FetchBody::MaybeTeeReadableStreamBody(JSContext* aCx, mReadableStreamBody = branch1; aBodyOut.set(branch2); + + aRv = FetchStreamReader::Create(aCx, mOwner, aStreamReader, aInputStream); + if (NS_WARN_IF(aRv.Failed())) { + return; + } } template void FetchBody::MaybeTeeReadableStreamBody(JSContext* aCx, JS::MutableHandle aMessage, + FetchStreamReader** aStreamReader, + nsIInputStream** aInputStream, ErrorResult& aRv); template void FetchBody::MaybeTeeReadableStreamBody(JSContext* aCx, JS::MutableHandle aMessage, + FetchStreamReader** aStreamReader, + nsIInputStream** aInputStream, ErrorResult& aRv); } // namespace dom diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index b16dac3bf1..d65b66b883 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -17,6 +17,7 @@ #include "mozilla/DebugOnly.h" #include "mozilla/ErrorResult.h" #include "mozilla/dom/Promise.h" +#include "mozilla/dom/FetchStreamReader.h" #include "mozilla/dom/RequestBinding.h" class nsIGlobalObject; @@ -183,6 +184,8 @@ public: void MaybeTeeReadableStreamBody(JSContext* aCx, JS::MutableHandle aBodyOut, + FetchStreamReader** aStreamReader, + nsIInputStream** aInputStream, ErrorResult& aRv); // Utility public methods accessed by various runnables. @@ -199,10 +202,13 @@ public: return mMimeType; } + // FetchStreamHolder void NullifyStream() override { mReadableStreamBody = nullptr; + mReadableStreamReader = nullptr; + mFetchStreamReader = nullptr; } virtual AbortSignal* @@ -219,6 +225,10 @@ protected: // This is the ReadableStream exposed to content. Its underlying source is a FetchStream object. JS::Heap mReadableStreamBody; + // This is the Reader used to retrieve data from the body. + JS::Heap mReadableStreamReader; + RefPtr mFetchStreamReader; + virtual ~FetchBody(); void diff --git a/dom/fetch/FetchStreamReader.cpp b/dom/fetch/FetchStreamReader.cpp new file mode 100644 index 0000000000..98d2588a0e --- /dev/null +++ b/dom/fetch/FetchStreamReader.cpp @@ -0,0 +1,307 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FetchStreamReader.h" +#include "InternalResponse.h" +#include "mozilla/dom/PromiseBinding.h" + +namespace mozilla { +namespace dom { + +using namespace workers; + +namespace { + +class FetchStreamReaderWorkerHolder final : public WorkerHolder +{ +public: + explicit FetchStreamReaderWorkerHolder(FetchStreamReader* aReader) + : WorkerHolder(WorkerHolder::Behavior::AllowIdleShutdownStart) + , mReader(aReader) + , mWasNotified(false) + {} + + bool Notify(Status aStatus) override + { + if (!mWasNotified) { + mWasNotified = true; + mReader->CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + } + + return true; + } + +private: + RefPtr mReader; + bool mWasNotified; +}; + +} // anonymous + +NS_IMPL_ISUPPORTS(FetchStreamReader, nsIOutputStreamCallback) + +/* static */ nsresult +FetchStreamReader::Create(JSContext* aCx, nsIGlobalObject* aGlobal, + FetchStreamReader** aStreamReader, + nsIInputStream** aInputStream) +{ + MOZ_ASSERT(aCx); + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(aStreamReader); + MOZ_ASSERT(aInputStream); + + RefPtr streamReader = new FetchStreamReader(aGlobal); + + nsCOMPtr pipeIn; + + nsresult rv = NS_NewPipe2(getter_AddRefs(pipeIn), + getter_AddRefs(streamReader->mPipeOut), + true, true, 0, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + MOZ_ASSERT(workerPrivate); + + // We need to know when the worker goes away. + UniquePtr holder( + new FetchStreamReaderWorkerHolder(streamReader)); + if (NS_WARN_IF(!holder->HoldWorker(workerPrivate, Closing))) { + streamReader->mPipeOut->CloseWithStatus(NS_ERROR_DOM_INVALID_STATE_ERR); + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + // These 2 objects create a ref-cycle here that is broken when the stream is + // closed or the worker shutsdown. + streamReader->mWorkerHolder = Move(holder); + } + + pipeIn.forget(aInputStream); + streamReader.forget(aStreamReader); + return NS_OK; +} + +FetchStreamReader::FetchStreamReader(nsIGlobalObject* aGlobal) + : mGlobal(aGlobal) + , mBufferRemaining(0) + , mBufferOffset(0) + , mStreamClosed(false) +{ + MOZ_ASSERT(aGlobal); +} + +FetchStreamReader::~FetchStreamReader() +{ + CloseAndRelease(NS_BASE_STREAM_CLOSED); +} + +void +FetchStreamReader::CloseAndRelease(nsresult aStatus) +{ + NS_ASSERT_OWNINGTHREAD(FetchStreamReader); + + if (mStreamClosed) { + // Already closed. + return; + } + + RefPtr kungFuDeathGrip = this; + + mStreamClosed = true; + + mGlobal = nullptr; + + mPipeOut->CloseWithStatus(aStatus); + mPipeOut = nullptr; + + mWorkerHolder = nullptr; + + mReader = nullptr; + mBuffer = nullptr; +} + +void +FetchStreamReader::StartConsuming(JSContext* aCx, + JS::HandleObject aStream, + JS::MutableHandle aReader, + ErrorResult& aRv) +{ + MOZ_DIAGNOSTIC_ASSERT(!mReader); + MOZ_DIAGNOSTIC_ASSERT(aStream); + + JS::Rooted reader(aCx, + JS::ReadableStreamGetReader(aCx, aStream, + JS::ReadableStreamReaderMode::Default)); + if (!reader) { + aRv.StealExceptionFromJSContext(aCx); + CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + mReader = reader; + aReader.set(reader); + + aRv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +// nsIOutputStreamCallback interface + +NS_IMETHODIMP +FetchStreamReader::OnOutputStreamReady(nsIAsyncOutputStream* aStream) +{ + NS_ASSERT_OWNINGTHREAD(FetchStreamReader); + MOZ_ASSERT(aStream == mPipeOut); + MOZ_ASSERT(mReader); + + if (mStreamClosed) { + return NS_OK; + } + + if (mBuffer) { + return WriteBuffer(); + } + + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(mGlobal))) { + CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + + JS::Rooted reader(cx, mReader); + JS::Rooted promise(cx, + JS::ReadableStreamDefaultReaderRead(cx, + reader)); + if (NS_WARN_IF(!promise)) { + // Let's close the stream. + CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + return NS_ERROR_FAILURE; + } + + RefPtr domPromise = Promise::CreateFromExisting(mGlobal, promise); + if (NS_WARN_IF(!domPromise)) { + // Let's close the stream. + CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + return NS_ERROR_FAILURE; + } + + // Let's wait. + domPromise->AppendNativeHandler(this); + return NS_OK; +} + +void +FetchStreamReader::ResolvedCallback(JSContext* aCx, + JS::Handle aValue) +{ + if (mStreamClosed) { + return; + } + + // This promise should be resolved with { done: boolean, value: something }, + // "value" is interesting only if done is false. + + // We don't want to play with JS api, let's WebIDL bindings doing it for us. + // FetchReadableStreamReadDataDone is a dictionary with just a boolean, if the + // parsing succeeded, we can proceed with the parsing of the "value", which it + // must be a Uint8Array. + FetchReadableStreamReadDataDone valueDone; + if (!valueDone.Init(aCx, aValue)) { + JS_ClearPendingException(aCx); + CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (valueDone.mDone) { + // Stream is completed. + CloseAndRelease(NS_BASE_STREAM_CLOSED); + return; + } + + UniquePtr value( + new FetchReadableStreamReadDataArray); + if (!value->Init(aCx, aValue) || !value->mValue.WasPassed()) { + JS_ClearPendingException(aCx); + CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + Uint8Array& array = value->mValue.Value(); + array.ComputeLengthAndData(); + uint32_t len = array.Length(); + + if (len == 0) { + // If there is nothing to read, let's do another reading. + OnOutputStreamReady(mPipeOut); + return; + } + + MOZ_DIAGNOSTIC_ASSERT(!mBuffer); + mBuffer = Move(value); + + mBufferOffset = 0; + mBufferRemaining = len; + + WriteBuffer(); +} + +nsresult +FetchStreamReader::WriteBuffer() +{ + MOZ_ASSERT(mBuffer); + MOZ_ASSERT(mBuffer->mValue.WasPassed()); + + Uint8Array& array = mBuffer->mValue.Value(); + char* data = reinterpret_cast(array.Data()); + + while (1) { + uint32_t written = 0; + nsresult rv = + mPipeOut->Write(data + mBufferOffset, mBufferRemaining, &written); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + break; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + CloseAndRelease(rv); + return rv; + } + + MOZ_ASSERT(written <= mBufferRemaining); + mBufferRemaining -= written; + mBufferOffset += written; + + if (mBufferRemaining == 0) { + mBuffer = nullptr; + break; + } + } + + nsresult rv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + CloseAndRelease(rv); + return rv; + } + + return NS_OK; +} + +void +FetchStreamReader::RejectedCallback(JSContext* aCx, + JS::Handle aValue) +{ + CloseAndRelease(NS_ERROR_FAILURE); +} + +} // dom namespace +} // mozilla namespace diff --git a/dom/fetch/FetchStreamReader.h b/dom/fetch/FetchStreamReader.h new file mode 100644 index 0000000000..8984a2ea73 --- /dev/null +++ b/dom/fetch/FetchStreamReader.h @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FetchStreamReader_h +#define mozilla_dom_FetchStreamReader_h + +#include "jsapi.h" +#include "mozilla/dom/FetchBinding.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "nsIAsyncOutputStream.h" + +namespace mozilla { +namespace dom { + +namespace workers { +class WorkerHolder; +} + +class FetchStreamReader final : public nsIOutputStreamCallback + , public PromiseNativeHandler +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAMCALLBACK + + // This creates a nsIInputStream able to retrieve data from the ReadableStream + // object. The reading starts when StartConsuming() is called. + static nsresult + Create(JSContext* aCx, nsIGlobalObject* aGlobal, + FetchStreamReader** aStreamReader, + nsIInputStream** aInputStream); + + void + ResolvedCallback(JSContext* aCx, JS::Handle aValue) override; + + void + RejectedCallback(JSContext* aCx, JS::Handle aValue) override; + + void + CloseAndRelease(nsresult aStatus); + + void + StartConsuming(JSContext* aCx, + JS::HandleObject aStream, + JS::MutableHandle aReader, + ErrorResult& aRv); + +private: + explicit FetchStreamReader(nsIGlobalObject* aGlobal); + ~FetchStreamReader(); + + nsresult + WriteBuffer(); + + nsCOMPtr mGlobal; + nsCOMPtr mOwningEventTarget; + + nsCOMPtr mPipeOut; + + UniquePtr mWorkerHolder; + + JS::Heap mReader; + + UniquePtr mBuffer; + uint32_t mBufferRemaining; + uint32_t mBufferOffset; + + bool mStreamClosed; +}; + +} // dom namespace +} // mozilla namespace + +#endif // mozilla_dom_FetchStreamReader_h diff --git a/dom/fetch/InternalResponse.cpp b/dom/fetch/InternalResponse.cpp index ec32c4e216..f4fd1912f9 100644 --- a/dom/fetch/InternalResponse.cpp +++ b/dom/fetch/InternalResponse.cpp @@ -139,18 +139,18 @@ InternalResponse::ToIPC(IPCInternalResponse* aIPCResponse, } already_AddRefed -InternalResponse::Clone() +InternalResponse::Clone(CloneType aCloneType) { RefPtr clone = CreateIncompleteCopy(); clone->mHeaders = new InternalHeaders(*mHeaders); if (mWrappedResponse) { - clone->mWrappedResponse = mWrappedResponse->Clone(); + clone->mWrappedResponse = mWrappedResponse->Clone(aCloneType); MOZ_ASSERT(!mBody); return clone.forget(); } - if (!mBody) { + if (!mBody || aCloneType == eDontCloneInputStream) { return clone.forget(); } diff --git a/dom/fetch/InternalResponse.h b/dom/fetch/InternalResponse.h index e4b4a0ab62..d5c759d577 100644 --- a/dom/fetch/InternalResponse.h +++ b/dom/fetch/InternalResponse.h @@ -46,7 +46,13 @@ public: M* aManager, UniquePtr& aAutoStream); - already_AddRefed Clone(); + enum CloneType + { + eCloneInputStream, + eDontCloneInputStream, + }; + + already_AddRefed Clone(CloneType eCloneType); static already_AddRefed NetworkError() diff --git a/dom/fetch/Request.cpp b/dom/fetch/Request.cpp index eec4828131..ec140a22d4 100644 --- a/dom/fetch/Request.cpp +++ b/dom/fetch/Request.cpp @@ -44,7 +44,8 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Request) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Request) - NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamBody) + MOZ_DIAGNOSTIC_ASSERT(!tmp->mReadableStreamReader); + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamReader) NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_TRACE_END diff --git a/dom/fetch/Response.cpp b/dom/fetch/Response.cpp index b92e918e7c..5055db4e24 100644 --- a/dom/fetch/Response.cpp +++ b/dom/fetch/Response.cpp @@ -21,6 +21,7 @@ #include "BodyExtractor.h" #include "FetchStream.h" +#include "FetchStreamReader.h" #include "InternalResponse.h" #include "WorkerPrivate.h" @@ -37,6 +38,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Response) NS_IMPL_CYCLE_COLLECTION_UNLINK(mHeaders) tmp->mReadableStreamBody = nullptr; + tmp->mReadableStreamReader = nullptr; NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_UNLINK_END @@ -48,6 +50,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Response) NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamBody) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamReader) NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_TRACE_END @@ -227,7 +230,7 @@ Response::Constructor(const GlobalObject& aGlobal, nsCString contentTypeWithCharset; nsCOMPtr bodyStream; - uint64_t bodySize = 0; + int64_t bodySize = InternalResponse::UNKNOWN_BODY_SIZE; if (aBody.Value().IsReadableStream()) { const ReadableStream& readableStream = @@ -245,37 +248,50 @@ Response::Constructor(const GlobalObject& aGlobal, r->SetReadableStreamBody(readableStreamObj); - // XXXMC: TODO - MOZ_ASSERT(JS::ReadableStreamGetMode(readableStreamObj) != - JS::ReadableStreamMode::ExternalSource); + if (JS::ReadableStreamGetMode(readableStreamObj) == + JS::ReadableStreamMode::ExternalSource) { + // If this is a DOM generated ReadableStream, we can extract the + // inputStream directly. + void* underlyingSource = nullptr; + if (!JS::ReadableStreamGetExternalUnderlyingSource(aGlobal.Context(), + readableStreamObj, + &underlyingSource)) { + aRv.StealExceptionFromJSContext(aGlobal.Context()); + return nullptr; + } - void* underlyingSource = nullptr; - if (!JS::ReadableStreamGetExternalUnderlyingSource(aGlobal.Context(), - readableStreamObj, - &underlyingSource)) { - aRv.StealExceptionFromJSContext(aGlobal.Context()); - return nullptr; + MOZ_ASSERT(underlyingSource); + + aRv = FetchStream::RetrieveInputStream(underlyingSource, + getter_AddRefs(bodyStream)); + + // The releasing of the external source is needed in order to avoid an + // extra stream lock. + JS::ReadableStreamReleaseExternalUnderlyingSource(readableStreamObj); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } else { + // If this is a JS-created ReadableStream, let's create a + // FetchStreamReader. + aRv = FetchStreamReader::Create(aGlobal.Context(), global, + getter_AddRefs(r->mFetchStreamReader), + getter_AddRefs(bodyStream)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } } - - bodySize = InternalResponse::UNKNOWN_BODY_SIZE; - - MOZ_ASSERT(underlyingSource); - aRv = FetchStream::RetrieveInputStream(underlyingSource, - getter_AddRefs(bodyStream)); - - // Releasing of the external source is needed in order to avoid an extra stream lock. - JS::ReadableStreamReleaseExternalUnderlyingSource(readableStreamObj); - if (NS_WARN_IF(aRv.Failed())) { - return nullptr; - } } else { + uint64_t size = 0; aRv = ExtractByteStreamFromBody(aBody.Value(), getter_AddRefs(bodyStream), contentTypeWithCharset, - bodySize); + size); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } + + bodySize = size; } internalResponse->SetBody(bodyStream, bodySize); @@ -306,21 +322,35 @@ Response::Clone(JSContext* aCx, ErrorResult& aRv) return nullptr; } - RefPtr ir = mInternalResponse->Clone(); - RefPtr response = new Response(mOwner, ir, mSignal); + RefPtr streamReader; + nsCOMPtr inputStream; JS::Rooted body(aCx); - MaybeTeeReadableStreamBody(aCx, &body, aRv); + MaybeTeeReadableStreamBody(aCx, &body, + getter_AddRefs(streamReader), + getter_AddRefs(inputStream), aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } + MOZ_ASSERT_IF(body, streamReader); + MOZ_ASSERT_IF(body, inputStream); + + RefPtr ir = + mInternalResponse->Clone(body + ? InternalResponse::eDontCloneInputStream + : InternalResponse::eCloneInputStream); + + RefPtr response = new Response(mOwner, ir, nullptr); + if (body) { // Maybe we have a body, but we receive null from MaybeTeeReadableStreamBody // if this body is a native stream. In this case, the InternalResponse will // have a clone of the native body and the ReadableStream will be created // lazily if needed. response->SetReadableStreamBody(body); + response->mFetchStreamReader = streamReader; + ir->SetBody(inputStream, InternalResponse::UNKNOWN_BODY_SIZE); } return response.forget(); @@ -334,22 +364,36 @@ Response::CloneUnfiltered(JSContext* aCx, ErrorResult& aRv) return nullptr; } - RefPtr clone = mInternalResponse->Clone(); - RefPtr ir = clone->Unfiltered(); - RefPtr ref = new Response(mOwner, ir, mSignal); + RefPtr streamReader; + nsCOMPtr inputStream; JS::Rooted body(aCx); - MaybeTeeReadableStreamBody(aCx, &body, aRv); + MaybeTeeReadableStreamBody(aCx, &body, + getter_AddRefs(streamReader), + getter_AddRefs(inputStream), aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } + MOZ_ASSERT_IF(body, streamReader); + MOZ_ASSERT_IF(body, inputStream); + + RefPtr clone = + mInternalResponse->Clone(body + ? InternalResponse::eDontCloneInputStream + : InternalResponse::eCloneInputStream); + + RefPtr ir = clone->Unfiltered(); + RefPtr ref = new Response(mOwner, ir, nullptr); + if (body) { // Maybe we have a body, but we receive null from MaybeTeeReadableStreamBody // if this body is a native stream. In this case, the InternalResponse will // have a clone of the native body and the ReadableStream will be created // lazily if needed. ref->SetReadableStreamBody(body); + ref->mFetchStreamReader = streamReader; + ir->SetBody(inputStream, InternalResponse::UNKNOWN_BODY_SIZE); } return ref.forget(); diff --git a/dom/fetch/moz.build b/dom/fetch/moz.build index d08bdf0cf2..f01254e042 100644 --- a/dom/fetch/moz.build +++ b/dom/fetch/moz.build @@ -10,6 +10,7 @@ EXPORTS.mozilla.dom += [ 'FetchDriver.h', 'FetchIPCTypes.h', 'FetchObserver.h', + 'FetchStreamReader.h', 'FetchUtil.h', 'Headers.h', 'InternalHeaders.h', @@ -27,6 +28,7 @@ UNIFIED_SOURCES += [ 'FetchDriver.cpp', 'FetchObserver.cpp', 'FetchStream.cpp', + 'FetchStreamReader.cpp', 'FetchUtil.cpp', 'Headers.cpp', 'InternalHeaders.cpp', diff --git a/dom/webidl/Fetch.webidl b/dom/webidl/Fetch.webidl index 137cbde354..bbb1faf7f7 100644 --- a/dom/webidl/Fetch.webidl +++ b/dom/webidl/Fetch.webidl @@ -24,3 +24,15 @@ interface Body { [Throws] Promise text(); }; + +// These are helper dictionaries for the parsing of a +// getReader().read().then(data) parsing. +// See more about how these 2 helpers are used in +// dom/fetch/FetchStreamReader.cpp +dictionary FetchReadableStreamReadDataDone { + boolean done = false; +}; + +dictionary FetchReadableStreamReadDataArray { + Uint8Array value; +}; From 842b9d47b35a2b00bf821bb1228f9601e4cbbd8b Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 28 Sep 2023 19:35:59 -0500 Subject: [PATCH 18/31] Issue #1442 - Part 14 - Starting body consuming and passing the JSContext down from the binding entrypoints to where the ReadableStream could be read. https://bugzilla.mozilla.org/show_bug.cgi?id=1128959 --- dom/bindings/Bindings.conf | 4 +- dom/cache/AutoUtils.cpp | 8 +-- dom/cache/AutoUtils.h | 2 +- dom/cache/Cache.cpp | 40 +++++++------ dom/cache/Cache.h | 16 +++--- dom/cache/CacheStorage.cpp | 6 +- dom/cache/CacheStorage.h | 6 +- dom/cache/TypeUtils.cpp | 25 +++++--- dom/cache/TypeUtils.h | 12 ++-- dom/fetch/Fetch.cpp | 73 ++++++++++++++++-------- dom/fetch/Fetch.h | 25 ++++++-- dom/fetch/Request.cpp | 5 +- dom/workers/ScriptLoader.cpp | 14 ++++- dom/workers/ServiceWorkerEvents.cpp | 45 ++++++++++++++- dom/workers/ServiceWorkerScriptCache.cpp | 10 ++-- 15 files changed, 203 insertions(+), 88 deletions(-) diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index 0907a0faf7..fcd84e28b6 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -116,11 +116,13 @@ DOMInterfaces = { }, 'Cache': { - 'implicitJSContext': [ 'add', 'addAll' ], + 'implicitJSContext': [ 'add', 'addAll', 'match', 'matchAll', 'put', + 'delete', 'keys' ], 'nativeType': 'mozilla::dom::cache::Cache', }, 'CacheStorage': { + 'implicitJSContext': [ 'match' ], 'nativeType': 'mozilla::dom::cache::CacheStorage', }, diff --git a/dom/cache/AutoUtils.cpp b/dom/cache/AutoUtils.cpp index d1f354336e..bfa4cffd23 100644 --- a/dom/cache/AutoUtils.cpp +++ b/dom/cache/AutoUtils.cpp @@ -289,9 +289,9 @@ MatchInPutList(InternalRequest* aRequest, } // namespace void -AutoChildOpArgs::Add(InternalRequest* aRequest, BodyAction aBodyAction, - SchemeAction aSchemeAction, Response& aResponse, - ErrorResult& aRv) +AutoChildOpArgs::Add(JSContext* aCx, InternalRequest* aRequest, + BodyAction aBodyAction, SchemeAction aSchemeAction, + Response& aResponse, ErrorResult& aRv) { MOZ_DIAGNOSTIC_ASSERT(!mSent); @@ -329,7 +329,7 @@ AutoChildOpArgs::Add(InternalRequest* aRequest, BodyAction aBodyAction, mTypeUtils->ToCacheRequest(pair.request(), aRequest, aBodyAction, aSchemeAction, mStreamCleanupList, aRv); if (!aRv.Failed()) { - mTypeUtils->ToCacheResponse(pair.response(), aResponse, + mTypeUtils->ToCacheResponse(aCx, pair.response(), aResponse, mStreamCleanupList, aRv); } diff --git a/dom/cache/AutoUtils.h b/dom/cache/AutoUtils.h index 244639f7c0..188c9d74e3 100644 --- a/dom/cache/AutoUtils.h +++ b/dom/cache/AutoUtils.h @@ -55,7 +55,7 @@ public: void Add(InternalRequest* aRequest, BodyAction aBodyAction, SchemeAction aSchemeAction, ErrorResult& aRv); - void Add(InternalRequest* aRequest, BodyAction aBodyAction, + void Add(JSContext* aCx, InternalRequest* aRequest, BodyAction aBodyAction, SchemeAction aSchemeAction, Response& aResponse, ErrorResult& aRv); const CacheOpArgs& SendAsOpArgs(); diff --git a/dom/cache/Cache.cpp b/dom/cache/Cache.cpp index 60e4f76b92..dd142881ec 100644 --- a/dom/cache/Cache.cpp +++ b/dom/cache/Cache.cpp @@ -184,7 +184,10 @@ public: // Now store the unwrapped Response list in the Cache. ErrorResult result; - RefPtr put = mCache->PutAll(mRequestList, responseList, result); + // TODO: Here we use the JSContext as received by the ResolvedCallback, and + // its state could be the wrong one. The spec doesn't say anything + // about it, yet (bug 1384006) + RefPtr put = mCache->PutAll(aCx, mRequestList, responseList, result); if (NS_WARN_IF(result.Failed())) { // TODO: abort the fetch requests we have running (bug 1157434) mPromise->MaybeReject(result); @@ -245,7 +248,7 @@ Cache::Cache(nsIGlobalObject* aGlobal, CacheChild* aActor) } already_AddRefed -Cache::Match(const RequestOrUSVString& aRequest, +Cache::Match(JSContext* aCx, const RequestOrUSVString& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { @@ -255,7 +258,8 @@ Cache::Match(const RequestOrUSVString& aRequest, CacheChild::AutoLock actorLock(mActor); - RefPtr ir = ToInternalRequest(aRequest, IgnoreBody, aRv); + RefPtr ir = + ToInternalRequest(aCx, aRequest, IgnoreBody, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } @@ -274,7 +278,7 @@ Cache::Match(const RequestOrUSVString& aRequest, } already_AddRefed -Cache::MatchAll(const Optional& aRequest, +Cache::MatchAll(JSContext* aCx, const Optional& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { @@ -290,8 +294,8 @@ Cache::MatchAll(const Optional& aRequest, AutoChildOpArgs args(this, CacheMatchAllArgs(void_t(), params), 1); if (aRequest.WasPassed()) { - RefPtr ir = ToInternalRequest(aRequest.Value(), - IgnoreBody, aRv); + RefPtr ir = ToInternalRequest(aCx, aRequest.Value(), + IgnoreBody, aRv); if (aRv.Failed()) { return nullptr; } @@ -390,8 +394,8 @@ Cache::AddAll(JSContext* aContext, } already_AddRefed -Cache::Put(const RequestOrUSVString& aRequest, Response& aResponse, - ErrorResult& aRv) +Cache::Put(JSContext* aCx, const RequestOrUSVString& aRequest, + Response& aResponse, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { aRv.Throw(NS_ERROR_UNEXPECTED); @@ -404,14 +408,14 @@ Cache::Put(const RequestOrUSVString& aRequest, Response& aResponse, return nullptr; } - RefPtr ir = ToInternalRequest(aRequest, ReadBody, aRv); + RefPtr ir = ToInternalRequest(aCx, aRequest, ReadBody, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } AutoChildOpArgs args(this, CachePutAllArgs(), 1); - args.Add(ir, ReadBody, TypeErrorOnInvalidScheme, + args.Add(aCx, ir, ReadBody, TypeErrorOnInvalidScheme, aResponse, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; @@ -421,7 +425,7 @@ Cache::Put(const RequestOrUSVString& aRequest, Response& aResponse, } already_AddRefed -Cache::Delete(const RequestOrUSVString& aRequest, +Cache::Delete(JSContext* aCx, const RequestOrUSVString& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { @@ -431,7 +435,8 @@ Cache::Delete(const RequestOrUSVString& aRequest, CacheChild::AutoLock actorLock(mActor); - RefPtr ir = ToInternalRequest(aRequest, IgnoreBody, aRv); + RefPtr ir = + ToInternalRequest(aCx, aRequest, IgnoreBody, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } @@ -450,7 +455,7 @@ Cache::Delete(const RequestOrUSVString& aRequest, } already_AddRefed -Cache::Keys(const Optional& aRequest, +Cache::Keys(JSContext* aCx, const Optional& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { @@ -466,8 +471,8 @@ Cache::Keys(const Optional& aRequest, AutoChildOpArgs args(this, CacheKeysArgs(void_t(), params), 1); if (aRequest.WasPassed()) { - RefPtr ir = ToInternalRequest(aRequest.Value(), - IgnoreBody, aRv); + RefPtr ir = + ToInternalRequest(aCx, aRequest.Value(), IgnoreBody, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } @@ -627,7 +632,7 @@ Cache::AddAll(const GlobalObject& aGlobal, } already_AddRefed -Cache::PutAll(const nsTArray>& aRequestList, +Cache::PutAll(JSContext* aCx, const nsTArray>& aRequestList, const nsTArray>& aResponseList, ErrorResult& aRv) { @@ -644,7 +649,8 @@ Cache::PutAll(const nsTArray>& aRequestList, for (uint32_t i = 0; i < aRequestList.Length(); ++i) { RefPtr ir = aRequestList[i]->GetInternalRequest(); - args.Add(ir, ReadBody, TypeErrorOnInvalidScheme, *aResponseList[i], aRv); + args.Add(aCx, ir, ReadBody, TypeErrorOnInvalidScheme, *aResponseList[i], + aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } diff --git a/dom/cache/Cache.h b/dom/cache/Cache.h index 04f891dca5..6fea3d36a0 100644 --- a/dom/cache/Cache.h +++ b/dom/cache/Cache.h @@ -43,10 +43,10 @@ public: // webidl interface methods already_AddRefed - Match(const RequestOrUSVString& aRequest, const CacheQueryOptions& aOptions, - ErrorResult& aRv); + Match(JSContext* aCx, const RequestOrUSVString& aRequest, + const CacheQueryOptions& aOptions, ErrorResult& aRv); already_AddRefed - MatchAll(const Optional& aRequest, + MatchAll(JSContext* aCx, const Optional& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv); already_AddRefed Add(JSContext* aContext, const RequestOrUSVString& aRequest, @@ -55,13 +55,13 @@ public: AddAll(JSContext* aContext, const Sequence& aRequests, ErrorResult& aRv); already_AddRefed - Put(const RequestOrUSVString& aRequest, Response& aResponse, + Put(JSContext* aCx, const RequestOrUSVString& aRequest, Response& aResponse, ErrorResult& aRv); already_AddRefed - Delete(const RequestOrUSVString& aRequest, const CacheQueryOptions& aOptions, - ErrorResult& aRv); + Delete(JSContext* aCx, const RequestOrUSVString& aRequest, + const CacheQueryOptions& aOptions, ErrorResult& aRv); already_AddRefed - Keys(const Optional& aRequest, + Keys(JSContext* aCx, const Optional& aRequest, const CacheQueryOptions& aParams, ErrorResult& aRv); // binding methods @@ -100,7 +100,7 @@ private: ErrorResult& aRv); already_AddRefed - PutAll(const nsTArray>& aRequestList, + PutAll(JSContext* aCx, const nsTArray>& aRequestList, const nsTArray>& aResponseList, ErrorResult& aRv); diff --git a/dom/cache/CacheStorage.cpp b/dom/cache/CacheStorage.cpp index 3ace28c156..05df8ddc69 100644 --- a/dom/cache/CacheStorage.cpp +++ b/dom/cache/CacheStorage.cpp @@ -316,7 +316,7 @@ CacheStorage::CacheStorage(nsresult aFailureResult) } already_AddRefed -CacheStorage::Match(const RequestOrUSVString& aRequest, +CacheStorage::Match(JSContext* aCx, const RequestOrUSVString& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv) { NS_ASSERT_OWNINGTHREAD(CacheStorage); @@ -326,8 +326,8 @@ CacheStorage::Match(const RequestOrUSVString& aRequest, return nullptr; } - RefPtr request = ToInternalRequest(aRequest, IgnoreBody, - aRv); + RefPtr request = + ToInternalRequest(aCx, aRequest, IgnoreBody, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } diff --git a/dom/cache/CacheStorage.h b/dom/cache/CacheStorage.h index 04a2fa0dd9..c1cef4b428 100644 --- a/dom/cache/CacheStorage.h +++ b/dom/cache/CacheStorage.h @@ -59,9 +59,9 @@ public: DefineCaches(JSContext* aCx, JS::Handle aGlobal); // webidl interface methods - already_AddRefed Match(const RequestOrUSVString& aRequest, - const CacheQueryOptions& aOptions, - ErrorResult& aRv); + already_AddRefed + Match(JSContext* aCx, const RequestOrUSVString& aRequest, + const CacheQueryOptions& aOptions, ErrorResult& aRv); already_AddRefed Has(const nsAString& aKey, ErrorResult& aRv); already_AddRefed Open(const nsAString& aKey, ErrorResult& aRv); already_AddRefed Delete(const nsAString& aKey, ErrorResult& aRv); diff --git a/dom/cache/TypeUtils.cpp b/dom/cache/TypeUtils.cpp index f849f18874..1af5ee9458 100644 --- a/dom/cache/TypeUtils.cpp +++ b/dom/cache/TypeUtils.cpp @@ -79,7 +79,7 @@ ToHeadersEntryList(nsTArray& aOut, InternalHeaders* aHeaders) } // namespace already_AddRefed -TypeUtils::ToInternalRequest(const RequestOrUSVString& aIn, +TypeUtils::ToInternalRequest(JSContext* aCx, const RequestOrUSVString& aIn, BodyAction aBodyAction, ErrorResult& aRv) { if (aIn.IsRequest()) { @@ -87,7 +87,7 @@ TypeUtils::ToInternalRequest(const RequestOrUSVString& aIn, // Check and set bodyUsed flag immediately because its on Request // instead of InternalRequest. - CheckAndSetBodyUsed(&request, aBodyAction, aRv); + CheckAndSetBodyUsed(aCx, &request, aBodyAction, aRv); if (aRv.Failed()) { return nullptr; } return request.GetInternalRequest(); @@ -97,7 +97,8 @@ TypeUtils::ToInternalRequest(const RequestOrUSVString& aIn, } already_AddRefed -TypeUtils::ToInternalRequest(const OwningRequestOrUSVString& aIn, +TypeUtils::ToInternalRequest(JSContext* aCx, + const OwningRequestOrUSVString& aIn, BodyAction aBodyAction, ErrorResult& aRv) { @@ -106,7 +107,7 @@ TypeUtils::ToInternalRequest(const OwningRequestOrUSVString& aIn, // Check and set bodyUsed flag immediately because its on Request // instead of InternalRequest. - CheckAndSetBodyUsed(request, aBodyAction, aRv); + CheckAndSetBodyUsed(aCx, request, aBodyAction, aRv); if (aRv.Failed()) { return nullptr; } return request->GetInternalRequest(); @@ -204,7 +205,7 @@ TypeUtils::ToCacheResponseWithoutBody(CacheResponse& aOut, } void -TypeUtils::ToCacheResponse(CacheResponse& aOut, Response& aIn, +TypeUtils::ToCacheResponse(JSContext* aCx, CacheResponse& aOut, Response& aIn, nsTArray>& aStreamCleanupList, ErrorResult& aRv) { @@ -222,7 +223,10 @@ TypeUtils::ToCacheResponse(CacheResponse& aOut, Response& aIn, nsCOMPtr stream; ir->GetUnfilteredBody(getter_AddRefs(stream)); if (stream) { - aIn.SetBodyUsed(); + aIn.SetBodyUsed(aCx, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } } SerializeCacheStream(stream, &aOut.body(), aStreamCleanupList, aRv); @@ -426,8 +430,8 @@ TypeUtils::ProcessURL(nsACString& aUrl, bool* aSchemeValidOut, } void -TypeUtils::CheckAndSetBodyUsed(Request* aRequest, BodyAction aBodyAction, - ErrorResult& aRv) +TypeUtils::CheckAndSetBodyUsed(JSContext* aCx, Request* aRequest, + BodyAction aBodyAction, ErrorResult& aRv) { MOZ_DIAGNOSTIC_ASSERT(aRequest); @@ -443,7 +447,10 @@ TypeUtils::CheckAndSetBodyUsed(Request* aRequest, BodyAction aBodyAction, nsCOMPtr stream; aRequest->GetBody(getter_AddRefs(stream)); if (stream) { - aRequest->SetBodyUsed(); + aRequest->SetBodyUsed(aCx, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } } } diff --git a/dom/cache/TypeUtils.h b/dom/cache/TypeUtils.h index 274586e3f7..ff5816db3e 100644 --- a/dom/cache/TypeUtils.h +++ b/dom/cache/TypeUtils.h @@ -73,12 +73,12 @@ public: GetIPCManager() = 0; already_AddRefed - ToInternalRequest(const RequestOrUSVString& aIn, BodyAction aBodyAction, - ErrorResult& aRv); + ToInternalRequest(JSContext* aCx, const RequestOrUSVString& aIn, + BodyAction aBodyAction, ErrorResult& aRv); already_AddRefed - ToInternalRequest(const OwningRequestOrUSVString& aIn, BodyAction aBodyAction, - ErrorResult& aRv); + ToInternalRequest(JSContext* aCx, const OwningRequestOrUSVString& aIn, + BodyAction aBodyAction, ErrorResult& aRv); void ToCacheRequest(CacheRequest& aOut, InternalRequest* aIn, @@ -91,7 +91,7 @@ public: ErrorResult& aRv); void - ToCacheResponse(CacheResponse& aOut, Response& aIn, + ToCacheResponse(JSContext* aCx, CacheResponse& aOut, Response& aIn, nsTArray>& aStreamCleanupList, ErrorResult& aRv); @@ -133,7 +133,7 @@ public: private: void - CheckAndSetBodyUsed(Request* aRequest, BodyAction aBodyAction, + CheckAndSetBodyUsed(JSContext* aCx, Request* aRequest, BodyAction aBodyAction, ErrorResult& aRv); already_AddRefed diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index ce21cd391a..53ada9378b 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -948,6 +948,52 @@ template bool FetchBody::BodyUsed() const; +template +void +FetchBody::SetBodyUsed(JSContext* aCx, ErrorResult& aRv) +{ + MOZ_ASSERT(aCx); + MOZ_ASSERT(mOwner->EventTargetFor(TaskCategory::Other)->IsOnCurrentThread()); + + if (mBodyUsed) { + return; + } + + mBodyUsed = true; + + // If we already have a ReadableStreamBody and it has been created by DOM, we + // have to lock it now because it can have been shared with other objects. + if (mReadableStreamBody) { + JS::Rooted readableStreamObj(aCx, mReadableStreamBody); + if (JS::ReadableStreamGetMode(readableStreamObj) == + JS::ReadableStreamMode::ExternalSource) { + LockStream(aCx, readableStreamObj, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } else { + // If this is not a native ReadableStream, let's activate the + // FetchStreamReader. + MOZ_ASSERT(mFetchStreamReader); + JS::Rooted reader(aCx); + mFetchStreamReader->StartConsuming(aCx, readableStreamObj, &reader, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + mReadableStreamReader = reader; + } + } +} + +template +void +FetchBody::SetBodyUsed(JSContext* aCx, ErrorResult& aRv); + +template +void +FetchBody::SetBodyUsed(JSContext* aCx, ErrorResult& aRv); + template already_AddRefed FetchBody::ConsumeBody(JSContext* aCx, FetchConsumeType aType, ErrorResult& aRv) @@ -963,30 +1009,9 @@ FetchBody::ConsumeBody(JSContext* aCx, FetchConsumeType aType, ErrorRes return nullptr; } - SetBodyUsed(); - - // If we already have a ReadableStreamBody and it has been created by DOM, we - // have to lock it now because it can have been shared with other objects. - if (mReadableStreamBody) { - JS::Rooted readableStreamObj(aCx, mReadableStreamBody); - if (JS::ReadableStreamGetMode(readableStreamObj) == - JS::ReadableStreamMode::ExternalSource) { - LockStream(aCx, readableStreamObj, aRv); - if (NS_WARN_IF(aRv.Failed())) { - return nullptr; - } - } else { - // If this is not a native ReadableStream, let's activate the - // FetchStreamReader. - MOZ_ASSERT(mFetchStreamReader); - JS::Rooted reader(aCx); - mFetchStreamReader->StartConsuming(aCx, readableStreamObj, &reader, aRv); - if (NS_WARN_IF(aRv.Failed())) { - return nullptr; - } - - mReadableStreamReader = reader; - } + SetBodyUsed(aCx, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; } nsCOMPtr global = DerivedClass()->GetParentObject(); diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index d65b66b883..01f0ab4ace 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -190,11 +190,28 @@ public: // Utility public methods accessed by various runnables. + // This method _must_ be called in order to set the body as used. If the body + // is a ReadableStream, this method will start reading the stream. + // More in details, this method does: + // 1) It uses an internal flag to track if the body is used. This is tracked + // separately from the ReadableStream disturbed state due to purely native + // streams. + // 2) If there is a ReadableStream reflector for the native stream it is + // Locked. + // 3) If there is a JS ReadableStream then we begin pumping it into the native + // body stream. This effectively locks and disturbs the stream. + // + // Note that JSContext is used only if there is a ReadableStream (this can + // happen because the body is a ReadableStream or because attribute body has + // already been used by content). If something goes wrong using + // ReadableStream, errors will be reported via ErrorResult and not as JS + // exceptions in JSContext. This is done in order to have a centralized error + // reporting way. + // + // Exceptions generated when reading from the ReadableStream are directly sent + // to the Console (NOTE FOR THE REVIEWER: this is part of patch 16) void - SetBodyUsed() - { - mBodyUsed = true; - } + SetBodyUsed(JSContext* aCx, ErrorResult& aRv); const nsCString& MimeType() const diff --git a/dom/fetch/Request.cpp b/dom/fetch/Request.cpp index ec140a22d4..b79b539772 100644 --- a/dom/fetch/Request.cpp +++ b/dom/fetch/Request.cpp @@ -614,7 +614,10 @@ Request::Constructor(const GlobalObject& aGlobal, inputReq->GetBody(getter_AddRefs(body)); if (body) { inputReq->SetBody(nullptr); - inputReq->SetBodyUsed(); + inputReq->SetBodyUsed(aGlobal.Context(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } } } return domRequest.forget(); diff --git a/dom/workers/ScriptLoader.cpp b/dom/workers/ScriptLoader.cpp index 308ef94e38..123a46526a 100644 --- a/dom/workers/ScriptLoader.cpp +++ b/dom/workers/ScriptLoader.cpp @@ -701,9 +701,14 @@ private: request.SetAsUSVString().Rebind(loadInfo.mFullURL.Data(), loadInfo.mFullURL.Length()); + // This JSContext will not end up executing JS code because here there are + // no ReadableStreams involved. + AutoJSAPI jsapi; + jsapi.Init(); + ErrorResult error; RefPtr cachePromise = - mCacheCreator->Cache_()->Put(request, *response, error); + mCacheCreator->Cache_()->Put(jsapi.cx(), request, *response, error); if (NS_WARN_IF(error.Failed())) { nsresult rv = error.StealNSResult(); channel->Cancel(rv); @@ -1668,8 +1673,13 @@ CacheScriptLoader::Load(Cache* aCache) mozilla::dom::CacheQueryOptions params; + // This JSContext will not end up executing JS code because here there are + // no ReadableStreams involved. + AutoJSAPI jsapi; + jsapi.Init(); + ErrorResult error; - RefPtr promise = aCache->Match(request, params, error); + RefPtr promise = aCache->Match(jsapi.cx(), request, params, error); if (NS_WARN_IF(error.Failed())) { Fail(error.StealNSResult()); return; diff --git a/dom/workers/ServiceWorkerEvents.cpp b/dom/workers/ServiceWorkerEvents.cpp index ce2e5e7aae..7c9add4060 100644 --- a/dom/workers/ServiceWorkerEvents.cpp +++ b/dom/workers/ServiceWorkerEvents.cpp @@ -505,6 +505,44 @@ public: } } + // This function steals the error message from a ErrorResult. + void + SetCancelErrorResult(JSContext* aCx, ErrorResult& aRv) + { + MOZ_DIAGNOSTIC_ASSERT(aRv.Failed()); + MOZ_DIAGNOSTIC_ASSERT(!JS_IsExceptionPending(aCx)); + + // Storing the error as exception in the JSContext. + if (!aRv.MaybeSetPendingException(aCx)) { + return; + } + + MOZ_ASSERT(!aRv.Failed()); + + // Let's take the pending exception. + JS::Rooted exn(aCx); + if (!JS_GetPendingException(aCx, &exn)) { + return; + } + + JS_ClearPendingException(aCx); + + // Converting the exception in a js::ErrorReport. + js::ErrorReport report(aCx); + if (!report.init(aCx, exn, js::ErrorReport::WithSideEffects)) { + JS_ClearPendingException(aCx); + return; + } + + MOZ_ASSERT(mOwner); + MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL")); + MOZ_ASSERT(mParams.Length() == 1); + + // Let's store the error message here. + mMessageName.Assign(report.toStringResult().c_str()); + mParams.Clear(); + } + template void SetCancelMessage(const nsACString& aMessageName, Params&&... aParams) { @@ -659,7 +697,12 @@ RespondWithHandler::ResolvedCallback(JSContext* aCx, JS::Handle aValu ir->GetUnfilteredBody(getter_AddRefs(body)); // Errors and redirects may not have a body. if (body) { - response->SetBodyUsed(); + IgnoredErrorResult error; + response->SetBodyUsed(aCx, error); + if (NS_WARN_IF(error.Failed())) { + autoCancel.SetCancelErrorResult(aCx, error); + return; + } nsCOMPtr responseBody; rv = mInterceptedChannel->GetResponseBody(getter_AddRefs(responseBody)); diff --git a/dom/workers/ServiceWorkerScriptCache.cpp b/dom/workers/ServiceWorkerScriptCache.cpp index 05be250da8..f343c35586 100644 --- a/dom/workers/ServiceWorkerScriptCache.cpp +++ b/dom/workers/ServiceWorkerScriptCache.cpp @@ -395,7 +395,7 @@ public: return; } - WriteToCache(cache); + WriteToCache(aCx, cache); return; } @@ -528,7 +528,7 @@ private: } void - WriteToCache(Cache* aCache) + WriteToCache(JSContext* aCx, Cache* aCache) { AssertIsOnMainThread(); MOZ_ASSERT(aCache); @@ -561,8 +561,10 @@ private: // For now we have to wait until the Put Promise is fulfilled before we can // continue since Cache does not yet support starting a read that is being // written to. - RefPtr cachePromise = aCache->Put(request, *response, result); + RefPtr cachePromise = aCache->Put(aCx, request, *response, result); if (NS_WARN_IF(result.Failed())) { + // No exception here because there are no ReadableStreams involved here. + MOZ_ASSERT(!result.IsJSException()); MOZ_ASSERT(!result.IsErrorWithMessage()); Fail(result.StealNSResult()); return; @@ -903,7 +905,7 @@ CompareCache::ManageCacheResult(JSContext* aCx, JS::Handle aValue) request.SetAsUSVString().Rebind(mURL.Data(), mURL.Length()); ErrorResult error; CacheQueryOptions params; - RefPtr promise = cache->Match(request, params, error); + RefPtr promise = cache->Match(aCx, request, params, error); if (NS_WARN_IF(error.Failed())) { mManager->CacheFinished(error.StealNSResult(), false); return; From b124e54fc7522b71c11e4528dcad0794b2211d5e Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 28 Sep 2023 19:51:09 -0500 Subject: [PATCH 19/31] Issue #1442 - Part 15 - Setting the correct global when ReadableStream.getReader() is called. https://bugzilla.mozilla.org/show_bug.cgi?id=1128959 --- dom/fetch/Fetch.cpp | 4 ++++ dom/fetch/FetchStreamReader.cpp | 16 ++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index 53ada9378b..dfc65c840e 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -1139,6 +1139,10 @@ FetchBody::LockStream(JSContext* aCx, JS::HandleObject aStream, ErrorResult& aRv) { + MOZ_ASSERT(JS::ReadableStreamGetMode(aStream) == + JS::ReadableStreamMode::ExternalSource); + + // This is native stream, creating a reader will not execute any JS code. JS::Rooted reader(aCx, JS::ReadableStreamGetReader(aCx, aStream, JS::ReadableStreamReaderMode::Default)); diff --git a/dom/fetch/FetchStreamReader.cpp b/dom/fetch/FetchStreamReader.cpp index 98d2588a0e..5d9719a0c1 100644 --- a/dom/fetch/FetchStreamReader.cpp +++ b/dom/fetch/FetchStreamReader.cpp @@ -169,17 +169,13 @@ FetchStreamReader::OnOutputStreamReady(nsIAsyncOutputStream* aStream) return WriteBuffer(); } - AutoJSAPI jsapi; - if (NS_WARN_IF(!jsapi.Init(mGlobal))) { - CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); - return NS_ERROR_FAILURE; - } + // TODO: We need to verify this is the correct global per the spec. + // See bug 1385890. + AutoEntryScript aes(mGlobal, "ReadableStreamReader.read", !mWorkerHolder); - JSContext* cx = jsapi.cx(); - - JS::Rooted reader(cx, mReader); - JS::Rooted promise(cx, - JS::ReadableStreamDefaultReaderRead(cx, + JS::Rooted reader(aes.cx(), mReader); + JS::Rooted promise(aes.cx(), + JS::ReadableStreamDefaultReaderRead(aes.cx(), reader)); if (NS_WARN_IF(!promise)) { // Let's close the stream. From 529bc13f05be04e48d8954736339a122f20f2509 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 28 Sep 2023 21:18:27 -0500 Subject: [PATCH 20/31] Issue #1442 - Part 16 - Report stream errors during consumption. https://bugzilla.mozilla.org/show_bug.cgi?id=1128959 --- dom/base/nsContentUtils.cpp | 71 ++++++++++++++++++++++++ dom/base/nsContentUtils.h | 4 ++ dom/fetch/Fetch.h | 2 +- dom/fetch/FetchStreamReader.cpp | 51 +++++++++++++++++ dom/fetch/FetchStreamReader.h | 3 + dom/workers/ServiceWorkerEvents.cpp | 85 +++-------------------------- 6 files changed, 139 insertions(+), 77 deletions(-) diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 130188ef98..69534f9149 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -39,6 +39,8 @@ #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/CustomElementRegistry.h" #include "mozilla/dom/DocumentFragment.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/DOMExceptionBinding.h" #include "mozilla/dom/DOMTypes.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/FileSystemSecurity.h" @@ -10081,3 +10083,72 @@ void nsContentUtils::StructuredClone(JSContext* aCx, nsTArray> ports = holder.TakeTransferredPorts(); Unused << ports; } + +/* static */ void +nsContentUtils::ExtractErrorValues(JSContext* aCx, + JS::Handle aValue, + nsACString& aSourceSpecOut, + uint32_t* aLineOut, + uint32_t* aColumnOut, + nsString& aMessageOut) +{ + MOZ_ASSERT(aLineOut); + MOZ_ASSERT(aColumnOut); + + if (aValue.isObject()) { + JS::Rooted obj(aCx, &aValue.toObject()); + RefPtr domException; + + // Try to process as an Error object. Use the file/line/column values + // from the Error as they will be more specific to the root cause of + // the problem. + JSErrorReport* err = obj ? JS_ErrorFromException(aCx, obj) : nullptr; + if (err) { + // Use xpc to extract the error message only. We don't actually send + // this report anywhere. + RefPtr report = new xpc::ErrorReport(); + report->Init(err, + "", // toString result + false, // chrome + 0); // window ID + + if (!report->mFileName.IsEmpty()) { + CopyUTF16toUTF8(report->mFileName, aSourceSpecOut); + *aLineOut = report->mLineNumber; + *aColumnOut = report->mColumn; + } + aMessageOut.Assign(report->mErrorMsg); + } + + // Next, try to unwrap the rejection value as a DOMException. + else if(NS_SUCCEEDED(UNWRAP_OBJECT(DOMException, obj, domException))) { + + nsAutoString filename; + domException->GetFilename(aCx, filename); + if (!filename.IsEmpty()) { + CopyUTF16toUTF8(filename, aSourceSpecOut); + *aLineOut = domException->LineNumber(aCx); + *aColumnOut = domException->ColumnNumber(); + } + + domException->GetName(aMessageOut); + aMessageOut.AppendLiteral(": "); + + nsAutoString message; + domException->GetMessageMoz(message); + aMessageOut.Append(message); + } + } + + // If we could not unwrap a specific error type, then perform default safe + // string conversions on primitives. Objects will result in "[Object]" + // unfortunately. + if (aMessageOut.IsEmpty()) { + nsAutoJSString jsString; + if (jsString.init(aCx, aValue)) { + aMessageOut = jsString; + } else { + JS_ClearPendingException(aCx); + } + } +} diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h index c1f8885f73..a60b102c53 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h @@ -1010,6 +1010,10 @@ public: static bool PrefetchEnabled(nsIDocShell* aDocShell); + static void ExtractErrorValues(JSContext* aCx, JS::Handle aValue, + nsACString& aSourceSpecOut, uint32_t *aLineOut, + uint32_t *aColumnOut, nsString& aMessageOut); + static nsresult CalculateBufferSizeForImage(const uint32_t& aStride, const mozilla::gfx::IntSize& aImageSize, const mozilla::gfx::SurfaceFormat& aFormat, diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index 01f0ab4ace..7b52dfdc62 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -209,7 +209,7 @@ public: // reporting way. // // Exceptions generated when reading from the ReadableStream are directly sent - // to the Console (NOTE FOR THE REVIEWER: this is part of patch 16) + // to the Console. void SetBodyUsed(JSContext* aCx, ErrorResult& aRv); diff --git a/dom/fetch/FetchStreamReader.cpp b/dom/fetch/FetchStreamReader.cpp index 5d9719a0c1..3a24844dd9 100644 --- a/dom/fetch/FetchStreamReader.cpp +++ b/dom/fetch/FetchStreamReader.cpp @@ -7,6 +7,9 @@ #include "FetchStreamReader.h" #include "InternalResponse.h" #include "mozilla/dom/PromiseBinding.h" +#include "nsContentUtils.h" +#include "nsIScriptError.h" +#include "nsPIDOMWindow.h" namespace mozilla { namespace dom { @@ -296,8 +299,56 @@ void FetchStreamReader::RejectedCallback(JSContext* aCx, JS::Handle aValue) { + ReportErrorToConsole(aCx, aValue); CloseAndRelease(NS_ERROR_FAILURE); } +void +FetchStreamReader::ReportErrorToConsole(JSContext* aCx, + JS::Handle aValue) +{ + nsCString sourceSpec; + uint32_t line = 0; + uint32_t column = 0; + nsString valueString; + + nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, + &column, valueString); + + nsTArray params; + params.AppendElement(valueString); + + RefPtr reporter = new ConsoleReportCollector(); + reporter->AddConsoleReport(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("ReadableStreamReader.read"), + nsContentUtils::eDOM_PROPERTIES, + sourceSpec, line, column, + NS_LITERAL_CSTRING("ReadableStreamReadingFailed"), + params); + + uint64_t innerWindowId = 0; + + if (NS_IsMainThread()) { + nsCOMPtr window = do_QueryInterface(mGlobal); + if (window) { + innerWindowId = window->WindowID(); + } + reporter->FlushReportsByWindowId(innerWindowId); + return; + } + + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + if (workerPrivate) { + innerWindowId = workerPrivate->WindowID(); + } + + RefPtr r = NS_NewRunnableFunction( + [reporter, innerWindowId] () { + reporter->FlushReportsByWindowId(innerWindowId); + }); + + workerPrivate->DispatchToMainThread(r.forget()); +} + } // dom namespace } // mozilla namespace diff --git a/dom/fetch/FetchStreamReader.h b/dom/fetch/FetchStreamReader.h index 8984a2ea73..d71021fdc8 100644 --- a/dom/fetch/FetchStreamReader.h +++ b/dom/fetch/FetchStreamReader.h @@ -55,6 +55,9 @@ private: nsresult WriteBuffer(); + void + ReportErrorToConsole(JSContext* aCx, JS::Handle aValue); + nsCOMPtr mGlobal; nsCOMPtr mOwningEventTarget; diff --git a/dom/workers/ServiceWorkerEvents.cpp b/dom/workers/ServiceWorkerEvents.cpp index 7c9add4060..569422da25 100644 --- a/dom/workers/ServiceWorkerEvents.cpp +++ b/dom/workers/ServiceWorkerEvents.cpp @@ -6,6 +6,7 @@ #include "ServiceWorkerEvents.h" #include "nsAutoPtr.h" +#include "nsContentUtils.h" #include "nsIConsoleReportCollector.h" #include "nsIHttpChannelInternal.h" #include "nsINetworkInterceptController.h" @@ -30,8 +31,6 @@ #include "mozilla/Move.h" #include "mozilla/Preferences.h" #include "mozilla/dom/BodyUtil.h" -#include "mozilla/dom/DOMException.h" -#include "mozilla/dom/DOMExceptionBinding.h" #include "mozilla/dom/EncodingUtils.h" #include "mozilla/dom/FetchEventBinding.h" #include "mozilla/dom/MessagePort.h" @@ -404,76 +403,6 @@ void RespondWithCopyComplete(void* aClosure, nsresult aStatus) MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(event)); } -namespace { - -void -ExtractErrorValues(JSContext* aCx, JS::Handle aValue, - nsACString& aSourceSpecOut, uint32_t *aLineOut, - uint32_t *aColumnOut, nsString& aMessageOut) -{ - MOZ_ASSERT(aLineOut); - MOZ_ASSERT(aColumnOut); - - if (aValue.isObject()) { - JS::Rooted obj(aCx, &aValue.toObject()); - RefPtr domException; - - // Try to process as an Error object. Use the file/line/column values - // from the Error as they will be more specific to the root cause of - // the problem. - JSErrorReport* err = obj ? JS_ErrorFromException(aCx, obj) : nullptr; - if (err) { - // Use xpc to extract the error message only. We don't actually send - // this report anywhere. - RefPtr report = new xpc::ErrorReport(); - report->Init(err, - "", // toString result - false, // chrome - 0); // window ID - - if (!report->mFileName.IsEmpty()) { - CopyUTF16toUTF8(report->mFileName, aSourceSpecOut); - *aLineOut = report->mLineNumber; - *aColumnOut = report->mColumn; - } - aMessageOut.Assign(report->mErrorMsg); - } - - // Next, try to unwrap the rejection value as a DOMException. - else if(NS_SUCCEEDED(UNWRAP_OBJECT(DOMException, obj, domException))) { - - nsAutoString filename; - domException->GetFilename(aCx, filename); - if (!filename.IsEmpty()) { - CopyUTF16toUTF8(filename, aSourceSpecOut); - *aLineOut = domException->LineNumber(aCx); - *aColumnOut = domException->ColumnNumber(); - } - - domException->GetName(aMessageOut); - aMessageOut.AppendLiteral(": "); - - nsAutoString message; - domException->GetMessageMoz(message); - aMessageOut.Append(message); - } - } - - // If we could not unwrap a specific error type, then perform default safe - // string conversions on primitives. Objects will result in "[Object]" - // unfortunately. - if (aMessageOut.IsEmpty()) { - nsAutoJSString jsString; - if (jsString.init(aCx, aValue)) { - aMessageOut = jsString; - } else { - JS_ClearPendingException(aCx); - } - } -} - -} // anonymous namespace - class MOZ_STACK_CLASS AutoCancel { RefPtr mOwner; @@ -595,7 +524,8 @@ RespondWithHandler::ResolvedCallback(JSContext* aCx, JS::Handle aValu uint32_t line = 0; uint32_t column = 0; nsString valueString; - ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, valueString); + nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, + valueString); autoCancel.SetCancelMessageAndLocation(sourceSpec, line, column, NS_LITERAL_CSTRING("InterceptedNonResponseWithURL"), @@ -610,7 +540,8 @@ RespondWithHandler::ResolvedCallback(JSContext* aCx, JS::Handle aValu uint32_t line = 0; uint32_t column = 0; nsString valueString; - ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, valueString); + nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, + valueString); autoCancel.SetCancelMessageAndLocation(sourceSpec, line, column, NS_LITERAL_CSTRING("InterceptedNonResponseWithURL"), @@ -758,7 +689,8 @@ RespondWithHandler::RejectedCallback(JSContext* aCx, JS::Handle aValu uint32_t column = mRespondWithColumnNumber; nsString valueString; - ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, valueString); + nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, + valueString); ::AsyncLog(mInterceptedChannel, sourceSpec, line, column, NS_LITERAL_CSTRING("InterceptionRejectedResponseWithURL"), @@ -896,7 +828,8 @@ public: nsCString spec; uint32_t line = 0; uint32_t column = 0; - ExtractErrorValues(aCx, aValue, spec, &line, &column, mRejectValue); + nsContentUtils::ExtractErrorValues(aCx, aValue, spec, &line, &column, + mRejectValue); // only use the extracted location if we found one if (!spec.IsEmpty()) { From e22845913df3fea5ce7760b1a35832711ac74555 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 28 Sep 2023 22:06:25 -0500 Subject: [PATCH 21/31] Issue #1442 - Part 17 - Creating FetchStream as a out param in order to avoid JS hazards https://bugzilla.mozilla.org/show_bug.cgi?id=1128959 --- dom/fetch/Fetch.cpp | 9 +++------ dom/fetch/FetchStream.cpp | 26 +++++++++++++++++--------- dom/fetch/FetchStream.h | 4 ++-- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index dfc65c840e..d53886e96c 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -1097,12 +1097,9 @@ FetchBody::GetBody(JSContext* aCx, return; } - JS::Rooted body(aCx, - FetchStream::Create(aCx, - this, - DerivedClass()->GetParentObject(), - inputStream, - aRv)); + JS::Rooted body(aCx); + FetchStream::Create(aCx, this, DerivedClass()->GetParentObject(), + inputStream, &body, aRv); if (NS_WARN_IF(aRv.Failed())) { return; } diff --git a/dom/fetch/FetchStream.cpp b/dom/fetch/FetchStream.cpp index 94837504f1..fa4802a990 100644 --- a/dom/fetch/FetchStream.cpp +++ b/dom/fetch/FetchStream.cpp @@ -104,10 +104,10 @@ private: NS_IMPL_ISUPPORTS(FetchStream, nsIInputStreamCallback, nsIObserver, nsISupportsWeakReference) -/* static */ JSObject* +/* static */ void FetchStream::Create(JSContext* aCx, FetchStreamHolder* aStreamHolder, nsIGlobalObject* aGlobal, nsIInputStream* aInputStream, - ErrorResult& aRv) + JS::MutableHandle aStream, ErrorResult& aRv) { MOZ_DIAGNOSTIC_ASSERT(aCx); MOZ_DIAGNOSTIC_ASSERT(aInputStream); @@ -119,12 +119,12 @@ FetchStream::Create(JSContext* aCx, FetchStreamHolder* aStreamHolder, nsCOMPtr os = mozilla::services::GetObserverService(); if (NS_WARN_IF(!os)) { aRv.Throw(NS_ERROR_FAILURE); - return nullptr; + return; } aRv = os->AddObserver(stream, DOM_WINDOW_DESTROYED_TOPIC, true); if (NS_WARN_IF(aRv.Failed())) { - return nullptr; + return; } } else { @@ -135,7 +135,7 @@ FetchStream::Create(JSContext* aCx, FetchStreamHolder* aStreamHolder, new FetchStreamWorkerHolder(stream)); if (NS_WARN_IF(!holder->HoldWorker(workerPrivate, Closing))) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); - return nullptr; + return; } // Note, this will create a ref-cycle between the holder and the stream. @@ -158,14 +158,17 @@ FetchStream::Create(JSContext* aCx, FetchStreamHolder* aStreamHolder, JS::NewReadableExternalSourceStreamObject(aCx, stream, FETCH_STREAM_FLAG)); if (!body) { aRv.StealExceptionFromJSContext(aCx); - return nullptr; + return; } stream->mReadableStream = body; - // JS engine will call the finalize callback. + // This will be released in FetchStream::FinalizeCallback(). We are + // guaranteed the jsapi will call FinalizeCallback when ReadableStream + // js object is finalized. NS_ADDREF(stream.get()); - return body; + + aStream.set(body); } /* static */ void @@ -307,7 +310,11 @@ FetchStream::CancelCallback(JSContext* aCx, JS::HandleObject aStream, MOZ_DIAGNOSTIC_ASSERT(aUnderlyingSource); MOZ_DIAGNOSTIC_ASSERT(aFlags == FETCH_STREAM_FLAG); - RefPtr stream = static_cast(aUnderlyingSource); + // This is safe because we created an extra reference in FetchStream::Create() + // that won't be released until FetchStream::FinalizeCallback() is called. + // We are guaranteed that won't happen until the js ReadableStream object + // is finalized. + FetchStream* stream = static_cast(aUnderlyingSource); if (stream->mInputStream) { stream->mInputStream->CloseWithStatus(NS_BASE_STREAM_CLOSED); @@ -343,6 +350,7 @@ FetchStream::FinalizeCallback(void* aUnderlyingSource, uint8_t aFlags) // This can be called in any thread. + // This takes ownership of the ref created in FetchStream::Create(). RefPtr stream = dont_AddRef(static_cast(aUnderlyingSource)); diff --git a/dom/fetch/FetchStream.h b/dom/fetch/FetchStream.h index b27588eda0..4562b3f351 100644 --- a/dom/fetch/FetchStream.h +++ b/dom/fetch/FetchStream.h @@ -35,10 +35,10 @@ public: NS_DECL_NSIINPUTSTREAMCALLBACK NS_DECL_NSIOBSERVER - static JSObject* + static void Create(JSContext* aCx, FetchStreamHolder* aStreamHolder, nsIGlobalObject* aGlobal, nsIInputStream* aInputStream, - ErrorResult& aRv); + JS::MutableHandle aStream, ErrorResult& aRv); void Close(); From b898780bb14f72be0cfd66b574e1db72055747ff Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 28 Sep 2023 22:17:06 -0500 Subject: [PATCH 22/31] Issue #1442 - Part 18 - Cycle collect FetchStreamReader. https://bugzilla.mozilla.org/show_bug.cgi?id=1128959 --- dom/fetch/Fetch.cpp | 7 +++---- dom/fetch/FetchStreamReader.cpp | 22 +++++++++++++++++++++- dom/fetch/FetchStreamReader.h | 3 ++- dom/fetch/Response.cpp | 2 ++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index d53886e96c..5943d60963 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -922,10 +922,9 @@ FetchBody::BodyUsed() const // If this object is disturbed or locked, return false. if (mReadableStreamBody) { AutoJSAPI jsapi; - // If we start tracking ownership, we need something like this. - //if (!jsapi.Init(mOwner)) { - // return true; - //} + if (!jsapi.Init(mOwner)) { + return true; + } JSContext* cx = jsapi.cx(); diff --git a/dom/fetch/FetchStreamReader.cpp b/dom/fetch/FetchStreamReader.cpp index 3a24844dd9..702b44a6b6 100644 --- a/dom/fetch/FetchStreamReader.cpp +++ b/dom/fetch/FetchStreamReader.cpp @@ -44,7 +44,27 @@ private: } // anonymous -NS_IMPL_ISUPPORTS(FetchStreamReader, nsIOutputStreamCallback) +NS_IMPL_CYCLE_COLLECTING_ADDREF(FetchStreamReader) +NS_IMPL_CYCLE_COLLECTING_RELEASE(FetchStreamReader) + +NS_IMPL_CYCLE_COLLECTION_CLASS(FetchStreamReader) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(FetchStreamReader) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(FetchStreamReader) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(FetchStreamReader) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReader) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchStreamReader) + NS_INTERFACE_MAP_ENTRY(nsIOutputStreamCallback) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIOutputStreamCallback) +NS_INTERFACE_MAP_END /* static */ nsresult FetchStreamReader::Create(JSContext* aCx, nsIGlobalObject* aGlobal, diff --git a/dom/fetch/FetchStreamReader.h b/dom/fetch/FetchStreamReader.h index d71021fdc8..6155ee17ff 100644 --- a/dom/fetch/FetchStreamReader.h +++ b/dom/fetch/FetchStreamReader.h @@ -23,7 +23,8 @@ class FetchStreamReader final : public nsIOutputStreamCallback , public PromiseNativeHandler { public: - NS_DECL_ISUPPORTS + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(FetchStreamReader, nsIOutputStreamCallback) NS_DECL_NSIOUTPUTSTREAMCALLBACK // This creates a nsIInputStream able to retrieve data from the ReadableStream diff --git a/dom/fetch/Response.cpp b/dom/fetch/Response.cpp index 5055db4e24..3ee5e93d4a 100644 --- a/dom/fetch/Response.cpp +++ b/dom/fetch/Response.cpp @@ -36,6 +36,7 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(Response) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Response) NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner) NS_IMPL_CYCLE_COLLECTION_UNLINK(mHeaders) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFetchStreamReader) tmp->mReadableStreamBody = nullptr; tmp->mReadableStreamReader = nullptr; @@ -46,6 +47,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Response) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHeaders) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFetchStreamReader) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Response) From 0e1efcb27309c0893e39bda9fed9dc2964300486 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 29 Sep 2023 09:12:08 -0500 Subject: [PATCH 23/31] Issue #1442 - Part 19 - FetchStream must support multiple read requests. https://bugzilla.mozilla.org/show_bug.cgi?id=1395220 --- dom/fetch/FetchStream.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dom/fetch/FetchStream.cpp b/dom/fetch/FetchStream.cpp index fa4802a990..ec224e1c8d 100644 --- a/dom/fetch/FetchStream.cpp +++ b/dom/fetch/FetchStream.cpp @@ -185,7 +185,13 @@ FetchStream::RequestDataCallback(JSContext* aCx, RefPtr stream = static_cast(aUnderlyingSource); MOZ_DIAGNOSTIC_ASSERT(stream->mState == eWaiting || - stream->mState == eChecking); + stream->mState == eChecking || + stream->mState == eReading); + + if (stream->mState == eReading) { + // We are already reading data. + return; + } if (stream->mState == eChecking) { // If we are looking for more data, there is nothing else we should do: From b1a12de924e0ddbca221b6ee2ae37e2c065f488c Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 29 Sep 2023 09:25:52 -0500 Subject: [PATCH 24/31] Issue #1442 - Part 20 - FetchStream should not have an extra JS::Heap. https://bugzilla.mozilla.org/show_bug.cgi?id=1390717 --- dom/fetch/Fetch.h | 9 +++++++++ dom/fetch/FetchStream.cpp | 7 ++----- dom/fetch/FetchStream.h | 2 -- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index 7b52dfdc62..0dcf1d859c 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -100,6 +100,8 @@ public: NS_IMETHOD_(MozExternalRefCountType) Release(void) = 0; virtual void NullifyStream() = 0; + + virtual JSObject* ReadableStreamBody() = 0; }; /* @@ -228,6 +230,13 @@ public: mFetchStreamReader = nullptr; } + JSObject* + ReadableStreamBody() override + { + MOZ_ASSERT(mReadableStreamBody); + return mReadableStreamBody; + } + virtual AbortSignal* GetSignal() const = 0; diff --git a/dom/fetch/FetchStream.cpp b/dom/fetch/FetchStream.cpp index ec224e1c8d..63e6f150b9 100644 --- a/dom/fetch/FetchStream.cpp +++ b/dom/fetch/FetchStream.cpp @@ -161,8 +161,6 @@ FetchStream::Create(JSContext* aCx, FetchStreamHolder* aStreamHolder, return; } - stream->mReadableStream = body; - // This will be released in FetchStream::FinalizeCallback(). We are // guaranteed the jsapi will call FinalizeCallback when ReadableStream // js object is finalized. @@ -371,7 +369,6 @@ FetchStream::FetchStream(nsIGlobalObject* aGlobal, , mStreamHolder(aStreamHolder) , mOriginalInputStream(aInputStream) , mOwningEventTarget(nullptr) - , mReadableStream(nullptr) { MOZ_DIAGNOSTIC_ASSERT(aInputStream); MOZ_DIAGNOSTIC_ASSERT(aStreamHolder); @@ -428,7 +425,7 @@ FetchStream::OnInputStreamReady(nsIAsyncInputStream* aStream) } JSContext* cx = jsapi.cx(); - JS::Rooted stream(cx, mReadableStream); + JS::Rooted stream(cx, mStreamHolder->ReadableStreamBody()); uint64_t size = 0; nsresult rv = mInputStream->Available(&size); @@ -494,7 +491,7 @@ FetchStream::Close() } JSContext* cx = jsapi.cx(); - JS::Rooted stream(cx, mReadableStream); + JS::Rooted stream(cx, mStreamHolder->ReadableStreamBody()); CloseAndReleaseObjects(cx, stream); } diff --git a/dom/fetch/FetchStream.h b/dom/fetch/FetchStream.h index 4562b3f351..80d120616d 100644 --- a/dom/fetch/FetchStream.h +++ b/dom/fetch/FetchStream.h @@ -125,8 +125,6 @@ private: nsCOMPtr mInputStream; UniquePtr mWorkerHolder; - - JS::Heap mReadableStream; }; } // dom namespace From 375d7cf3b2230b56956fcb938e62f8bd9f863959 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 29 Sep 2023 13:09:28 -0500 Subject: [PATCH 25/31] Issue #1442 - Part 21 - Fix debug builds by removing an invalid assert. --- dom/fetch/Fetch.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index 5943d60963..a9d8514765 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -952,7 +952,6 @@ void FetchBody::SetBodyUsed(JSContext* aCx, ErrorResult& aRv) { MOZ_ASSERT(aCx); - MOZ_ASSERT(mOwner->EventTargetFor(TaskCategory::Other)->IsOnCurrentThread()); if (mBodyUsed) { return; From 41409ab6133437f2d0606a7e0755cc2b79ed30d5 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 29 Sep 2023 15:34:10 -0500 Subject: [PATCH 26/31] Issue #1442 & #1691 Follow-up - Part 22 - Changes that came with Dynamic Module Import. At the time we did not have ReadableStreams in our tree so these changes were left out. https://bugzilla.mozilla.org/show_bug.cgi?id=1499140 Implement support for dynamic module import in the interpreter. --- js/src/builtin/Stream.cpp | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/js/src/builtin/Stream.cpp b/js/src/builtin/Stream.cpp index de2414d351..c8d8e3e324 100644 --- a/js/src/builtin/Stream.cpp +++ b/js/src/builtin/Stream.cpp @@ -284,15 +284,6 @@ ReportArgTypeError(JSContext* cx, const char* funName, const char* expectedType, funName, expectedType, bytes.get()); } -static MOZ_MUST_USE bool -RejectWithPendingError(JSContext* cx, Handle promise) { - // Not much we can do about uncatchable exceptions, just bail. - RootedValue exn(cx); - if (!GetAndClearException(cx, &exn)) - return false; - return PromiseObject::reject(cx, promise, exn); -} - static MOZ_MUST_USE bool ReturnPromiseRejectedWithPendingError(JSContext* cx, const CallArgs& args) { @@ -1183,7 +1174,7 @@ ReadableStreamTee_Cancel(JSContext* cx, Handle teeState, // Step b: Let cancelResult be ! ReadableStreamCancel(stream, compositeReason). RootedObject cancelResult(cx, ReadableStream::cancel(cx, stream, compositeReasonVal)); if (!cancelResult) { - if (!RejectWithPendingError(cx, promise)) + if (!RejectPromiseWithPendingError(cx, promise)) return nullptr; } else { // Step c: Resolve teeState.[[promise]] with cancelResult. From 836d36dc6df1c124f425286c86576a61e36b7549 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 30 Sep 2023 11:19:06 -0500 Subject: [PATCH 27/31] Issue #1442 - Part 23 - Align FetchStream with Firefox 68ESR. https://bugzilla.mozilla.org/show_bug.cgi?id=1612308 https://bugzilla.mozilla.org/show_bug.cgi?id=1445587 Partial part 2 implementing synchronization changes. --- dom/fetch/Fetch.h | 8 +++ dom/fetch/FetchStream.cpp | 132 +++++++++++++++++++++++++++++--------- dom/fetch/FetchStream.h | 31 ++++++++- 3 files changed, 139 insertions(+), 32 deletions(-) diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index 0dcf1d859c..2c86b79c52 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -101,6 +101,8 @@ public: virtual void NullifyStream() = 0; + virtual void MarkAsRead() = 0; + virtual JSObject* ReadableStreamBody() = 0; }; @@ -237,6 +239,12 @@ public: return mReadableStreamBody; } + void + MarkAsRead() override + { + mBodyUsed = true; + } + virtual AbortSignal* GetSignal() const = 0; diff --git a/dom/fetch/FetchStream.cpp b/dom/fetch/FetchStream.cpp index 63e6f150b9..8d2ac7b1df 100644 --- a/dom/fetch/FetchStream.cpp +++ b/dom/fetch/FetchStream.cpp @@ -4,6 +4,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "FetchStream.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/Maybe.h" #include "nsNetCID.h" #include "nsITransport.h" #include "nsIStreamTransportService.h" @@ -181,8 +184,12 @@ FetchStream::RequestDataCallback(JSContext* aCx, MOZ_DIAGNOSTIC_ASSERT(JS::ReadableStreamIsDisturbed(aStream)); RefPtr stream = static_cast(aUnderlyingSource); + stream->AssertIsOnOwningThread(); - MOZ_DIAGNOSTIC_ASSERT(stream->mState == eWaiting || + MutexAutoLock lock(stream->mMutex); + + MOZ_DIAGNOSTIC_ASSERT(stream->mState == eInitializing || + stream->mState == eWaiting || stream->mState == eChecking || stream->mState == eReading); @@ -199,6 +206,11 @@ FetchStream::RequestDataCallback(JSContext* aCx, return; } + if (stream->mState == eInitializing) { + // The stream has been used for the first time. + stream->mStreamHolder->MarkAsRead(); + } + stream->mState = eReading; if (!stream->mInputStream) { @@ -209,7 +221,7 @@ FetchStream::RequestDataCallback(JSContext* aCx, bool nonBlocking = false; nsresult rv = stream->mOriginalInputStream->IsNonBlocking(&nonBlocking); if (NS_WARN_IF(NS_FAILED(rv))) { - stream->ErrorPropagation(aCx, aStream, rv); + stream->ErrorPropagation(aCx, lock, aStream, rv); return; } @@ -219,7 +231,7 @@ FetchStream::RequestDataCallback(JSContext* aCx, nsCOMPtr sts = do_GetService(kStreamTransportServiceCID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { - stream->ErrorPropagation(aCx, aStream, rv); + stream->ErrorPropagation(aCx, lock, aStream, rv); return; } @@ -230,7 +242,7 @@ FetchStream::RequestDataCallback(JSContext* aCx, /* aCloseWhenDone */ true, getter_AddRefs(transport)); if (NS_WARN_IF(NS_FAILED(rv))) { - stream->ErrorPropagation(aCx, aStream, rv); + stream->ErrorPropagation(aCx, lock, aStream, rv); return; } @@ -240,7 +252,7 @@ FetchStream::RequestDataCallback(JSContext* aCx, /* aSegmentCount */ 0, getter_AddRefs(wrapper)); if (NS_WARN_IF(NS_FAILED(rv))) { - stream->ErrorPropagation(aCx, aStream, rv); + stream->ErrorPropagation(aCx, lock, aStream, rv); return; } @@ -257,7 +269,7 @@ FetchStream::RequestDataCallback(JSContext* aCx, nsresult rv = stream->mInputStream->AsyncWait(stream, 0, 0, stream->mOwningEventTarget); if (NS_WARN_IF(NS_FAILED(rv))) { - stream->ErrorPropagation(aCx, aStream, rv); + stream->ErrorPropagation(aCx, lock, aStream, rv); return; } @@ -277,6 +289,9 @@ FetchStream::WriteIntoReadRequestCallback(JSContext* aCx, MOZ_DIAGNOSTIC_ASSERT(aByteWritten); RefPtr stream = static_cast(aUnderlyingSource); + stream->AssertIsOnOwningThread(); + + MutexAutoLock lock(stream->mMutex); MOZ_DIAGNOSTIC_ASSERT(stream->mInputStream); MOZ_DIAGNOSTIC_ASSERT(stream->mState == eWriting); @@ -286,20 +301,20 @@ FetchStream::WriteIntoReadRequestCallback(JSContext* aCx, nsresult rv = stream->mInputStream->Read(static_cast(aBuffer), aLength, &written); if (NS_WARN_IF(NS_FAILED(rv))) { - stream->ErrorPropagation(aCx, aStream, rv); + stream->ErrorPropagation(aCx, lock, aStream, rv); return; } *aByteWritten = written; if (written == 0) { - stream->CloseAndReleaseObjects(aCx, aStream); + stream->CloseAndReleaseObjects(aCx, lock, aStream); return; } rv = stream->mInputStream->AsyncWait(stream, 0, 0, stream->mOwningEventTarget); if (NS_WARN_IF(NS_FAILED(rv))) { - stream->ErrorPropagation(aCx, aStream, rv); + stream->ErrorPropagation(aCx, lock, aStream, rv); return; } @@ -319,6 +334,7 @@ FetchStream::CancelCallback(JSContext* aCx, JS::HandleObject aStream, // We are guaranteed that won't happen until the js ReadableStream object // is finalized. FetchStream* stream = static_cast(aUnderlyingSource); + stream->AssertIsOnOwningThread(); if (stream->mInputStream) { stream->mInputStream->CloseWithStatus(NS_BASE_STREAM_CLOSED); @@ -344,6 +360,24 @@ FetchStream::ErroredCallback(JSContext* aCx, JS::HandleObject aStream, { MOZ_DIAGNOSTIC_ASSERT(aUnderlyingSource); MOZ_DIAGNOSTIC_ASSERT(aFlags == FETCH_STREAM_FLAG); + + // This is safe because we created an extra reference in FetchStream::Create() + // that won't be released until FetchStream::FinalizeCallback() is called. + // We are guaranteed that won't happen until the js ReadableStream object + // is finalized. + FetchStream* stream = static_cast(aUnderlyingSource); + stream->AssertIsOnOwningThread(); + + if (stream->mState == eInitializing) { + // The stream has been used for the first time. + stream->mStreamHolder->MarkAsRead(); + } + + if (stream->mInputStream) { + stream->mInputStream->CloseWithStatus(NS_BASE_STREAM_CLOSED); + } + + stream->ReleaseObjects(); } void @@ -364,7 +398,8 @@ FetchStream::FinalizeCallback(void* aUnderlyingSource, uint8_t aFlags) FetchStream::FetchStream(nsIGlobalObject* aGlobal, FetchStreamHolder* aStreamHolder, nsIInputStream* aInputStream) - : mState(eWaiting) + : mMutex("FetchStream::mMutex") + , mState(eInitializing) , mGlobal(aGlobal) , mStreamHolder(aStreamHolder) , mOriginalInputStream(aInputStream) @@ -379,9 +414,13 @@ FetchStream::~FetchStream() } void -FetchStream::ErrorPropagation(JSContext* aCx, JS::HandleObject aStream, +FetchStream::ErrorPropagation(JSContext* aCx, + const MutexAutoLock& aProofOfLock, + JS::HandleObject aStream, nsresult aError) { + AssertIsOnOwningThread(); + // Nothing to do. if (mState == eClosed) { return; @@ -389,7 +428,7 @@ FetchStream::ErrorPropagation(JSContext* aCx, JS::HandleObject aStream, // Let's close the stream. if (aError == NS_BASE_STREAM_CLOSED) { - CloseAndReleaseObjects(aCx, aStream); + CloseAndReleaseObjects(aCx, aProofOfLock, aStream); return; } @@ -400,31 +439,34 @@ FetchStream::ErrorPropagation(JSContext* aCx, JS::HandleObject aStream, JS::Rooted errorValue(aCx); if (ToJSValue(aCx, error, &errorValue)) { + MutexAutoUnlock unlock(mMutex); JS::ReadableStreamError(aCx, aStream, errorValue); } + + ReleaseObjects(aProofOfLock); } NS_IMETHODIMP FetchStream::OnInputStreamReady(nsIAsyncInputStream* aStream) { + AssertIsOnOwningThread(); MOZ_DIAGNOSTIC_ASSERT(aStream); + Maybe lock; + lock.emplace(mMutex); + // Already closed. We have nothing else to do here. if (mState == eClosed) { return NS_OK; } + nsAutoMicroTask mt; + AutoEntryScript aes(mGlobal, "fetch body data available"); + MOZ_DIAGNOSTIC_ASSERT(mInputStream); MOZ_DIAGNOSTIC_ASSERT(mState == eReading || mState == eChecking); - - AutoJSAPI jsapi; - if (NS_WARN_IF(!jsapi.Init(mGlobal))) { - // Without JSContext we are not able to close the stream or to propagate the - // error. - return NS_ERROR_FAILURE; - } - - JSContext* cx = jsapi.cx(); + + JSContext* cx = aes.cx(); JS::Rooted stream(cx, mStreamHolder->ReadableStreamBody()); uint64_t size = 0; @@ -437,7 +479,7 @@ FetchStream::OnInputStreamReady(nsIAsyncInputStream* aStream) // No warning for stream closed. if (rv == NS_BASE_STREAM_CLOSED || NS_WARN_IF(NS_FAILED(rv))) { - ErrorPropagation(cx, stream, rv); + ErrorPropagation(cx, *lock, stream, rv); return NS_OK; } @@ -448,10 +490,10 @@ FetchStream::OnInputStreamReady(nsIAsyncInputStream* aStream) } mState = eWriting; - JS::ReadableStreamUpdateDataAvailableFromSource(cx, stream, size); - // The WriteInto callback changes mState to eChecking. - MOZ_DIAGNOSTIC_ASSERT(mState == eChecking); + lock.reset(); + + JS::ReadableStreamUpdateDataAvailableFromSource(cx, stream, size); return NS_OK; } @@ -465,6 +507,7 @@ FetchStream::RetrieveInputStream(void* aUnderlyingReadableStreamSource, RefPtr stream = static_cast(aUnderlyingReadableStreamSource); + stream->AssertIsOnOwningThread(); // if mOriginalInputStream is null, the reading already started. We don't want // to expose the internal inputStream. @@ -480,28 +523,37 @@ FetchStream::RetrieveInputStream(void* aUnderlyingReadableStreamSource, void FetchStream::Close() { + AssertIsOnOwningThread(); + + MutexAutoLock lock(mMutex); + if (mState == eClosed) { return; } AutoJSAPI jsapi; if (NS_WARN_IF(!jsapi.Init(mGlobal))) { - ReleaseObjects(); + ReleaseObjects(lock); return; } JSContext* cx = jsapi.cx(); JS::Rooted stream(cx, mStreamHolder->ReadableStreamBody()); - CloseAndReleaseObjects(cx, stream); + CloseAndReleaseObjects(cx, lock, stream); } void -FetchStream::CloseAndReleaseObjects(JSContext* aCx, JS::HandleObject aStream) +FetchStream::CloseAndReleaseObjects(JSContext* aCx, + const MutexAutoLock& aProofOfLock, + JS::HandleObject aStream) { + AssertIsOnOwningThread(); MOZ_DIAGNOSTIC_ASSERT(mState != eClosed); - ReleaseObjects(); + ReleaseObjects(aProofOfLock); + + MutexAutoUnlock unlock(mMutex); if (JS::ReadableStreamIsReadable(aStream)) { JS::ReadableStreamClose(aCx, aStream); @@ -511,7 +563,20 @@ FetchStream::CloseAndReleaseObjects(JSContext* aCx, JS::HandleObject aStream) void FetchStream::ReleaseObjects() { + MutexAutoLock lock(mMutex); + ReleaseObjects(lock); +} + +void +FetchStream::ReleaseObjects(const MutexAutoLock& aProofOfLock) +{ + // This method can be called on 2 possible threads: the owning one and a JS + // thread used to release resources. If we are on the JS thread, we need to + // dispatch a runnable to go back to the owning thread in order to release + // resources correctly. + if (mState == eClosed) { + // Already gone. Nothing to do. return; } @@ -541,6 +606,14 @@ FetchStream::ReleaseObjects() } } +#ifdef DEBUG +void +FetchStream::AssertIsOnOwningThread() +{ + NS_ASSERT_OWNINGTHREAD(FetchStream); +} +#endif + // nsIObserver // ----------- @@ -549,6 +622,7 @@ FetchStream::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { AssertIsOnMainThread(); + AssertIsOnOwningThread(); MOZ_ASSERT(strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) == 0); diff --git a/dom/fetch/FetchStream.h b/dom/fetch/FetchStream.h index 80d120616d..66e1de5642 100644 --- a/dom/fetch/FetchStream.h +++ b/dom/fetch/FetchStream.h @@ -52,6 +52,14 @@ private: nsIInputStream* aInputStream); ~FetchStream(); +#ifdef DEBUG + void + AssertIsOnOwningThread(); +#else + void + AssertIsOnOwningThread() {} +#endif + static void RequestDataCallback(JSContext* aCx, JS::HandleObject aStream, void* aUnderlyingSource, uint8_t aFlags, @@ -81,10 +89,19 @@ private: FinalizeCallback(void* aUnderlyingSource, uint8_t aFlags); void - ErrorPropagation(JSContext* aCx, JS::HandleObject aStream, nsresult aRv); + ErrorPropagation(JSContext* aCx, + const MutexAutoLock& aProofOfLock, + JS::HandleObject aStream, nsresult aRv); void - CloseAndReleaseObjects(JSContext* aCx, JS::HandleObject aStream); + CloseAndReleaseObjects(JSContext* aCx, + const MutexAutoLock& aProofOfLock, + JS::HandleObject aSteam); + + class WorkerShutdown; + + void + ReleaseObjects(const MutexAutoLock& aProofOfLock); void ReleaseObjects(); @@ -92,6 +109,9 @@ private: // Common methods enum State { + // This is the beginning state before any reading operation. + eInitializing, + // RequestDataCallback has not been called yet. We haven't started to read // data from the stream yet. eWaiting, @@ -111,7 +131,12 @@ private: eClosed, }; - // Touched only on the target thread. + // We need a mutex because JS engine can release FetchStream on a non-owning + // thread. We must be sure that the releasing of resources doesn't trigger + // race conditions. + Mutex mMutex; + + // Protected by mutex. State mState; nsCOMPtr mGlobal; From f3991f9821d82886184d97adc90b58ff38858951 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 2 Oct 2023 04:42:24 -0500 Subject: [PATCH 28/31] Issue #1442 - Part 24 - Prevent FetchStream from incorrectly running on the main thread. This prevents a deadlock and JSContext related crashes, however this should eventually be replaced by an EventTargetFor() call when the Dispatcher API is implemented. Also now that ReadableStream appears stable, toggle the preferences true by default. --- dom/fetch/FetchStream.cpp | 4 +++- modules/libpref/init/all.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dom/fetch/FetchStream.cpp b/dom/fetch/FetchStream.cpp index 8d2ac7b1df..1c22a71405 100644 --- a/dom/fetch/FetchStream.cpp +++ b/dom/fetch/FetchStream.cpp @@ -403,7 +403,9 @@ FetchStream::FetchStream(nsIGlobalObject* aGlobal, , mGlobal(aGlobal) , mStreamHolder(aStreamHolder) , mOriginalInputStream(aInputStream) - , mOwningEventTarget(nullptr) + // TODO: Replace with mGlobal->EventTargetFor(TaskCategory::Other) + // When we have the Dispatcher API in the tree, see Issue #1442 + , mOwningEventTarget(NS_GetCurrentThread()) { MOZ_DIAGNOSTIC_ASSERT(aInputStream); MOZ_DIAGNOSTIC_ASSERT(aStreamHolder); diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 3d6325655e..b7d915996e 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -1335,7 +1335,7 @@ pref("javascript.options.main_thread_stack_quota_cap", 2097152); pref("javascript.options.dynamicImport", true); // Streams API -pref("javascript.options.streams", false); +pref("javascript.options.streams", true); // advanced prefs pref("advanced.mailftp", false); @@ -4586,7 +4586,7 @@ pref("layout.animated-image-layers.enabled", false); pref("dom.abortController.enabled", true); // Streams API -pref("dom.streams.enabled", false); +pref("dom.streams.enabled", true); // Push pref("dom.push.enabled", false); From 52572d57552e42d5de668759432e10f977e15f7f Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 2 Oct 2023 18:08:46 -0400 Subject: [PATCH 29/31] Issue #1442 - Part 25 - Fix bustage on X11 platforms. --- dom/fetch/Fetch.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index 2c86b79c52..0db4479feb 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -18,7 +18,7 @@ #include "mozilla/ErrorResult.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/FetchStreamReader.h" -#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/FetchIPCTypes.h" class nsIGlobalObject; class nsIEventTarget; From 04c9d661fa97fa9fc205cd40f916ee52afc412d1 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 2 Oct 2023 18:38:02 -0400 Subject: [PATCH 30/31] Issue #1442 - Part 25b - Second try that hopefully doesn't break Windows. --- dom/fetch/Fetch.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index 0db4479feb..8508929e27 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -18,7 +18,9 @@ #include "mozilla/ErrorResult.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/FetchStreamReader.h" -#include "mozilla/dom/FetchIPCTypes.h" +// Fix X11 header brain damage that conflicts with HeadersGuardEnum::None +#undef None +#include "mozilla/dom/RequestBinding.h" class nsIGlobalObject; class nsIEventTarget; From f21cc50c03627a0bc21c24ec124eef0a2c5e2db5 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 3 Oct 2023 01:45:08 -0500 Subject: [PATCH 31/31] Issue #1442 - Part 26 - FetchStreamReader needs to cancel its reader when it encounters write errors. https://bugzilla.mozilla.org/show_bug.cgi?id=1416879 Part 5 Also same fix as in Part 24 for FetchStream but in FetchStreamReader. --- dom/fetch/FetchStreamReader.cpp | 57 +++++++++++++++++++++++++-------- dom/fetch/FetchStreamReader.h | 6 +++- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/dom/fetch/FetchStreamReader.cpp b/dom/fetch/FetchStreamReader.cpp index 702b44a6b6..c6f5ca4d1d 100644 --- a/dom/fetch/FetchStreamReader.cpp +++ b/dom/fetch/FetchStreamReader.cpp @@ -7,6 +7,7 @@ #include "FetchStreamReader.h" #include "InternalResponse.h" #include "mozilla/dom/PromiseBinding.h" +#include "mozilla/dom/DOMException.h" #include "nsContentUtils.h" #include "nsIScriptError.h" #include "nsPIDOMWindow.h" @@ -31,7 +32,10 @@ public: { if (!mWasNotified) { mWasNotified = true; - mReader->CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + // The WorkerPrivate does have a context available, and we could pass it + // here to trigger cancellation of the reader, but the author of this + // comment chickened out. + mReader->CloseAndRelease(nullptr, NS_ERROR_DOM_INVALID_STATE_ERR); } return true; @@ -111,6 +115,9 @@ FetchStreamReader::Create(JSContext* aCx, nsIGlobalObject* aGlobal, FetchStreamReader::FetchStreamReader(nsIGlobalObject* aGlobal) : mGlobal(aGlobal) + // TODO: Replace with mGlobal->EventTargetFor(TaskCategory::Other) + // When we have the Dispatcher API in the tree, see Issue #1442 + , mOwningEventTarget(NS_GetCurrentThread()) , mBufferRemaining(0) , mBufferOffset(0) , mStreamClosed(false) @@ -120,11 +127,17 @@ FetchStreamReader::FetchStreamReader(nsIGlobalObject* aGlobal) FetchStreamReader::~FetchStreamReader() { - CloseAndRelease(NS_BASE_STREAM_CLOSED); + CloseAndRelease(nullptr, NS_BASE_STREAM_CLOSED); } +// If a context is provided, an attempt will be made to cancel the reader. The +// only situation where we don't expect to have a context is when closure is +// being triggered from the destructor or the WorkerHolder is notifying. If +// we're at the destructor, it's far too late to cancel anything. And if the +// WorkerHolder is being notified, the global is going away, so there's also +// no need to do further JS work. void -FetchStreamReader::CloseAndRelease(nsresult aStatus) +FetchStreamReader::CloseAndRelease(JSContext* aCx, nsresult aStatus) { NS_ASSERT_OWNINGTHREAD(FetchStreamReader); @@ -135,6 +148,21 @@ FetchStreamReader::CloseAndRelease(nsresult aStatus) RefPtr kungFuDeathGrip = this; + if (aCx) { + MOZ_ASSERT(mReader); + + RefPtr error = DOMException::Create(aStatus); + + JS::Rooted errorValue(aCx); + if (ToJSValue(aCx, error, &errorValue)) { + JS::Rooted reader(aCx, mReader); + // It's currently safe to cancel an already closed reader because, per the + // comments in ReadableStream::cancel() conveying the spec, step 2 of + // 3.4.3 that specified ReadableStreamCancel is: If stream.[[state]] is + // "closed", return a new promise resolved with undefined. + JS::ReadableStreamReaderCancel(aCx, reader, errorValue); + } + } mStreamClosed = true; mGlobal = nullptr; @@ -162,7 +190,7 @@ FetchStreamReader::StartConsuming(JSContext* aCx, JS::ReadableStreamReaderMode::Default)); if (!reader) { aRv.StealExceptionFromJSContext(aCx); - CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR); return; } @@ -202,14 +230,14 @@ FetchStreamReader::OnOutputStreamReady(nsIAsyncOutputStream* aStream) reader)); if (NS_WARN_IF(!promise)) { // Let's close the stream. - CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + CloseAndRelease(aes.cx(), NS_ERROR_DOM_INVALID_STATE_ERR); return NS_ERROR_FAILURE; } RefPtr domPromise = Promise::CreateFromExisting(mGlobal, promise); if (NS_WARN_IF(!domPromise)) { // Let's close the stream. - CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + CloseAndRelease(aes.cx(), NS_ERROR_DOM_INVALID_STATE_ERR); return NS_ERROR_FAILURE; } @@ -236,13 +264,13 @@ FetchStreamReader::ResolvedCallback(JSContext* aCx, FetchReadableStreamReadDataDone valueDone; if (!valueDone.Init(aCx, aValue)) { JS_ClearPendingException(aCx); - CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR); return; } if (valueDone.mDone) { // Stream is completed. - CloseAndRelease(NS_BASE_STREAM_CLOSED); + CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR); return; } @@ -250,7 +278,7 @@ FetchStreamReader::ResolvedCallback(JSContext* aCx, new FetchReadableStreamReadDataArray); if (!value->Init(aCx, aValue) || !value->mValue.WasPassed()) { JS_ClearPendingException(aCx); - CloseAndRelease(NS_ERROR_DOM_INVALID_STATE_ERR); + CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR); return; } @@ -270,7 +298,12 @@ FetchStreamReader::ResolvedCallback(JSContext* aCx, mBufferOffset = 0; mBufferRemaining = len; - WriteBuffer(); + nsresult rv = WriteBuffer(); + if (NS_FAILED(rv)) { + // DOMException only understands errors from domerr.msg, so we normalize to + // identifying an abort if the write fails. + CloseAndRelease(aCx, NS_ERROR_DOM_ABORT_ERR); + } } nsresult @@ -292,7 +325,6 @@ FetchStreamReader::WriteBuffer() } if (NS_WARN_IF(NS_FAILED(rv))) { - CloseAndRelease(rv); return rv; } @@ -308,7 +340,6 @@ FetchStreamReader::WriteBuffer() nsresult rv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget); if (NS_WARN_IF(NS_FAILED(rv))) { - CloseAndRelease(rv); return rv; } @@ -320,7 +351,7 @@ FetchStreamReader::RejectedCallback(JSContext* aCx, JS::Handle aValue) { ReportErrorToConsole(aCx, aValue); - CloseAndRelease(NS_ERROR_FAILURE); + CloseAndRelease(aCx, NS_ERROR_FAILURE); } void diff --git a/dom/fetch/FetchStreamReader.h b/dom/fetch/FetchStreamReader.h index 6155ee17ff..c25685046c 100644 --- a/dom/fetch/FetchStreamReader.h +++ b/dom/fetch/FetchStreamReader.h @@ -40,8 +40,12 @@ public: void RejectedCallback(JSContext* aCx, JS::Handle aValue) override; + // Idempotently close the output stream and null out all state. If aCx is + // provided, the reader will also be canceled. aStatus must be a DOM error + // as understood by DOMException because it will be provided as the + // cancellation reason. void - CloseAndRelease(nsresult aStatus); + CloseAndRelease(JSContext* aCx, nsresult aStatus); void StartConsuming(JSContext* aCx,