From d92c45cab714543c2bd971ebecaf78675895db1c Mon Sep 17 00:00:00 2001 From: Jeff Walden Date: Tue, 3 Mar 2020 21:07:26 +0000 Subject: [PATCH] Bug 1502355 - Fill in more of |ReadableStreamPipeTo|, as regards consing up a |PipeToState| closure to store state variables. r=arai Differential Revision: https://phabricator.services.mozilla.com/D65058 --HG-- extra : moz-landing-system : lando --- js/src/builtin/streams/PipeToState.cpp | 104 +++++++++++++++++- js/src/builtin/streams/PipeToState.h | 92 +++++++++++++++- js/src/builtin/streams/ReadableStream.cpp | 24 +++- .../streams/ReadableStreamOperations.cpp | 35 +++++- .../streams/ReadableStreamOperations.h | 4 +- js/src/js.msg | 1 + 6 files changed, 242 insertions(+), 18 deletions(-) diff --git a/js/src/builtin/streams/PipeToState.cpp b/js/src/builtin/streams/PipeToState.cpp index b0a73c77ce1c..9d7d74a74243 100644 --- a/js/src/builtin/streams/PipeToState.cpp +++ b/js/src/builtin/streams/PipeToState.cpp @@ -8,25 +8,119 @@ #include "builtin/streams/PipeToState.h" -#include "js/Class.h" // JSClass, JSCLASS_HAS_RESERVED_SLOTS -#include "js/RootingAPI.h" // JS::Rooted +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "jsapi.h" // JS_ReportErrorNumberASCII +#include "jsfriendapi.h" // js::GetErrorMessage, JSMSG_* + +#include "builtin/Promise.h" // js::RejectPromiseWithPendingError +#include "builtin/streams/ReadableStream.h" // js::ReadableStream +#include "builtin/streams/ReadableStreamReader.h" // js::CreateReadableStreamDefaultReader +#include "builtin/streams/WritableStream.h" // js::WritableStream +#include "builtin/streams/WritableStreamDefaultWriter.h" // js::CreateWritableStreamDefaultWriter +#include "js/Class.h" // JSClass, JSCLASS_HAS_RESERVED_SLOTS +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted +#include "vm/PromiseObject.h" // js::PromiseObject + +#include "vm/JSContext-inl.h" // JSContext::check #include "vm/JSObject-inl.h" // js::NewBuiltinClassInstance +using JS::Handle; using JS::Int32Value; +using JS::ObjectValue; using JS::Rooted; using js::PipeToState; -/* static */ PipeToState* PipeToState::create(JSContext* cx) { +/** + * Stream spec, 3.4.11. ReadableStreamPipeTo ( source, dest, + * preventClose, preventAbort, + * preventCancel, signal ) + * Steps 4-11, 13-14. + */ +/* static */ PipeToState* PipeToState::create( + JSContext* cx, Handle promise, + Handle unwrappedSource, + Handle unwrappedDest, bool preventClose, bool preventAbort, + bool preventCancel, Handle signal) { + cx->check(promise); + + // Step 4. Assert: signal is undefined or signal is an instance of the + // AbortSignal interface. +#ifdef DEBUG + if (signal) { + // XXX jwalden need to add JSAPI hooks to recognize AbortSignal instances + } +#endif + + // Step 5: Assert: ! IsReadableStreamLocked(source) is false. + MOZ_ASSERT(!unwrappedSource->locked()); + + // Step 6: Assert: ! IsWritableStreamLocked(dest) is false. + MOZ_ASSERT(!unwrappedDest->isLocked()); + Rooted state(cx, NewBuiltinClassInstance(cx)); if (!state) { return nullptr; } - state->setFixedSlot(Slot_Flags, Int32Value(0)); + MOZ_ASSERT(state->getFixedSlot(Slot_Promise).isUndefined()); + state->initFixedSlot(Slot_Promise, ObjectValue(*promise)); - return state; + // Step 7: If ! IsReadableByteStreamController( + // source.[[readableStreamController]]) is true, let reader + // be either ! AcquireReadableStreamBYOBReader(source) or + // ! AcquireReadableStreamDefaultReader(source), at the user agent’s + // discretion. + // Step 8: Otherwise, let reader be + // ! AcquireReadableStreamDefaultReader(source). + // We don't implement byte streams, so we always acquire a default reader. + { + ReadableStreamDefaultReader* reader = + CreateReadableStreamDefaultReader(cx, unwrappedSource); + if (!reader) { + return nullptr; + } + + MOZ_ASSERT(state->getFixedSlot(Slot_Reader).isUndefined()); + state->initFixedSlot(Slot_Reader, ObjectValue(*reader)); + } + + // Step 9: Let writer be ! AcquireWritableStreamDefaultWriter(dest). + { + WritableStreamDefaultWriter* writer = + CreateWritableStreamDefaultWriter(cx, unwrappedDest); + if (!writer) { + return nullptr; + } + + MOZ_ASSERT(state->getFixedSlot(Slot_Writer).isUndefined()); + state->initFixedSlot(Slot_Writer, ObjectValue(*writer)); + } + + // Step 10: Set source.[[disturbed]] to true. + unwrappedSource->setDisturbed(); + + state->initFlags(preventClose, preventAbort, preventCancel); + MOZ_ASSERT(state->preventClose() == preventClose); + MOZ_ASSERT(state->preventAbort() == preventAbort); + MOZ_ASSERT(state->preventCancel() == preventCancel); + + // Step 11: Let shuttingDown be false. + MOZ_ASSERT(!state->shuttingDown(), "should be set to false by initFlags"); + + // Step 12 ("Let promise be a new promise.") was performed by the caller and + // |promise| was its result. + + // Step 13: If signal is not undefined, + // XXX jwalden need JSAPI to add an algorithm/steps to an AbortSignal + + // Step 14: In parallel, using reader and writer, read all chunks from source + // and write them to dest. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, + "pipeTo"); + return nullptr; } const JSClass PipeToState::class_ = {"PipeToState", diff --git a/js/src/builtin/streams/PipeToState.h b/js/src/builtin/streams/PipeToState.h index 83de00feb288..5199c04c0f73 100644 --- a/js/src/builtin/streams/PipeToState.h +++ b/js/src/builtin/streams/PipeToState.h @@ -13,12 +13,21 @@ #include // uint32_t +#include "builtin/streams/ReadableStreamReader.h" // js::ReadableStreamDefaultReader +#include "builtin/streams/WritableStreamDefaultWriter.h" // js::WritableStreamDefaultWriter #include "js/Class.h" // JSClass -#include "js/Value.h" // JS::Int32Value +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::Int32Value, JS::ObjectValue #include "vm/NativeObject.h" // js::NativeObject +#include "vm/PromiseObject.h" // js::PromiseObject + +class JS_PUBLIC_API JSObject; namespace js { +class ReadableStream; +class WritableStream; + /** * PipeToState objects implement the local variables in Streams spec 3.4.11 * ReadableStreamPipeTo across all sub-operations that occur in that algorithm. @@ -28,11 +37,51 @@ class PipeToState : public NativeObject { /** * Memory layout for PipeToState instances. */ - enum Slots { Slot_Flags, SlotCount }; + enum Slots { + /** Integer bit field of various flags. */ + Slot_Flags = 0, + + /** + * The promise resolved or rejected when the overall pipe-to operation + * completes. + * + * This promise is created directly under |ReadableStreamPipeTo|, at the + * same time the corresponding |PipeToState| is created, so it is always + * same-compartment with this and is guaranteed to hold a |PromiseObject*| + * if initialization succeeded. + */ + Slot_Promise, + + /** + * A |ReadableStreamDefaultReader| used to read from the readable stream + * being piped from. + * + * This reader is created at the same time as its |PipeToState|, so this + * reader is same-compartment with this and is guaranteed to be a + * |ReadableStreamDefaultReader*| if initialization succeeds. + */ + Slot_Reader, + + /** + * A |WritableStreamDefaultWriter| used to write to the writable stream + * being piped to. + * + * This writer is created at the same time as its |PipeToState|, so this + * writer is same-compartment with this and is guaranteed to be a + * |WritableStreamDefaultWriter*| if initialization succeeds. + */ + Slot_Writer, + + SlotCount, + }; private: - enum Flags { - Flag_ShuttingDown = 1 << 0, + enum Flags : uint32_t { + Flag_ShuttingDown = 0b0001, + + Flag_PreventClose = 0b0010, + Flag_PreventAbort = 0b0100, + Flag_PreventCancel = 0b1000, }; uint32_t flags() const { return getFixedSlot(Slot_Flags).toInt32(); } @@ -43,13 +92,46 @@ class PipeToState : public NativeObject { public: static const JSClass class_; + PromiseObject* promise() const { + return &getFixedSlot(Slot_Promise).toObject().as(); + } + + ReadableStreamDefaultReader* reader() const { + return &getFixedSlot(Slot_Reader) + .toObject() + .as(); + } + + WritableStreamDefaultWriter* writer() const { + return &getFixedSlot(Slot_Writer) + .toObject() + .as(); + } + bool shuttingDown() const { return flags() & Flag_ShuttingDown; } void setShuttingDown() { MOZ_ASSERT(!shuttingDown()); setFlags(flags() | Flag_ShuttingDown); } - static PipeToState* create(JSContext* cx); + bool preventClose() const { return flags() & Flag_PreventClose; } + bool preventAbort() const { return flags() & Flag_PreventAbort; } + bool preventCancel() const { return flags() & Flag_PreventCancel; } + + void initFlags(bool preventClose, bool preventAbort, bool preventCancel) { + MOZ_ASSERT(getFixedSlot(Slot_Flags).isUndefined()); + + uint32_t flagBits = (preventClose ? Flag_PreventClose : 0) | + (preventAbort ? Flag_PreventAbort : 0) | + (preventCancel ? Flag_PreventCancel : 0); + setFlags(flagBits); + } + + static PipeToState* create(JSContext* cx, JS::Handle promise, + JS::Handle unwrappedSource, + JS::Handle unwrappedDest, + bool preventClose, bool preventAbort, + bool preventCancel, JS::Handle signal); }; } // namespace js diff --git a/js/src/builtin/streams/ReadableStream.cpp b/js/src/builtin/streams/ReadableStream.cpp index c2134c76b7fa..93a0501a8d97 100644 --- a/js/src/builtin/streams/ReadableStream.cpp +++ b/js/src/builtin/streams/ReadableStream.cpp @@ -368,10 +368,10 @@ static bool ReadableStream_pipeTo(JSContext* cx, unsigned argc, Value* vp) { // second argument destructuring pattern. But as |ToBoolean| is infallible // and has no observable side effects, we may as well do step 3 here too. bool preventClose, preventAbort, preventCancel; - Rooted signal(cx); + Rooted signalVal(cx); { // (P)(Re)use the |signal| root. - auto& v = signal; + auto& v = signalVal; if (!GetProperty(cx, options, cx->names().preventClose, &v)) { return false; @@ -388,7 +388,7 @@ static bool ReadableStream_pipeTo(JSContext* cx, unsigned argc, Value* vp) { } preventCancel = JS::ToBoolean(v); } - if (!GetProperty(cx, options, cx->names().signal, &signal)) { + if (!GetProperty(cx, options, cx->names().signal, &signalVal)) { return false; } @@ -416,7 +416,23 @@ static bool ReadableStream_pipeTo(JSContext* cx, unsigned argc, Value* vp) { // Step 4: If signal is not undefined, and signal is not an instance of the // AbortSignal interface, return a promise rejected with a TypeError // exception. - // XXX jwalden need some hooks for this, or something + Rooted signal(cx, nullptr); + do { + if (signalVal.isUndefined()) { + break; + } + + if (signalVal.isObject()) { + // XXX jwalden need some JSAPI hooks to detect AbortSignal instances, or + // something + + signal = &signalVal.toObject(); + } + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_PIPETO_BAD_SIGNAL); + return ReturnPromiseRejectedWithPendingError(cx, args); + } while (false); // Step 5: If ! IsReadableStreamLocked(this) is true, return a promise // rejected with a TypeError exception. diff --git a/js/src/builtin/streams/ReadableStreamOperations.cpp b/js/src/builtin/streams/ReadableStreamOperations.cpp index 58ea3fda5714..6ba3eada25c2 100644 --- a/js/src/builtin/streams/ReadableStreamOperations.cpp +++ b/js/src/builtin/streams/ReadableStreamOperations.cpp @@ -15,6 +15,7 @@ #include "builtin/Array.h" // js::NewDenseFullyAllocatedArray #include "builtin/Promise.h" // js::RejectPromiseWithPendingError +#include "builtin/streams/PipeToState.h" // js::PipeToState #include "builtin/streams/ReadableStream.h" // js::ReadableStream #include "builtin/streams/ReadableStreamController.h" // js::ReadableStream{,Default}Controller #include "builtin/streams/ReadableStreamDefaultControllerOperations.h" // js::ReadableStreamDefaultController{Close,Enqueue}, js::ReadableStreamControllerError, js::SourceAlgorithms @@ -617,7 +618,35 @@ PromiseObject* js::ReadableStreamPipeTo(JSContext* cx, Handle unwrappedDest, bool preventClose, bool preventAbort, bool preventCancel, - Handle signal) { - JS_ReportErrorASCII(cx, "XXX ceci n'est pas une pipe"); - return nullptr; + Handle signal) { + // Step 1. Assert: ! IsReadableStream(source) is true. + // Step 2. Assert: ! IsWritableStream(dest) is true. + // Step 3. Assert: Type(preventClose) is Boolean, Type(preventAbort) is + // Boolean, and Type(preventCancel) is Boolean. + // (These are guaranteed by the type system.) + + // Step 12: Let promise be a new promise. + // + // We reorder this so that this promise can be rejected and returned in case + // of internal error. + Rooted promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) { + return nullptr; + } + + // Steps 4-11, 13-14. + Rooted pipeToState( + cx, + PipeToState::create(cx, promise, unwrappedSource, unwrappedDest, + preventClose, preventAbort, preventCancel, signal)); + if (!pipeToState) { + if (!RejectPromiseWithPendingError(cx, promise)) { + return nullptr; + } + + return promise; + } + + // Step 15. + return promise; } diff --git a/js/src/builtin/streams/ReadableStreamOperations.h b/js/src/builtin/streams/ReadableStreamOperations.h index 38cf94020160..963fffe882d9 100644 --- a/js/src/builtin/streams/ReadableStreamOperations.h +++ b/js/src/builtin/streams/ReadableStreamOperations.h @@ -14,6 +14,8 @@ #include "js/RootingAPI.h" // JS::Handle #include "js/Value.h" // JS::Value +class JS_PUBLIC_API JSObject; + namespace js { class PromiseObject; @@ -38,7 +40,7 @@ extern MOZ_MUST_USE bool ReadableStreamTee( extern MOZ_MUST_USE PromiseObject* ReadableStreamPipeTo( JSContext* cx, JS::Handle unwrappedSource, JS::Handle unwrappedDest, bool preventClose, - bool preventAbort, bool preventCancel, JS::Handle signal); + bool preventAbort, bool preventCancel, JS::Handle signal); } // namespace js diff --git a/js/src/js.msg b/js/src/js.msg index 3c32c93dd027..45aba3938cd2 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -695,6 +695,7 @@ MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_CLOSE_PENDING_PULL, 0, JSEXN_TYPEERR, 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") +MSG_DEF(JSMSG_READABLESTREAM_PIPETO_BAD_SIGNAL, 0, JSEXN_TYPEERR, "signal must be either undefined or an AbortSignal") // WritableStream MSG_DEF(JSMSG_READABLESTREAM_UNDERLYINGSINK_TYPE_WRONG, 0, JSEXN_RANGEERR,"'underlyingSink.type' must be undefined.")