diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /js/src/builtin/streams | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/builtin/streams')
48 files changed, 12199 insertions, 0 deletions
diff --git a/js/src/builtin/streams/ClassSpecMacro.h b/js/src/builtin/streams/ClassSpecMacro.h new file mode 100644 index 0000000000..17d1fda930 --- /dev/null +++ b/js/src/builtin/streams/ClassSpecMacro.h @@ -0,0 +1,40 @@ +/* -*- 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/. */ + +/* A JS_STREAMS_CLASS_SPEC macro for defining streams classes. */ + +#ifndef builtin_streams_ClassSpecMacro_h +#define builtin_streams_ClassSpecMacro_h + +#include "gc/AllocKind.h" // js::gc::AllocKind +#include "js/Class.h" // js::ClassSpec, JSClass, JSCLASS_HAS_{CACHED_PROTO,RESERVED_SLOTS}, JS_NULL_CLASS_OPS +#include "js/ProtoKey.h" // JSProto_* +#include "vm/GlobalObject.h" // js::GenericCreate{Constructor,Prototype} + +#define JS_STREAMS_CLASS_SPEC(cls, nCtorArgs, nSlots, specFlags, classFlags, \ + classOps) \ + const js::ClassSpec cls::classSpec_ = { \ + js::GenericCreateConstructor<cls::constructor, nCtorArgs, \ + js::gc::AllocKind::FUNCTION>, \ + js::GenericCreatePrototype<cls>, \ + nullptr, \ + nullptr, \ + cls##_methods, \ + cls##_properties, \ + nullptr, \ + specFlags}; \ + \ + const JSClass cls::class_ = {#cls, \ + JSCLASS_HAS_RESERVED_SLOTS(nSlots) | \ + JSCLASS_HAS_CACHED_PROTO(JSProto_##cls) | \ + classFlags, \ + classOps, &cls::classSpec_}; \ + \ + const JSClass cls::protoClass_ = {#cls ".prototype", \ + JSCLASS_HAS_CACHED_PROTO(JSProto_##cls), \ + JS_NULL_CLASS_OPS, &cls::classSpec_}; + +#endif // builtin_streams_ClassSpecMacro_h diff --git a/js/src/builtin/streams/MiscellaneousOperations-inl.h b/js/src/builtin/streams/MiscellaneousOperations-inl.h new file mode 100644 index 0000000000..1592a2bdc5 --- /dev/null +++ b/js/src/builtin/streams/MiscellaneousOperations-inl.h @@ -0,0 +1,115 @@ +/* -*- 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/. */ + +/* Miscellaneous operations. */ + +#ifndef builtin_streams_MiscellaneousOperations_inl_h +#define builtin_streams_MiscellaneousOperations_inl_h + +#include "builtin/streams/MiscellaneousOperations.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "js/Promise.h" // JS::{Resolve,Reject}Promise +#include "js/RootingAPI.h" // JS::Rooted, JS::{,Mutable}Handle +#include "js/Value.h" // JS::UndefinedHandleValue, JS::Value +#include "vm/Compartment.h" // JS::Compartment +#include "vm/Interpreter.h" // js::Call +#include "vm/JSContext.h" // JSContext +#include "vm/JSObject.h" // JSObject +#include "vm/PromiseObject.h" // js::PromiseObject + +#include "vm/Compartment-inl.h" // JS::Compartment::wrap +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/JSObject-inl.h" // js::IsCallable + +namespace js { + +/** + * Streams spec, 6.3.5. PromiseCall ( F, V, args ) + * There must be 0-2 |args| arguments, all convertible to JS::Handle<JS::Value>. + */ +template <class... Args> +inline MOZ_MUST_USE JSObject* PromiseCall(JSContext* cx, + JS::Handle<JS::Value> F, + JS::Handle<JS::Value> V, + Args&&... args) { + cx->check(F); + cx->check(V); + cx->check(args...); + + // Step 1: Assert: ! IsCallable(F) is true. + MOZ_ASSERT(IsCallable(F)); + + // Step 2: Assert: V is not undefined. + MOZ_ASSERT(!V.isUndefined()); + + // Step 3: Assert: args is a List (implicit). + // Step 4: Let returnValue be Call(F, V, args). + JS::Rooted<JS::Value> rval(cx); + if (!Call(cx, F, V, args..., &rval)) { + // Step 5: If returnValue is an abrupt completion, return a promise rejected + // with returnValue.[[Value]]. + return PromiseRejectedWithPendingError(cx); + } + + // Step 6: Otherwise, return a promise resolved with returnValue.[[Value]]. + return PromiseObject::unforgeableResolve(cx, rval); +} + +/** + * Resolve the unwrapped promise |unwrappedPromise| with |value|. + */ +inline MOZ_MUST_USE bool ResolveUnwrappedPromiseWithValue( + JSContext* cx, JSObject* unwrappedPromise, JS::Handle<JS::Value> value) { + cx->check(value); + + JS::Rooted<JSObject*> promise(cx, unwrappedPromise); + if (!cx->compartment()->wrap(cx, &promise)) { + return false; + } + + return JS::ResolvePromise(cx, promise, value); +} + +/** + * Resolve the unwrapped promise |unwrappedPromise| with |undefined|. + */ +inline MOZ_MUST_USE bool ResolveUnwrappedPromiseWithUndefined( + JSContext* cx, JSObject* unwrappedPromise) { + return ResolveUnwrappedPromiseWithValue(cx, unwrappedPromise, + JS::UndefinedHandleValue); +} + +/** + * Reject the unwrapped promise |unwrappedPromise| with |error|, overwriting + * |*unwrappedPromise| with its wrapped form. + */ +inline MOZ_MUST_USE bool RejectUnwrappedPromiseWithError( + JSContext* cx, JS::MutableHandle<JSObject*> unwrappedPromise, + JS::Handle<JS::Value> error) { + cx->check(error); + + if (!cx->compartment()->wrap(cx, unwrappedPromise)) { + return false; + } + + return JS::RejectPromise(cx, unwrappedPromise, error); +} + +/** + * Reject the unwrapped promise |unwrappedPromise| with |error|. + */ +inline MOZ_MUST_USE bool RejectUnwrappedPromiseWithError( + JSContext* cx, JSObject* unwrappedPromise, JS::Handle<JS::Value> error) { + JS::Rooted<JSObject*> promise(cx, unwrappedPromise); + return RejectUnwrappedPromiseWithError(cx, &promise, error); +} + +} // namespace js + +#endif // builtin_streams_MiscellaneousOperations_inl_h diff --git a/js/src/builtin/streams/MiscellaneousOperations.cpp b/js/src/builtin/streams/MiscellaneousOperations.cpp new file mode 100644 index 0000000000..3d08886dcb --- /dev/null +++ b/js/src/builtin/streams/MiscellaneousOperations.cpp @@ -0,0 +1,194 @@ +/* -*- 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/. */ + +/* Miscellaneous operations. */ + +#include "builtin/streams/MiscellaneousOperations.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE +#include "mozilla/FloatingPoint.h" // mozilla::IsNaN + +#include "jsapi.h" // JS_ReportErrorNumberASCII + +#include "js/Conversions.h" // JS::ToNumber +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/RootingAPI.h" // JS::{,Mutable}Handle, JS::Rooted +#include "vm/Interpreter.h" // js::{Call,GetAndClearException} +#include "vm/JSContext.h" // JSContext +#include "vm/ObjectOperations.h" // js::GetProperty +#include "vm/PromiseObject.h" // js::PromiseObject +#include "vm/StringType.h" // js::PropertyName + +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/JSObject-inl.h" // js::IsCallable + +using JS::Handle; +using JS::MutableHandle; +using JS::ToNumber; +using JS::Value; + +MOZ_MUST_USE js::PromiseObject* js::PromiseRejectedWithPendingError( + JSContext* cx) { + Rooted<Value> exn(cx); + if (!cx->isExceptionPending() || !GetAndClearException(cx, &exn)) { + // Uncatchable error. This happens when a slow script is killed or a + // worker is terminated. Propagate the uncatchable error. This will + // typically kill off the calling asynchronous process: the caller + // can't hook its continuation to the new rejected promise. + return nullptr; + } + return PromiseObject::unforgeableReject(cx, exn); +} + +/*** 6.3. Miscellaneous operations ******************************************/ + +/** + * Streams spec, 6.3.1. + * CreateAlgorithmFromUnderlyingMethod ( underlyingObject, methodName, + * algoArgCount, extraArgs ) + * + * This function only partly implements the standard algorithm. We do not + * actually create a new JSFunction completely encapsulating the new algorithm. + * Instead, this just gets the specified method and checks for errors. It's the + * caller's responsibility to make sure that later, when the algorithm is + * "performed", the appropriate steps are carried out. + */ +MOZ_MUST_USE bool js::CreateAlgorithmFromUnderlyingMethod( + JSContext* cx, Handle<Value> underlyingObject, + const char* methodNameForErrorMessage, Handle<PropertyName*> methodName, + MutableHandle<Value> method) { + cx->check(underlyingObject); + cx->check(methodName); + cx->check(method); + + // Step 1: Assert: underlyingObject is not undefined. + MOZ_ASSERT(!underlyingObject.isUndefined()); + + // Step 2: Assert: ! IsPropertyKey(methodName) is true (implicit). + // Step 3: Assert: algoArgCount is 0 or 1 (omitted). + // Step 4: Assert: extraArgs is a List (omitted). + + // Step 5: Let method be ? GetV(underlyingObject, methodName). + if (!GetProperty(cx, underlyingObject, methodName, method)) { + return false; + } + + // Step 6: If method is not undefined, + if (!method.isUndefined()) { + // Step a: If ! IsCallable(method) is false, throw a TypeError + // exception. + if (!IsCallable(method)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_FUNCTION, methodNameForErrorMessage); + return false; + } + + // Step b: If algoArgCount is 0, return an algorithm that performs the + // following steps: + // Step i: Return ! PromiseCall(method, underlyingObject, + // extraArgs). + // Step c: Otherwise, return an algorithm that performs the following + // steps, taking an arg argument: + // Step i: Let fullArgs be a List consisting of arg followed by the + // elements of extraArgs in order. + // Step ii: Return ! PromiseCall(method, underlyingObject, + // fullArgs). + // (These steps are deferred to the code that performs the algorithm. + // See Perform{Write,Close}Algorithm, ReadableStreamControllerCancelSteps, + // and ReadableStreamControllerCallPullIfNeeded.) + return true; + } + + // Step 7: Return an algorithm which returns a promise resolved with + // undefined (implicit). + return true; +} + +/** + * Streams spec, 6.3.2. InvokeOrNoop ( O, P, args ) + * As it happens, all callers pass exactly one argument. + */ +MOZ_MUST_USE bool js::InvokeOrNoop(JSContext* cx, Handle<Value> O, + Handle<PropertyName*> P, Handle<Value> arg, + MutableHandle<Value> rval) { + cx->check(O, P, 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 (implicit). + // Step 4: Let method be ? GetV(O, P). + Rooted<Value> method(cx); + if (!GetProperty(cx, O, P, &method)) { + return false; + } + + // Step 5: If method is undefined, return. + if (method.isUndefined()) { + return true; + } + + // Step 6: Return ? Call(method, O, args). + return Call(cx, method, O, arg, rval); +} + +/** + * Streams spec, 6.3.7. ValidateAndNormalizeHighWaterMark ( highWaterMark ) + */ +MOZ_MUST_USE bool js::ValidateAndNormalizeHighWaterMark( + JSContext* cx, Handle<Value> highWaterMarkVal, double* highWaterMark) { + // Step 1: Set highWaterMark to ? ToNumber(highWaterMark). + if (!ToNumber(cx, highWaterMarkVal, highWaterMark)) { + return false; + } + + // Step 2: If highWaterMark is NaN or highWaterMark < 0, throw a RangeError + // exception. + if (mozilla::IsNaN(*highWaterMark) || *highWaterMark < 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_STREAM_INVALID_HIGHWATERMARK); + return false; + } + + // Step 3: Return highWaterMark. + return true; +} + +/** + * Streams spec, 6.3.8. MakeSizeAlgorithmFromSizeFunction ( size ) + * + * The standard makes a big deal of turning JavaScript functions (grubby, + * touched by users, covered with germs) into algorithms (pristine, + * respectable, purposeful). We don't bother. Here we only check for errors and + * leave `size` unchanged. Then, in ReadableStreamDefaultControllerEnqueue and + * WritableStreamDefaultControllerGetChunkSize where this value is used, we + * check for undefined and behave as if we had "made" an "algorithm" for it. + */ +MOZ_MUST_USE bool js::MakeSizeAlgorithmFromSizeFunction(JSContext* cx, + Handle<Value> size) { + cx->check(size); + + // Step 1: If size is undefined, return an algorithm that returns 1. + if (size.isUndefined()) { + // Deferred. Size algorithm users must check for undefined. + return true; + } + + // Step 2: If ! IsCallable(size) is false, throw a TypeError exception. + if (!IsCallable(size)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, + "ReadableStream argument options.size"); + return false; + } + + // Step 3: Return an algorithm that performs the following steps, taking a + // chunk argument: + // a. Return ? Call(size, undefined, « chunk »). + // Deferred. Size algorithm users must know how to call the size function. + return true; +} diff --git a/js/src/builtin/streams/MiscellaneousOperations.h b/js/src/builtin/streams/MiscellaneousOperations.h new file mode 100644 index 0000000000..93dbedbfbc --- /dev/null +++ b/js/src/builtin/streams/MiscellaneousOperations.h @@ -0,0 +1,86 @@ +/* -*- 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/. */ + +/* Miscellaneous operations. */ + +#ifndef builtin_streams_MiscellaneousOperations_h +#define builtin_streams_MiscellaneousOperations_h + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jstypes.h" // JS_PUBLIC_API +#include "js/CallArgs.h" // JS::CallArgs +#include "js/RootingAPI.h" // JS::{,Mutable}Handle +#include "js/Value.h" // JS::Value +#include "vm/JSObject.h" // JSObject +#include "vm/PromiseObject.h" // js::PromiseObject + +struct JS_PUBLIC_API JSContext; + +namespace js { + +class PropertyName; + +extern MOZ_MUST_USE PromiseObject* PromiseRejectedWithPendingError( + JSContext* cx); + +inline MOZ_MUST_USE bool ReturnPromiseRejectedWithPendingError( + JSContext* cx, const JS::CallArgs& args) { + PromiseObject* promise = PromiseRejectedWithPendingError(cx); + if (!promise) { + return false; + } + + args.rval().setObject(*promise); + return true; +} + +/** + * Streams spec, 6.3.1. + * CreateAlgorithmFromUnderlyingMethod ( underlyingObject, methodName, + * algoArgCount, extraArgs ) + * + * This function only partly implements the standard algorithm. We do not + * actually create a new JSFunction completely encapsulating the new algorithm. + * Instead, this just gets the specified method and checks for errors. It's the + * caller's responsibility to make sure that later, when the algorithm is + * "performed", the appropriate steps are carried out. + */ +extern MOZ_MUST_USE bool CreateAlgorithmFromUnderlyingMethod( + JSContext* cx, JS::Handle<JS::Value> underlyingObject, + const char* methodNameForErrorMessage, JS::Handle<PropertyName*> methodName, + JS::MutableHandle<JS::Value> method); + +/** + * Streams spec, 6.3.2. InvokeOrNoop ( O, P, args ) + * As it happens, all callers pass exactly one argument. + */ +extern MOZ_MUST_USE bool InvokeOrNoop(JSContext* cx, JS::Handle<JS::Value> O, + JS::Handle<PropertyName*> P, + JS::Handle<JS::Value> arg, + JS::MutableHandle<JS::Value> rval); + +/** + * Streams spec, 6.3.7. ValidateAndNormalizeHighWaterMark ( highWaterMark ) + */ +extern MOZ_MUST_USE bool ValidateAndNormalizeHighWaterMark( + JSContext* cx, JS::Handle<JS::Value> highWaterMarkVal, + double* highWaterMark); + +/** + * Streams spec, 6.3.8. MakeSizeAlgorithmFromSizeFunction ( size ) + */ +extern MOZ_MUST_USE bool MakeSizeAlgorithmFromSizeFunction( + JSContext* cx, JS::Handle<JS::Value> size); + +template <class T> +inline bool IsMaybeWrapped(const JS::Handle<JS::Value> v) { + return v.isObject() && v.toObject().canUnwrapAs<T>(); +} + +} // namespace js + +#endif // builtin_streams_MiscellaneousOperations_h diff --git a/js/src/builtin/streams/PipeToState-inl.h b/js/src/builtin/streams/PipeToState-inl.h new file mode 100644 index 0000000000..5006185aec --- /dev/null +++ b/js/src/builtin/streams/PipeToState-inl.h @@ -0,0 +1,53 @@ +/* -*- 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/. */ + +/* ReadableStream pipe-to operation captured state. */ + +#ifndef builtin_streams_PipeToState_inl_h +#define builtin_streams_PipeToState_inl_h + +#include "builtin/streams/PipeToState.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jstypes.h" // JS_PUBLIC_API + +#include "js/RootingAPI.h" // JS::Handle +#include "vm/JSContext.h" // JSContext +#include "vm/Runtime.h" // JSRuntime + +#include "vm/Compartment-inl.h" // js::UnwrapAndDowncastValue +#include "vm/JSContext-inl.h" // JSContext::check + +struct JS_PUBLIC_API JSContext; +class JS_PUBLIC_API JSObject; + +namespace js { + +/** + * Returns the unwrapped |AbortSignal| instance associated with a given pipe-to + * operation. + * + * The pipe-to operation must be known to have had an |AbortSignal| associated + * with it. + * + * If the signal is a wrapper, it will be unwrapped, so the result might not be + * an object from the currently active compartment. + */ +inline MOZ_MUST_USE JSObject* UnwrapSignalFromPipeToState( + JSContext* cx, JS::Handle<PipeToState*> pipeToState) { + cx->check(pipeToState); + + MOZ_ASSERT(pipeToState->hasSignal()); + return UnwrapAndDowncastValue( + cx, pipeToState->getFixedSlot(PipeToState::Slot_Signal), + cx->runtime()->maybeAbortSignalClass()); +} + +} // namespace js + +#endif // builtin_streams_PipeToState_inl_h diff --git a/js/src/builtin/streams/PipeToState.cpp b/js/src/builtin/streams/PipeToState.cpp new file mode 100644 index 0000000000..f79e81d3fe --- /dev/null +++ b/js/src/builtin/streams/PipeToState.cpp @@ -0,0 +1,1271 @@ +/* -*- 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/. */ + +/* ReadableStream.prototype.pipeTo state. */ + +#include "builtin/streams/PipeToState-inl.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE +#include "mozilla/Maybe.h" // mozilla::Maybe, mozilla::Nothing, mozilla::Some + +#include "jsapi.h" // JS_ReportErrorNumberASCII + +#include "builtin/Promise.h" // js::RejectPromiseWithPendingError +#include "builtin/streams/ReadableStream.h" // js::ReadableStream +#include "builtin/streams/ReadableStreamReader.h" // js::CreateReadableStreamDefaultReader, js::ForAuthorCodeBool, js::ReadableStreamDefaultReader, js::ReadableStreamReaderGenericRelease +#include "builtin/streams/WritableStream.h" // js::WritableStream +#include "builtin/streams/WritableStreamDefaultWriter.h" // js::CreateWritableStreamDefaultWriter, js::WritableStreamDefaultWriter +#include "builtin/streams/WritableStreamOperations.h" // js::WritableStreamCloseQueuedOrInFlight +#include "builtin/streams/WritableStreamWriterOperations.h" // js::WritableStreamDefaultWriter{GetDesiredSize,Release,Write} +#include "js/CallArgs.h" // JS::CallArgsFromVp, JS::CallArgs +#include "js/Class.h" // JSClass, JSCLASS_HAS_RESERVED_SLOTS +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/Promise.h" // JS::AddPromiseReactions +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted +#include "js/Value.h" // JS::{,Int32,Magic,Object}Value, JS::UndefinedHandleValue +#include "vm/JSContext.h" // JSContext +#include "vm/PromiseObject.h" // js::PromiseObject +#include "vm/Runtime.h" // JSRuntime + +#include "builtin/HandlerFunction-inl.h" // js::ExtraValueFromHandler, js::NewHandler{,WithExtraValue}, js::TargetFromHandler +#include "builtin/streams/ReadableStreamReader-inl.h" // js::UnwrapReaderFromStream, js::UnwrapStreamFromReader +#include "builtin/streams/WritableStream-inl.h" // js::UnwrapWriterFromStream +#include "builtin/streams/WritableStreamDefaultWriter-inl.h" // js::UnwrapStreamFromWriter +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/JSObject-inl.h" // js::NewBuiltinClassInstance +#include "vm/Realm-inl.h" // js::AutoRealm + +using mozilla::Maybe; +using mozilla::Nothing; +using mozilla::Some; + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::Int32Value; +using JS::MagicValue; +using JS::ObjectValue; +using JS::Rooted; +using JS::UndefinedHandleValue; +using JS::Value; + +using js::ExtraValueFromHandler; +using js::GetErrorMessage; +using js::NewHandler; +using js::NewHandlerWithExtraValue; +using js::PipeToState; +using js::PromiseObject; +using js::ReadableStream; +using js::ReadableStreamDefaultReader; +using js::ReadableStreamReaderGenericRelease; +using js::TargetFromHandler; +using js::UnwrapReaderFromStream; +using js::UnwrapStreamFromWriter; +using js::UnwrapWriterFromStream; +using js::WritableStream; +using js::WritableStreamDefaultWriter; +using js::WritableStreamDefaultWriterRelease; +using js::WritableStreamDefaultWriterWrite; + +static ReadableStream* GetUnwrappedSource(JSContext* cx, + Handle<PipeToState*> state) { + cx->check(state); + + Rooted<ReadableStreamDefaultReader*> reader(cx, state->reader()); + cx->check(reader); + + return UnwrapStreamFromReader(cx, reader); +} + +static WritableStream* GetUnwrappedDest(JSContext* cx, + Handle<PipeToState*> state) { + cx->check(state); + + Rooted<WritableStreamDefaultWriter*> writer(cx, state->writer()); + cx->check(writer); + + return UnwrapStreamFromWriter(cx, writer); +} + +static bool WritableAndNotClosing(const WritableStream* unwrappedDest) { + return unwrappedDest->writable() && + WritableStreamCloseQueuedOrInFlight(unwrappedDest); +} + +static MOZ_MUST_USE bool Finalize(JSContext* cx, Handle<PipeToState*> state, + Handle<Maybe<Value>> error) { + cx->check(state); + cx->check(error); + + // Step 1: Perform ! WritableStreamDefaultWriterRelease(writer). + Rooted<WritableStreamDefaultWriter*> writer(cx, state->writer()); + cx->check(writer); + if (!WritableStreamDefaultWriterRelease(cx, writer)) { + return false; + } + + // Step 2: Perform ! ReadableStreamReaderGenericRelease(reader). + Rooted<ReadableStreamDefaultReader*> reader(cx, state->reader()); + cx->check(reader); + if (!ReadableStreamReaderGenericRelease(cx, reader)) { + return false; + } + + // Step 3: If signal is not undefined, remove abortAlgorithm from signal. + // XXX + + Rooted<PromiseObject*> promise(cx, state->promise()); + cx->check(promise); + + // Step 4: If error was given, reject promise with error. + if (error.isSome()) { + Rooted<Value> errorVal(cx, *error.get()); + return PromiseObject::reject(cx, promise, errorVal); + } + + // Step 5: Otherwise, resolve promise with undefined. + return PromiseObject::resolve(cx, promise, UndefinedHandleValue); +} + +static MOZ_MUST_USE bool Finalize(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<PipeToState*> state(cx, TargetFromHandler<PipeToState>(args)); + cx->check(state); + + Rooted<Maybe<Value>> optionalError(cx, Nothing()); + if (Value maybeError = ExtraValueFromHandler(args); + !maybeError.isMagic(JS_READABLESTREAM_PIPETO_FINALIZE_WITHOUT_ERROR)) { + optionalError = Some(maybeError); + } + cx->check(optionalError); + + if (!Finalize(cx, state, optionalError)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +// Shutdown with an action, steps d-f: +// d. Let p be the result of performing action. +// e. Upon fulfillment of p, finalize, passing along originalError if it was +// given. +// f. Upon rejection of p with reason newError, finalize with newError. +static MOZ_MUST_USE bool ActAndFinalize(JSContext* cx, + Handle<PipeToState*> state, + Handle<Maybe<Value>> error) { + // Step d: Let p be the result of performing action. + Rooted<JSObject*> p(cx); + switch (state->shutdownAction()) { + // This corresponds to the action performed by |abortAlgorithm| in + // ReadableStreamPipeTo step 14.1.5. + case PipeToState::ShutdownAction::AbortAlgorithm: { + MOZ_ASSERT(error.get().isSome()); + + // From ReadableStreamPipeTo: + // Step 14.1.2: Let actions be an empty ordered set. + // Step 14.1.3: If preventAbort is false, append the following action to + // actions: + // Step 14.1.3.1: If dest.[[state]] is "writable", return + // ! WritableStreamAbort(dest, error). + // Step 14.1.3.2: Otherwise, return a promise resolved with undefined. + // Step 14.1.4: If preventCancel is false, append the following action + // action to actions: + // Step 14.1.4.1.: If source.[[state]] is "readable", return + // ! ReadableStreamCancel(source, error). + // Step 14.1.4.2: Otherwise, return a promise resolved with undefined. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, + "any required actions during abortAlgorithm"); + return false; + } + + // This corresponds to the action in "shutdown with an action of + // ! WritableStreamAbort(dest, source.[[storedError]]) and with + // source.[[storedError]]." + case PipeToState::ShutdownAction::AbortDestStream: { + MOZ_ASSERT(error.get().isSome()); + + Rooted<WritableStream*> unwrappedDest(cx, GetUnwrappedDest(cx, state)); + if (!unwrappedDest) { + return false; + } + + Rooted<Value> sourceStoredError(cx, *error.get()); + cx->check(sourceStoredError); + + p = WritableStreamAbort(cx, unwrappedDest, sourceStoredError); + break; + } + + // This corresponds to two actions: + // + // * The action in "shutdown with an action of + // ! ReadableStreamCancel(source, dest.[[storedError]]) and with + // dest.[[storedError]]" as used in "Errors must be propagated backward: + // if dest.[[state]] is or becomes 'errored'". + // * The action in "shutdown with an action of + // ! ReadableStreamCancel(source, destClosed) and with destClosed" as used + // in "Closing must be propagated backward: if + // ! WritableStreamCloseQueuedOrInFlight(dest) is true or dest.[[state]] + // is 'closed'". + // + // The different reason-values are passed as |error|. + case PipeToState::ShutdownAction::CancelSource: { + MOZ_ASSERT(error.get().isSome()); + + Rooted<ReadableStream*> unwrappedSource(cx, + GetUnwrappedSource(cx, state)); + if (!unwrappedSource) { + return false; + } + + Rooted<Value> reason(cx, *error.get()); + cx->check(reason); + + p = ReadableStreamCancel(cx, unwrappedSource, reason); + break; + } + + // This corresponds to the action in "shutdown with an action of + // ! WritableStreamDefaultWriterCloseWithErrorPropagation(writer)" as done + // in "Closing must be propagated forward: if source.[[state]] is or becomes + // 'closed'". + case PipeToState::ShutdownAction::CloseWriterWithErrorPropagation: { + MOZ_ASSERT(error.get().isNothing()); + + Rooted<WritableStreamDefaultWriter*> writer(cx, state->writer()); + cx->check(writer); // just for good measure: we don't depend on this + + p = WritableStreamDefaultWriterCloseWithErrorPropagation(cx, writer); + break; + } + } + if (!p) { + return false; + } + + // Step e: Upon fulfillment of p, finalize, passing along originalError if it + // was given. + Rooted<JSFunction*> onFulfilled(cx); + { + Rooted<Value> optionalError( + cx, error.isSome() + ? *error.get() + : MagicValue(JS_READABLESTREAM_PIPETO_FINALIZE_WITHOUT_ERROR)); + onFulfilled = NewHandlerWithExtraValue(cx, Finalize, state, optionalError); + if (!onFulfilled) { + return false; + } + } + + // Step f: Upon rejection of p with reason newError, finalize with newError. + auto OnRejected = [](JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<PipeToState*> state(cx, TargetFromHandler<PipeToState>(args)); + cx->check(state); + + Rooted<Maybe<Value>> newError(cx, Some(args[0])); + cx->check(newError); + if (!Finalize(cx, state, newError)) { + return false; + } + + args.rval().setUndefined(); + return true; + }; + + Rooted<JSFunction*> onRejected(cx, NewHandler(cx, OnRejected, state)); + if (!onRejected) { + return false; + } + + return JS::AddPromiseReactions(cx, p, onFulfilled, onRejected); +} + +static MOZ_MUST_USE bool ActAndFinalize(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<PipeToState*> state(cx, TargetFromHandler<PipeToState>(args)); + cx->check(state); + + Rooted<Maybe<Value>> optionalError(cx, Nothing()); + if (Value maybeError = ExtraValueFromHandler(args); + !maybeError.isMagic(JS_READABLESTREAM_PIPETO_FINALIZE_WITHOUT_ERROR)) { + optionalError = Some(maybeError); + } + cx->check(optionalError); + + if (!ActAndFinalize(cx, state, optionalError)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +// Shutdown with an action: if any of the above requirements ask to shutdown +// with an action action, optionally with an error originalError, then: +static MOZ_MUST_USE bool ShutdownWithAction( + JSContext* cx, Handle<PipeToState*> state, + PipeToState::ShutdownAction action, Handle<Maybe<Value>> originalError) { + cx->check(state); + cx->check(originalError); + + // Step a: If shuttingDown is true, abort these substeps. + if (state->shuttingDown()) { + return true; + } + + // Step b: Set shuttingDown to true. + state->setShuttingDown(); + + // Save the action away for later, potentially asynchronous, use. + state->setShutdownAction(action); + + // Step c: If dest.[[state]] is "writable" and + // ! WritableStreamCloseQueuedOrInFlight(dest) is false, + WritableStream* unwrappedDest = GetUnwrappedDest(cx, state); + if (!unwrappedDest) { + return false; + } + if (WritableAndNotClosing(unwrappedDest)) { + // Step c.i: If any chunks have been read but not yet written, write them + // to dest. + // + // Any chunk that has been read, will have been processed and a pending + // write for it created by this point. (A pending read has not been "read". + // And any pending read, will not be processed into a pending write because + // of the |state->setShuttingDown()| above in concert with the early exit + // in this case in |ReadFulfilled|.) + + // Step c.ii: Wait until every chunk that has been read has been written + // (i.e. the corresponding promises have settled). + if (PromiseObject* p = state->lastWriteRequest()) { + Rooted<PromiseObject*> lastWriteRequest(cx, p); + + Rooted<Value> extra( + cx, + originalError.isSome() + ? *originalError.get() + : MagicValue(JS_READABLESTREAM_PIPETO_FINALIZE_WITHOUT_ERROR)); + + Rooted<JSFunction*> actAndfinalize( + cx, NewHandlerWithExtraValue(cx, ActAndFinalize, state, extra)); + if (!actAndfinalize) { + return false; + } + + return JS::AddPromiseReactions(cx, lastWriteRequest, actAndfinalize, + actAndfinalize); + } + + // If no last write request was ever created, we can fall through and + // synchronously perform the remaining steps. + } + + // Step d: Let p be the result of performing action. + // Step e: Upon fulfillment of p, finalize, passing along originalError if it + // was given. + // Step f: Upon rejection of p with reason newError, finalize with newError. + return ActAndFinalize(cx, state, originalError); +} + +// Shutdown: if any of the above requirements or steps ask to shutdown, +// optionally with an error error, then: +static MOZ_MUST_USE bool Shutdown(JSContext* cx, Handle<PipeToState*> state, + Handle<Maybe<Value>> error) { + cx->check(state); + cx->check(error); + + // Step a: If shuttingDown is true, abort these substeps. + if (state->shuttingDown()) { + return true; + } + + // Step b: Set shuttingDown to true. + state->setShuttingDown(); + + // Step c: If dest.[[state]] is "writable" and + // ! WritableStreamCloseQueuedOrInFlight(dest) is false, + WritableStream* unwrappedDest = GetUnwrappedDest(cx, state); + if (!unwrappedDest) { + return false; + } + if (WritableAndNotClosing(unwrappedDest)) { + // Step 1: If any chunks have been read but not yet written, write them to + // dest. + // + // Any chunk that has been read, will have been processed and a pending + // write for it created by this point. (A pending read has not been "read". + // And any pending read, will not be processed into a pending write because + // of the |state->setShuttingDown()| above in concert with the early exit + // in this case in |ReadFulfilled|.) + + // Step 2: Wait until every chunk that has been read has been written + // (i.e. the corresponding promises have settled). + if (PromiseObject* p = state->lastWriteRequest()) { + Rooted<PromiseObject*> lastWriteRequest(cx, p); + + Rooted<Value> extra( + cx, + error.isSome() + ? *error.get() + : MagicValue(JS_READABLESTREAM_PIPETO_FINALIZE_WITHOUT_ERROR)); + + Rooted<JSFunction*> finalize( + cx, NewHandlerWithExtraValue(cx, Finalize, state, extra)); + if (!finalize) { + return false; + } + + return JS::AddPromiseReactions(cx, lastWriteRequest, finalize, finalize); + } + + // If no last write request was ever created, we can fall through and + // synchronously perform the remaining steps. + } + + // Step d: Finalize, passing along error if it was given. + return Finalize(cx, state, error); +} + +/** + * Streams spec, 3.4.11. ReadableStreamPipeTo step 14: + * "a. Errors must be propagated forward: if source.[[state]] is or becomes + * 'errored', then..." + */ +static MOZ_MUST_USE bool OnSourceErrored( + JSContext* cx, Handle<PipeToState*> state, + Handle<ReadableStream*> unwrappedSource) { + cx->check(state); + + Rooted<Maybe<Value>> storedError(cx, Some(unwrappedSource->storedError())); + if (!cx->compartment()->wrap(cx, &storedError)) { + return false; + } + + // If |source| becomes errored not during a pending read, it's clear we must + // react immediately. + // + // But what if |source| becomes errored *during* a pending read? Should this + // first error, or the pending-read second error, predominate? Two semantics + // are possible when |source|/|dest| become closed or errored while there's a + // pending read: + // + // 1. Wait until the read fulfills or rejects, then respond to the + // closure/error without regard to the read having fulfilled or rejected. + // (This will simply not react to the read being rejected, or it will + // queue up the read chunk to be written during shutdown.) + // 2. React to the closure/error immediately per "Error and close states + // must be propagated". Then when the read fulfills or rejects later, do + // nothing. + // + // The spec doesn't clearly require either semantics. It requires that + // *already-read* chunks be written (at least if |dest| didn't become errored + // or closed such that no further writes can occur). But it's silent as to + // not-fully-read chunks. (These semantic differences may only be observable + // with very carefully constructed readable/writable streams.) + // + // It seems best, generally, to react to the temporally-earliest problem that + // arises, so we implement option #2. (Blink, in contrast, currently + // implements option #1.) + // + // All specified reactions to a closure/error invoke either the shutdown, or + // shutdown with an action, algorithms. Those algorithms each abort if either + // shutdown algorithm has already been invoked. So we don't need to do + // anything special here to deal with a pending read. + + // ii. Otherwise (if preventAbort is true), shutdown with + // source.[[storedError]]. + if (state->preventAbort()) { + if (!Shutdown(cx, state, storedError)) { + return false; + } + } + // i. (If preventAbort is false,) shutdown with an action of + // ! WritableStreamAbort(dest, source.[[storedError]]) and with + // source.[[storedError]]. + else { + if (!ShutdownWithAction(cx, state, + PipeToState::ShutdownAction::AbortDestStream, + storedError)) { + return false; + } + } + + return true; +} + +/** + * Streams spec, 3.4.11. ReadableStreamPipeTo step 14: + * "b. Errors must be propagated backward: if dest.[[state]] is or becomes + * 'errored', then..." + */ +static MOZ_MUST_USE bool OnDestErrored(JSContext* cx, + Handle<PipeToState*> state, + Handle<WritableStream*> unwrappedDest) { + cx->check(state); + + Rooted<Maybe<Value>> storedError(cx, Some(unwrappedDest->storedError())); + if (!cx->compartment()->wrap(cx, &storedError)) { + return false; + } + + // As in |OnSourceErrored| above, we must deal with the case of |dest| + // erroring before a pending read has fulfilled or rejected. + // + // As noted there, we handle the *first* error that arises. And because this + // algorithm immediately invokes a shutdown algorithm, and shutting down will + // inhibit future shutdown attempts, we don't need to do anything special + // *here*, either. + + // ii. Otherwise (if preventCancel is true), shutdown with + // dest.[[storedError]]. + if (state->preventCancel()) { + if (!Shutdown(cx, state, storedError)) { + return false; + } + } + // i. If preventCancel is false, shutdown with an action of + // ! ReadableStreamCancel(source, dest.[[storedError]]) and with + // dest.[[storedError]]. + else { + if (!ShutdownWithAction(cx, state, + PipeToState::ShutdownAction::CancelSource, + storedError)) { + return false; + } + } + + return true; +} + +/** + * Streams spec, 3.4.11. ReadableStreamPipeTo step 14: + * "c. Closing must be propagated forward: if source.[[state]] is or becomes + * 'closed', then..." + */ +static MOZ_MUST_USE bool OnSourceClosed(JSContext* cx, + Handle<PipeToState*> state) { + cx->check(state); + + Rooted<Maybe<Value>> noError(cx, Nothing()); + + // It shouldn't be possible for |source| to become closed *during* a pending + // read: such spontaneous closure *should* be enqueued for processing *after* + // the settling of the pending read. (Note also that a [[closedPromise]] + // resolution in |ReadableStreamClose| occurs only after all pending reads are + // resolved.) So we need not do anything to handle a source closure while a + // read is in progress. + + // ii. Otherwise (if preventClose is true), shutdown. + if (state->preventClose()) { + if (!Shutdown(cx, state, noError)) { + return false; + } + } + // i. If preventClose is false, shutdown with an action of + // ! WritableStreamDefaultWriterCloseWithErrorPropagation(writer). + else { + if (!ShutdownWithAction( + cx, state, + PipeToState::ShutdownAction::CloseWriterWithErrorPropagation, + noError)) { + return false; + } + } + + return true; +} + +/** + * Streams spec, 3.4.11. ReadableStreamPipeTo step 14: + * "d. Closing must be propagated backward: if + * ! WritableStreamCloseQueuedOrInFlight(dest) is true or dest.[[state]] is + * 'closed', then..." + */ +static MOZ_MUST_USE bool OnDestClosed(JSContext* cx, + Handle<PipeToState*> state) { + cx->check(state); + + // i. Assert: no chunks have been read or written. + // + // This assertion holds when this function is called by + // |SourceOrDestErroredOrClosed|, before any async internal piping operations + // happen. + // + // But it wouldn't hold for streams that can spontaneously close of their own + // accord, like say a hypothetical DOM TCP socket. I think? + // + // XXX Add this assertion if it really does hold (and is easily performed), + // else report a spec bug. + + // ii. Let destClosed be a new TypeError. + Rooted<Maybe<Value>> destClosed(cx, Nothing()); + { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAM_WRITE_CLOSING_OR_CLOSED); + Rooted<Value> v(cx); + if (!cx->isExceptionPending() || !GetAndClearException(cx, &v)) { + return false; + } + + destClosed = Some(v.get()); + } + + // As in all the |On{Source,Dest}{Closed,Errored}| above, we must consider the + // possibility that we're in the middle of a pending read. |state->writer()| + // has a lock on |dest| here, so we know only we can be writing chunks to + // |dest| -- but there's no reason why |dest| couldn't become closed of its + // own accord here (for example, a socket might become closed on its own), and + // such closure may or may not be equivalent to error. + // + // For the reasons noted in |OnSourceErrored|, we process closure in the + // middle of a pending read immediately, without delaying for that read to + // fulfill or reject. We trigger a shutdown operation below, which will + // ensure shutdown only occurs once, so we need not do anything special here. + + // iv. Otherwise (if preventCancel is true), shutdown with destClosed. + if (state->preventCancel()) { + if (!Shutdown(cx, state, destClosed)) { + return false; + } + } + // iii. If preventCancel is false, shutdown with an action of + // ! ReadableStreamCancel(source, destClosed) and with destClosed. + else { + if (!ShutdownWithAction( + cx, state, PipeToState::ShutdownAction::CancelSource, destClosed)) { + return false; + } + } + + return true; +} + +/** + * Streams spec, 3.4.11. ReadableStreamPipeTo step 14: + * "Error and close states must be propagated: the following conditions must be + * applied in order.", as applied at the very start of piping, before any reads + * from source or writes to dest have been triggered. + */ +static MOZ_MUST_USE bool SourceOrDestErroredOrClosed( + JSContext* cx, Handle<PipeToState*> state, + Handle<ReadableStream*> unwrappedSource, + Handle<WritableStream*> unwrappedDest, bool* erroredOrClosed) { + cx->check(state); + + *erroredOrClosed = true; + + // a. Errors must be propagated forward: if source.[[state]] is or becomes + // "errored", then + if (unwrappedSource->errored()) { + return OnSourceErrored(cx, state, unwrappedSource); + } + + // b. Errors must be propagated backward: if dest.[[state]] is or becomes + // "errored", then + if (unwrappedDest->errored()) { + return OnDestErrored(cx, state, unwrappedDest); + } + + // c. Closing must be propagated forward: if source.[[state]] is or becomes + // "closed", then + if (unwrappedSource->closed()) { + return OnSourceClosed(cx, state); + } + + // d. Closing must be propagated backward: if + // ! WritableStreamCloseQueuedOrInFlight(dest) is true or dest.[[state]] is + // "closed", then + if (WritableStreamCloseQueuedOrInFlight(unwrappedDest) || + unwrappedDest->closed()) { + return OnDestClosed(cx, state); + } + + *erroredOrClosed = false; + return true; +} + +static MOZ_MUST_USE bool OnSourceClosed(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<PipeToState*> state(cx, TargetFromHandler<PipeToState>(args)); + cx->check(state); + + if (!OnSourceClosed(cx, state)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static MOZ_MUST_USE bool OnSourceErrored(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<PipeToState*> state(cx, TargetFromHandler<PipeToState>(args)); + cx->check(state); + + Rooted<ReadableStream*> unwrappedSource(cx, GetUnwrappedSource(cx, state)); + if (!unwrappedSource) { + return false; + } + + if (!OnSourceErrored(cx, state, unwrappedSource)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static MOZ_MUST_USE bool OnDestClosed(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<PipeToState*> state(cx, TargetFromHandler<PipeToState>(args)); + cx->check(state); + + if (!OnDestClosed(cx, state)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static MOZ_MUST_USE bool OnDestErrored(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<PipeToState*> state(cx, TargetFromHandler<PipeToState>(args)); + cx->check(state); + + Rooted<WritableStream*> unwrappedDest(cx, GetUnwrappedDest(cx, state)); + if (!unwrappedDest) { + return false; + } + + if (!OnDestErrored(cx, state, unwrappedDest)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +template <class StreamAccessor, class Stream> +static inline JSObject* GetClosedPromise( + JSContext* cx, Handle<Stream*> unwrappedStream, + StreamAccessor* (&unwrapAccessorFromStream)(JSContext*, Handle<Stream*>)) { + StreamAccessor* unwrappedAccessor = + unwrapAccessorFromStream(cx, unwrappedStream); + if (!unwrappedAccessor) { + return nullptr; + } + + return unwrappedAccessor->closedPromise(); +} + +static MOZ_MUST_USE bool ReadFromSource(JSContext* cx, + Handle<PipeToState*> state); + +static bool ReadFulfilled(JSContext* cx, Handle<PipeToState*> state, + Handle<JSObject*> result) { + cx->check(state); + cx->check(result); + + state->clearPendingRead(); + + // "Shutdown must stop activity: if shuttingDown becomes true, the user agent + // must not initiate further reads from reader, and must only perform writes + // of already-read chunks". + // + // We may reach this point after |On{Source,Dest}{Clos,Error}ed| has responded + // to an out-of-band change. Per the comment in |OnSourceErrored|, we want to + // allow the implicated shutdown to proceed, and we don't want to interfere + // with or additionally alter its operation. Particularly, we don't want to + // queue up the successfully-read chunk (if there was one, and this isn't just + // reporting "done") to be written: it wasn't "already-read" when that + // error/closure happened. + // + // All specified reactions to a closure/error invoke either the shutdown, or + // shutdown with an action, algorithms. Those algorithms each abort if either + // shutdown algorithm has already been invoked. So we check for shutdown here + // in case of asynchronous closure/error and abort if shutdown has already + // started (and possibly finished). + if (state->shuttingDown()) { + return true; + } + + { + bool done; + { + Rooted<Value> doneVal(cx); + if (!GetProperty(cx, result, result, cx->names().done, &doneVal)) { + return false; + } + done = doneVal.toBoolean(); + } + + if (done) { + // All chunks have been read from |reader| and written to |writer| (but + // not necessarily fulfilled yet, in the latter case). Proceed as if + // |source| is now closed. (This will asynchronously wait until any + // pending writes have fulfilled.) + return OnSourceClosed(cx, state); + } + } + + // A chunk was read, and *at the time the read was requested*, |dest| was + // ready to accept a write. (Only one read is processed at a time per + // |state->hasPendingRead()|, so this condition remains true now.) Write the + // chunk to |dest|. + { + Rooted<Value> chunk(cx); + if (!GetProperty(cx, result, result, cx->names().value, &chunk)) { + return false; + } + + Rooted<WritableStreamDefaultWriter*> writer(cx, state->writer()); + cx->check(writer); + + PromiseObject* writeRequest = + WritableStreamDefaultWriterWrite(cx, writer, chunk); + if (!writeRequest) { + return false; + } + + // Stash away this new last write request. (The shutdown process will react + // to this write request to finish shutdown only once all pending writes are + // completed.) + state->updateLastWriteRequest(writeRequest); + } + + // Read another chunk if this write didn't fill up |dest|. + // + // While we (properly) ignored |state->shuttingDown()| earlier, this call will + // *not* initiate a fresh read if |!state->shuttingDown()|. + return ReadFromSource(cx, state); +} + +static bool ReadFulfilled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + Rooted<PipeToState*> state(cx, TargetFromHandler<PipeToState>(args)); + cx->check(state); + + Rooted<JSObject*> result(cx, &args[0].toObject()); + cx->check(result); + + if (!ReadFulfilled(cx, state, result)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool ReadFromSource(JSContext* cx, unsigned argc, Value* vp); + +static MOZ_MUST_USE bool ReadFromSource(JSContext* cx, + Handle<PipeToState*> state) { + cx->check(state); + + MOZ_ASSERT(!state->hasPendingRead(), + "should only have one read in flight at a time, because multiple " + "reads could cause the latter read to ignore backpressure " + "signals"); + + // "Shutdown must stop activity: if shuttingDown becomes true, the user agent + // must not initiate further reads from reader..." + if (state->shuttingDown()) { + return true; + } + + Rooted<WritableStreamDefaultWriter*> writer(cx, state->writer()); + cx->check(writer); + + // "While WritableStreamDefaultWriterGetDesiredSize(writer) is ≤ 0 or is null, + // the user agent must not read from reader." + Rooted<Value> desiredSize(cx); + if (!WritableStreamDefaultWriterGetDesiredSize(cx, writer, &desiredSize)) { + return false; + } + + // If we're in the middle of erroring or are fully errored, either way the + // |dest|-closed reaction queued up in |StartPiping| will do the right + // thing, so do nothing here. + if (desiredSize.isNull()) { +#ifdef DEBUG + { + WritableStream* unwrappedDest = GetUnwrappedDest(cx, state); + if (!unwrappedDest) { + return false; + } + + MOZ_ASSERT(unwrappedDest->erroring() || unwrappedDest->errored()); + } +#endif + + return true; + } + + // If |dest| isn't ready to receive writes yet (i.e. backpressure applies), + // resume when it is. + MOZ_ASSERT(desiredSize.isNumber()); + if (desiredSize.toNumber() <= 0) { + Rooted<JSObject*> readyPromise(cx, writer->readyPromise()); + cx->check(readyPromise); + + Rooted<JSFunction*> readFromSource(cx, + NewHandler(cx, ReadFromSource, state)); + if (!readFromSource) { + return false; + } + + // Resume when there's writable capacity. Don't bother handling rejection: + // if this happens, the stream is going to be errored shortly anyway, and + // |StartPiping| has us ready to react to that already. + // + // XXX Double-check the claim that we need not handle rejections and that a + // rejection of [[readyPromise]] *necessarily* is always followed by + // rejection of [[closedPromise]]. + return JS::AddPromiseReactionsIgnoringUnhandledRejection( + cx, readyPromise, readFromSource, nullptr); + } + + // |dest| is ready to receive at least one write. Read one chunk from the + // reader now that we're not subject to backpressure. + Rooted<ReadableStreamDefaultReader*> reader(cx, state->reader()); + cx->check(reader); + + Rooted<PromiseObject*> readRequest( + cx, js::ReadableStreamDefaultReaderRead(cx, reader)); + if (!readRequest) { + return false; + } + + Rooted<JSFunction*> readFulfilled(cx, NewHandler(cx, ReadFulfilled, state)); + if (!readFulfilled) { + return false; + } + +#ifdef DEBUG + MOZ_ASSERT(!state->pendingReadWouldBeRejected()); + + // The specification for ReadableStreamError ensures that rejecting a read or + // read-into request is immediately followed by rejecting the reader's + // [[closedPromise]]. Therefore, it does not appear *necessary* to handle the + // rejected case -- the [[closedPromise]] reaction will do so for us. + // + // However, this is all very stateful and gnarly, so we implement a rejection + // handler that sets a flag to indicate the read was rejected. Then if the + // [[closedPromise]] reaction function is invoked, we can assert that *if* + // a read is recorded as pending at that instant, a reject handler would have + // been invoked for it. + auto ReadRejected = [](JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + Rooted<PipeToState*> state(cx, TargetFromHandler<PipeToState>(args)); + cx->check(state); + + state->setPendingReadWouldBeRejected(); + + args.rval().setUndefined(); + return true; + }; + + Rooted<JSFunction*> readRejected(cx, NewHandler(cx, ReadRejected, state)); + if (!readRejected) { + return false; + } +#else + auto readRejected = nullptr; +#endif + + // Once the chunk is read, immediately write it and attempt to read more. + // Don't bother handling a rejection: |source| will be closed/errored, and + // |StartPiping| poised us to react to that already. + if (!JS::AddPromiseReactionsIgnoringUnhandledRejection( + cx, readRequest, readFulfilled, readRejected)) { + return false; + } + + // The spec is clear that a write started before an error/stream-closure is + // encountered must be completed before shutdown. It is *not* clear that a + // read that hasn't yet fulfilled should delay shutdown (or until that read's + // successive write is completed). + // + // It seems easiest to explain, both from a user perspective (no read is ever + // just dropped on the ground) and an implementer perspective (if we *don't* + // delay, then a read could be started, a shutdown could be started, then the + // read could finish but we can't write it which arguably conflicts with the + // requirement that chunks that have been read must be written before shutdown + // completes), to delay. XXX file a spec issue to require this! + state->setPendingRead(); + return true; +} + +static bool ReadFromSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<PipeToState*> state(cx, TargetFromHandler<PipeToState>(args)); + cx->check(state); + + if (!ReadFromSource(cx, state)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static MOZ_MUST_USE bool StartPiping(JSContext* cx, Handle<PipeToState*> state, + Handle<ReadableStream*> unwrappedSource, + Handle<WritableStream*> unwrappedDest) { + cx->check(state); + + // "Shutdown must stop activity: if shuttingDown becomes true, the user agent + // must not initiate further reads from reader..." + MOZ_ASSERT(!state->shuttingDown(), "can't be shutting down when starting"); + + // "Error and close states must be propagated: the following conditions must + // be applied in order." + // + // Before piping has started, we have to check for source/dest being errored + // or closed manually. + bool erroredOrClosed; + if (!SourceOrDestErroredOrClosed(cx, state, unwrappedSource, unwrappedDest, + &erroredOrClosed)) { + return false; + } + if (erroredOrClosed) { + return true; + } + + // *After* piping has started, add reactions to respond to source/dest + // becoming errored or closed. + { + Rooted<JSObject*> unwrappedClosedPromise(cx); + Rooted<JSObject*> onClosed(cx); + Rooted<JSObject*> onErrored(cx); + + auto ReactWhenClosedOrErrored = + [&unwrappedClosedPromise, &onClosed, &onErrored, &state]( + JSContext* cx, JSNative onClosedFunc, JSNative onErroredFunc) { + onClosed = NewHandler(cx, onClosedFunc, state); + if (!onClosed) { + return false; + } + + onErrored = NewHandler(cx, onErroredFunc, state); + if (!onErrored) { + return false; + } + + return JS::AddPromiseReactions(cx, unwrappedClosedPromise, onClosed, + onErrored); + }; + + unwrappedClosedPromise = + GetClosedPromise(cx, unwrappedSource, UnwrapReaderFromStream); + if (!unwrappedClosedPromise) { + return false; + } + + if (!ReactWhenClosedOrErrored(cx, OnSourceClosed, OnSourceErrored)) { + return false; + } + + unwrappedClosedPromise = + GetClosedPromise(cx, unwrappedDest, UnwrapWriterFromStream); + if (!unwrappedClosedPromise) { + return false; + } + + if (!ReactWhenClosedOrErrored(cx, OnDestClosed, OnDestErrored)) { + return false; + } + } + + return ReadFromSource(cx, state); +} + +/** + * Stream spec, 4.8.1. ReadableStreamPipeTo ( source, dest, + * preventClose, preventAbort, + * preventCancel[, signal] ) + * Step 14.1 abortAlgorithm. + */ +static MOZ_MUST_USE bool PerformAbortAlgorithm(JSContext* cx, + Handle<PipeToState*> state) { + cx->check(state); + + // Step 14.1: Let abortAlgorithm be the following steps: + // Step 14.1.1: Let error be a new "AbortError" DOMException. + // Step 14.1.2: Let actions be an empty ordered set. + // Step 14.1.3: If preventAbort is false, append the following action to + // actions: + // Step 14.1.3.1: If dest.[[state]] is "writable", return + // ! WritableStreamAbort(dest, error). + // Step 14.1.3.2: Otherwise, return a promise resolved with undefined. + // Step 14.1.4: If preventCancel is false, append the following action action + // to actions: + // Step 14.1.4.1: If source.[[state]] is "readable", return + // ! ReadableStreamCancel(source, error). + // Step 14.1.4.2: Otherwise, return a promise resolved with undefined. + // Step 14.1.5: Shutdown with an action consisting of getting a promise to + // wait for all of the actions in actions, and with error. + // XXX jwalden + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, + "abortAlgorithm steps"); + return false; +} + +/** + * Stream spec, 3.4.11. ReadableStreamPipeTo ( source, dest, + * preventClose, preventAbort, + * preventCancel, signal ) + * Steps 4-11, 13-14. + */ +/* static */ PipeToState* PipeToState::create( + JSContext* cx, Handle<PromiseObject*> promise, + Handle<ReadableStream*> unwrappedSource, + Handle<WritableStream*> unwrappedDest, bool preventClose, bool preventAbort, + bool preventCancel, Handle<JSObject*> signal) { + cx->check(promise); + cx->check(signal); + + Rooted<PipeToState*> state(cx, + NewTenuredBuiltinClassInstance<PipeToState>(cx)); + if (!state) { + return nullptr; + } + + // Step 4. Assert: signal is undefined or signal is an instance of the + // AbortSignal interface. + MOZ_ASSERT(state->getFixedSlot(Slot_Signal).isUndefined()); + if (signal) { + // |signal| is double-checked to be an |AbortSignal| further down. + state->initFixedSlot(Slot_Signal, ObjectValue(*signal)); + } + + // Step 5: Assert: ! IsReadableStreamLocked(source) is false. + MOZ_ASSERT(!unwrappedSource->locked()); + + // Step 6: Assert: ! IsWritableStreamLocked(dest) is false. + MOZ_ASSERT(!unwrappedDest->isLocked()); + + MOZ_ASSERT(state->getFixedSlot(Slot_Promise).isUndefined()); + state->initFixedSlot(Slot_Promise, ObjectValue(*promise)); + + // 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, ForAuthorCodeBool::No); + 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. + + // XXX This used to be step 13 but is now step 14, all the step-comments of + // the overall algorithm need renumbering. + // Step 13: If signal is not undefined, + if (signal) { + // Step 14.2: If signal’s aborted flag is set, perform abortAlgorithm and + // return promise. + bool aborted; + { + // Sadly, we can't assert |signal| is an |AbortSignal| here because it + // could have become a nuked CCW since it was type-checked. + JSObject* unwrappedSignal = UnwrapSignalFromPipeToState(cx, state); + if (!unwrappedSignal) { + return nullptr; + } + + JSRuntime* rt = cx->runtime(); + MOZ_ASSERT(unwrappedSignal->hasClass(rt->maybeAbortSignalClass())); + + AutoRealm ar(cx, unwrappedSignal); + aborted = rt->abortSignalIsAborted(unwrappedSignal); + } + if (aborted) { + if (!PerformAbortAlgorithm(cx, state)) { + return nullptr; + } + + // Returning |state| here will cause |promise| to be returned by the + // overall algorithm. + return state; + } + + // Step 14.3: Add abortAlgorithm to signal. + // XXX jwalden need JSAPI to add an algorithm/steps to an AbortSignal + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, + "adding abortAlgorithm to signal"); + return nullptr; + } + + // Step 14: In parallel, using reader and writer, read all chunks from source + // and write them to dest. + if (!StartPiping(cx, state, unwrappedSource, unwrappedDest)) { + return nullptr; + } + + return state; +} + +const JSClass PipeToState::class_ = {"PipeToState", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount)}; diff --git a/js/src/builtin/streams/PipeToState.h b/js/src/builtin/streams/PipeToState.h new file mode 100644 index 0000000000..b15cacb28f --- /dev/null +++ b/js/src/builtin/streams/PipeToState.h @@ -0,0 +1,291 @@ +/* -*- 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/. */ + +/* ReadableStream.prototype.pipeTo state. */ + +#ifndef builtin_streams_PipeToState_h +#define builtin_streams_PipeToState_h + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/WrappingOperations.h" // mozilla::WrapToSigned + +#include <stdint.h> // uint32_t + +#include "builtin/streams/ReadableStreamReader.h" // js::ReadableStreamDefaultReader +#include "builtin/streams/WritableStreamDefaultWriter.h" // js::WritableStreamDefaultWriter +#include "js/Class.h" // JSClass +#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. + */ +class PipeToState : public NativeObject { + public: + /** + * Memory layout for PipeToState instances. + */ + 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, + + /** + * The |PromiseObject*| of the last write performed to the destinationg + * |WritableStream| using the writer in |Slot_Writer|. If no writes have + * yet been performed, this slot contains |undefined|. + * + * This promise is created inside a handler function in the same compartment + * and realm as this |PipeToState|, so it is always a |PromiseObject*| and + * never a wrapper around one. + */ + Slot_LastWriteRequest, + + /** + * Either |undefined| or an |AbortSignal| instance specified by the user, + * whose controller may be used to externally abort the piping algorithm. + * + * This signal is user-provided, so it may be a wrapper around an + * |AbortSignal| not from the same compartment as this. + */ + Slot_Signal, + + SlotCount, + }; + + // The set of possible actions to be passed to the "shutdown with an action" + // algorithm. + // + // We store actions as numbers because 1) handler functions already devote + // their extra slots to target and extra value; and 2) storing a full function + // pointer would require an extra slot, while storing as number packs into + // existing flag storage. + enum class ShutdownAction { + /** The action used during |abortAlgorithm|.*/ + AbortAlgorithm, + + /** + * The action taken when |source| errors and aborting is not prevented, to + * abort |dest| with |source|'s error. + */ + AbortDestStream, + + /** + * The action taken when |dest| becomes errored or closed and canceling is + * not prevented, to cancel |source| with |dest|'s error. + */ + CancelSource, + + /** + * The action taken when |source| closes and closing is not prevented, to + * close the writer while propagating any error in it. + */ + CloseWriterWithErrorPropagation, + + }; + + private: + enum Flags : uint32_t { + /** + * The action passed to the "shutdown with an action" algorithm. + * + * Note that because only the first "shutdown" and "shutdown with an action" + * operation has any effect, we can store this action in |PipeToState| in + * the first invocation of either operation without worrying about it being + * overwritten. + * + * Purely for convenience, we encode this in the lowest bits so that the + * result of a mask is the underlying value of the correct |ShutdownAction|. + */ + Flag_ShutdownActionBits = 0b0000'0011, + + Flag_ShuttingDown = 0b0000'0100, + + Flag_PendingRead = 0b0000'1000, +#ifdef DEBUG + Flag_PendingReadWouldBeRejected = 0b0001'0000, +#endif + + Flag_PreventClose = 0b0010'0000, + Flag_PreventAbort = 0b0100'0000, + Flag_PreventCancel = 0b1000'0000, + }; + + uint32_t flags() const { return getFixedSlot(Slot_Flags).toInt32(); } + void setFlags(uint32_t flags) { + setFixedSlot(Slot_Flags, JS::Int32Value(mozilla::WrapToSigned(flags))); + } + + // Flags start out zeroed, so the initially-stored shutdown action value will + // be this value. (This is also the value of an *initialized* shutdown + // action, but it doesn't seem worth the trouble to store an extra bit to + // detect this specific action being recorded multiple times, purely for + // assertions.) + static constexpr ShutdownAction UninitializedAction = + ShutdownAction::AbortAlgorithm; + + static_assert(Flag_ShutdownActionBits & 1, + "shutdown action bits must be low-order bits so that we can " + "cast ShutdownAction values directly to bits to store"); + + static constexpr uint32_t MaxAction = + static_cast<uint32_t>(ShutdownAction::CloseWriterWithErrorPropagation); + + static_assert(MaxAction <= Flag_ShutdownActionBits, + "max action shouldn't overflow available bits to store it"); + + public: + static const JSClass class_; + + PromiseObject* promise() const { + return &getFixedSlot(Slot_Promise).toObject().as<PromiseObject>(); + } + + ReadableStreamDefaultReader* reader() const { + return &getFixedSlot(Slot_Reader) + .toObject() + .as<ReadableStreamDefaultReader>(); + } + + WritableStreamDefaultWriter* writer() const { + return &getFixedSlot(Slot_Writer) + .toObject() + .as<WritableStreamDefaultWriter>(); + } + + PromiseObject* lastWriteRequest() const { + const auto& slot = getFixedSlot(Slot_LastWriteRequest); + if (slot.isUndefined()) { + return nullptr; + } + + return &slot.toObject().as<PromiseObject>(); + } + + void updateLastWriteRequest(PromiseObject* writeRequest) { + MOZ_ASSERT(writeRequest != nullptr); + setFixedSlot(Slot_LastWriteRequest, JS::ObjectValue(*writeRequest)); + } + + bool hasSignal() const { + JS::Value v = getFixedSlot(Slot_Signal); + MOZ_ASSERT(v.isObject() || v.isUndefined()); + return v.isObject(); + } + + bool shuttingDown() const { return flags() & Flag_ShuttingDown; } + void setShuttingDown() { + MOZ_ASSERT(!shuttingDown()); + setFlags(flags() | Flag_ShuttingDown); + } + + ShutdownAction shutdownAction() const { + MOZ_ASSERT(shuttingDown(), + "must be shutting down to have a shutdown action"); + + uint32_t bits = flags() & Flag_ShutdownActionBits; + static_assert(Flag_ShutdownActionBits & 1, + "shutdown action bits are assumed to be low-order bits that " + "don't have to be shifted down to ShutdownAction's range"); + + MOZ_ASSERT(bits <= MaxAction, "bits must encode a valid action"); + + return static_cast<ShutdownAction>(bits); + } + + void setShutdownAction(ShutdownAction action) { + MOZ_ASSERT(shuttingDown(), + "must be protected by the |shuttingDown| boolean to save the " + "shutdown action"); + MOZ_ASSERT(shutdownAction() == UninitializedAction, + "should only set shutdown action once"); + + setFlags(flags() | static_cast<uint32_t>(action)); + } + + bool preventClose() const { return flags() & Flag_PreventClose; } + bool preventAbort() const { return flags() & Flag_PreventAbort; } + bool preventCancel() const { return flags() & Flag_PreventCancel; } + + bool hasPendingRead() const { return flags() & Flag_PendingRead; } + void setPendingRead() { + MOZ_ASSERT(!hasPendingRead()); + setFlags(flags() | Flag_PendingRead); + } + void clearPendingRead() { + MOZ_ASSERT(hasPendingRead()); + setFlags(flags() & ~Flag_PendingRead); + } + +#ifdef DEBUG + bool pendingReadWouldBeRejected() const { + return flags() & Flag_PendingReadWouldBeRejected; + } + void setPendingReadWouldBeRejected() { + MOZ_ASSERT(!pendingReadWouldBeRejected()); + setFlags(flags() | Flag_PendingReadWouldBeRejected); + } +#endif + + 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<PromiseObject*> promise, + JS::Handle<ReadableStream*> unwrappedSource, + JS::Handle<WritableStream*> unwrappedDest, + bool preventClose, bool preventAbort, + bool preventCancel, JS::Handle<JSObject*> signal); +}; + +} // namespace js + +#endif // builtin_streams_PipeToState_h diff --git a/js/src/builtin/streams/PullIntoDescriptor.cpp b/js/src/builtin/streams/PullIntoDescriptor.cpp new file mode 100644 index 0000000000..1b6c99a45b --- /dev/null +++ b/js/src/builtin/streams/PullIntoDescriptor.cpp @@ -0,0 +1,48 @@ +/* -*- 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/. */ + +/* Pull descriptor objects for tracking byte stream pull-into requests. */ + +#include "builtin/streams/PullIntoDescriptor.h" + +#include <stdint.h> // uint32_t + +#include "js/Class.h" // JSClass, JSCLASS_HAS_RESERVED_SLOTS +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted + +#include "vm/JSObject-inl.h" // js::NewBuiltinClassInstance + +using js::PullIntoDescriptor; + +using JS::Handle; +using JS::Int32Value; +using JS::ObjectOrNullValue; +using JS::ObjectValue; +using JS::Rooted; + +/* static */ PullIntoDescriptor* PullIntoDescriptor::create( + JSContext* cx, Handle<ArrayBufferObject*> buffer, uint32_t byteOffset, + uint32_t byteLength, uint32_t bytesFilled, uint32_t elementSize, + Handle<JSObject*> ctor, ReaderType readerType) { + Rooted<PullIntoDescriptor*> descriptor( + cx, NewBuiltinClassInstance<PullIntoDescriptor>(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(static_cast<int32_t>(readerType))); + return descriptor; +} + +const JSClass PullIntoDescriptor::class_ = { + "PullIntoDescriptor", JSCLASS_HAS_RESERVED_SLOTS(SlotCount)}; diff --git a/js/src/builtin/streams/PullIntoDescriptor.h b/js/src/builtin/streams/PullIntoDescriptor.h new file mode 100644 index 0000000000..41378dd92e --- /dev/null +++ b/js/src/builtin/streams/PullIntoDescriptor.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/. */ + +/* Pull descriptor objects for tracking byte stream pull-into requests. */ + +#ifndef builtin_streams_PullIntoDescriptor_h +#define builtin_streams_PullIntoDescriptor_h + +#include <stdint.h> // int32_t, uint32_t + +#include "js/Class.h" // JSClass +#include "vm/ArrayBufferObject.h" // js::ArrayBufferObject; +#include "vm/NativeObject.h" // js::NativeObject + +namespace js { + +enum class ReaderType : int32_t { Default = 0, BYOB = 1 }; + +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 JSClass class_; + + ArrayBufferObject* buffer() { + return &getFixedSlot(Slot_buffer).toObject().as<ArrayBufferObject>(); + } + 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(); + } + ReaderType readerType() const { + int32_t n = getFixedSlot(Slot_ReaderType).toInt32(); + MOZ_ASSERT(n == int32_t(ReaderType::Default) || + n == int32_t(ReaderType::BYOB)); + return ReaderType(n); + } + + static PullIntoDescriptor* create(JSContext* cx, + JS::Handle<ArrayBufferObject*> buffer, + uint32_t byteOffset, uint32_t byteLength, + uint32_t bytesFilled, uint32_t elementSize, + JS::Handle<JSObject*> ctor, + ReaderType readerType); +}; + +} // namespace js + +#endif // builtin_streams_PullIntoDescriptor_h diff --git a/js/src/builtin/streams/QueueWithSizes-inl.h b/js/src/builtin/streams/QueueWithSizes-inl.h new file mode 100644 index 0000000000..deb1aabbd5 --- /dev/null +++ b/js/src/builtin/streams/QueueWithSizes-inl.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/. */ + +/* Queue-with-sizes operations. */ + +#ifndef builtin_streams_QueueWithSizes_inl_h +#define builtin_streams_QueueWithSizes_inl_h + +#include "builtin/streams/QueueWithSizes.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT + +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::Value +#include "vm/List.h" // js::ListObject + +#include "vm/List-inl.h" // js::ListObject::* + +struct JS_PUBLIC_API JSContext; + +namespace js { + +namespace detail { + +// The *internal* representation of a queue-with-sizes is a List of even length +// where elements (2 * n, 2 * n + 1) represent the nth (value, size) element in +// the queue. + +inline JS::Value QueueFirstValue(ListObject* unwrappedQueue) { + MOZ_ASSERT(!unwrappedQueue->isEmpty(), + "can't examine first value in an empty queue-with-sizes"); + MOZ_ASSERT((unwrappedQueue->length() % 2) == 0, + "queue-with-sizes must consist of (value, size) element pairs and " + "so must have even length"); + return unwrappedQueue->get(0); +} + +inline double QueueFirstSize(ListObject* unwrappedQueue) { + MOZ_ASSERT(!unwrappedQueue->isEmpty(), + "can't examine first value in an empty queue-with-sizes"); + MOZ_ASSERT((unwrappedQueue->length() % 2) == 0, + "queue-with-sizes must consist of (value, size) element pairs and " + "so must have even length"); + return unwrappedQueue->get(1).toDouble(); +} + +inline void QueueRemoveFirstValueAndSize(ListObject* unwrappedQueue, + JSContext* cx) { + MOZ_ASSERT(!unwrappedQueue->isEmpty(), + "can't remove first value from an empty queue-with-sizes"); + MOZ_ASSERT((unwrappedQueue->length() % 2) == 0, + "queue-with-sizes must consist of (value, size) element pairs and " + "so must have even length"); + unwrappedQueue->popFirstPair(cx); +} + +inline MOZ_MUST_USE bool QueueAppendValueAndSize( + JSContext* cx, JS::Handle<ListObject*> unwrappedQueue, + JS::Handle<JS::Value> value, double size) { + return unwrappedQueue->appendValueAndSize(cx, value, size); +} + +} // namespace detail + +/** + * Streams spec, 6.2.3. PeekQueueValue ( container ) nothrow + */ +inline JS::Value PeekQueueValue(ListObject* queue) { + return detail::QueueFirstValue(queue); +} + +} // namespace js + +#endif // builtin_streams_QueueWithSizes_inl_h diff --git a/js/src/builtin/streams/QueueWithSizes.cpp b/js/src/builtin/streams/QueueWithSizes.cpp new file mode 100644 index 0000000000..4b2ce84084 --- /dev/null +++ b/js/src/builtin/streams/QueueWithSizes.cpp @@ -0,0 +1,173 @@ +/* -*- 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/. */ + +/* Queue-with-sizes operations. */ + +#include "builtin/streams/QueueWithSizes-inl.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE +#include "mozilla/FloatingPoint.h" // mozilla::Is{Infinite,NaN} + +#include "jsapi.h" // JS_ReportErrorNumberASCII + +#include "builtin/streams/StreamController.h" // js::StreamController +#include "js/Class.h" // JSClass, JSCLASS_HAS_RESERVED_SLOTS +#include "js/Conversions.h" // JS::ToNumber +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/RootingAPI.h" // JS::Rooted +#include "js/Value.h" // JS::Value, JS::{Number,Object}Value +#include "vm/Compartment.h" // JSCompartment +#include "vm/JSContext.h" // JSContext +#include "vm/List.h" // js::ListObject +#include "vm/NativeObject.h" // js::NativeObject + +#include "vm/Compartment-inl.h" // JSCompartment::wrap +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/JSObject-inl.h" // js::NewBuiltinClassInstance +#include "vm/List-inl.h" // js::ListObject::*, js::StoreNewListInFixedSlot +#include "vm/Realm-inl.h" // js::AutoRealm + +using JS::Handle; +using JS::MutableHandle; +using JS::NumberValue; +using JS::ObjectValue; +using JS::Rooted; +using JS::ToNumber; +using JS::Value; + +/*** 6.2. Queue-with-sizes operations ***************************************/ + +/** + * Streams spec, 6.2.1. DequeueValue ( container ) nothrow + */ +MOZ_MUST_USE bool js::DequeueValue(JSContext* cx, + Handle<StreamController*> unwrappedContainer, + MutableHandle<Value> chunk) { + // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal + // slots (implicit). + // Step 2: Assert: queue is not empty. + Rooted<ListObject*> unwrappedQueue(cx, unwrappedContainer->queue()); + + // Step 3. Let pair be the first element of queue. + double chunkSize = detail::QueueFirstSize(unwrappedQueue); + chunk.set(detail::QueueFirstValue(unwrappedQueue)); + + // Step 4. Remove pair from queue, shifting all other elements downward + // (so that the second becomes the first, and so on). + detail::QueueRemoveFirstValueAndSize(unwrappedQueue, cx); + + // 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 = unwrappedContainer->queueTotalSize(); + totalSize -= chunkSize; + if (totalSize < 0) { + totalSize = 0; + } + unwrappedContainer->setQueueTotalSize(totalSize); + + // Step 7: Return pair.[[value]]. + return cx->compartment()->wrap(cx, chunk); +} + +void js::DequeueValue(StreamController* unwrappedContainer, JSContext* cx) { + // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal + // slots (implicit). + // Step 2: Assert: queue is not empty. + ListObject* unwrappedQueue = unwrappedContainer->queue(); + + // Step 3. Let pair be the first element of queue. + // (The value is being discarded, so all we must extract is the size.) + double chunkSize = detail::QueueFirstSize(unwrappedQueue); + + // Step 4. Remove pair from queue, shifting all other elements downward + // (so that the second becomes the first, and so on). + detail::QueueRemoveFirstValueAndSize(unwrappedQueue, cx); + + // 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 = unwrappedContainer->queueTotalSize(); + totalSize -= chunkSize; + if (totalSize < 0) { + totalSize = 0; + } + unwrappedContainer->setQueueTotalSize(totalSize); + + // Step 7: Return pair.[[value]]. (omitted because not used) +} + +/** + * Streams spec, 6.2.2. EnqueueValueWithSize ( container, value, size ) throws + */ +MOZ_MUST_USE bool js::EnqueueValueWithSize( + JSContext* cx, Handle<StreamController*> unwrappedContainer, + Handle<Value> value, Handle<Value> sizeVal) { + cx->check(value, sizeVal); + + // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal + // slots (implicit). + // 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]]. + { + AutoRealm ar(cx, unwrappedContainer); + Rooted<ListObject*> unwrappedQueue(cx, unwrappedContainer->queue()); + Rooted<Value> wrappedVal(cx, value); + if (!cx->compartment()->wrap(cx, &wrappedVal)) { + return false; + } + + if (!detail::QueueAppendValueAndSize(cx, unwrappedQueue, wrappedVal, + size)) { + return false; + } + } + + // Step 5: Set container.[[queueTotalSize]] to + // container.[[queueTotalSize]] + size. + unwrappedContainer->setQueueTotalSize(unwrappedContainer->queueTotalSize() + + size); + + return true; +} + +/** + * Streams spec, 6.2.4. ResetQueue ( container ) nothrow + */ +MOZ_MUST_USE bool js::ResetQueue(JSContext* cx, + Handle<StreamController*> unwrappedContainer) { + // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal + // slots (implicit). + // Step 2: Set container.[[queue]] to a new empty List. + if (!StoreNewListInFixedSlot(cx, unwrappedContainer, + StreamController::Slot_Queue)) { + return false; + } + + // Step 3: Set container.[[queueTotalSize]] to 0. + unwrappedContainer->setQueueTotalSize(0); + + return true; +} diff --git a/js/src/builtin/streams/QueueWithSizes.h b/js/src/builtin/streams/QueueWithSizes.h new file mode 100644 index 0000000000..c781a46bfd --- /dev/null +++ b/js/src/builtin/streams/QueueWithSizes.h @@ -0,0 +1,65 @@ +/* -*- 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/. */ + +/* Queue-with-sizes operations. */ + +#ifndef builtin_streams_QueueWithSizes_h +#define builtin_streams_QueueWithSizes_h + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jstypes.h" // JS_PUBLIC_API +#include "js/RootingAPI.h" // JS::{,Mutable}Handle +#include "js/Value.h" // JS::Value +#include "vm/List.h" // js::ListObject + +struct JS_PUBLIC_API JSContext; + +namespace js { + +class StreamController; + +/** + * Streams spec, 6.2.1. DequeueValue ( container ) nothrow + */ +extern MOZ_MUST_USE bool DequeueValue( + JSContext* cx, JS::Handle<StreamController*> unwrappedContainer, + JS::MutableHandle<JS::Value> chunk); + +/** + * Streams spec, 6.2.1. DequeueValue ( container ) nothrow + * when the dequeued value is ignored. + */ +extern void DequeueValue(StreamController* unwrappedContainer, JSContext* cx); + +/** + * Streams spec, 6.2.2. EnqueueValueWithSize ( container, value, size ) throws + */ +extern MOZ_MUST_USE bool EnqueueValueWithSize( + JSContext* cx, JS::Handle<StreamController*> unwrappedContainer, + JS::Handle<JS::Value> value, JS::Handle<JS::Value> sizeVal); + +/** + * Streams spec, 6.2.4. ResetQueue ( container ) nothrow + */ +extern MOZ_MUST_USE bool ResetQueue( + JSContext* cx, JS::Handle<StreamController*> unwrappedContainer); + +inline bool QueueIsEmpty(ListObject* unwrappedQueue) { + if (unwrappedQueue->isEmpty()) { + return true; + } + + MOZ_ASSERT((unwrappedQueue->length() % 2) == 0, + "queue-with-sizes must consist of (value, size) element pairs and " + "so must have even length"); + return false; +} + +} // namespace js + +#endif // builtin_streams_QueueWithSizes_h diff --git a/js/src/builtin/streams/QueueingStrategies.cpp b/js/src/builtin/streams/QueueingStrategies.cpp new file mode 100644 index 0000000000..e6b625e24e --- /dev/null +++ b/js/src/builtin/streams/QueueingStrategies.cpp @@ -0,0 +1,171 @@ +/* -*- 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/. */ + +/* Queuing strategies. */ + +#include "builtin/streams/QueueingStrategies.h" + +#include "builtin/streams/ClassSpecMacro.h" // JS_STREAMS_CLASS_SPEC +#include "js/CallArgs.h" // JS::CallArgs{,FromVp} +#include "js/Class.h" // JS::ObjectOpResult, JS_NULL_CLASS_OPS +#include "js/PropertySpec.h" // JS{Property,Function}Spec, JS_FN, JS_FS_END, JS_PS_END +#include "js/ProtoKey.h" // JSProto_{ByteLength,Count}QueuingStrategy +#include "js/RootingAPI.h" // JS::{Handle,Rooted} +#include "vm/JSObject.h" // js::GetPrototypeFromBuiltinConstructor +#include "vm/ObjectOperations.h" // js::{Define,Get}Property +#include "vm/Runtime.h" // JSAtomState +#include "vm/StringType.h" // js::NameToId, PropertyName + +#include "vm/JSObject-inl.h" // js::NewObjectWithClassProto +#include "vm/NativeObject-inl.h" // js::ThrowIfNotConstructing + +using js::ByteLengthQueuingStrategy; +using js::CountQueuingStrategy; +using js::PropertyName; + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::ObjectOpResult; +using JS::Rooted; +using JS::ToObject; +using JS::Value; + +/*** 6.1. Queuing strategies ************************************************/ + +/** + * ECMA-262 7.3.4 CreateDataProperty(O, P, V) + */ +static MOZ_MUST_USE bool CreateDataProperty(JSContext* cx, + Handle<JSObject*> obj, + Handle<PropertyName*> key, + Handle<Value> value, + ObjectOpResult& result) { + Rooted<jsid> id(cx, js::NameToId(key)); + Rooted<JS::PropertyDescriptor> desc(cx); + desc.setDataDescriptor(value, JSPROP_ENUMERATE); + return js::DefineProperty(cx, obj, id, desc, result); +} + +// Streams spec, 6.1.2.2. new ByteLengthQueuingStrategy({ highWaterMark }) +bool js::ByteLengthQueuingStrategy::constructor(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "ByteLengthQueuingStrategy")) { + return false; + } + + // Implicit in the spec: Create the new strategy object. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor( + cx, args, JSProto_ByteLengthQueuingStrategy, &proto)) { + return false; + } + Rooted<JSObject*> strategy( + cx, NewObjectWithClassProto<ByteLengthQueuingStrategy>(cx, proto)); + if (!strategy) { + return false; + } + + // Implicit in the spec: Argument destructuring. + Rooted<JSObject*> argObj(cx, ToObject(cx, args.get(0))); + if (!argObj) { + return false; + } + Rooted<Value> highWaterMark(cx); + if (!GetProperty(cx, argObj, argObj, cx->names().highWaterMark, + &highWaterMark)) { + return false; + } + + // Step 1: Perform ! CreateDataProperty(this, "highWaterMark", + // highWaterMark). + ObjectOpResult ignored; + if (!CreateDataProperty(cx, strategy, cx->names().highWaterMark, + highWaterMark, ignored)) { + return false; + } + + args.rval().setObject(*strategy); + return true; +} + +// Streams spec 6.1.2.3.1. size ( chunk ) +static 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}; + +JS_STREAMS_CLASS_SPEC(ByteLengthQueuingStrategy, 1, 0, 0, 0, JS_NULL_CLASS_OPS); + +// Streams spec, 6.1.3.2. new CountQueuingStrategy({ highWaterMark }) +bool js::CountQueuingStrategy::constructor(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "CountQueuingStrategy")) { + return false; + } + + // Implicit in the spec: Create the new strategy object. + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor( + cx, args, JSProto_CountQueuingStrategy, &proto)) { + return false; + } + Rooted<CountQueuingStrategy*> strategy( + cx, NewObjectWithClassProto<CountQueuingStrategy>(cx, proto)); + if (!strategy) { + return false; + } + + // Implicit in the spec: Argument destructuring. + 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; + } + + // Step 1: Perform ! CreateDataProperty(this, "highWaterMark", highWaterMark). + ObjectOpResult ignored; + if (!CreateDataProperty(cx, strategy, cx->names().highWaterMark, + highWaterMark, ignored)) { + return false; + } + + args.rval().setObject(*strategy); + return true; +} + +// Streams spec 6.1.3.3.1. size ( chunk ) +static 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}; + +JS_STREAMS_CLASS_SPEC(CountQueuingStrategy, 1, 0, 0, 0, JS_NULL_CLASS_OPS); diff --git a/js/src/builtin/streams/QueueingStrategies.h b/js/src/builtin/streams/QueueingStrategies.h new file mode 100644 index 0000000000..c9c0db5d63 --- /dev/null +++ b/js/src/builtin/streams/QueueingStrategies.h @@ -0,0 +1,39 @@ +/* -*- 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/. */ + +/* Queuing strategies. */ + +#ifndef builtin_stream_QueueingStrategies_h +#define builtin_stream_QueueingStrategies_h + +#include "js/Class.h" // JSClass, js::ClassSpec +#include "js/Value.h" // JS::Value +#include "vm/JSContext.h" // JSContext +#include "vm/NativeObject.h" // js::NativeObject + +namespace js { + +class ByteLengthQueuingStrategy : public NativeObject { + public: + static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp); + static const ClassSpec classSpec_; + static const JSClass class_; + static const ClassSpec protoClassSpec_; + static const JSClass protoClass_; +}; + +class CountQueuingStrategy : public NativeObject { + public: + static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp); + static const ClassSpec classSpec_; + static const JSClass class_; + static const ClassSpec protoClassSpec_; + static const JSClass protoClass_; +}; + +} // namespace js + +#endif // builtin_stream_QueueingStrategies_h diff --git a/js/src/builtin/streams/ReadableStream.cpp b/js/src/builtin/streams/ReadableStream.cpp new file mode 100644 index 0000000000..ba167178c5 --- /dev/null +++ b/js/src/builtin/streams/ReadableStream.cpp @@ -0,0 +1,552 @@ +/* -*- 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/. */ + +/* Class ReadableStream. */ + +#include "builtin/streams/ReadableStream.h" + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jsapi.h" // JS_ReportErrorNumberASCII +#include "jspubtd.h" // JSProto_ReadableStream + +#include "builtin/Array.h" // js::NewDenseFullyAllocatedArray +#include "builtin/streams/ClassSpecMacro.h" // JS_STREAMS_CLASS_SPEC +#include "builtin/streams/MiscellaneousOperations.h" // js::MakeSizeAlgorithmFromSizeFunction, js::ValidateAndNormalizeHighWaterMark, js::ReturnPromiseRejectedWithPendingError +#include "builtin/streams/ReadableStreamController.h" // js::ReadableStream{,Default}Controller, js::ReadableByteStreamController +#include "builtin/streams/ReadableStreamDefaultControllerOperations.h" // js::SetUpReadableStreamDefaultControllerFromUnderlyingSource +#include "builtin/streams/ReadableStreamInternals.h" // js::ReadableStreamCancel +#include "builtin/streams/ReadableStreamOperations.h" // js::ReadableStream{PipeTo,Tee} +#include "builtin/streams/ReadableStreamReader.h" // js::CreateReadableStream{BYOB,Default}Reader, js::ForAuthorCodeBool +#include "builtin/streams/WritableStream.h" // js::WritableStream +#include "js/CallArgs.h" // JS::CallArgs{,FromVp} +#include "js/Class.h" // JSCLASS_PRIVATE_IS_NSISUPPORTS, JSCLASS_HAS_PRIVATE, JS_NULL_CLASS_OPS +#include "js/Conversions.h" // JS::ToBoolean +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" // JS{Function,Property}Spec, JS_FN, JS_PSG, JS_{FS,PS}_END +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted, js::CanGC +#include "js/Stream.h" // JS::ReadableStream{Mode,UnderlyingSource} +#include "js/Value.h" // JS::Value +#include "vm/JSContext.h" // JSContext +#include "vm/JSObject.h" // js::GetPrototypeFromBuiltinConstructor +#include "vm/ObjectOperations.h" // js::GetProperty +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/Runtime.h" // JSAtomState, JSRuntime +#include "vm/StringType.h" // js::EqualStrings, js::ToString + +#include "vm/Compartment-inl.h" // js::UnwrapAndTypeCheck{Argument,This,Value} +#include "vm/JSObject-inl.h" // js::NewBuiltinClassInstance +#include "vm/NativeObject-inl.h" // js::ThrowIfNotConstructing + +using js::CanGC; +using js::ClassSpec; +using js::CreateReadableStreamDefaultReader; +using js::EqualStrings; +using js::ForAuthorCodeBool; +using js::GetErrorMessage; +using js::NativeObject; +using js::NewBuiltinClassInstance; +using js::NewDenseFullyAllocatedArray; +using js::PlainObject; +using js::ReadableStream; +using js::ReadableStreamPipeTo; +using js::ReadableStreamTee; +using js::ReturnPromiseRejectedWithPendingError; +using js::ToString; +using js::UnwrapAndTypeCheckArgument; +using js::UnwrapAndTypeCheckThis; +using js::UnwrapAndTypeCheckValue; +using js::WritableStream; + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::ObjectValue; +using JS::Rooted; +using JS::Value; + +/*** 3.2. Class ReadableStream **********************************************/ + +JS::ReadableStreamMode ReadableStream::mode() const { + ReadableStreamController* controller = this->controller(); + if (controller->is<ReadableStreamDefaultController>()) { + return JS::ReadableStreamMode::Default; + } + return controller->as<ReadableByteStreamController>().hasExternalSource() + ? JS::ReadableStreamMode::ExternalSource + : JS::ReadableStreamMode::Byte; +} + +ReadableStream* ReadableStream::createExternalSourceStream( + JSContext* cx, JS::ReadableStreamUnderlyingSource* source, + void* nsISupportsObject_alreadyAddreffed /* = nullptr */, + Handle<JSObject*> proto /* = nullptr */) { + Rooted<ReadableStream*> stream( + cx, create(cx, nsISupportsObject_alreadyAddreffed, proto)); + if (!stream) { + return nullptr; + } + + if (!SetUpExternalReadableByteStreamController(cx, stream, source)) { + return nullptr; + } + + return stream; +} + +/** + * Streams spec, 3.2.3. new ReadableStream(underlyingSource = {}, strategy = {}) + */ +bool ReadableStream::constructor(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "ReadableStream")) { + return false; + } + + // Implicit in the spec: argument default values. + Rooted<Value> underlyingSource(cx, args.get(0)); + if (underlyingSource.isUndefined()) { + JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx); + if (!emptyObj) { + return false; + } + underlyingSource = ObjectValue(*emptyObj); + } + + Rooted<Value> strategy(cx, args.get(1)); + if (strategy.isUndefined()) { + JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx); + if (!emptyObj) { + return false; + } + strategy = ObjectValue(*emptyObj); + } + + // Implicit in the spec: Set this to + // OrdinaryCreateFromConstructor(NewTarget, ...). + // Step 1: Perform ! InitializeReadableStream(this). + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_ReadableStream, + &proto)) { + return false; + } + Rooted<ReadableStream*> stream(cx, + ReadableStream::create(cx, nullptr, proto)); + if (!stream) { + return false; + } + + // Step 2: Let size be ? GetV(strategy, "size"). + Rooted<Value> size(cx); + if (!GetProperty(cx, strategy, cx->names().size, &size)) { + return false; + } + + // Step 3: Let highWaterMark be ? GetV(strategy, "highWaterMark"). + Rooted<Value> highWaterMarkVal(cx); + if (!GetProperty(cx, strategy, cx->names().highWaterMark, + &highWaterMarkVal)) { + return false; + } + + // Step 4: Let type be ? GetV(underlyingSource, "type"). + Rooted<Value> type(cx); + if (!GetProperty(cx, underlyingSource, cx->names().type, &type)) { + return false; + } + + // Step 5: Let typeString be ? ToString(type). + Rooted<JSString*> typeString(cx, ToString<CanGC>(cx, type)); + if (!typeString) { + return false; + } + + // Step 6: If typeString is "bytes", + bool equal; + if (!EqualStrings(cx, typeString, cx->names().bytes, &equal)) { + return false; + } + if (equal) { + // The rest of step 6 is unimplemented, since we don't support + // user-defined byte streams yet. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_BYTES_TYPE_NOT_IMPLEMENTED); + return false; + } + + // Step 7: Otherwise, if type is undefined, + if (type.isUndefined()) { + // Step 7.a: Let sizeAlgorithm be ? MakeSizeAlgorithmFromSizeFunction(size). + if (!MakeSizeAlgorithmFromSizeFunction(cx, size)) { + return false; + } + + // Step 7.b: If highWaterMark is undefined, let highWaterMark be 1. + double highWaterMark; + if (highWaterMarkVal.isUndefined()) { + highWaterMark = 1; + } else { + // Step 7.c: Set highWaterMark to ? + // ValidateAndNormalizeHighWaterMark(highWaterMark). + if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal, + &highWaterMark)) { + return false; + } + } + + // Step 7.d: Perform + // ? SetUpReadableStreamDefaultControllerFromUnderlyingSource( + // this, underlyingSource, highWaterMark, sizeAlgorithm). + if (!SetUpReadableStreamDefaultControllerFromUnderlyingSource( + cx, stream, underlyingSource, highWaterMark, size)) { + return false; + } + + args.rval().setObject(*stream); + return true; + } + + // Step 8: Otherwise, throw a RangeError exception. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG); + return false; +} + +/** + * Streams spec, 3.2.5.1. get locked + */ +static MOZ_MUST_USE bool ReadableStream_locked(JSContext* cx, unsigned argc, + JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception. + Rooted<ReadableStream*> unwrappedStream( + cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "get locked")); + if (!unwrappedStream) { + return false; + } + + // Step 2: Return ! IsReadableStreamLocked(this). + args.rval().setBoolean(unwrappedStream->locked()); + return true; +} + +/** + * Streams spec, 3.2.5.2. cancel ( reason ) + */ +static MOZ_MUST_USE bool ReadableStream_cancel(JSContext* cx, unsigned argc, + JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsReadableStream(this) is false, return a promise rejected + // with a TypeError exception. + Rooted<ReadableStream*> unwrappedStream( + cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "cancel")); + if (!unwrappedStream) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: If ! IsReadableStreamLocked(this) is true, return a promise + // rejected with a TypeError exception. + if (unwrappedStream->locked()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_LOCKED_METHOD, "cancel"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: Return ! ReadableStreamCancel(this, reason). + Rooted<JSObject*> cancelPromise( + cx, js::ReadableStreamCancel(cx, unwrappedStream, args.get(0))); + if (!cancelPromise) { + return false; + } + args.rval().setObject(*cancelPromise); + return true; +} + +// Streams spec, 3.2.5.3. +// getIterator({ preventCancel } = {}) +// +// Not implemented. + +/** + * Streams spec, 3.2.5.4. getReader({ mode } = {}) + */ +static MOZ_MUST_USE bool ReadableStream_getReader(JSContext* cx, unsigned argc, + JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Implicit in the spec: Argument defaults and destructuring. + Rooted<Value> optionsVal(cx, args.get(0)); + if (optionsVal.isUndefined()) { + JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx); + if (!emptyObj) { + return false; + } + optionsVal.setObject(*emptyObj); + } + Rooted<Value> modeVal(cx); + if (!GetProperty(cx, optionsVal, cx->names().mode, &modeVal)) { + return false; + } + + // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception. + Rooted<ReadableStream*> unwrappedStream( + cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "getReader")); + if (!unwrappedStream) { + return false; + } + + // Step 2: If mode is undefined, return + // ? AcquireReadableStreamDefaultReader(this, true). + Rooted<JSObject*> reader(cx); + if (modeVal.isUndefined()) { + reader = CreateReadableStreamDefaultReader(cx, unwrappedStream, + ForAuthorCodeBool::Yes); + } else { + // Step 3: Set mode to ? ToString(mode) (implicit). + Rooted<JSString*> mode(cx, ToString<CanGC>(cx, modeVal)); + if (!mode) { + return false; + } + + // Step 5: (If mode is not "byob",) Throw a RangeError exception. + bool equal; + if (!EqualStrings(cx, mode, cx->names().byob, &equal)) { + return false; + } + if (!equal) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_INVALID_READER_MODE); + return false; + } + + // Step 4: If mode is "byob", + // return ? AcquireReadableStreamBYOBReader(this, true). + reader = CreateReadableStreamBYOBReader(cx, unwrappedStream, + ForAuthorCodeBool::Yes); + } + + // Reordered second part of steps 2 and 4. + if (!reader) { + return false; + } + args.rval().setObject(*reader); + return true; +} + +// Streams spec, 3.2.5.5. +// pipeThrough({ writable, readable }, +// { preventClose, preventAbort, preventCancel, signal }) +// +// Not implemented. + +/** + * Streams spec, 3.2.5.6. + * pipeTo(dest, { preventClose, preventAbort, preventCancel, signal } = {}) + */ +static bool ReadableStream_pipeTo(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Implicit in the spec: argument default values. + Rooted<Value> options(cx, args.get(1)); + if (options.isUndefined()) { + JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx); + if (!emptyObj) { + return false; + } + options.setObject(*emptyObj); + } + // Step 3 (reordered). + // Implicit in the spec: get the values of the named parameters inside the + // 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<Value> signalVal(cx); + { + // (P)(Re)use the |signal| root. + auto& v = signalVal; + + if (!GetProperty(cx, options, cx->names().preventClose, &v)) { + return false; + } + preventClose = JS::ToBoolean(v); + + if (!GetProperty(cx, options, cx->names().preventAbort, &v)) { + return false; + } + preventAbort = JS::ToBoolean(v); + + if (!GetProperty(cx, options, cx->names().preventCancel, &v)) { + return false; + } + preventCancel = JS::ToBoolean(v); + } + if (!GetProperty(cx, options, cx->names().signal, &signalVal)) { + return false; + } + + // Step 1: If ! IsReadableStream(this) is false, return a promise rejected + // with a TypeError exception. + Rooted<ReadableStream*> unwrappedThis( + cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "pipeTo")); + if (!unwrappedThis) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: If ! IsWritableStream(dest) is false, return a promise rejected + // with a TypeError exception. + Rooted<WritableStream*> unwrappedDest( + cx, UnwrapAndTypeCheckArgument<WritableStream>(cx, args, "pipeTo", 0)); + if (!unwrappedDest) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: Set preventClose to ! ToBoolean(preventClose), set preventAbort to + // ! ToBoolean(preventAbort), and set preventCancel to + // ! ToBoolean(preventCancel). + // This already happened above. + + // 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. + Rooted<JSObject*> signal(cx, nullptr); + if (!signalVal.isUndefined()) { + if (!UnwrapAndTypeCheckValue( + cx, signalVal, cx->runtime()->maybeAbortSignalClass(), [cx] { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_PIPETO_BAD_SIGNAL); + })) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Note: |signal| can be a wrapper. + signal = &signalVal.toObject(); + } + + // Step 5: If ! IsReadableStreamLocked(this) is true, return a promise + // rejected with a TypeError exception. + if (unwrappedThis->locked()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_LOCKED_METHOD, "pipeTo"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 6: If ! IsWritableStreamLocked(dest) is true, return a promise + // rejected with a TypeError exception. + if (unwrappedDest->isLocked()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAM_ALREADY_LOCKED); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 7: Return + // ! ReadableStreamPipeTo(this, dest, preventClose, preventAbort, + // preventCancel, signal). + JSObject* promise = + ReadableStreamPipeTo(cx, unwrappedThis, unwrappedDest, preventClose, + preventAbort, preventCancel, signal); + if (!promise) { + return false; + } + + args.rval().setObject(*promise); + return true; +} + +/** + * Streams spec, 3.2.5.7. tee() + */ +static bool ReadableStream_tee(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception. + Rooted<ReadableStream*> unwrappedStream( + cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "tee")); + if (!unwrappedStream) { + return false; + } + + // Step 2: Let branches be ? ReadableStreamTee(this, false). + Rooted<ReadableStream*> branch1(cx); + Rooted<ReadableStream*> branch2(cx); + if (!ReadableStreamTee(cx, unwrappedStream, false, &branch1, &branch2)) { + return false; + } + + // Step 3: Return ! CreateArrayFromList(branches). + Rooted<NativeObject*> 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; +} + +// Streams spec, 3.2.5.8. +// [@@asyncIterator]({ preventCancel } = {}) +// +// Not implemented. + +static const JSFunctionSpec ReadableStream_methods[] = { + JS_FN("cancel", ReadableStream_cancel, 1, 0), + JS_FN("getReader", ReadableStream_getReader, 0, 0), + // pipeTo is only conditionally supported right now, so it must be manually + // added below if desired. + JS_FN("tee", ReadableStream_tee, 0, 0), JS_FS_END}; + +static const JSPropertySpec ReadableStream_properties[] = { + JS_PSG("locked", ReadableStream_locked, 0), JS_PS_END}; + +static bool FinishReadableStreamClassInit(JSContext* cx, Handle<JSObject*> ctor, + Handle<JSObject*> proto) { + // This function and everything below should be replaced with + // + // JS_STREAMS_CLASS_SPEC(ReadableStream, 0, SlotCount, 0, + // JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE, + // JS_NULL_CLASS_OPS); + // + // when "pipeTo" is always enabled. + const auto& rco = cx->realm()->creationOptions(); + if (rco.getStreamsEnabled() && rco.getWritableStreamsEnabled() && + rco.getReadableStreamPipeToEnabled()) { + Rooted<jsid> pipeTo(cx, NameToId(cx->names().pipeTo)); + if (!DefineFunction(cx, proto, pipeTo, ReadableStream_pipeTo, 2, + JSPROP_RESOLVING)) { + return false; + } + } + + return true; +} + +const ClassSpec ReadableStream::classSpec_ = { + js::GenericCreateConstructor<ReadableStream::constructor, 2, + js::gc::AllocKind::FUNCTION>, + js::GenericCreatePrototype<ReadableStream>, + nullptr, + nullptr, + ReadableStream_methods, + ReadableStream_properties, + FinishReadableStreamClassInit, + 0}; + +const JSClass ReadableStream::class_ = { + "ReadableStream", + JSCLASS_HAS_RESERVED_SLOTS(ReadableStream::SlotCount) | + JSCLASS_HAS_CACHED_PROTO(JSProto_ReadableStream) | + JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE, + JS_NULL_CLASS_OPS, &ReadableStream::classSpec_}; + +const JSClass ReadableStream::protoClass_ = { + "ReadableStream.prototype", + JSCLASS_HAS_CACHED_PROTO(JSProto_ReadableStream), JS_NULL_CLASS_OPS, + &ReadableStream::classSpec_}; diff --git a/js/src/builtin/streams/ReadableStream.h b/js/src/builtin/streams/ReadableStream.h new file mode 100644 index 0000000000..c11596d356 --- /dev/null +++ b/js/src/builtin/streams/ReadableStream.h @@ -0,0 +1,139 @@ +/* -*- 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/. */ + +/* Class ReadableStream. */ + +#ifndef builtin_streams_ReadableStream_h +#define builtin_streams_ReadableStream_h + +#include "mozilla/Assertions.h" // MOZ_ASSERT{,_IF} +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include <stdint.h> // uint32_t + +#include "jstypes.h" // JS_PUBLIC_API +#include "js/Class.h" // JSClass, js::ClassSpec +#include "js/RootingAPI.h" // JS::Handle +#include "js/Stream.h" // JS::ReadableStream{Mode,UnderlyingSource} +#include "js/Value.h" // JS::Int32Value, JS::ObjectValue, JS::UndefinedValue +#include "vm/NativeObject.h" // js::NativeObject + +class JS_PUBLIC_API JSObject; + +namespace js { + +class ReadableStreamController; + +class ReadableStream : public NativeObject { + public: + /** + * Memory layout of Stream instances. + * + * See https://streams.spec.whatwg.org/#rs-internal-slots for details on + * the stored state. [[state]] and [[disturbed]] are stored in + * StreamSlot_State as ReadableStream::State enum values. + * + * Of the stored values, Reader and StoredError might be cross-compartment + * wrappers. This can happen if the Reader was created by applying a + * different compartment's ReadableStream.prototype.getReader method. + * + * A stream's associated controller is always created from under the + * stream's constructor and thus cannot be in a different compartment. + */ + enum Slots { + Slot_Controller, + Slot_Reader, + Slot_State, + Slot_StoredError, + SlotCount + }; + + private: + enum StateBits { + Readable = 0, + Closed = 1, + Errored = 2, + StateMask = 0x000000ff, + Disturbed = 0x00000100 + }; + + uint32_t stateBits() const { return getFixedSlot(Slot_State).toInt32(); } + void initStateBits(uint32_t stateBits) { + MOZ_ASSERT((stateBits & ~Disturbed) <= Errored); + setFixedSlot(Slot_State, JS::Int32Value(stateBits)); + } + void setStateBits(uint32_t stateBits) { +#ifdef DEBUG + bool wasDisturbed = disturbed(); + bool wasClosedOrErrored = closed() || errored(); +#endif + initStateBits(stateBits); + MOZ_ASSERT_IF(wasDisturbed, disturbed()); + MOZ_ASSERT_IF(wasClosedOrErrored, !readable()); + } + + StateBits state() const { return StateBits(stateBits() & StateMask); } + void setState(StateBits state) { + MOZ_ASSERT(state <= Errored); + uint32_t current = stateBits() & ~StateMask; + setStateBits(current | state); + } + + public: + bool readable() const { return state() == Readable; } + bool closed() const { return state() == Closed; } + void setClosed() { setState(Closed); } + bool errored() const { return state() == Errored; } + void setErrored() { setState(Errored); } + bool disturbed() const { return stateBits() & Disturbed; } + void setDisturbed() { setStateBits(stateBits() | Disturbed); } + + bool hasController() const { + return !getFixedSlot(Slot_Controller).isUndefined(); + } + inline ReadableStreamController* controller() const; + inline void setController(ReadableStreamController* controller); + void clearController() { + setFixedSlot(Slot_Controller, JS::UndefinedValue()); + } + + bool hasReader() const { return !getFixedSlot(Slot_Reader).isUndefined(); } + void setReader(JSObject* reader) { + setFixedSlot(Slot_Reader, JS::ObjectValue(*reader)); + } + void clearReader() { setFixedSlot(Slot_Reader, JS::UndefinedValue()); } + + JS::Value storedError() const { return getFixedSlot(Slot_StoredError); } + void setStoredError(JS::Handle<JS::Value> value) { + setFixedSlot(Slot_StoredError, value); + } + + JS::ReadableStreamMode mode() const; + + bool locked() const; + + static MOZ_MUST_USE ReadableStream* create( + JSContext* cx, void* nsISupportsObject_alreadyAddreffed = nullptr, + JS::Handle<JSObject*> proto = nullptr); + static ReadableStream* createExternalSourceStream( + JSContext* cx, JS::ReadableStreamUnderlyingSource* source, + void* nsISupportsObject_alreadyAddreffed = nullptr, + JS::Handle<JSObject*> proto = nullptr); + + static bool constructor(JSContext* cx, unsigned argc, Value* vp); + static const ClassSpec classSpec_; + static const JSClass class_; + static const ClassSpec protoClassSpec_; + static const JSClass protoClass_; +}; + +extern MOZ_MUST_USE bool SetUpExternalReadableByteStreamController( + JSContext* cx, JS::Handle<ReadableStream*> stream, + JS::ReadableStreamUnderlyingSource* source); + +} // namespace js + +#endif // builtin_streams_ReadableStream_h diff --git a/js/src/builtin/streams/ReadableStreamBYOBReader.cpp b/js/src/builtin/streams/ReadableStreamBYOBReader.cpp new file mode 100644 index 0000000000..9d8225dde4 --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamBYOBReader.cpp @@ -0,0 +1,51 @@ +/* -*- 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/. */ + +/* + * Class ReadableStreamBYOBReader. + * + * Byte streams and BYOB readers are unimplemented, so this is skeletal -- yet + * helpful to ensure certain trivial tests of the functionality in wpt, that + * don't actually test fully-constructed byte streams/BYOB readers, pass. 🙄 + */ + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jsapi.h" // JS_ReportErrorNumberASCII + +#include "builtin/streams/ReadableStream.h" // js::ReadableStream +#include "builtin/streams/ReadableStreamReader.h" // js::CreateReadableStreamBYOBReader, js::ForAuthorCodeBool +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* + +using JS::Handle; + +/*** 3.7. Class ReadableStreamBYOBReader *********************************/ + +/** + * Stream spec, 3.7.3. new ReadableStreamBYOBReader ( stream ) + * Steps 2-5. + */ +MOZ_MUST_USE JSObject* js::CreateReadableStreamBYOBReader( + JSContext* cx, Handle<ReadableStream*> unwrappedStream, + ForAuthorCodeBool forAuthorCode, Handle<JSObject*> proto /* = nullptr */) { + // Step 2: If ! IsReadableByteStreamController( + // stream.[[readableStreamController]]) is false, throw a + // TypeError exception. + // We don't implement byte stream controllers yet, so always throw here. Note + // that JSMSG_READABLESTREAM_BYTES_TYPE_NOT_IMPLEMENTED can't be used here + // because it's a RangeError (and sadly wpt actually tests this and we have a + // spurious failure if we don't make this a TypeError). + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_BYOB_READER_FOR_NON_BYTE_STREAM); + + // Step 3: If ! IsReadableStreamLocked(stream) is true, throw a TypeError + // exception. + // Step 4: Perform ! ReadableStreamReaderGenericInitialize(this, stream). + // Step 5: Set this.[[readIntoRequests]] to a new empty List. + // Steps 3-5 are presently unreachable. + return nullptr; +} diff --git a/js/src/builtin/streams/ReadableStreamController.h b/js/src/builtin/streams/ReadableStreamController.h new file mode 100644 index 0000000000..2cb1fc9e2a --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamController.h @@ -0,0 +1,266 @@ +/* -*- 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/. */ + +/* ReadableStream controller classes and functions. */ + +#ifndef builtin_streams_ReadableStreamController_h +#define builtin_streams_ReadableStreamController_h + +#include "mozilla/Assertions.h" // MOZ_ASSERT + +#include <stdint.h> // uint32_t + +#include "builtin/streams/ReadableStream.h" // js::ReadableStream +#include "builtin/streams/StreamController.h" // js::StreamController +#include "js/Class.h" // JSClass, js::ClassSpec +#include "js/RootingAPI.h" // JS::Handle +#include "js/Stream.h" // JS::ReadableStreamUnderlyingSource +#include "js/Value.h" // JS::Value, JS::{Number,Object,Private,Undefined}Value, JS::UndefinedHandleValue +#include "vm/List.h" // js::ListObject +#include "vm/NativeObject.h" // js::NativeObject + +namespace js { + +class PromiseObject; + +class ReadableStreamController : public StreamController { + public: + /** + * Memory layout for ReadableStream controllers, starting after the slots + * reserved for queue container usage. + * + * Storage of the internal slots listed in the standard is fairly + * straightforward except for [[pullAlgorithm]] and [[cancelAlgorithm]]. + * These algorithms are not stored as JSFunction objects. Rather, there are + * three cases: + * + * - Streams created with `new ReadableStream`: The methods are stored + * in Slot_PullMethod and Slot_CancelMethod. The underlying source + * object (`this` for these methods) is in Slot_UnderlyingSource. + * + * - External source streams. Slot_UnderlyingSource is a PrivateValue + * pointing to the JS::ReadableStreamUnderlyingSource object. The + * algorithms are implemented using the .pull() and .cancel() methods + * of that object. Slot_Pull/CancelMethod are undefined. + * + * - Tee streams. Slot_UnderlyingSource is a TeeState object. The + * pull/cancel algorithms are implemented as separate functions in + * Stream.cpp. Slot_Pull/CancelMethod are undefined. + * + * UnderlyingSource, PullMethod, and CancelMethod can be wrappers to objects + * in other compartments. + * + * StrategyHWM and Flags are both primitive (numeric) values. + */ + enum Slots { + Slot_Stream = StreamController::SlotCount, + Slot_UnderlyingSource, + Slot_PullMethod, + Slot_CancelMethod, + Slot_StrategyHWM, + Slot_Flags, + SlotCount + }; + + enum ControllerFlags { + Flag_Started = 1 << 0, + Flag_Pulling = 1 << 1, + Flag_PullAgain = 1 << 2, + Flag_CloseRequested = 1 << 3, + Flag_TeeBranch1 = 1 << 4, + Flag_TeeBranch2 = 1 << 5, + Flag_ExternalSource = 1 << 6, + Flag_SourceLocked = 1 << 7, + }; + + ReadableStream* stream() const { + return &getFixedSlot(Slot_Stream).toObject().as<ReadableStream>(); + } + void setStream(ReadableStream* stream) { + setFixedSlot(Slot_Stream, JS::ObjectValue(*stream)); + } + JS::Value underlyingSource() const { + return getFixedSlot(Slot_UnderlyingSource); + } + void setUnderlyingSource(const JS::Value& underlyingSource) { + setFixedSlot(Slot_UnderlyingSource, underlyingSource); + } + JS::Value pullMethod() const { return getFixedSlot(Slot_PullMethod); } + void setPullMethod(const JS::Value& pullMethod) { + setFixedSlot(Slot_PullMethod, pullMethod); + } + JS::Value cancelMethod() const { return getFixedSlot(Slot_CancelMethod); } + void setCancelMethod(const JS::Value& cancelMethod) { + setFixedSlot(Slot_CancelMethod, cancelMethod); + } + JS::ReadableStreamUnderlyingSource* externalSource() const { + static_assert(alignof(JS::ReadableStreamUnderlyingSource) >= 2, + "External underling sources are stored as PrivateValues, " + "so they must have even addresses"); + MOZ_ASSERT(hasExternalSource()); + return static_cast<JS::ReadableStreamUnderlyingSource*>( + underlyingSource().toPrivate()); + } + void setExternalSource(JS::ReadableStreamUnderlyingSource* underlyingSource) { + setUnderlyingSource(JS::PrivateValue(underlyingSource)); + addFlags(Flag_ExternalSource); + } + static void clearUnderlyingSource( + JS::Handle<ReadableStreamController*> controller, + bool finalizeSource = true) { + if (controller->hasExternalSource()) { + if (finalizeSource) { + controller->externalSource()->finalize(); + } + controller->setFlags(controller->flags() & ~Flag_ExternalSource); + } + controller->setUnderlyingSource(JS::UndefinedHandleValue); + } + double strategyHWM() const { + return getFixedSlot(Slot_StrategyHWM).toNumber(); + } + void setStrategyHWM(double highWaterMark) { + setFixedSlot(Slot_StrategyHWM, NumberValue(highWaterMark)); + } + uint32_t flags() const { return getFixedSlot(Slot_Flags).toInt32(); } + void setFlags(uint32_t flags) { setFixedSlot(Slot_Flags, Int32Value(flags)); } + void addFlags(uint32_t flags) { setFlags(this->flags() | flags); } + void removeFlags(uint32_t flags) { setFlags(this->flags() & ~flags); } + bool started() const { return flags() & Flag_Started; } + void setStarted() { addFlags(Flag_Started); } + bool pulling() const { return flags() & Flag_Pulling; } + void setPulling() { addFlags(Flag_Pulling); } + void clearPullFlags() { removeFlags(Flag_Pulling | Flag_PullAgain); } + bool pullAgain() const { return flags() & Flag_PullAgain; } + void setPullAgain() { addFlags(Flag_PullAgain); } + bool closeRequested() const { return flags() & Flag_CloseRequested; } + void setCloseRequested() { addFlags(Flag_CloseRequested); } + bool isTeeBranch1() const { return flags() & Flag_TeeBranch1; } + void setTeeBranch1() { + MOZ_ASSERT(!isTeeBranch2()); + addFlags(Flag_TeeBranch1); + } + bool isTeeBranch2() const { return flags() & Flag_TeeBranch2; } + void setTeeBranch2() { + MOZ_ASSERT(!isTeeBranch1()); + addFlags(Flag_TeeBranch2); + } + bool hasExternalSource() const { return flags() & Flag_ExternalSource; } + bool sourceLocked() const { return flags() & Flag_SourceLocked; } + void setSourceLocked() { addFlags(Flag_SourceLocked); } + void clearSourceLocked() { removeFlags(Flag_SourceLocked); } +}; + +class ReadableStreamDefaultController : public ReadableStreamController { + private: + /** + * Memory layout for ReadableStreamDefaultControllers, starting after the + * slots shared among all types of controllers. + * + * StrategySize is treated as an opaque value when stored. The only use site + * ensures that it's wrapped into the current cx compartment. + */ + enum Slots { + Slot_StrategySize = ReadableStreamController::SlotCount, + SlotCount + }; + + public: + JS::Value strategySize() const { return getFixedSlot(Slot_StrategySize); } + void setStrategySize(const JS::Value& size) { + setFixedSlot(Slot_StrategySize, size); + } + + static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp); + static const ClassSpec classSpec_; + static const JSClass class_; + static const ClassSpec protoClassSpec_; + static const JSClass protoClass_; +}; + +class ReadableByteStreamController : public ReadableStreamController { + public: + /** + * Memory layout for ReadableByteStreamControllers, starting after the + * slots shared among all types of controllers. + * + * PendingPullIntos is guaranteed to be in the same compartment as the + * controller, but might contain wrappers for objects from other + * compartments. + * + * AutoAllocateSize is a primitive (numeric) value. + */ + enum Slots { + Slot_BYOBRequest = ReadableStreamController::SlotCount, + Slot_PendingPullIntos, + Slot_AutoAllocateSize, + SlotCount + }; + + JS::Value byobRequest() const { return getFixedSlot(Slot_BYOBRequest); } + void clearBYOBRequest() { + setFixedSlot(Slot_BYOBRequest, JS::UndefinedValue()); + } + ListObject* pendingPullIntos() const { + return &getFixedSlot(Slot_PendingPullIntos).toObject().as<ListObject>(); + } + JS::Value autoAllocateChunkSize() const { + return getFixedSlot(Slot_AutoAllocateSize); + } + void setAutoAllocateChunkSize(const JS::Value& size) { + setFixedSlot(Slot_AutoAllocateSize, size); + } + + static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp); + static const ClassSpec classSpec_; + static const JSClass class_; + static const ClassSpec protoClassSpec_; + static const JSClass protoClass_; +}; + +extern MOZ_MUST_USE bool CheckReadableStreamControllerCanCloseOrEnqueue( + JSContext* cx, JS::Handle<ReadableStreamController*> unwrappedController, + const char* action); + +extern MOZ_MUST_USE JSObject* ReadableStreamControllerCancelSteps( + JSContext* cx, JS::Handle<ReadableStreamController*> unwrappedController, + JS::Handle<JS::Value> reason); + +extern PromiseObject* ReadableStreamDefaultControllerPullSteps( + JSContext* cx, + JS::Handle<ReadableStreamDefaultController*> unwrappedController); + +extern bool ReadableStreamControllerStartHandler(JSContext* cx, unsigned argc, + JS::Value* vp); + +extern bool ReadableStreamControllerStartFailedHandler(JSContext* cx, + unsigned argc, + JS::Value* vp); + +} // namespace js + +template <> +inline bool JSObject::is<js::ReadableStreamController>() const { + return is<js::ReadableStreamDefaultController>() || + is<js::ReadableByteStreamController>(); +} + +namespace js { + +inline ReadableStreamController* ReadableStream::controller() const { + return &getFixedSlot(Slot_Controller) + .toObject() + .as<ReadableStreamController>(); +} + +inline void ReadableStream::setController( + ReadableStreamController* controller) { + setFixedSlot(Slot_Controller, JS::ObjectValue(*controller)); +} + +} // namespace js + +#endif // builtin_streams_ReadableStreamController_h diff --git a/js/src/builtin/streams/ReadableStreamDefaultController.cpp b/js/src/builtin/streams/ReadableStreamDefaultController.cpp new file mode 100644 index 0000000000..e1765c2518 --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamDefaultController.cpp @@ -0,0 +1,514 @@ +/* -*- 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/. */ + +/* Class ReadableStreamDefaultController. */ + +#include "mozilla/Assertions.h" // MOZ_ASSERT{,_IF} +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jsapi.h" // JS_ReportErrorNumberASCII +#include "jsfriendapi.h" // js::AssertSameCompartment + +#include "builtin/streams/ClassSpecMacro.h" // JS_STREAMS_CLASS_SPEC +#include "builtin/streams/MiscellaneousOperations.h" // js::IsMaybeWrapped +#include "builtin/streams/PullIntoDescriptor.h" // js::PullIntoDescriptor +#include "builtin/streams/QueueWithSizes.h" // js::{DequeueValue,ResetQueue} +#include "builtin/streams/ReadableStream.h" // js::ReadableStream, js::SetUpExternalReadableByteStreamController +#include "builtin/streams/ReadableStreamController.h" // js::ReadableStream{,Default}Controller, js::ReadableByteStreamController, js::CheckReadableStreamControllerCanCloseOrEnqueue, js::ReadableStreamControllerCancelSteps, js::ReadableStreamDefaultControllerPullSteps, js::ReadableStreamControllerStart{,Failed}Handler +#include "builtin/streams/ReadableStreamDefaultControllerOperations.h" // js::ReadableStreamController{CallPullIfNeeded,ClearAlgorithms,Error,GetDesiredSizeUnchecked}, js::ReadableStreamDefaultController{Close,Enqueue} +#include "builtin/streams/ReadableStreamInternals.h" // js::ReadableStream{AddReadOrReadIntoRequest,CloseInternal,CreateReadResult} +#include "builtin/streams/ReadableStreamOperations.h" // js::ReadableStreamTee_Cancel +#include "builtin/streams/ReadableStreamReader.h" // js::ReadableStream{,Default}Reader +#include "builtin/streams/StreamController.h" // js::StreamController +#include "builtin/streams/TeeState.h" // js::TeeState +#include "js/ArrayBuffer.h" // JS::NewArrayBuffer +#include "js/Class.h" // js::ClassSpec +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" +#include "vm/Interpreter.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseResolvedWithUndefined +#include "vm/SelfHosting.h" + +#include "builtin/HandlerFunction-inl.h" // js::TargetFromHandler +#include "builtin/streams/MiscellaneousOperations-inl.h" // js::PromiseCall +#include "builtin/streams/ReadableStreamReader-inl.h" // js::UnwrapReaderFromStream +#include "vm/Compartment-inl.h" // JS::Compartment::wrap, js::UnwrapAnd{DowncastObject,TypeCheckThis} +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/Realm-inl.h" // js::AutoRealm + +using js::ClassSpec; +using js::PromiseObject; +using js::ReadableStream; +using js::ReadableStreamController; +using js::ReadableStreamControllerCallPullIfNeeded; +using js::ReadableStreamControllerClearAlgorithms; +using js::ReadableStreamControllerError; +using js::ReadableStreamControllerGetDesiredSizeUnchecked; +using js::ReadableStreamDefaultController; +using js::ReadableStreamDefaultControllerClose; +using js::ReadableStreamDefaultControllerEnqueue; +using js::TargetFromHandler; +using js::UnwrapAndTypeCheckThis; + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::ObjectValue; +using JS::Rooted; +using JS::Value; + +/*** 3.9. Class ReadableStreamDefaultController *****************************/ + +/** + * Streams spec, 3.10.11. SetUpReadableStreamDefaultController, step 11 + * and + * Streams spec, 3.13.26. SetUpReadableByteStreamController, step 16: + * Upon fulfillment of startPromise, [...] + */ +bool js::ReadableStreamControllerStartHandler(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + Rooted<ReadableStreamController*> controller( + cx, TargetFromHandler<ReadableStreamController>(args)); + + // Step a: Set controller.[[started]] to true. + controller->setStarted(); + + // Step b: Assert: controller.[[pulling]] is false. + MOZ_ASSERT(!controller->pulling()); + + // Step c: Assert: controller.[[pullAgain]] is false. + MOZ_ASSERT(!controller->pullAgain()); + + // Step d: Perform + // ! ReadableStreamDefaultControllerCallPullIfNeeded(controller) + // (or ReadableByteStreamControllerCallPullIfNeeded(controller)). + if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 3.10.11. SetUpReadableStreamDefaultController, step 12 + * and + * Streams spec, 3.13.26. SetUpReadableByteStreamController, step 17: + * Upon rejection of startPromise with reason r, [...] + */ +bool js::ReadableStreamControllerStartFailedHandler(JSContext* cx, + unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + Rooted<ReadableStreamController*> controller( + cx, TargetFromHandler<ReadableStreamController>(args)); + + // Step a: Perform + // ! ReadableStreamDefaultControllerError(controller, r) + // (or ReadableByteStreamControllerError(controller, r)). + if (!ReadableStreamControllerError(cx, controller, args.get(0))) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 3.9.3. + * new ReadableStreamDefaultController( stream, underlyingSource, size, + * highWaterMark ) + */ +bool ReadableStreamDefaultController::constructor(JSContext* cx, unsigned argc, + Value* vp) { + // Step 1: Throw a TypeError. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BOGUS_CONSTRUCTOR, + "ReadableStreamDefaultController"); + return false; +} + +/** + * Streams spec, 3.9.4.1. get desiredSize + */ +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); + Rooted<ReadableStreamController*> unwrappedController( + cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultController>( + cx, args, "get desiredSize")); + if (!unwrappedController) { + return false; + } + + // 3.10.8. ReadableStreamDefaultControllerGetDesiredSize, steps 1-4. + // 3.10.8. Step 1: Let stream be controller.[[controlledReadableStream]]. + ReadableStream* unwrappedStream = unwrappedController->stream(); + + // 3.10.8. Step 2: Let state be stream.[[state]]. + // 3.10.8. Step 3: If state is "errored", return null. + if (unwrappedStream->errored()) { + args.rval().setNull(); + return true; + } + + // 3.10.8. Step 4: If state is "closed", return 0. + if (unwrappedStream->closed()) { + args.rval().setInt32(0); + return true; + } + + // Step 2: Return ! ReadableStreamDefaultControllerGetDesiredSize(this). + args.rval().setNumber( + ReadableStreamControllerGetDesiredSizeUnchecked(unwrappedController)); + return true; +} + +/** + * Unified implementation of step 2 of 3.9.4.2 and 3.9.4.3, + * and steps 2-3 of 3.11.4.3. + */ +MOZ_MUST_USE bool js::CheckReadableStreamControllerCanCloseOrEnqueue( + JSContext* cx, Handle<ReadableStreamController*> unwrappedController, + const char* action) { + // 3.9.4.2. close(), step 2, and + // 3.9.4.3. enqueue(chunk), step 2: + // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(this) is false, + // throw a TypeError exception. + // RSDCCanCloseOrEnqueue returns false in two cases: (1) + // controller.[[closeRequested]] is true; (2) the stream is not readable, + // i.e. already closed or errored. This amounts to exactly the same thing as + // 3.11.4.3 steps 2-3 below, and we want different error messages for the two + // cases anyway. + + // 3.11.4.3. Step 2: If this.[[closeRequested]] is true, throw a TypeError + // exception. + if (unwrappedController->closeRequested()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_CLOSED, action); + return false; + } + + // 3.11.4.3. Step 3: If this.[[controlledReadableByteStream]].[[state]] is + // not "readable", throw a TypeError exception. + ReadableStream* unwrappedStream = unwrappedController->stream(); + if (!unwrappedStream->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, + action); + return false; + } + + return true; +} + +/** + * Streams spec, 3.9.4.2 close() + */ +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); + Rooted<ReadableStreamDefaultController*> unwrappedController( + cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultController>(cx, args, + "close")); + if (!unwrappedController) { + return false; + } + + // Step 2: If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(this) is + // false, throw a TypeError exception. + if (!CheckReadableStreamControllerCanCloseOrEnqueue(cx, unwrappedController, + "close")) { + return false; + } + + // Step 3: Perform ! ReadableStreamDefaultControllerClose(this). + if (!ReadableStreamDefaultControllerClose(cx, unwrappedController)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 3.9.4.3. enqueue ( chunk ) + */ +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); + Rooted<ReadableStreamDefaultController*> unwrappedController( + cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultController>(cx, args, + "enqueue")); + if (!unwrappedController) { + return false; + } + + // Step 2: If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(this) is + // false, throw a TypeError exception. + if (!CheckReadableStreamControllerCanCloseOrEnqueue(cx, unwrappedController, + "enqueue")) { + return false; + } + + // Step 3: Return ! ReadableStreamDefaultControllerEnqueue(this, chunk). + if (!ReadableStreamDefaultControllerEnqueue(cx, unwrappedController, + args.get(0))) { + return false; + } + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 3.9.4.4. error ( e ) + */ +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); + Rooted<ReadableStreamDefaultController*> unwrappedController( + cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultController>(cx, args, + "enqueue")); + if (!unwrappedController) { + return false; + } + + // Step 2: Perform ! ReadableStreamDefaultControllerError(this, e). + if (!ReadableStreamControllerError(cx, unwrappedController, args.get(0))) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +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}; + +JS_STREAMS_CLASS_SPEC(ReadableStreamDefaultController, 0, SlotCount, + ClassSpec::DontDefineConstructor, 0, JS_NULL_CLASS_OPS); + +/** + * Unified implementation of ReadableStream controllers' [[CancelSteps]] + * internal methods. + * Streams spec, 3.9.5.1. [[CancelSteps]] ( reason ) + * and + * Streams spec, 3.11.5.1. [[CancelSteps]] ( reason ) + */ +MOZ_MUST_USE JSObject* js::ReadableStreamControllerCancelSteps( + JSContext* cx, Handle<ReadableStreamController*> unwrappedController, + Handle<Value> reason) { + AssertSameCompartment(cx, reason); + + // Step 1 of 3.11.5.1: If this.[[pendingPullIntos]] is not empty, + if (!unwrappedController->is<ReadableStreamDefaultController>()) { + Rooted<ListObject*> unwrappedPendingPullIntos( + cx, unwrappedController->as<ReadableByteStreamController>() + .pendingPullIntos()); + + if (unwrappedPendingPullIntos->length() != 0) { + // Step a: Let firstDescriptor be the first element of + // this.[[pendingPullIntos]]. + PullIntoDescriptor* unwrappedDescriptor = + UnwrapAndDowncastObject<PullIntoDescriptor>( + cx, &unwrappedPendingPullIntos->get(0).toObject()); + if (!unwrappedDescriptor) { + return nullptr; + } + + // Step b: Set firstDescriptor.[[bytesFilled]] to 0. + unwrappedDescriptor->setBytesFilled(0); + } + } + + Rooted<Value> unwrappedUnderlyingSource( + cx, unwrappedController->underlyingSource()); + + // Step 1 of 3.9.5.1, step 2 of 3.11.5.1: Perform ! ResetQueue(this). + if (!ResetQueue(cx, unwrappedController)) { + return nullptr; + } + + // Step 2 of 3.9.5.1, step 3 of 3.11.5.1: Let result be the result of + // performing this.[[cancelAlgorithm]], passing reason. + // + // Our representation of cancel algorithms is a bit awkward, for + // performance, so we must figure out which algorithm is being invoked. + Rooted<JSObject*> result(cx); + if (IsMaybeWrapped<TeeState>(unwrappedUnderlyingSource)) { + // The cancel algorithm given in ReadableStreamTee step 13 or 14. + MOZ_ASSERT(unwrappedUnderlyingSource.toObject().is<TeeState>(), + "tee streams and controllers are always same-compartment with " + "the TeeState object"); + Rooted<TeeState*> unwrappedTeeState( + cx, &unwrappedUnderlyingSource.toObject().as<TeeState>()); + Rooted<ReadableStreamDefaultController*> unwrappedDefaultController( + cx, &unwrappedController->as<ReadableStreamDefaultController>()); + result = ReadableStreamTee_Cancel(cx, unwrappedTeeState, + unwrappedDefaultController, reason); + } else if (unwrappedController->hasExternalSource()) { + // An embedding-provided cancel algorithm. + Rooted<Value> rval(cx); + { + AutoRealm ar(cx, unwrappedController); + JS::ReadableStreamUnderlyingSource* source = + unwrappedController->externalSource(); + Rooted<ReadableStream*> stream(cx, unwrappedController->stream()); + Rooted<Value> wrappedReason(cx, reason); + if (!cx->compartment()->wrap(cx, &wrappedReason)) { + return nullptr; + } + + cx->check(stream, wrappedReason); + rval = source->cancel(cx, stream, wrappedReason); + } + + // Make sure the ReadableStreamControllerClearAlgorithms call below is + // reached, even on error. + if (!cx->compartment()->wrap(cx, &rval)) { + result = nullptr; + } else { + result = PromiseObject::unforgeableResolve(cx, rval); + } + } else { + // The algorithm created in + // SetUpReadableByteStreamControllerFromUnderlyingSource step 5. + Rooted<Value> unwrappedCancelMethod(cx, + unwrappedController->cancelMethod()); + if (unwrappedCancelMethod.isUndefined()) { + // CreateAlgorithmFromUnderlyingMethod step 7. + result = PromiseResolvedWithUndefined(cx); + } else { + // CreateAlgorithmFromUnderlyingMethod steps 6.c.i-ii. + { + AutoRealm ar(cx, unwrappedController); + + // |unwrappedCancelMethod| and |unwrappedUnderlyingSource| come directly + // from |unwrappedController| slots so must be same-compartment with it. + cx->check(unwrappedCancelMethod); + cx->check(unwrappedUnderlyingSource); + + Rooted<Value> wrappedReason(cx, reason); + if (!cx->compartment()->wrap(cx, &wrappedReason)) { + return nullptr; + } + + // If PromiseCall fails, don't bail out until after the + // ReadableStreamControllerClearAlgorithms call below. + result = PromiseCall(cx, unwrappedCancelMethod, + unwrappedUnderlyingSource, wrappedReason); + } + if (!cx->compartment()->wrap(cx, &result)) { + result = nullptr; + } + } + } + + // Step 3 (or 4): Perform + // ! ReadableStreamDefaultControllerClearAlgorithms(this) + // (or ReadableByteStreamControllerClearAlgorithms(this)). + ReadableStreamControllerClearAlgorithms(unwrappedController); + + // Step 4 (or 5): Return result. + return result; +} + +/** + * Streams spec, 3.9.5.2. + * ReadableStreamDefaultController [[PullSteps]]( forAuthorCode ) + */ +PromiseObject* js::ReadableStreamDefaultControllerPullSteps( + JSContext* cx, + Handle<ReadableStreamDefaultController*> unwrappedController) { + // Step 1: Let stream be this.[[controlledReadableStream]]. + Rooted<ReadableStream*> unwrappedStream(cx, unwrappedController->stream()); + + // Step 2: If this.[[queue]] is not empty, + Rooted<ListObject*> unwrappedQueue(cx); + Rooted<Value> val( + cx, unwrappedController->getFixedSlot(StreamController::Slot_Queue)); + if (val.isObject()) { + unwrappedQueue = &val.toObject().as<ListObject>(); + } + + if (unwrappedQueue && unwrappedQueue->length() != 0) { + // Step a: Let chunk be ! DequeueValue(this). + Rooted<Value> chunk(cx); + if (!DequeueValue(cx, unwrappedController, &chunk)) { + return nullptr; + } + + // Step b: If this.[[closeRequested]] is true and this.[[queue]] is empty, + if (unwrappedController->closeRequested() && + unwrappedQueue->length() == 0) { + // Step i: Perform ! ReadableStreamDefaultControllerClearAlgorithms(this). + ReadableStreamControllerClearAlgorithms(unwrappedController); + + // Step ii: Perform ! ReadableStreamClose(stream). + if (!ReadableStreamCloseInternal(cx, unwrappedStream)) { + return nullptr; + } + } + + // Step c: Otherwise, perform + // ! ReadableStreamDefaultControllerCallPullIfNeeded(this). + else { + if (!ReadableStreamControllerCallPullIfNeeded(cx, unwrappedController)) { + return nullptr; + } + } + + // Step d: Return a promise resolved with + // ! ReadableStreamCreateReadResult(chunk, false, forAuthorCode). + cx->check(chunk); + ReadableStreamReader* unwrappedReader = + UnwrapReaderFromStream(cx, unwrappedStream); + if (!unwrappedReader) { + return nullptr; + } + + PlainObject* readResultObj = ReadableStreamCreateReadResult( + cx, chunk, false, unwrappedReader->forAuthorCode()); + if (!readResultObj) { + return nullptr; + } + + Rooted<Value> readResult(cx, ObjectValue(*readResultObj)); + return PromiseObject::unforgeableResolveWithNonPromise(cx, readResult); + } + + // Step 3: Let pendingPromise be + // ! ReadableStreamAddReadRequest(stream, forAuthorCode). + Rooted<PromiseObject*> pendingPromise( + cx, ReadableStreamAddReadOrReadIntoRequest(cx, unwrappedStream)); + if (!pendingPromise) { + return nullptr; + } + + // Step 4: Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this). + if (!ReadableStreamControllerCallPullIfNeeded(cx, unwrappedController)) { + return nullptr; + } + + // Step 5: Return pendingPromise. + return pendingPromise; +} diff --git a/js/src/builtin/streams/ReadableStreamDefaultControllerOperations.cpp b/js/src/builtin/streams/ReadableStreamDefaultControllerOperations.cpp new file mode 100644 index 0000000000..1be4073498 --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamDefaultControllerOperations.cpp @@ -0,0 +1,682 @@ +/* -*- 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/. */ + +/* Readable stream default controller abstract operations. */ + +#include "builtin/streams/ReadableStreamDefaultControllerOperations.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT{,_IF} +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jsfriendapi.h" // js::AssertSameCompartment + +#include "builtin/Stream.h" // js::ReadableByteStreamControllerClearPendingPullIntos +#include "builtin/streams/MiscellaneousOperations.h" // js::CreateAlgorithmFromUnderlyingMethod, js::InvokeOrNoop, js::IsMaybeWrapped +#include "builtin/streams/QueueWithSizes.h" // js::EnqueueValueWithSize, js::ResetQueue +#include "builtin/streams/ReadableStreamController.h" // js::ReadableStream{,Default}Controller, js::ReadableByteStreamController, js::ReadableStreamControllerStart{,Failed}Handler +#include "builtin/streams/ReadableStreamInternals.h" // js::ReadableStream{CloseInternal,ErrorInternal,FulfillReadOrReadIntoRequest,GetNumReadRequests} +#include "builtin/streams/ReadableStreamOperations.h" // js::ReadableStreamTee_Pull, js::SetUpReadableStreamDefaultController +#include "builtin/streams/TeeState.h" // js::TeeState +#include "js/CallArgs.h" // JS::CallArgs{,FromVp} +#include "js/Promise.h" // JS::AddPromiseReactions +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted +#include "js/Stream.h" // JS::ReadableStreamUnderlyingSource +#include "js/Value.h" // JS::{,Int32,Object}Value, JS::UndefinedHandleValue +#include "vm/Compartment.h" // JS::Compartment +#include "vm/Interpreter.h" // js::Call, js::GetAndClearExceptionAndStack +#include "vm/JSContext.h" // JSContext +#include "vm/JSObject.h" // JSObject +#include "vm/List.h" // js::ListObject +#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseResolvedWithUndefined +#include "vm/Runtime.h" // JSAtomState +#include "vm/SavedFrame.h" // js::SavedFrame + +#include "builtin/HandlerFunction-inl.h" // js::NewHandler +#include "builtin/streams/MiscellaneousOperations-inl.h" // js::PromiseCall +#include "vm/Compartment-inl.h" // JS::Compartment::wrap, js::UnwrapCalleeSlot +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/JSObject-inl.h" // js::IsCallable, js::NewBuiltinClassInstance +#include "vm/Realm-inl.h" // js::AutoRealm + +using js::ReadableByteStreamController; +using js::ReadableStream; +using js::ReadableStreamController; +using js::ReadableStreamControllerCallPullIfNeeded; +using js::ReadableStreamControllerError; +using js::ReadableStreamGetNumReadRequests; +using js::UnwrapCalleeSlot; + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::Rooted; +using JS::UndefinedHandleValue; +using JS::Value; + +/*** 3.10. Readable stream default controller abstract operations ***********/ + +// Streams spec, 3.10.1. IsReadableStreamDefaultController ( x ) +// Implemented via is<ReadableStreamDefaultController>() + +/** + * Streams spec, 3.10.2 and 3.13.3. step 7: + * Upon fulfillment of pullPromise, [...] + */ +static bool ControllerPullHandler(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<ReadableStreamController*> unwrappedController( + cx, UnwrapCalleeSlot<ReadableStreamController>(cx, args, 0)); + if (!unwrappedController) { + return false; + } + + bool pullAgain = unwrappedController->pullAgain(); + + // Step a: Set controller.[[pulling]] to false. + // Step b.i: Set controller.[[pullAgain]] to false. + unwrappedController->clearPullFlags(); + + // Step b: If controller.[[pullAgain]] is true, + if (pullAgain) { + // Step ii: Perform + // ! ReadableStreamDefaultControllerCallPullIfNeeded(controller) + // (or ReadableByteStreamControllerCallPullIfNeeded(controller)). + if (!ReadableStreamControllerCallPullIfNeeded(cx, unwrappedController)) { + return false; + } + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 3.10.2 and 3.13.3. step 8: + * Upon rejection of pullPromise with reason e, + */ +static bool ControllerPullFailedHandler(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + Handle<Value> e = args.get(0); + + Rooted<ReadableStreamController*> controller( + cx, UnwrapCalleeSlot<ReadableStreamController>(cx, args, 0)); + if (!controller) { + return false; + } + + // Step a: Perform ! ReadableStreamDefaultControllerError(controller, e). + // (ReadableByteStreamControllerError in 3.12.3.) + if (!ReadableStreamControllerError(cx, controller, e)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool ReadableStreamControllerShouldCallPull( + ReadableStreamController* unwrappedController); + +/** + * Streams spec, 3.10.2 + * ReadableStreamDefaultControllerCallPullIfNeeded ( controller ) + * Streams spec, 3.13.3. + * ReadableByteStreamControllerCallPullIfNeeded ( controller ) + */ +MOZ_MUST_USE bool js::ReadableStreamControllerCallPullIfNeeded( + JSContext* cx, Handle<ReadableStreamController*> unwrappedController) { + // Step 1: Let shouldPull be + // ! ReadableStreamDefaultControllerShouldCallPull(controller). + // (ReadableByteStreamDefaultControllerShouldCallPull in 3.13.3.) + bool shouldPull = ReadableStreamControllerShouldCallPull(unwrappedController); + + // Step 2: If shouldPull is false, return. + if (!shouldPull) { + return true; + } + + // Step 3: If controller.[[pulling]] is true, + if (unwrappedController->pulling()) { + // Step a: Set controller.[[pullAgain]] to true. + unwrappedController->setPullAgain(); + + // Step b: Return. + return true; + } + + // Step 4: Assert: controller.[[pullAgain]] is false. + MOZ_ASSERT(!unwrappedController->pullAgain()); + + // Step 5: Set controller.[[pulling]] to true. + unwrappedController->setPulling(); + + // We use this variable in step 7. For ease of error-handling, we wrap it + // early. + Rooted<JSObject*> wrappedController(cx, unwrappedController); + if (!cx->compartment()->wrap(cx, &wrappedController)) { + return false; + } + + // Step 6: Let pullPromise be the result of performing + // controller.[[pullAlgorithm]]. + // Our representation of pull algorithms is a bit awkward, for performance, + // so we must figure out which algorithm is being invoked. + Rooted<JSObject*> pullPromise(cx); + Rooted<Value> unwrappedUnderlyingSource( + cx, unwrappedController->underlyingSource()); + + if (IsMaybeWrapped<TeeState>(unwrappedUnderlyingSource)) { + // The pull algorithm given in ReadableStreamTee step 12. + MOZ_ASSERT(unwrappedUnderlyingSource.toObject().is<TeeState>(), + "tee streams and controllers are always same-compartment with " + "the TeeState object"); + Rooted<TeeState*> unwrappedTeeState( + cx, &unwrappedUnderlyingSource.toObject().as<TeeState>()); + pullPromise = ReadableStreamTee_Pull(cx, unwrappedTeeState); + } else if (unwrappedController->hasExternalSource()) { + // An embedding-provided pull algorithm. + { + AutoRealm ar(cx, unwrappedController); + JS::ReadableStreamUnderlyingSource* source = + unwrappedController->externalSource(); + Rooted<ReadableStream*> stream(cx, unwrappedController->stream()); + double desiredSize = + ReadableStreamControllerGetDesiredSizeUnchecked(unwrappedController); + source->requestData(cx, stream, desiredSize); + } + pullPromise = PromiseResolvedWithUndefined(cx); + } else { + // The pull algorithm created in + // SetUpReadableStreamDefaultControllerFromUnderlyingSource step 4. + Rooted<Value> unwrappedPullMethod(cx, unwrappedController->pullMethod()); + if (unwrappedPullMethod.isUndefined()) { + // CreateAlgorithmFromUnderlyingMethod step 7. + pullPromise = PromiseResolvedWithUndefined(cx); + } else { + // CreateAlgorithmFromUnderlyingMethod step 6.b.i. + { + AutoRealm ar(cx, unwrappedController); + + // |unwrappedPullMethod| and |unwrappedUnderlyingSource| come directly + // from |unwrappedController| slots so must be same-compartment with it. + cx->check(unwrappedPullMethod); + cx->check(unwrappedUnderlyingSource); + + Rooted<Value> controller(cx, ObjectValue(*unwrappedController)); + cx->check(controller); + + pullPromise = PromiseCall(cx, unwrappedPullMethod, + unwrappedUnderlyingSource, controller); + if (!pullPromise) { + return false; + } + } + if (!cx->compartment()->wrap(cx, &pullPromise)) { + return false; + } + } + } + if (!pullPromise) { + return false; + } + + // Step 7: Upon fulfillment of pullPromise, [...] + // Step 8. Upon rejection of pullPromise with reason e, [...] + Rooted<JSObject*> onPullFulfilled( + cx, NewHandler(cx, ControllerPullHandler, wrappedController)); + if (!onPullFulfilled) { + return false; + } + Rooted<JSObject*> onPullRejected( + cx, NewHandler(cx, ControllerPullFailedHandler, wrappedController)); + if (!onPullRejected) { + return false; + } + return JS::AddPromiseReactions(cx, pullPromise, onPullFulfilled, + onPullRejected); +} + +/** + * Streams spec, 3.10.3. + * ReadableStreamDefaultControllerShouldCallPull ( controller ) + * Streams spec, 3.13.25. + * ReadableByteStreamControllerShouldCallPull ( controller ) + */ +static bool ReadableStreamControllerShouldCallPull( + ReadableStreamController* unwrappedController) { + // Step 1: Let stream be controller.[[controlledReadableStream]] + // (or [[controlledReadableByteStream]]). + ReadableStream* unwrappedStream = unwrappedController->stream(); + + // 3.10.3. Step 2: + // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(controller) + // is false, return false. + // This turns out to be the same as 3.13.25 steps 2-3. + + // 3.13.25 Step 2: If stream.[[state]] is not "readable", return false. + if (!unwrappedStream->readable()) { + return false; + } + + // 3.13.25 Step 3: If controller.[[closeRequested]] is true, return false. + if (unwrappedController->closeRequested()) { + return false; + } + + // Step 3 (or 4): + // If controller.[[started]] is false, return false. + if (!unwrappedController->started()) { + return false; + } + + // 3.10.3. + // Step 4: If ! IsReadableStreamLocked(stream) is true and + // ! ReadableStreamGetNumReadRequests(stream) > 0, return true. + // + // 3.13.25. + // Step 5: If ! ReadableStreamHasDefaultReader(stream) is true and + // ! ReadableStreamGetNumReadRequests(stream) > 0, return true. + // Step 6: If ! ReadableStreamHasBYOBReader(stream) is true and + // ! ReadableStreamGetNumReadIntoRequests(stream) > 0, return true. + // + // All of these amount to the same thing in this implementation: + if (unwrappedStream->locked() && + ReadableStreamGetNumReadRequests(unwrappedStream) > 0) { + return true; + } + + // Step 5 (or 7): + // Let desiredSize be + // ! ReadableStreamDefaultControllerGetDesiredSize(controller). + // (ReadableByteStreamControllerGetDesiredSize in 3.13.25.) + double desiredSize = + ReadableStreamControllerGetDesiredSizeUnchecked(unwrappedController); + + // Step 6 (or 8): Assert: desiredSize is not null (implicit). + // Step 7 (or 9): If desiredSize > 0, return true. + // Step 8 (or 10): Return false. + return desiredSize > 0; +} + +/** + * Streams spec, 3.10.4. + * ReadableStreamDefaultControllerClearAlgorithms ( controller ) + * and 3.13.4. + * ReadableByteStreamControllerClearAlgorithms ( controller ) + */ +void js::ReadableStreamControllerClearAlgorithms( + Handle<ReadableStreamController*> controller) { + // Step 1: Set controller.[[pullAlgorithm]] to undefined. + // Step 2: Set controller.[[cancelAlgorithm]] to undefined. + // (In this implementation, the UnderlyingSource slot is part of the + // representation of these algorithms.) + controller->setPullMethod(UndefinedHandleValue); + controller->setCancelMethod(UndefinedHandleValue); + ReadableStreamController::clearUnderlyingSource(controller); + + // Step 3 (of 3.10.4 only) : Set controller.[[strategySizeAlgorithm]] to + // undefined. + if (controller->is<ReadableStreamDefaultController>()) { + controller->as<ReadableStreamDefaultController>().setStrategySize( + UndefinedHandleValue); + } +} + +/** + * Streams spec, 3.10.5. ReadableStreamDefaultControllerClose ( controller ) + */ +MOZ_MUST_USE bool js::ReadableStreamDefaultControllerClose( + JSContext* cx, + Handle<ReadableStreamDefaultController*> unwrappedController) { + // Step 1: Let stream be controller.[[controlledReadableStream]]. + Rooted<ReadableStream*> unwrappedStream(cx, unwrappedController->stream()); + + // Step 2: Assert: + // ! ReadableStreamDefaultControllerCanCloseOrEnqueue(controller) + // is true. + MOZ_ASSERT(!unwrappedController->closeRequested()); + MOZ_ASSERT(unwrappedStream->readable()); + + // Step 3: Set controller.[[closeRequested]] to true. + unwrappedController->setCloseRequested(); + + // Step 4: If controller.[[queue]] is empty, + Rooted<ListObject*> unwrappedQueue(cx, unwrappedController->queue()); + if (unwrappedQueue->length() == 0) { + // Step a: Perform + // ! ReadableStreamDefaultControllerClearAlgorithms(controller). + ReadableStreamControllerClearAlgorithms(unwrappedController); + + // Step b: Perform ! ReadableStreamClose(stream). + return ReadableStreamCloseInternal(cx, unwrappedStream); + } + + return true; +} + +/** + * Streams spec, 3.10.6. + * ReadableStreamDefaultControllerEnqueue ( controller, chunk ) + */ +MOZ_MUST_USE bool js::ReadableStreamDefaultControllerEnqueue( + JSContext* cx, Handle<ReadableStreamDefaultController*> unwrappedController, + Handle<Value> chunk) { + AssertSameCompartment(cx, chunk); + + // Step 1: Let stream be controller.[[controlledReadableStream]]. + Rooted<ReadableStream*> unwrappedStream(cx, unwrappedController->stream()); + + // Step 2: Assert: + // ! ReadableStreamDefaultControllerCanCloseOrEnqueue(controller) is + // true. + MOZ_ASSERT(!unwrappedController->closeRequested()); + MOZ_ASSERT(unwrappedStream->readable()); + + // Step 3: If ! IsReadableStreamLocked(stream) is true and + // ! ReadableStreamGetNumReadRequests(stream) > 0, perform + // ! ReadableStreamFulfillReadRequest(stream, chunk, false). + if (unwrappedStream->locked() && + ReadableStreamGetNumReadRequests(unwrappedStream) > 0) { + if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, unwrappedStream, chunk, + false)) { + return false; + } + } else { + // Step 4: Otherwise, + // Step a: Let result be the result of performing + // controller.[[strategySizeAlgorithm]], passing in chunk, and + // interpreting the result as an ECMAScript completion value. + // Step c: (on success) Let chunkSize be result.[[Value]]. + Rooted<Value> chunkSize(cx, Int32Value(1)); + bool success = true; + Rooted<Value> strategySize(cx, unwrappedController->strategySize()); + if (!strategySize.isUndefined()) { + if (!cx->compartment()->wrap(cx, &strategySize)) { + return false; + } + success = Call(cx, strategySize, UndefinedHandleValue, chunk, &chunkSize); + } + + // Step d: Let enqueueResult be + // EnqueueValueWithSize(controller, chunk, chunkSize). + if (success) { + success = EnqueueValueWithSize(cx, unwrappedController, chunk, chunkSize); + } + + // Step b: If result is an abrupt completion, + // and + // Step e: If enqueueResult is an abrupt completion, + if (!success) { + Rooted<Value> exn(cx); + Rooted<SavedFrame*> stack(cx); + if (!cx->isExceptionPending() || + !GetAndClearExceptionAndStack(cx, &exn, &stack)) { + // Uncatchable error. Die immediately without erroring the + // stream. + return false; + } + + // Step b.i: Perform ! ReadableStreamDefaultControllerError( + // controller, result.[[Value]]). + // Step e.i: Perform ! ReadableStreamDefaultControllerError( + // controller, enqueueResult.[[Value]]). + if (!ReadableStreamControllerError(cx, unwrappedController, exn)) { + return false; + } + + // Step b.ii: Return result. + // Step e.ii: Return enqueueResult. + // (I.e., propagate the exception.) + cx->setPendingException(exn, stack); + return false; + } + } + + // Step 5: Perform + // ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). + return ReadableStreamControllerCallPullIfNeeded(cx, unwrappedController); +} + +/** + * Streams spec, 3.10.7. ReadableStreamDefaultControllerError ( controller, e ) + * Streams spec, 3.13.11. ReadableByteStreamControllerError ( controller, e ) + */ +MOZ_MUST_USE bool js::ReadableStreamControllerError( + JSContext* cx, Handle<ReadableStreamController*> unwrappedController, + Handle<Value> e) { + MOZ_ASSERT(!cx->isExceptionPending()); + AssertSameCompartment(cx, e); + + // Step 1: Let stream be controller.[[controlledReadableStream]] + // (or controller.[[controlledReadableByteStream]]). + Rooted<ReadableStream*> unwrappedStream(cx, unwrappedController->stream()); + + // Step 2: If stream.[[state]] is not "readable", return. + if (!unwrappedStream->readable()) { + return true; + } + + // Step 3 of 3.13.10: + // Perform ! ReadableByteStreamControllerClearPendingPullIntos(controller). + if (unwrappedController->is<ReadableByteStreamController>()) { + Rooted<ReadableByteStreamController*> unwrappedByteStreamController( + cx, &unwrappedController->as<ReadableByteStreamController>()); + if (!ReadableByteStreamControllerClearPendingPullIntos( + cx, unwrappedByteStreamController)) { + return false; + } + } + + // Step 3 (or 4): Perform ! ResetQueue(controller). + if (!ResetQueue(cx, unwrappedController)) { + return false; + } + + // Step 4 (or 5): + // Perform ! ReadableStreamDefaultControllerClearAlgorithms(controller) + // (or ReadableByteStreamControllerClearAlgorithms(controller)). + ReadableStreamControllerClearAlgorithms(unwrappedController); + + // Step 5 (or 6): Perform ! ReadableStreamError(stream, e). + return ReadableStreamErrorInternal(cx, unwrappedStream, e); +} + +/** + * Streams spec, 3.10.8. + * ReadableStreamDefaultControllerGetDesiredSize ( controller ) + * Streams spec 3.13.14. + * ReadableByteStreamControllerGetDesiredSize ( controller ) + */ +MOZ_MUST_USE double js::ReadableStreamControllerGetDesiredSizeUnchecked( + ReadableStreamController* controller) { + // Steps 1-4 done at callsites, so only assert that they have been done. +#if DEBUG + ReadableStream* stream = controller->stream(); + MOZ_ASSERT(!(stream->errored() || stream->closed())); +#endif // DEBUG + + // Step 5: Return controller.[[strategyHWM]] − controller.[[queueTotalSize]]. + return controller->strategyHWM() - controller->queueTotalSize(); +} + +/** + * Streams spec, 3.10.11. + * SetUpReadableStreamDefaultController(stream, controller, + * startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, + * sizeAlgorithm ) + * + * The standard algorithm takes a `controller` argument which must be a new, + * blank object. This implementation creates a new controller instead. + * + * In the spec, three algorithms (startAlgorithm, pullAlgorithm, + * cancelAlgorithm) are passed as arguments to this routine. This + * implementation passes these "algorithms" as data, using four arguments: + * sourceAlgorithms, underlyingSource, pullMethod, and cancelMethod. The + * sourceAlgorithms argument tells how to interpret the other three: + * + * - SourceAlgorithms::Script - We're creating a stream from a JS source. + * The caller is `new ReadableStream(underlyingSource)` or + * `JS::NewReadableDefaultStreamObject`. `underlyingSource` is the + * source; `pullMethod` and `cancelMethod` are its .pull and + * .cancel methods, which the caller has already extracted and + * type-checked: each one must be either a callable JS object or undefined. + * + * Script streams use the start/pull/cancel algorithms defined in + * 3.10.12. SetUpReadableStreamDefaultControllerFromUnderlyingSource, which + * call JS methods of the underlyingSource. + * + * - SourceAlgorithms::Tee - We're creating a tee stream. `underlyingSource` + * is a TeeState object. `pullMethod` and `cancelMethod` are undefined. + * + * Tee streams use the start/pull/cancel algorithms given in + * 3.4.10. ReadableStreamTee. + * + * Note: All arguments must be same-compartment with cx. ReadableStream + * controllers are always created in the same compartment as the stream. + */ +MOZ_MUST_USE bool js::SetUpReadableStreamDefaultController( + JSContext* cx, Handle<ReadableStream*> stream, + SourceAlgorithms sourceAlgorithms, Handle<Value> underlyingSource, + Handle<Value> pullMethod, Handle<Value> cancelMethod, double highWaterMark, + Handle<Value> size) { + cx->check(stream, underlyingSource, size); + MOZ_ASSERT(pullMethod.isUndefined() || IsCallable(pullMethod)); + MOZ_ASSERT(cancelMethod.isUndefined() || IsCallable(cancelMethod)); + MOZ_ASSERT_IF(sourceAlgorithms != SourceAlgorithms::Script, + pullMethod.isUndefined()); + MOZ_ASSERT_IF(sourceAlgorithms != SourceAlgorithms::Script, + cancelMethod.isUndefined()); + MOZ_ASSERT(highWaterMark >= 0); + MOZ_ASSERT(size.isUndefined() || IsCallable(size)); + + // Done elsewhere in the standard: Create the new controller. + Rooted<ReadableStreamDefaultController*> controller( + cx, NewBuiltinClassInstance<ReadableStreamDefaultController>(cx)); + if (!controller) { + return false; + } + + // Step 1: Assert: stream.[[readableStreamController]] is undefined. + MOZ_ASSERT(!stream->hasController()); + + // Step 2: Set controller.[[controlledReadableStream]] to stream. + controller->setStream(stream); + + // Step 3: Set controller.[[queue]] and controller.[[queueTotalSize]] to + // undefined (implicit), then perform ! ResetQueue(controller). + if (!ResetQueue(cx, controller)) { + return false; + } + + // Step 4: Set controller.[[started]], controller.[[closeRequested]], + // controller.[[pullAgain]], and controller.[[pulling]] to false. + controller->setFlags(0); + + // Step 5: Set controller.[[strategySizeAlgorithm]] to sizeAlgorithm + // and controller.[[strategyHWM]] to highWaterMark. + controller->setStrategySize(size); + controller->setStrategyHWM(highWaterMark); + + // Step 6: Set controller.[[pullAlgorithm]] to pullAlgorithm. + // (In this implementation, the pullAlgorithm is determined by the + // underlyingSource in combination with the pullMethod field.) + controller->setUnderlyingSource(underlyingSource); + controller->setPullMethod(pullMethod); + + // Step 7: Set controller.[[cancelAlgorithm]] to cancelAlgorithm. + controller->setCancelMethod(cancelMethod); + + // Step 8: Set stream.[[readableStreamController]] to controller. + stream->setController(controller); + + // Step 9: Let startResult be the result of performing startAlgorithm. + Rooted<Value> startResult(cx); + if (sourceAlgorithms == SourceAlgorithms::Script) { + Rooted<Value> controllerVal(cx, ObjectValue(*controller)); + if (!InvokeOrNoop(cx, underlyingSource, cx->names().start, controllerVal, + &startResult)) { + return false; + } + } + + // Step 10: Let startPromise be a promise resolved with startResult. + Rooted<JSObject*> startPromise( + cx, PromiseObject::unforgeableResolve(cx, startResult)); + if (!startPromise) { + return false; + } + + // Step 11: Upon fulfillment of startPromise, [...] + // Step 12: Upon rejection of startPromise with reason r, [...] + Rooted<JSObject*> onStartFulfilled( + cx, NewHandler(cx, ReadableStreamControllerStartHandler, controller)); + if (!onStartFulfilled) { + return false; + } + Rooted<JSObject*> onStartRejected( + cx, + NewHandler(cx, ReadableStreamControllerStartFailedHandler, controller)); + if (!onStartRejected) { + return false; + } + if (!JS::AddPromiseReactions(cx, startPromise, onStartFulfilled, + onStartRejected)) { + return false; + } + + return true; +} + +/** + * Streams spec, 3.10.12. + * SetUpReadableStreamDefaultControllerFromUnderlyingSource( stream, + * underlyingSource, highWaterMark, sizeAlgorithm ) + */ +MOZ_MUST_USE bool js::SetUpReadableStreamDefaultControllerFromUnderlyingSource( + JSContext* cx, Handle<ReadableStream*> stream, + Handle<Value> underlyingSource, double highWaterMark, + Handle<Value> sizeAlgorithm) { + // Step 1: Assert: underlyingSource is not undefined. + MOZ_ASSERT(!underlyingSource.isUndefined()); + + // Step 2: Let controller be ObjectCreate(the original value of + // ReadableStreamDefaultController's prototype property). + // (Deferred to SetUpReadableStreamDefaultController.) + + // Step 3: Let startAlgorithm be the following steps: + // a. Return ? InvokeOrNoop(underlyingSource, "start", + // « controller »). + SourceAlgorithms sourceAlgorithms = SourceAlgorithms::Script; + + // Step 4: Let pullAlgorithm be + // ? CreateAlgorithmFromUnderlyingMethod(underlyingSource, "pull", + // 0, « controller »). + Rooted<Value> pullMethod(cx); + if (!CreateAlgorithmFromUnderlyingMethod(cx, underlyingSource, + "ReadableStream source.pull method", + cx->names().pull, &pullMethod)) { + return false; + } + + // Step 5. Let cancelAlgorithm be + // ? CreateAlgorithmFromUnderlyingMethod(underlyingSource, + // "cancel", 1, « »). + Rooted<Value> cancelMethod(cx); + if (!CreateAlgorithmFromUnderlyingMethod( + cx, underlyingSource, "ReadableStream source.cancel method", + cx->names().cancel, &cancelMethod)) { + return false; + } + + // Step 6. Perform ? SetUpReadableStreamDefaultController(stream, + // controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, + // highWaterMark, sizeAlgorithm). + return SetUpReadableStreamDefaultController( + cx, stream, sourceAlgorithms, underlyingSource, pullMethod, cancelMethod, + highWaterMark, sizeAlgorithm); +} diff --git a/js/src/builtin/streams/ReadableStreamDefaultControllerOperations.h b/js/src/builtin/streams/ReadableStreamDefaultControllerOperations.h new file mode 100644 index 0000000000..9d17a855ae --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamDefaultControllerOperations.h @@ -0,0 +1,73 @@ +/* -*- 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/. */ + +/* Readable stream default controller abstract operations. */ + +#ifndef builtin_streams_ReadableStreamDefaultControllerOperations_h +#define builtin_streams_ReadableStreamDefaultControllerOperations_h + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jstypes.h" // JS_PUBLIC_API +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::Value + +struct JS_PUBLIC_API JSContext; + +namespace js { + +class ReadableStream; +class ReadableStreamController; +class ReadableStreamDefaultController; + +extern MOZ_MUST_USE bool ReadableStreamDefaultControllerEnqueue( + JSContext* cx, + JS::Handle<ReadableStreamDefaultController*> unwrappedController, + JS::Handle<JS::Value> chunk); + +extern MOZ_MUST_USE bool ReadableStreamControllerError( + JSContext* cx, JS::Handle<ReadableStreamController*> unwrappedController, + JS::Handle<JS::Value> e); + +extern MOZ_MUST_USE bool ReadableStreamDefaultControllerClose( + JSContext* cx, + JS::Handle<ReadableStreamDefaultController*> unwrappedController); + +extern MOZ_MUST_USE double ReadableStreamControllerGetDesiredSizeUnchecked( + ReadableStreamController* controller); + +extern MOZ_MUST_USE bool ReadableStreamControllerCallPullIfNeeded( + JSContext* cx, JS::Handle<ReadableStreamController*> unwrappedController); + +extern void ReadableStreamControllerClearAlgorithms( + JS::Handle<ReadableStreamController*> controller); + +/** + * Characterizes the family of algorithms, (startAlgorithm, pullAlgorithm, + * cancelAlgorithm), associated with a readable stream. + * + * See the comment on SetUpReadableStreamDefaultController(). + */ +enum class SourceAlgorithms { + Script, + Tee, +}; + +extern MOZ_MUST_USE bool SetUpReadableStreamDefaultController( + JSContext* cx, JS::Handle<ReadableStream*> stream, + SourceAlgorithms sourceAlgorithms, JS::Handle<JS::Value> underlyingSource, + JS::Handle<JS::Value> pullMethod, JS::Handle<JS::Value> cancelMethod, + double highWaterMark, JS::Handle<JS::Value> size); + +extern MOZ_MUST_USE bool +SetUpReadableStreamDefaultControllerFromUnderlyingSource( + JSContext* cx, JS::Handle<ReadableStream*> stream, + JS::Handle<JS::Value> underlyingSource, double highWaterMark, + JS::Handle<JS::Value> sizeAlgorithm); + +} // namespace js + +#endif // builtin_streams_ReadableStreamDefaultControllerOperations_h diff --git a/js/src/builtin/streams/ReadableStreamDefaultReader.cpp b/js/src/builtin/streams/ReadableStreamDefaultReader.cpp new file mode 100644 index 0000000000..cf7cda48f6 --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamDefaultReader.cpp @@ -0,0 +1,265 @@ +/* -*- 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/. */ + +/* Class ReadableStreamDefaultReader. */ + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jsapi.h" // JS_ReportErrorNumberASCII + +#include "builtin/streams/ClassSpecMacro.h" // JS_STREAMS_CLASS_SPEC +#include "builtin/streams/MiscellaneousOperations.h" // js::ReturnPromiseRejectedWithPendingError +#include "builtin/streams/ReadableStream.h" // js::ReadableStream +#include "builtin/streams/ReadableStreamReader.h" // js::ForAuthorCodeBool, js::ReadableStream{,Default}Reader +#include "js/CallArgs.h" // JS::CallArgs{,FromVp} +#include "js/Class.h" // JSClass, JS_NULL_CLASS_OPS +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted +#include "vm/PromiseObject.h" // js::PromiseObject + +#include "vm/Compartment-inl.h" // js::UnwrapAndTypeCheckThis +#include "vm/JSObject-inl.h" // js::NewObjectWithClassProto +#include "vm/NativeObject-inl.h" // js::ThrowIfNotConstructing + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::Rooted; +using JS::Value; + +using js::ForAuthorCodeBool; +using js::GetErrorMessage; +using js::ListObject; +using js::NewObjectWithClassProto; +using js::PromiseObject; +using js::ReadableStream; +using js::ReadableStreamDefaultReader; +using js::ReadableStreamReader; +using js::UnwrapAndTypeCheckThis; + +/*** 3.6. Class ReadableStreamDefaultReader *********************************/ + +/** + * Stream spec, 3.6.3. new ReadableStreamDefaultReader ( stream ) + * Steps 2-4. + */ +MOZ_MUST_USE ReadableStreamDefaultReader* js::CreateReadableStreamDefaultReader( + JSContext* cx, Handle<ReadableStream*> unwrappedStream, + ForAuthorCodeBool forAuthorCode, Handle<JSObject*> proto /* = nullptr */) { + Rooted<ReadableStreamDefaultReader*> reader( + cx, NewObjectWithClassProto<ReadableStreamDefaultReader>(cx, proto)); + if (!reader) { + return nullptr; + } + + // Step 2: If ! IsReadableStreamLocked(stream) is true, throw a TypeError + // exception. + if (unwrappedStream->locked()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_LOCKED); + return nullptr; + } + + // Step 3: Perform ! ReadableStreamReaderGenericInitialize(this, stream). + // Step 4: Set this.[[readRequests]] to a new empty List. + if (!ReadableStreamReaderGenericInitialize(cx, reader, unwrappedStream, + forAuthorCode)) { + return nullptr; + } + + return reader; +} + +/** + * Stream spec, 3.6.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; + } + + // Implicit in the spec: Find the prototype object to use. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Null, &proto)) { + return false; + } + + // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError + // exception. + Rooted<ReadableStream*> unwrappedStream( + cx, UnwrapAndTypeCheckArgument<ReadableStream>( + cx, args, "ReadableStreamDefaultReader constructor", 0)); + if (!unwrappedStream) { + return false; + } + + Rooted<JSObject*> reader( + cx, CreateReadableStreamDefaultReader(cx, unwrappedStream, + ForAuthorCodeBool::Yes, proto)); + if (!reader) { + return false; + } + + args.rval().setObject(*reader); + return true; +} + +/** + * Streams spec, 3.6.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. + Rooted<ReadableStreamDefaultReader*> unwrappedReader( + cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultReader>(cx, args, + "get closed")); + if (!unwrappedReader) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: Return this.[[closedPromise]]. + Rooted<JSObject*> closedPromise(cx, unwrappedReader->closedPromise()); + if (!cx->compartment()->wrap(cx, &closedPromise)) { + return false; + } + + args.rval().setObject(*closedPromise); + return true; +} + +/** + * Streams spec, 3.6.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. + Rooted<ReadableStreamDefaultReader*> unwrappedReader( + cx, + UnwrapAndTypeCheckThis<ReadableStreamDefaultReader>(cx, args, "cancel")); + if (!unwrappedReader) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise + // rejected with a TypeError exception. + if (!unwrappedReader->hasStream()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMREADER_NOT_OWNED, "cancel"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: Return ! ReadableStreamReaderGenericCancel(this, reason). + JSObject* cancelPromise = + ReadableStreamReaderGenericCancel(cx, unwrappedReader, args.get(0)); + if (!cancelPromise) { + return false; + } + args.rval().setObject(*cancelPromise); + return true; +} + +/** + * Streams spec, 3.6.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. + Rooted<ReadableStreamDefaultReader*> unwrappedReader( + cx, + UnwrapAndTypeCheckThis<ReadableStreamDefaultReader>(cx, args, "read")); + if (!unwrappedReader) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise + // rejected with a TypeError exception. + if (!unwrappedReader->hasStream()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMREADER_NOT_OWNED, "read"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: Return ! ReadableStreamDefaultReaderRead(this, true). + PromiseObject* readPromise = + js::ReadableStreamDefaultReaderRead(cx, unwrappedReader); + if (!readPromise) { + return false; + } + args.rval().setObject(*readPromise); + return true; +} + +/** + * Streams spec, 3.6.4.4. releaseLock ( ) + */ +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); + Rooted<ReadableStreamDefaultReader*> reader( + cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultReader>(cx, args, + "releaseLock")); + if (!reader) { + return false; + } + + // Step 2: If this.[[ownerReadableStream]] is undefined, return. + if (!reader->hasStream()) { + args.rval().setUndefined(); + return true; + } + + // Step 3: If this.[[readRequests]] is not empty, throw a TypeError exception. + Value val = reader->getFixedSlot(ReadableStreamReader::Slot_Requests); + if (!val.isUndefined()) { + ListObject* readRequests = &val.toObject().as<ListObject>(); + if (readRequests->length() != 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMREADER_NOT_EMPTY, + "releaseLock"); + return false; + } + } + + // Step 4: Perform ! ReadableStreamReaderGenericRelease(this). + if (!js::ReadableStreamReaderGenericRelease(cx, reader)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +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}; + +const JSClass ReadableStreamReader::class_ = {"ReadableStreamReader"}; + +JS_STREAMS_CLASS_SPEC(ReadableStreamDefaultReader, 1, SlotCount, + js::ClassSpec::DontDefineConstructor, 0, + JS_NULL_CLASS_OPS); diff --git a/js/src/builtin/streams/ReadableStreamInternals.cpp b/js/src/builtin/streams/ReadableStreamInternals.cpp new file mode 100644 index 0000000000..a6f3fbc826 --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamInternals.cpp @@ -0,0 +1,473 @@ +/* -*- 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/. */ + +/* The interface between readable streams and controllers. */ + +#include "builtin/streams/ReadableStreamInternals.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT{,_IF} +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include <stdint.h> // uint32_t + +#include "jsfriendapi.h" // js::AssertSameCompartment + +#include "builtin/streams/ReadableStreamController.h" // js::ReadableStreamController{,CancelSteps} +#include "builtin/streams/ReadableStreamReader.h" // js::ReadableStream{,Default}Reader, js::ForAuthorCodeBool +#include "gc/AllocKind.h" // js::gc::AllocKind +#include "js/CallArgs.h" // JS::CallArgs{,FromVp} +#include "js/GCAPI.h" // JS::AutoSuppressGCAnalysis +#include "js/Promise.h" // JS::CallOriginalPromiseThen, JS::ResolvePromise +#include "js/Result.h" // JS_TRY_VAR_OR_RETURN_NULL +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted +#include "js/Stream.h" // JS::ReadableStreamUnderlyingSource, JS::ReadableStreamMode +#include "js/Value.h" // JS::Value, JS::{Boolean,Object}Value, JS::UndefinedHandleValue +#include "vm/JSContext.h" // JSContext +#include "vm/JSFunction.h" // JSFunction, js::NewNativeFunction +#include "vm/NativeObject.h" // js::NativeObject, js::PlainObject +#include "vm/ObjectGroup.h" // js::GenericObject +#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseResolvedWithUndefined +#include "vm/Realm.h" // JS::Realm +#include "vm/StringType.h" // js::PropertyName + +#include "builtin/Promise-inl.h" // js::SetSettledPromiseIsHandled +#include "builtin/streams/MiscellaneousOperations-inl.h" // js::{Reject,Resolve}UnwrappedPromiseWithUndefined +#include "builtin/streams/ReadableStreamReader-inl.h" // js::js::UnwrapReaderFromStream{,NoThrow} +#include "vm/Compartment-inl.h" // JS::Compartment::wrap +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/List-inl.h" // js::ListObject, js::AppendToListInFixedSlot, js::StoreNewListInFixedSlot +#include "vm/PlainObject-inl.h" // js::PlainObject::createWithTemplate +#include "vm/Realm-inl.h" // JS::Realm + +using JS::BooleanValue; +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::ObjectValue; +using JS::ResolvePromise; +using JS::Rooted; +using JS::UndefinedHandleValue; +using JS::Value; + +using js::PlainObject; +using js::ReadableStream; + +/*** 3.5. The interface between readable streams and controllers ************/ + +/** + * Streams spec, 3.5.1. + * ReadableStreamAddReadIntoRequest ( stream, forAuthorCode ) + * Streams spec, 3.5.2. + * ReadableStreamAddReadRequest ( stream, forAuthorCode ) + * + * Our implementation does not pass around forAuthorCode parameters in the same + * places as the standard, but the effect is the same. See the comment on + * `ReadableStreamReader::forAuthorCode()`. + */ +MOZ_MUST_USE js::PromiseObject* js::ReadableStreamAddReadOrReadIntoRequest( + JSContext* cx, Handle<ReadableStream*> unwrappedStream) { + // Step 1: Assert: ! IsReadableStream{BYOB,Default}Reader(stream.[[reader]]) + // is true. + // (Only default readers exist so far.) + Rooted<ReadableStreamReader*> unwrappedReader( + cx, UnwrapReaderFromStream(cx, unwrappedStream)); + if (!unwrappedReader) { + return nullptr; + } + MOZ_ASSERT(unwrappedReader->is<ReadableStreamDefaultReader>()); + + // Step 2 of 3.5.1: Assert: stream.[[state]] is "readable" or "closed". + // Step 2 of 3.5.2: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(unwrappedStream->readable() || unwrappedStream->closed()); + MOZ_ASSERT_IF(unwrappedReader->is<ReadableStreamDefaultReader>(), + unwrappedStream->readable()); + + // Step 3: Let promise be a new promise. + Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) { + return nullptr; + } + + // Step 4: Let read{Into}Request be + // Record {[[promise]]: promise, [[forAuthorCode]]: forAuthorCode}. + // Step 5: Append read{Into}Request as the last element of + // stream.[[reader]].[[read{Into}Requests]]. + // Since we don't need the [[forAuthorCode]] field (see the comment on + // `ReadableStreamReader::forAuthorCode()`), we elide the Record and store + // only the promise. + if (!AppendToListInFixedSlot(cx, unwrappedReader, + ReadableStreamReader::Slot_Requests, promise)) { + return nullptr; + } + + // Step 6: Return promise. + return promise; +} + +/** + * 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.5.3. ReadableStreamCancel ( stream, reason ) + */ +MOZ_MUST_USE JSObject* js::ReadableStreamCancel( + JSContext* cx, Handle<ReadableStream*> unwrappedStream, + Handle<Value> reason) { + AssertSameCompartment(cx, reason); + + // Step 1: Set stream.[[disturbed]] to true. + unwrappedStream->setDisturbed(); + + // Step 2: If stream.[[state]] is "closed", return a promise resolved with + // undefined. + if (unwrappedStream->closed()) { + return PromiseResolvedWithUndefined(cx); + } + + // Step 3: If stream.[[state]] is "errored", return a promise rejected with + // stream.[[storedError]]. + if (unwrappedStream->errored()) { + Rooted<Value> storedError(cx, unwrappedStream->storedError()); + if (!cx->compartment()->wrap(cx, &storedError)) { + return nullptr; + } + return PromiseObject::unforgeableReject(cx, storedError); + } + + // Step 4: Perform ! ReadableStreamClose(stream). + if (!ReadableStreamCloseInternal(cx, unwrappedStream)) { + return nullptr; + } + + // Step 5: Let sourceCancelPromise be + // ! stream.[[readableStreamController]].[[CancelSteps]](reason). + Rooted<ReadableStreamController*> unwrappedController( + cx, unwrappedStream->controller()); + Rooted<JSObject*> sourceCancelPromise( + cx, ReadableStreamControllerCancelSteps(cx, unwrappedController, reason)); + if (!sourceCancelPromise) { + return nullptr; + } + + // Step 6: Return the result of reacting to sourceCancelPromise with a + // fulfillment step that returns undefined. + Handle<PropertyName*> funName = cx->names().empty; + Rooted<JSFunction*> returnUndefined( + cx, NewNativeFunction(cx, ReturnUndefined, 0, funName, + gc::AllocKind::FUNCTION, GenericObject)); + if (!returnUndefined) { + return nullptr; + } + return JS::CallOriginalPromiseThen(cx, sourceCancelPromise, returnUndefined, + nullptr); +} + +/** + * Streams spec, 3.5.4. ReadableStreamClose ( stream ) + */ +MOZ_MUST_USE bool js::ReadableStreamCloseInternal( + JSContext* cx, Handle<ReadableStream*> unwrappedStream) { + // Step 1: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(unwrappedStream->readable()); + + // Step 2: Set stream.[[state]] to "closed". + unwrappedStream->setClosed(); + + // Step 4: If reader is undefined, return (reordered). + if (!unwrappedStream->hasReader()) { + return true; + } + + // Step 3: Let reader be stream.[[reader]]. + Rooted<ReadableStreamReader*> unwrappedReader( + cx, UnwrapReaderFromStream(cx, unwrappedStream)); + if (!unwrappedReader) { + return false; + } + + // Step 5: If ! IsReadableStreamDefaultReader(reader) is true, + if (unwrappedReader->is<ReadableStreamDefaultReader>()) { + ForAuthorCodeBool forAuthorCode = unwrappedReader->forAuthorCode(); + + // Step a: Repeat for each readRequest that is an element of + // reader.[[readRequests]], + Rooted<ListObject*> unwrappedReadRequests(cx, unwrappedReader->requests()); + uint32_t len = unwrappedReadRequests->length(); + Rooted<JSObject*> readRequest(cx); + Rooted<JSObject*> resultObj(cx); + Rooted<Value> resultVal(cx); + for (uint32_t i = 0; i < len; i++) { + // Step i: Resolve readRequest.[[promise]] with + // ! ReadableStreamCreateReadResult(undefined, true, + // readRequest.[[forAuthorCode]]). + readRequest = &unwrappedReadRequests->getAs<JSObject>(i); + if (!cx->compartment()->wrap(cx, &readRequest)) { + return false; + } + + resultObj = js::ReadableStreamCreateReadResult(cx, UndefinedHandleValue, + true, forAuthorCode); + if (!resultObj) { + return false; + } + resultVal = ObjectValue(*resultObj); + if (!ResolvePromise(cx, readRequest, resultVal)) { + return false; + } + } + + // Step b: Set reader.[[readRequests]] to an empty List. + unwrappedReader->clearRequests(); + } + + // Step 6: Resolve reader.[[closedPromise]] with undefined. + if (!ResolveUnwrappedPromiseWithUndefined(cx, + unwrappedReader->closedPromise())) { + return false; + } + + if (unwrappedStream->mode() == JS::ReadableStreamMode::ExternalSource) { + // Make sure we're in the stream's compartment. + AutoRealm ar(cx, unwrappedStream); + JS::ReadableStreamUnderlyingSource* source = + unwrappedStream->controller()->externalSource(); + source->onClosed(cx, unwrappedStream); + } + + return true; +} + +/** + * Streams spec, 3.5.5. ReadableStreamCreateReadResult ( value, done, + * forAuthorCode ) + */ +MOZ_MUST_USE PlainObject* js::ReadableStreamCreateReadResult( + JSContext* cx, Handle<Value> value, bool done, + ForAuthorCodeBool forAuthorCode) { + // Step 1: Let prototype be null. + // Step 2: If forAuthorCode is true, set prototype to %ObjectPrototype%. + Rooted<PlainObject*> templateObject( + cx, + forAuthorCode == ForAuthorCodeBool::Yes + ? cx->realm()->getOrCreateIterResultTemplateObject(cx) + : cx->realm()->getOrCreateIterResultWithoutPrototypeTemplateObject( + cx)); + if (!templateObject) { + return nullptr; + } + + // Step 3: Assert: Type(done) is Boolean (implicit). + + // Step 4: Let obj be ObjectCreate(prototype). + PlainObject* obj; + JS_TRY_VAR_OR_RETURN_NULL( + cx, obj, PlainObject::createWithTemplate(cx, templateObject)); + + // Step 5: Perform CreateDataProperty(obj, "value", value). + obj->setSlot(Realm::IterResultObjectValueSlot, value); + + // Step 6: Perform CreateDataProperty(obj, "done", done). + obj->setSlot(Realm::IterResultObjectDoneSlot, BooleanValue(done)); + + // Step 7: Return obj. + return obj; +} + +/** + * Streams spec, 3.5.6. ReadableStreamError ( stream, e ) + */ +MOZ_MUST_USE bool js::ReadableStreamErrorInternal( + JSContext* cx, Handle<ReadableStream*> unwrappedStream, Handle<Value> e) { + // Step 1: Assert: ! IsReadableStream(stream) is true (implicit). + + // Step 2: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(unwrappedStream->readable()); + + // Step 3: Set stream.[[state]] to "errored". + unwrappedStream->setErrored(); + + // Step 4: Set stream.[[storedError]] to e. + { + AutoRealm ar(cx, unwrappedStream); + Rooted<Value> wrappedError(cx, e); + if (!cx->compartment()->wrap(cx, &wrappedError)) { + return false; + } + unwrappedStream->setStoredError(wrappedError); + } + + // Step 6: If reader is undefined, return (reordered). + if (!unwrappedStream->hasReader()) { + return true; + } + + // Step 5: Let reader be stream.[[reader]]. + Rooted<ReadableStreamReader*> unwrappedReader( + cx, UnwrapReaderFromStream(cx, unwrappedStream)); + if (!unwrappedReader) { + return false; + } + + // Steps 7-8: (Identical in our implementation.) + // Step 7.a/8.b: Repeat for each read{Into}Request that is an element of + // reader.[[read{Into}Requests]], + { + Rooted<ListObject*> unwrappedReadRequests(cx, unwrappedReader->requests()); + Rooted<JSObject*> readRequest(cx); + uint32_t len = unwrappedReadRequests->length(); + for (uint32_t i = 0; i < len; i++) { + // Step i: Reject read{Into}Request.[[promise]] with e. + // Responses have to be created in the compartment from which the error + // was triggered, which might not be the same as the one the request was + // created in, so we have to wrap requests here. + readRequest = &unwrappedReadRequests->get(i).toObject(); + if (!RejectUnwrappedPromiseWithError(cx, &readRequest, e)) { + return false; + } + } + } + + // Step 7.b/8.c: Set reader.[[read{Into}Requests]] to a new empty List. + if (!StoreNewListInFixedSlot(cx, unwrappedReader, + ReadableStreamReader::Slot_Requests)) { + return false; + } + + // Step 9: Reject reader.[[closedPromise]] with e. + if (!RejectUnwrappedPromiseWithError(cx, unwrappedReader->closedPromise(), + e)) { + return false; + } + + // Step 10: Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. + // + // `closedPromise` can return a CCW, but that case is filtered out by step 6, + // given the only place that can set [[closedPromise]] to a CCW is + // 3.8.5 ReadableStreamReaderGenericRelease step 4, and + // 3.8.5 ReadableStreamReaderGenericRelease step 6 sets + // stream.[[reader]] to undefined. + Rooted<JSObject*> closedPromise(cx, unwrappedReader->closedPromise()); + js::SetSettledPromiseIsHandled(cx, closedPromise.as<PromiseObject>()); + + if (unwrappedStream->mode() == JS::ReadableStreamMode::ExternalSource) { + // Make sure we're in the stream's compartment. + AutoRealm ar(cx, unwrappedStream); + JS::ReadableStreamUnderlyingSource* source = + unwrappedStream->controller()->externalSource(); + + // Ensure that the embedding doesn't have to deal with + // mixed-compartment arguments to the callback. + Rooted<Value> error(cx, e); + if (!cx->compartment()->wrap(cx, &error)) { + return false; + } + source->onErrored(cx, unwrappedStream, error); + } + + return true; +} + +/** + * Streams spec, 3.5.7. + * ReadableStreamFulfillReadIntoRequest( stream, chunk, done ) + * Streams spec, 3.5.8. + * ReadableStreamFulfillReadRequest ( stream, chunk, done ) + * These two spec functions are identical in our implementation. + */ +MOZ_MUST_USE bool js::ReadableStreamFulfillReadOrReadIntoRequest( + JSContext* cx, Handle<ReadableStream*> unwrappedStream, Handle<Value> chunk, + bool done) { + cx->check(chunk); + + // Step 1: Let reader be stream.[[reader]]. + Rooted<ReadableStreamReader*> unwrappedReader( + cx, UnwrapReaderFromStream(cx, unwrappedStream)); + if (!unwrappedReader) { + return false; + } + + // Step 2: Let read{Into}Request be the first element of + // reader.[[read{Into}Requests]]. + // Step 3: Remove read{Into}Request from reader.[[read{Into}Requests]], + // shifting all other elements downward (so that the second becomes + // the first, and so on). + Rooted<ListObject*> unwrappedReadIntoRequests(cx, + unwrappedReader->requests()); + Rooted<JSObject*> readIntoRequest( + cx, &unwrappedReadIntoRequests->popFirstAs<JSObject>(cx)); + MOZ_ASSERT(readIntoRequest); + if (!cx->compartment()->wrap(cx, &readIntoRequest)) { + return false; + } + + // Step 4: Resolve read{Into}Request.[[promise]] with + // ! ReadableStreamCreateReadResult(chunk, done, + // readIntoRequest.[[forAuthorCode]]). + PlainObject* iterResult = ReadableStreamCreateReadResult( + cx, chunk, done, unwrappedReader->forAuthorCode()); + if (!iterResult) { + return false; + } + + Rooted<Value> val(cx, ObjectValue(*iterResult)); + return ResolvePromise(cx, readIntoRequest, val); +} + +/** + * Streams spec, 3.5.9. ReadableStreamGetNumReadIntoRequests ( stream ) + * Streams spec, 3.5.10. ReadableStreamGetNumReadRequests ( stream ) + * (Identical implementation.) + */ +uint32_t js::ReadableStreamGetNumReadRequests(ReadableStream* stream) { + // Step 1: Return the number of elements in + // stream.[[reader]].[[read{Into}Requests]]. + if (!stream->hasReader()) { + return 0; + } + + JS::AutoSuppressGCAnalysis nogc; + ReadableStreamReader* reader = UnwrapReaderFromStreamNoThrow(stream); + + // Reader is a dead wrapper, treat it as non-existent. + if (!reader) { + return 0; + } + + return reader->requests()->length(); +} + +// Streams spec, 3.5.11. ReadableStreamHasBYOBReader ( stream ) +// +// Not implemented. + +/** + * Streams spec 3.5.12. ReadableStreamHasDefaultReader ( stream ) + */ +MOZ_MUST_USE bool js::ReadableStreamHasDefaultReader( + JSContext* cx, Handle<ReadableStream*> unwrappedStream, bool* result) { + // Step 1: Let reader be stream.[[reader]]. + // Step 2: If reader is undefined, return false. + if (!unwrappedStream->hasReader()) { + *result = false; + return true; + } + Rooted<ReadableStreamReader*> unwrappedReader( + cx, UnwrapReaderFromStream(cx, unwrappedStream)); + if (!unwrappedReader) { + return false; + } + + // Step 3: If ! ReadableStreamDefaultReader(reader) is false, return false. + // Step 4: Return true. + *result = unwrappedReader->is<ReadableStreamDefaultReader>(); + return true; +} diff --git a/js/src/builtin/streams/ReadableStreamInternals.h b/js/src/builtin/streams/ReadableStreamInternals.h new file mode 100644 index 0000000000..3fe6400ec7 --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamInternals.h @@ -0,0 +1,57 @@ +/* -*- 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/. */ + +/* The interface between readable streams and controllers. */ + +#ifndef builtin_streams_ReadableStreamInternals_h +#define builtin_streams_ReadableStreamInternals_h + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jstypes.h" // JS_PUBLIC_API +#include "builtin/streams/ReadableStreamReader.h" // js::ForAuthorCodeBool +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::Value + +struct JS_PUBLIC_API JSContext; +class JS_PUBLIC_API JSObject; + +namespace js { + +class PlainObject; +class PromiseObject; +class ReadableStream; + +extern MOZ_MUST_USE PromiseObject* ReadableStreamAddReadOrReadIntoRequest( + JSContext* cx, JS::Handle<ReadableStream*> unwrappedStream); + +extern MOZ_MUST_USE JSObject* ReadableStreamCancel( + JSContext* cx, JS::Handle<ReadableStream*> unwrappedStream, + JS::Handle<JS::Value> reason); + +extern MOZ_MUST_USE bool ReadableStreamCloseInternal( + JSContext* cx, JS::Handle<ReadableStream*> unwrappedStream); + +extern MOZ_MUST_USE PlainObject* ReadableStreamCreateReadResult( + JSContext* cx, JS::Handle<JS::Value> value, bool done, + ForAuthorCodeBool forAuthorCode); + +extern MOZ_MUST_USE bool ReadableStreamErrorInternal( + JSContext* cx, JS::Handle<ReadableStream*> unwrappedStream, + JS::Handle<JS::Value> e); + +extern MOZ_MUST_USE bool ReadableStreamFulfillReadOrReadIntoRequest( + JSContext* cx, JS::Handle<ReadableStream*> unwrappedStream, + JS::Handle<JS::Value> chunk, bool done); + +extern uint32_t ReadableStreamGetNumReadRequests(ReadableStream* stream); + +extern MOZ_MUST_USE bool ReadableStreamHasDefaultReader( + JSContext* cx, JS::Handle<ReadableStream*> unwrappedStream, bool* result); + +} // namespace js + +#endif // builtin_streams_ReadableStreamInternals_h diff --git a/js/src/builtin/streams/ReadableStreamOperations.cpp b/js/src/builtin/streams/ReadableStreamOperations.cpp new file mode 100644 index 0000000000..f40e375749 --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamOperations.cpp @@ -0,0 +1,653 @@ +/* -*- 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/. */ + +/* General readable stream abstract operations. */ + +#include "builtin/streams/ReadableStreamOperations.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT{,_IF} +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#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 +#include "builtin/streams/ReadableStreamInternals.h" // js::ReadableStreamCancel +#include "builtin/streams/ReadableStreamReader.h" // js::CreateReadableStreamDefaultReader, js::ForAuthorCodeBool, js::ReadableStream{,Default}Reader, js::ReadableStreamDefaultReaderRead +#include "builtin/streams/TeeState.h" // js::TeeState +#include "js/CallArgs.h" // JS::CallArgs{,FromVp} +#include "js/Promise.h" // JS::CallOriginalPromiseThen, JS::AddPromiseReactions +#include "js/RootingAPI.h" // JS::{,Mutable}Handle, JS::Rooted +#include "js/Value.h" // JS::Value, JS::UndefinedHandleValue +#include "vm/JSContext.h" // JSContext +#include "vm/NativeObject.h" // js::NativeObject +#include "vm/ObjectOperations.h" // js::GetProperty +#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseResolvedWithUndefined + +#include "builtin/HandlerFunction-inl.h" // js::NewHandler, js::TargetFromHandler +#include "builtin/streams/MiscellaneousOperations-inl.h" // js::ResolveUnwrappedPromiseWithValue +#include "builtin/streams/ReadableStreamReader-inl.h" // js::UnwrapReaderFromStream +#include "vm/Compartment-inl.h" // JS::Compartment::wrap, js::Unwrap{Callee,Internal}Slot +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/JSObject-inl.h" // js::IsCallable, js::NewObjectWithClassProto +#include "vm/Realm-inl.h" // js::AutoRealm + +using js::IsCallable; +using js::NewHandler; +using js::NewObjectWithClassProto; +using js::PromiseObject; +using js::ReadableStream; +using js::ReadableStreamDefaultController; +using js::ReadableStreamDefaultControllerEnqueue; +using js::ReadableStreamDefaultReader; +using js::ReadableStreamReader; +using js::SourceAlgorithms; +using js::TargetFromHandler; +using js::TeeState; +using js::UnwrapCalleeSlot; + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::MutableHandle; +using JS::ObjectValue; +using JS::Rooted; +using JS::UndefinedHandleValue; +using JS::Value; + +/*** 3.4. General readable stream abstract operations ***********************/ + +// Streams spec, 3.4.1. AcquireReadableStreamBYOBReader ( stream ) +// Always inlined. + +// Streams spec, 3.4.2. AcquireReadableStreamDefaultReader ( stream ) +// Always inlined. See CreateReadableStreamDefaultReader. + +/** + * Streams spec, 3.4.3. CreateReadableStream ( + * startAlgorithm, pullAlgorithm, cancelAlgorithm + * [, highWaterMark [, sizeAlgorithm ] ] ) + * + * The start/pull/cancelAlgorithm arguments are represented instead as four + * arguments: sourceAlgorithms, underlyingSource, pullMethod, cancelMethod. + * See the comment on SetUpReadableStreamDefaultController. + */ +static MOZ_MUST_USE ReadableStream* CreateReadableStream( + JSContext* cx, SourceAlgorithms sourceAlgorithms, + Handle<Value> underlyingSource, + Handle<Value> pullMethod = UndefinedHandleValue, + Handle<Value> cancelMethod = UndefinedHandleValue, double highWaterMark = 1, + Handle<Value> sizeAlgorithm = UndefinedHandleValue, + Handle<JSObject*> proto = nullptr) { + cx->check(underlyingSource, sizeAlgorithm, proto); + MOZ_ASSERT(sizeAlgorithm.isUndefined() || IsCallable(sizeAlgorithm)); + + // Step 1: If highWaterMark was not passed, set it to 1 (implicit). + // Step 2: If sizeAlgorithm was not passed, set it to an algorithm that + // returns 1 (implicit). + // Step 3: Assert: ! IsNonNegativeNumber(highWaterMark) is true. + MOZ_ASSERT(highWaterMark >= 0); + + // Step 4: Let stream be ObjectCreate(the original value of ReadableStream's + // prototype property). + // Step 5: Perform ! InitializeReadableStream(stream). + Rooted<ReadableStream*> stream(cx, + ReadableStream::create(cx, nullptr, proto)); + if (!stream) { + return nullptr; + } + + // Step 6: Let controller be ObjectCreate(the original value of + // ReadableStreamDefaultController's prototype property). + // Step 7: Perform ? SetUpReadableStreamDefaultController(stream, + // controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, + // highWaterMark, sizeAlgorithm). + if (!SetUpReadableStreamDefaultController( + cx, stream, sourceAlgorithms, underlyingSource, pullMethod, + cancelMethod, highWaterMark, sizeAlgorithm)) { + return nullptr; + } + + // Step 8: Return stream. + return stream; +} + +// Streams spec, 3.4.4. CreateReadableByteStream ( +// startAlgorithm, pullAlgorithm, cancelAlgorithm +// [, highWaterMark [, autoAllocateChunkSize ] ] ) +// Not implemented. + +/** + * Streams spec, 3.4.5. InitializeReadableStream ( stream ) + */ +/* static */ MOZ_MUST_USE ReadableStream* ReadableStream::create( + JSContext* cx, void* nsISupportsObject_alreadyAddreffed /* = nullptr */, + Handle<JSObject*> proto /* = nullptr */) { + // In the spec, InitializeReadableStream is always passed a newly created + // ReadableStream object. We instead create it here and return it below. + Rooted<ReadableStream*> stream( + cx, NewObjectWithClassProto<ReadableStream>(cx, proto)); + if (!stream) { + return nullptr; + } + + stream->setPrivate(nsISupportsObject_alreadyAddreffed); + + // Step 1: Set stream.[[state]] to "readable". + stream->initStateBits(Readable); + MOZ_ASSERT(stream->readable()); + + // Step 2: Set stream.[[reader]] and stream.[[storedError]] to + // undefined (implicit). + MOZ_ASSERT(!stream->hasReader()); + MOZ_ASSERT(stream->storedError().isUndefined()); + + // Step 3: Set stream.[[disturbed]] to false (done in step 1). + MOZ_ASSERT(!stream->disturbed()); + + return stream; +} + +// Streams spec, 3.4.6. IsReadableStream ( x ) +// Using UnwrapAndTypeCheck templates instead. + +// Streams spec, 3.4.7. IsReadableStreamDisturbed ( stream ) +// Using stream->disturbed() instead. + +/** + * Streams spec, 3.4.8. IsReadableStreamLocked ( 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. + // 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 stream->controller(), but the source also + // can't be locked yet. + if (hasController() && controller()->sourceLocked()) { + return true; + } + return hasReader(); +} + +// Streams spec, 3.4.9. IsReadableStreamAsyncIterator ( x ) +// +// Not implemented. + +/** + * Streams spec, 3.4.10. ReadableStreamTee steps 12.c.i-x. + */ +static bool TeeReaderReadHandler(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<TeeState*> unwrappedTeeState(cx, + UnwrapCalleeSlot<TeeState>(cx, args, 0)); + if (!unwrappedTeeState) { + return false; + } + + Handle<Value> resultVal = args.get(0); + + // Step 12.c.i: Set reading to false. + unwrappedTeeState->unsetReading(); + + // Step 12.c.ii: Assert: Type(result) is Object. + Rooted<JSObject*> result(cx, &resultVal.toObject()); + + bool done; + { + // Step 12.c.iii: Let done be ? Get(result, "done"). + // (This can fail only if `result` was nuked.) + Rooted<Value> doneVal(cx); + if (!GetProperty(cx, result, result, cx->names().done, &doneVal)) { + return false; + } + + // Step 12.c.iv: Assert: Type(done) is Boolean. + done = doneVal.toBoolean(); + } + + // Step 12.c.v: If done is true, + if (done) { + // Step 12.c.v.1: If canceled1 is false, + if (!unwrappedTeeState->canceled1()) { + // Step 12.c.v.1.a: Perform + // ! ReadableStreamDefaultControllerClose( + // branch1.[[readableStreamController]]). + Rooted<ReadableStreamDefaultController*> unwrappedBranch1( + cx, unwrappedTeeState->branch1()); + if (!ReadableStreamDefaultControllerClose(cx, unwrappedBranch1)) { + return false; + } + } + + // Step 12.c.v.2: If canceled2 is false, + if (!unwrappedTeeState->canceled2()) { + // Step 12.c.v.2.a: Perform + // ! ReadableStreamDefaultControllerClose( + // branch2.[[readableStreamController]]). + Rooted<ReadableStreamDefaultController*> unwrappedBranch2( + cx, unwrappedTeeState->branch2()); + if (!ReadableStreamDefaultControllerClose(cx, unwrappedBranch2)) { + return false; + } + } + + args.rval().setUndefined(); + return true; + } + + // Step 12.c.vi: Let value be ! Get(result, "value"). + // (This can fail only if `result` was nuked.) + Rooted<Value> value(cx); + if (!GetProperty(cx, result, result, cx->names().value, &value)) { + return false; + } + + // Step 12.c.vii: Let value1 and value2 be value. + // Step 12.c.viii: If canceled2 is false and cloneForBranch2 is true, set + // value2 to + // ? StructuredDeserialize(? StructuredSerialize(value2), + // the current Realm Record). + // We don't yet support any specifications that use cloneForBranch2, and + // the Streams spec doesn't offer any way for author code to enable it, + // so it's always false here. + auto& value1 = value; + MOZ_ASSERT(!unwrappedTeeState->cloneForBranch2(), + "support for cloneForBranch2=true is not yet implemented"); + auto& value2 = value; + + Rooted<ReadableStreamDefaultController*> unwrappedController(cx); + + // Step 12.c.ix: If canceled1 is false, perform + // ? ReadableStreamDefaultControllerEnqueue( + // branch1.[[readableStreamController]], value1). + if (!unwrappedTeeState->canceled1()) { + unwrappedController = unwrappedTeeState->branch1(); + if (!ReadableStreamDefaultControllerEnqueue(cx, unwrappedController, + value1)) { + return false; + } + } + + // Step 12.c.x: If canceled2 is false, perform + // ? ReadableStreamDefaultControllerEnqueue( + // branch2.[[readableStreamController]], value2). + if (!unwrappedTeeState->canceled2()) { + unwrappedController = unwrappedTeeState->branch2(); + if (!ReadableStreamDefaultControllerEnqueue(cx, unwrappedController, + value2)) { + return false; + } + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 3.4.10. ReadableStreamTee step 12, "Let pullAlgorithm be the + * following steps:" + */ +MOZ_MUST_USE PromiseObject* js::ReadableStreamTee_Pull( + JSContext* cx, JS::Handle<TeeState*> unwrappedTeeState) { + // Combine step 12.a/12.e far below, and handle steps 12.b-12.d after + // inverting step 12.a's "If reading is true" condition. + if (!unwrappedTeeState->reading()) { + // Step 12.b: Set reading to true. + unwrappedTeeState->setReading(); + + // Implicit in the spec: Unpack `reader` from the TeeState (by way of the + // stream stored in one of its slots). + Rooted<ReadableStreamDefaultReader*> unwrappedReader(cx); + { + Rooted<ReadableStream*> unwrappedStream( + cx, UnwrapInternalSlot<ReadableStream>(cx, unwrappedTeeState, + TeeState::Slot_Stream)); + if (!unwrappedStream) { + return nullptr; + } + ReadableStreamReader* unwrappedReaderObj = + UnwrapReaderFromStream(cx, unwrappedStream); + if (!unwrappedReaderObj) { + return nullptr; + } + + unwrappedReader = &unwrappedReaderObj->as<ReadableStreamDefaultReader>(); + } + + // Step 12.c: Let readPromise be the result of reacting to + // ! ReadableStreamDefaultReaderRead(reader) with the following + // fulfillment steps given the argument result: [...] + // Step 12.d: Set readPromise.[[PromiseIsHandled]] to true. + + // First, perform |ReadableStreamDefaultReaderRead(reader)|. + Rooted<PromiseObject*> readerReadResultPromise( + cx, js::ReadableStreamDefaultReaderRead(cx, unwrappedReader)); + if (!readerReadResultPromise) { + return nullptr; + } + + // Next, create a function to perform the fulfillment steps under step 12.c + // (implemented in the |TeeReaderReadHandler| C++ function). + Rooted<JSObject*> teeState(cx, unwrappedTeeState); + if (!cx->compartment()->wrap(cx, &teeState)) { + return nullptr; + } + + Rooted<JSObject*> onFulfilled( + cx, NewHandler(cx, TeeReaderReadHandler, teeState)); + if (!onFulfilled) { + return nullptr; + } + + // Finally, perform those fulfillment steps when |readerReadResultPromise| + // fulfills. (Step 12.c doesn't provide rejection steps, so don't handle + // rejection.) + // + // The spec's |readPromise| promise is unobservable, so implement this using + // a JSAPI function that acts as if it created |readPromise| but doesn't + // actually do so. + // + // Step 12.d causes |readPromise| to be treated as handled, even if it + // rejects. Use |JS::AddPromiseReactionsIgnoringUnhandledRejection|, not + // |JS::AddPromiseReactions|, to avoid reporting a freshly-consed-up promise + // as rejected if |readerReadResultPromise| rejects. + if (!JS::AddPromiseReactionsIgnoringUnhandledRejection( + cx, readerReadResultPromise, onFulfilled, nullptr)) { + return nullptr; + } + } + + // Step 12.a: (If reading is true,) return a promise resolved with undefined. + // Step 12.e: Return a promise resolved with undefined. + return PromiseResolvedWithUndefined(cx); +} + +/** + * Cancel one branch of a tee'd stream with the given |reason_|. + * + * Streams spec, 3.4.10. ReadableStreamTee steps 13 and 14: "Let + * cancel1Algorithm/cancel2Algorithm be the following steps, taking a reason + * argument:" + */ +MOZ_MUST_USE JSObject* js::ReadableStreamTee_Cancel( + JSContext* cx, JS::Handle<TeeState*> unwrappedTeeState, + JS::Handle<ReadableStreamDefaultController*> unwrappedBranch, + JS::Handle<Value> reason) { + Rooted<ReadableStream*> unwrappedStream( + cx, UnwrapInternalSlot<ReadableStream>(cx, unwrappedTeeState, + TeeState::Slot_Stream)); + if (!unwrappedStream) { + return nullptr; + } + + bool bothBranchesCanceled = false; + + // Step 13/14.a: Set canceled1/canceled2 to true. + // Step 13/14.b: Set reason1/reason2 to reason. + { + AutoRealm ar(cx, unwrappedTeeState); + + Rooted<Value> unwrappedReason(cx, reason); + if (!cx->compartment()->wrap(cx, &unwrappedReason)) { + return nullptr; + } + + if (unwrappedBranch->isTeeBranch1()) { + unwrappedTeeState->setCanceled1(unwrappedReason); + bothBranchesCanceled = unwrappedTeeState->canceled2(); + } else { + MOZ_ASSERT(unwrappedBranch->isTeeBranch2()); + unwrappedTeeState->setCanceled2(unwrappedReason); + bothBranchesCanceled = unwrappedTeeState->canceled1(); + } + } + + Rooted<PromiseObject*> unwrappedCancelPromise( + cx, unwrappedTeeState->cancelPromise()); + MOZ_ASSERT(unwrappedCancelPromise != nullptr); + + // Step 13/14.c: If canceled2/canceled1 is true, + if (bothBranchesCanceled) { + // Step 13/14.c.i: Let compositeReason be + // ! CreateArrayFromList(« reason1, reason2 »). + Rooted<Value> compositeReason(cx); + { + Rooted<Value> reason1(cx, unwrappedTeeState->reason1()); + Rooted<Value> reason2(cx, unwrappedTeeState->reason2()); + if (!cx->compartment()->wrap(cx, &reason1) || + !cx->compartment()->wrap(cx, &reason2)) { + return nullptr; + } + + ArrayObject* reasonArray = NewDenseFullyAllocatedArray(cx, 2); + if (!reasonArray) { + return nullptr; + } + reasonArray->setDenseInitializedLength(2); + reasonArray->initDenseElement(0, reason1); + reasonArray->initDenseElement(1, reason2); + + compositeReason = ObjectValue(*reasonArray); + } + + // Step 13/14.c.ii: Let cancelResult be + // ! ReadableStreamCancel(stream, compositeReason). + // In our implementation, this can fail with OOM. The best course then + // is to reject cancelPromise with an OOM error. + Rooted<JSObject*> cancelResult( + cx, js::ReadableStreamCancel(cx, unwrappedStream, compositeReason)); + if (!cancelResult) { + // Handle the OOM case mentioned above. + AutoRealm ar(cx, unwrappedCancelPromise); + if (!RejectPromiseWithPendingError(cx, unwrappedCancelPromise)) { + return nullptr; + } + } else { + // Step 13/14.c.iii: Resolve cancelPromise with cancelResult. + Rooted<Value> cancelResultVal(cx, ObjectValue(*cancelResult)); + if (!ResolveUnwrappedPromiseWithValue(cx, unwrappedCancelPromise, + cancelResultVal)) { + return nullptr; + } + } + } + + // Step 13/14.d: Return cancelPromise. + Rooted<JSObject*> cancelPromise(cx, unwrappedCancelPromise); + if (!cx->compartment()->wrap(cx, &cancelPromise)) { + return nullptr; + } + + return cancelPromise; +} + +/** + * Streams spec, 3.4.10. step 18: + * Upon rejection of reader.[[closedPromise]] with reason r, + */ +static bool TeeReaderErroredHandler(JSContext* cx, unsigned argc, + JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<TeeState*> teeState(cx, TargetFromHandler<TeeState>(args)); + Handle<Value> reason = args.get(0); + + Rooted<ReadableStreamDefaultController*> unwrappedBranchController(cx); + + // Step 18.a.i: Perform + // ! ReadableStreamDefaultControllerError( + // branch1.[[readableStreamController]], r). + unwrappedBranchController = teeState->branch1(); + if (!ReadableStreamControllerError(cx, unwrappedBranchController, reason)) { + return false; + } + + // Step a.ii: Perform + // ! ReadableStreamDefaultControllerError( + // branch2.[[readableStreamController]], r). + unwrappedBranchController = teeState->branch2(); + if (!ReadableStreamControllerError(cx, unwrappedBranchController, reason)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 3.4.10. ReadableStreamTee ( stream, cloneForBranch2 ) + */ +MOZ_MUST_USE bool js::ReadableStreamTee( + JSContext* cx, JS::Handle<ReadableStream*> unwrappedStream, + bool cloneForBranch2, JS::MutableHandle<ReadableStream*> branch1Stream, + JS::MutableHandle<ReadableStream*> branch2Stream) { + // Step 1: Assert: ! IsReadableStream(stream) is true (implicit). + + // Step 2: Assert: Type(cloneForBranch2) is Boolean (implicit). + // + // The streams spec only ever passes |cloneForBranch2 = false|. It's expected + // that external specs that pass |cloneForBranch2 = true| will at some point + // come into existence, but we don't presently implement any such specs. + MOZ_ASSERT(!cloneForBranch2, + "support for cloneForBranch2=true is not yet implemented"); + + // Step 3: Let reader be ? AcquireReadableStreamDefaultReader(stream). + Rooted<ReadableStreamDefaultReader*> reader( + cx, CreateReadableStreamDefaultReader(cx, unwrappedStream, + ForAuthorCodeBool::No)); + if (!reader) { + return false; + } + + // Several algorithms close over the variables initialized in the next few + // steps, so we allocate them in an object, the TeeState. The algorithms + // also close over `stream` and `reader`, so TeeState gets a reference to + // the stream. + // + // Step 4: Let reading be false. + // Step 5: Let canceled1 be false. + // Step 6: Let canceled2 be false. + // Step 7: Let reason1 be undefined. + // Step 8: Let reason2 be undefined. + // Step 9: Let branch1 be undefined. + // Step 10: Let branch2 be undefined. + // Step 11: Let cancelPromise be a new promise. + Rooted<TeeState*> teeState(cx, TeeState::create(cx, unwrappedStream)); + if (!teeState) { + return false; + } + + MOZ_ASSERT(!teeState->reading()); + MOZ_ASSERT(!teeState->canceled1()); + MOZ_ASSERT(!teeState->canceled2()); + + // Step 12: Let pullAlgorithm be the following steps: [...] + // Step 13: Let cancel1Algorithm be the following steps: [...] + // Step 14: Let cancel2Algorithm be the following steps: [...] + // Step 15: Let startAlgorithm be an algorithm that returns undefined. + // + // Implicit. Our implementation does not use objects to represent + // [[pullAlgorithm]], [[cancelAlgorithm]], and so on. Instead, we decide + // which one to perform based on class checks. For example, our + // implementation of ReadableStreamControllerCallPullIfNeeded checks + // whether the stream's underlyingSource is a TeeState object. + + // Step 16: Set branch1 to + // ! CreateReadableStream(startAlgorithm, pullAlgorithm, + // cancel1Algorithm). + Rooted<Value> underlyingSource(cx, ObjectValue(*teeState)); + branch1Stream.set( + CreateReadableStream(cx, SourceAlgorithms::Tee, underlyingSource)); + if (!branch1Stream) { + return false; + } + + Rooted<ReadableStreamDefaultController*> branch1(cx); + branch1 = &branch1Stream->controller()->as<ReadableStreamDefaultController>(); + branch1->setTeeBranch1(); + teeState->setBranch1(branch1); + + // Step 17: Set branch2 to + // ! CreateReadableStream(startAlgorithm, pullAlgorithm, + // cancel2Algorithm). + branch2Stream.set( + CreateReadableStream(cx, SourceAlgorithms::Tee, underlyingSource)); + if (!branch2Stream) { + return false; + } + + Rooted<ReadableStreamDefaultController*> branch2(cx); + branch2 = &branch2Stream->controller()->as<ReadableStreamDefaultController>(); + branch2->setTeeBranch2(); + teeState->setBranch2(branch2); + + // Step 18: Upon rejection of reader.[[closedPromise]] with reason r, [...] + Rooted<JSObject*> closedPromise(cx, reader->closedPromise()); + + Rooted<JSObject*> onRejected( + cx, NewHandler(cx, TeeReaderErroredHandler, teeState)); + if (!onRejected) { + return false; + } + + if (!JS::AddPromiseReactions(cx, closedPromise, nullptr, onRejected)) { + return false; + } + + // Step 19: Return « branch1, branch2 ». + return true; +} + +/** + * Streams spec, 3.4.10. + * ReadableStreamPipeTo ( source, dest, preventClose, preventAbort, + * preventCancel, signal ) + */ +PromiseObject* js::ReadableStreamPipeTo(JSContext* cx, + Handle<ReadableStream*> unwrappedSource, + Handle<WritableStream*> unwrappedDest, + bool preventClose, bool preventAbort, + bool preventCancel, + Handle<JSObject*> signal) { + cx->check(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<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) { + return nullptr; + } + + // Steps 4-11, 13-14. + Rooted<PipeToState*> 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 new file mode 100644 index 0000000000..963fffe882 --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamOperations.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +/* General readable stream abstract operations. */ + +#ifndef builtin_streams_ReadableStreamOperations_h +#define builtin_streams_ReadableStreamOperations_h + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::Value + +class JS_PUBLIC_API JSObject; + +namespace js { + +class PromiseObject; +class ReadableStream; +class ReadableStreamDefaultController; +class TeeState; +class WritableStream; + +extern MOZ_MUST_USE PromiseObject* ReadableStreamTee_Pull( + JSContext* cx, JS::Handle<TeeState*> unwrappedTeeState); + +extern MOZ_MUST_USE JSObject* ReadableStreamTee_Cancel( + JSContext* cx, JS::Handle<TeeState*> unwrappedTeeState, + JS::Handle<ReadableStreamDefaultController*> unwrappedBranch, + JS::Handle<JS::Value> reason); + +extern MOZ_MUST_USE bool ReadableStreamTee( + JSContext* cx, JS::Handle<ReadableStream*> unwrappedStream, + bool cloneForBranch2, JS::MutableHandle<ReadableStream*> branch1Stream, + JS::MutableHandle<ReadableStream*> branch2Stream); + +extern MOZ_MUST_USE PromiseObject* ReadableStreamPipeTo( + JSContext* cx, JS::Handle<ReadableStream*> unwrappedSource, + JS::Handle<WritableStream*> unwrappedDest, bool preventClose, + bool preventAbort, bool preventCancel, JS::Handle<JSObject*> signal); + +} // namespace js + +#endif // builtin_streams_ReadableStreamOperations_h diff --git a/js/src/builtin/streams/ReadableStreamReader-inl.h b/js/src/builtin/streams/ReadableStreamReader-inl.h new file mode 100644 index 0000000000..5d084c3a76 --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamReader-inl.h @@ -0,0 +1,71 @@ +/* -*- 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 builtin_streams_ReadableStreamReader_inl_h +#define builtin_streams_ReadableStreamReader_inl_h + +#include "builtin/streams/ReadableStreamReader.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jsfriendapi.h" // JS_IsDeadWrapper + +#include "builtin/streams/ReadableStream.h" // js::ReadableStream +#include "js/Proxy.h" // js::IsProxy +#include "js/RootingAPI.h" // JS::Handle +#include "vm/NativeObject.h" // js::NativeObject::getFixedSlot + +#include "vm/Compartment-inl.h" // js::UnwrapInternalSlot + +namespace js { + +/** + * Returns the stream associated with the given reader. + */ +inline MOZ_MUST_USE ReadableStream* UnwrapStreamFromReader( + JSContext* cx, JS::Handle<ReadableStreamReader*> reader) { + MOZ_ASSERT(reader->hasStream()); + return UnwrapInternalSlot<ReadableStream>(cx, reader, + ReadableStreamReader::Slot_Stream); +} + +/** + * Returns the reader associated with the given stream. + * + * Must only be called on ReadableStreams that already have a reader + * associated with them. + * + * If the reader is a wrapper, it will be unwrapped, so the result might not be + * an object from the currently active compartment. + */ +inline MOZ_MUST_USE ReadableStreamReader* UnwrapReaderFromStream( + JSContext* cx, JS::Handle<ReadableStream*> stream) { + return UnwrapInternalSlot<ReadableStreamReader>(cx, stream, + ReadableStream::Slot_Reader); +} + +inline MOZ_MUST_USE ReadableStreamReader* UnwrapReaderFromStreamNoThrow( + ReadableStream* stream) { + JSObject* readerObj = + &stream->getFixedSlot(ReadableStream::Slot_Reader).toObject(); + if (IsProxy(readerObj)) { + if (JS_IsDeadWrapper(readerObj)) { + return nullptr; + } + + readerObj = readerObj->maybeUnwrapAs<ReadableStreamReader>(); + if (!readerObj) { + return nullptr; + } + } + + return &readerObj->as<ReadableStreamReader>(); +} + +} // namespace js + +#endif // builtin_streams_ReadableStreamReader_inl_h diff --git a/js/src/builtin/streams/ReadableStreamReader.cpp b/js/src/builtin/streams/ReadableStreamReader.cpp new file mode 100644 index 0000000000..f384bce83e --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamReader.cpp @@ -0,0 +1,276 @@ +/* -*- 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/. */ + +/* ReadableStream reader abstract operations. */ + +#include "builtin/streams/ReadableStreamReader-inl.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT{,_IF} +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jsfriendapi.h" // JS_ReportErrorNumberASCII + +#include "builtin/Stream.h" // js::ReadableStreamController, js::ReadableStreamControllerPullSteps +#include "builtin/streams/ReadableStream.h" // js::ReadableStream +#include "builtin/streams/ReadableStreamController.h" // js::ReadableStreamController +#include "builtin/streams/ReadableStreamInternals.h" // js::ReadableStream{Cancel,CreateReadResult} +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted +#include "js/Value.h" // JS::Value, JS::UndefinedHandleValue +#include "vm/Interpreter.h" // js::GetAndClearException +#include "vm/JSContext.h" // JSContext +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseResolvedWithUndefined +#include "vm/Runtime.h" // JSRuntime + +#include "builtin/Promise-inl.h" // js::SetSettledPromiseIsHandled +#include "vm/Compartment-inl.h" // JS::Compartment::wrap, js::UnwrapInternalSlot +#include "vm/List-inl.h" // js::StoreNewListInFixedSlot +#include "vm/Realm-inl.h" // js::AutoRealm + +using JS::Handle; +using JS::Rooted; +using JS::Value; + +using js::PromiseObject; +using js::ReadableStreamController; +using js::UnwrapStreamFromReader; + +/*** 3.8. Readable stream reader abstract operations ************************/ + +// Streams spec, 3.8.1. IsReadableStreamDefaultReader ( x ) +// Implemented via is<ReadableStreamDefaultReader>() + +// Streams spec, 3.8.2. IsReadableStreamBYOBReader ( x ) +// Implemented via is<ReadableStreamBYOBReader>() + +/** + * Streams spec, 3.8.3. ReadableStreamReaderGenericCancel ( reader, reason ) + */ +MOZ_MUST_USE JSObject* js::ReadableStreamReaderGenericCancel( + JSContext* cx, Handle<ReadableStreamReader*> unwrappedReader, + Handle<Value> reason) { + // Step 1: Let stream be reader.[[ownerReadableStream]]. + // Step 2: Assert: stream is not undefined (implicit). + Rooted<ReadableStream*> unwrappedStream( + cx, UnwrapStreamFromReader(cx, unwrappedReader)); + if (!unwrappedStream) { + return nullptr; + } + + // Step 3: Return ! ReadableStreamCancel(stream, reason). + return js::ReadableStreamCancel(cx, unwrappedStream, reason); +} + +/** + * Streams spec, 3.8.4. + * ReadableStreamReaderGenericInitialize ( reader, stream ) + */ +MOZ_MUST_USE bool js::ReadableStreamReaderGenericInitialize( + JSContext* cx, Handle<ReadableStreamReader*> reader, + Handle<ReadableStream*> unwrappedStream, ForAuthorCodeBool forAuthorCode) { + cx->check(reader); + + // Step 1: Set reader.[[forAuthorCode]] to true. + reader->setForAuthorCode(forAuthorCode); + + // Step 2: Set reader.[[ownerReadableStream]] to stream. + { + Rooted<JSObject*> readerCompartmentStream(cx, unwrappedStream); + if (!cx->compartment()->wrap(cx, &readerCompartmentStream)) { + return false; + } + reader->setStream(readerCompartmentStream); + } + + // Step 3 is moved to the end. + + // Step 4: If stream.[[state]] is "readable", + Rooted<PromiseObject*> promise(cx); + if (unwrappedStream->readable()) { + // Step a: Set reader.[[closedPromise]] to a new promise. + promise = PromiseObject::createSkippingExecutor(cx); + } else if (unwrappedStream->closed()) { + // Step 5: Otherwise, if stream.[[state]] is "closed", + // Step a: Set reader.[[closedPromise]] to a promise resolved with + // undefined. + promise = PromiseResolvedWithUndefined(cx); + } else { + // Step 6: Otherwise, + // Step a: Assert: stream.[[state]] is "errored". + MOZ_ASSERT(unwrappedStream->errored()); + + // Step b: Set reader.[[closedPromise]] to a promise rejected with + // stream.[[storedError]]. + Rooted<Value> storedError(cx, unwrappedStream->storedError()); + if (!cx->compartment()->wrap(cx, &storedError)) { + return false; + } + promise = PromiseObject::unforgeableReject(cx, storedError); + if (!promise) { + return false; + } + + // Step c. Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. + js::SetSettledPromiseIsHandled(cx, promise); + } + + if (!promise) { + return false; + } + + reader->setClosedPromise(promise); + + // Step 4 of caller 3.6.3. new ReadableStreamDefaultReader(stream): + // Step 5 of caller 3.7.3. new ReadableStreamBYOBReader(stream): + // Set this.[[read{Into}Requests]] to a new empty List. + if (!StoreNewListInFixedSlot(cx, reader, + ReadableStreamReader::Slot_Requests)) { + return false; + } + + // Step 3: Set stream.[[reader]] to reader. + // Doing this last prevents a partially-initialized reader from being + // attached to the stream (and possibly left there on OOM). + { + AutoRealm ar(cx, unwrappedStream); + Rooted<JSObject*> streamCompartmentReader(cx, reader); + if (!cx->compartment()->wrap(cx, &streamCompartmentReader)) { + return false; + } + unwrappedStream->setReader(streamCompartmentReader); + } + + return true; +} + +/** + * Streams spec, 3.8.5. ReadableStreamReaderGenericRelease ( reader ) + */ +MOZ_MUST_USE bool js::ReadableStreamReaderGenericRelease( + JSContext* cx, Handle<ReadableStreamReader*> unwrappedReader) { + // Step 1: Assert: reader.[[ownerReadableStream]] is not undefined. + Rooted<ReadableStream*> unwrappedStream( + cx, UnwrapStreamFromReader(cx, unwrappedReader)); + if (!unwrappedStream) { + return false; + } + + // Step 2: Assert: reader.[[ownerReadableStream]].[[reader]] is reader. +#ifdef DEBUG + // The assertion is weakened a bit to allow for nuked wrappers. + ReadableStreamReader* unwrappedReader2 = + UnwrapReaderFromStreamNoThrow(unwrappedStream); + MOZ_ASSERT_IF(unwrappedReader2, unwrappedReader2 == unwrappedReader); +#endif + + // 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); + Rooted<Value> exn(cx); + if (!cx->isExceptionPending() || !GetAndClearException(cx, &exn)) { + // Uncatchable error. Die immediately without resolving + // reader.[[closedPromise]]. + return false; + } + + // Step 3: If reader.[[ownerReadableStream]].[[state]] is "readable", reject + // reader.[[closedPromise]] with a TypeError exception. + Rooted<PromiseObject*> unwrappedClosedPromise(cx); + if (unwrappedStream->readable()) { + unwrappedClosedPromise = UnwrapInternalSlot<PromiseObject>( + cx, unwrappedReader, ReadableStreamReader::Slot_ClosedPromise); + if (!unwrappedClosedPromise) { + return false; + } + + AutoRealm ar(cx, unwrappedClosedPromise); + if (!cx->compartment()->wrap(cx, &exn)) { + return false; + } + if (!PromiseObject::reject(cx, unwrappedClosedPromise, exn)) { + return false; + } + } else { + // Step 4: Otherwise, set reader.[[closedPromise]] to a new promise + // rejected with a TypeError exception. + Rooted<JSObject*> closedPromise(cx, + PromiseObject::unforgeableReject(cx, exn)); + if (!closedPromise) { + return false; + } + unwrappedClosedPromise = &closedPromise->as<PromiseObject>(); + + AutoRealm ar(cx, unwrappedReader); + if (!cx->compartment()->wrap(cx, &closedPromise)) { + return false; + } + unwrappedReader->setClosedPromise(closedPromise); + } + + // Step 5: Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. + js::SetSettledPromiseIsHandled(cx, unwrappedClosedPromise); + + // Step 6: Set reader.[[ownerReadableStream]].[[reader]] to undefined. + unwrappedStream->clearReader(); + + // Step 7: Set reader.[[ownerReadableStream]] to undefined. + unwrappedReader->clearStream(); + + return true; +} + +/** + * Streams spec, 3.8.7. + * ReadableStreamDefaultReaderRead ( reader [, forAuthorCode ] ) + */ +MOZ_MUST_USE PromiseObject* js::ReadableStreamDefaultReaderRead( + JSContext* cx, Handle<ReadableStreamDefaultReader*> unwrappedReader) { + // Step 1: If forAuthorCode was not passed, set it to false (implicit). + + // Step 2: Let stream be reader.[[ownerReadableStream]]. + // Step 3: Assert: stream is not undefined. + Rooted<ReadableStream*> unwrappedStream( + cx, UnwrapStreamFromReader(cx, unwrappedReader)); + if (!unwrappedStream) { + return nullptr; + } + + // Step 4: Set stream.[[disturbed]] to true. + unwrappedStream->setDisturbed(); + + // Step 5: If stream.[[state]] is "closed", return a promise resolved with + // ! ReadableStreamCreateReadResult(undefined, true, forAuthorCode). + if (unwrappedStream->closed()) { + PlainObject* iterResult = ReadableStreamCreateReadResult( + cx, UndefinedHandleValue, true, unwrappedReader->forAuthorCode()); + if (!iterResult) { + return nullptr; + } + + Rooted<Value> iterResultVal(cx, JS::ObjectValue(*iterResult)); + return PromiseObject::unforgeableResolveWithNonPromise(cx, iterResultVal); + } + + // Step 6: If stream.[[state]] is "errored", return a promise rejected + // with stream.[[storedError]]. + if (unwrappedStream->errored()) { + Rooted<Value> storedError(cx, unwrappedStream->storedError()); + if (!cx->compartment()->wrap(cx, &storedError)) { + return nullptr; + } + return PromiseObject::unforgeableReject(cx, storedError); + } + + // Step 7: Assert: stream.[[state]] is "readable". + MOZ_ASSERT(unwrappedStream->readable()); + + // Step 8: Return ! stream.[[readableStreamController]].[[PullSteps]](). + Rooted<ReadableStreamController*> unwrappedController( + cx, unwrappedStream->controller()); + return ReadableStreamControllerPullSteps(cx, unwrappedController); +} diff --git a/js/src/builtin/streams/ReadableStreamReader.h b/js/src/builtin/streams/ReadableStreamReader.h new file mode 100644 index 0000000000..96dd2efed2 --- /dev/null +++ b/js/src/builtin/streams/ReadableStreamReader.h @@ -0,0 +1,156 @@ +/* -*- 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/. */ + +/* ReadableStream readers and generic reader operations. */ + +#ifndef builtin_streams_ReadableStreamReader_h +#define builtin_streams_ReadableStreamReader_h + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jstypes.h" // JS_PUBLIC_API +#include "js/Class.h" // JSClass, js::ClassSpec +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::{,Boolean,Object,Undefined}Value +#include "vm/JSObject.h" // JSObject::is +#include "vm/List.h" // js::ListObject +#include "vm/NativeObject.h" // js::NativeObject + +struct JS_PUBLIC_API JSContext; + +namespace js { + +class PromiseObject; +class ReadableStream; + +/** + * Tells whether or not read() result objects inherit from Object.prototype. + * Generally, they should do so only if the reader was created by author code. + * See <https://streams.spec.whatwg.org/#readable-stream-create-read-result>. + */ +enum class ForAuthorCodeBool { No, Yes }; + +class ReadableStreamReader : public NativeObject { + public: + /** + * Memory layout of Stream Reader instances. + * + * See https://streams.spec.whatwg.org/#default-reader-internal-slots and + * https://streams.spec.whatwg.org/#byob-reader-internal-slots for details. + * + * Note that [[readRequests]] and [[readIntoRequests]] are treated the same + * in our implementation. + * + * Of the stored values, Stream and ClosedPromise might be + * cross-compartment wrapper wrappers. + * + * For Stream, this can happen if the Reader was created by applying a + * different compartment's ReadableStream.prototype.getReader method. + * + * For ClosedPromise, it can be caused by applying a different + * compartment's ReadableStream*Reader.prototype.releaseLock method. + * + * Requests is guaranteed to be in the same compartment as the Reader, but + * can contain wrapped request objects from other globals. + */ + enum Slots { + Slot_Stream, + Slot_Requests, + Slot_ClosedPromise, + Slot_ForAuthorCode, + SlotCount, + }; + + bool hasStream() const { return !getFixedSlot(Slot_Stream).isUndefined(); } + void setStream(JSObject* stream) { + setFixedSlot(Slot_Stream, JS::ObjectValue(*stream)); + } + void clearStream() { setFixedSlot(Slot_Stream, JS::UndefinedValue()); } + bool isClosed() { return !hasStream(); } + + /** + * Tells whether this reader was created by author code. + * + * This returns Yes for readers created using `stream.getReader()`, and No + * for readers created for the internal use of algorithms like + * `stream.tee()` and `new Response(stream)`. + * + * The standard does not have this field. Instead, eight algorithms take a + * forAuthorCode parameter, and a [[forAuthorCode]] field is part of each + * read request. But the behavior is always equivalent to treating readers + * created by author code as having a bit set on them. We implement it that + * way for simplicity. + */ + ForAuthorCodeBool forAuthorCode() const { + return getFixedSlot(Slot_ForAuthorCode).toBoolean() ? ForAuthorCodeBool::Yes + : ForAuthorCodeBool::No; + } + void setForAuthorCode(ForAuthorCodeBool value) { + setFixedSlot(Slot_ForAuthorCode, + JS::BooleanValue(value == ForAuthorCodeBool::Yes)); + } + + ListObject* requests() const { + return &getFixedSlot(Slot_Requests).toObject().as<ListObject>(); + } + void clearRequests() { setFixedSlot(Slot_Requests, JS::UndefinedValue()); } + + JSObject* closedPromise() const { + return &getFixedSlot(Slot_ClosedPromise).toObject(); + } + void setClosedPromise(JSObject* wrappedPromise) { + setFixedSlot(Slot_ClosedPromise, JS::ObjectValue(*wrappedPromise)); + } + + static const JSClass class_; +}; + +class ReadableStreamDefaultReader : public ReadableStreamReader { + public: + static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp); + static const ClassSpec classSpec_; + static const JSClass class_; + static const ClassSpec protoClassSpec_; + static const JSClass protoClass_; +}; + +extern MOZ_MUST_USE ReadableStreamDefaultReader* +CreateReadableStreamDefaultReader(JSContext* cx, + JS::Handle<ReadableStream*> unwrappedStream, + ForAuthorCodeBool forAuthorCode, + JS::Handle<JSObject*> proto = nullptr); + +extern MOZ_MUST_USE JSObject* ReadableStreamReaderGenericCancel( + JSContext* cx, JS::Handle<ReadableStreamReader*> unwrappedReader, + JS::Handle<JS::Value> reason); + +extern MOZ_MUST_USE bool ReadableStreamReaderGenericInitialize( + JSContext* cx, JS::Handle<ReadableStreamReader*> reader, + JS::Handle<ReadableStream*> unwrappedStream, + ForAuthorCodeBool forAuthorCode); + +extern MOZ_MUST_USE bool ReadableStreamReaderGenericRelease( + JSContext* cx, JS::Handle<ReadableStreamReader*> unwrappedReader); + +extern MOZ_MUST_USE PromiseObject* ReadableStreamDefaultReaderRead( + JSContext* cx, JS::Handle<ReadableStreamDefaultReader*> unwrappedReader); + +} // namespace js + +template <> +inline bool JSObject::is<js::ReadableStreamReader>() const { + return is<js::ReadableStreamDefaultReader>(); +} + +namespace js { + +extern MOZ_MUST_USE JSObject* CreateReadableStreamBYOBReader( + JSContext* cx, JS::Handle<ReadableStream*> unwrappedStream, + ForAuthorCodeBool forAuthorCode, JS::Handle<JSObject*> proto = nullptr); + +} // namespace js + +#endif // builtin_streams_ReadableStreamReader_h diff --git a/js/src/builtin/streams/StreamAPI.cpp b/js/src/builtin/streams/StreamAPI.cpp new file mode 100644 index 0000000000..0a9abd168c --- /dev/null +++ b/js/src/builtin/streams/StreamAPI.cpp @@ -0,0 +1,612 @@ +/* -*- 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/. */ + +/* Public and friend stream APIs for external use. */ + +#include "mozilla/Assertions.h" // MOZ_ASSERT{,_IF} +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include <stdint.h> // uint32_t, uintptr_t + +#include "jsapi.h" // js::AssertHeapIsIdle, JS_ReportErrorNumberASCII +#include "jsfriendapi.h" // js::IsObjectInContextCompartment +#include "jstypes.h" // JS_{FRIEND,PUBLIC}_API + +#include "builtin/Stream.h" // js::ReadableByteStreamController{,Close}, js::ReadableStreamDefaultController{,Close}, js::StreamController +#include "builtin/streams/ReadableStream.h" // js::ReadableStream +#include "builtin/streams/ReadableStreamController.h" // js::CheckReadableStreamControllerCanCloseOrEnqueue +#include "builtin/streams/ReadableStreamDefaultControllerOperations.h" // js::ReadableStreamController{Error,GetDesiredSizeUnchecked}, js::SetUpReadableStreamDefaultControllerFromUnderlyingSource +#include "builtin/streams/ReadableStreamInternals.h" // js::ReadableStream{Cancel,FulfillReadOrReadIntoRequest,GetNumReadRequests,HasDefaultReader} +#include "builtin/streams/ReadableStreamOperations.h" // js::ReadableStreamTee +#include "builtin/streams/ReadableStreamReader.h" // js::ReadableStream{,Default}Reader, js::ForAuthorCodeBool +#include "builtin/streams/StreamController.h" // js::StreamController +#include "gc/Zone.h" // JS::Zone +#include "js/experimental/TypedData.h" // JS_GetArrayBufferViewData, JS_NewUint8Array +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/GCAPI.h" // JS::AutoCheckCannotGC, JS::AutoSuppressGCAnalysis +#include "js/Object.h" // JS::SetPrivate +#include "js/RootingAPI.h" // JS::{,Mutable}Handle, JS::Rooted +#include "js/Stream.h" // JS::ReadableStreamUnderlyingSource +#include "js/Value.h" // JS::{,Object,Undefined}Value +#include "vm/ArrayBufferViewObject.h" // js::ArrayBufferViewObject +#include "vm/JSContext.h" // JSContext, CHECK_THREAD +#include "vm/JSObject.h" // JSObject +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/PromiseObject.h" // js::PromiseObject + +#include "builtin/streams/ReadableStreamReader-inl.h" // js::UnwrapStreamFromReader +#include "vm/Compartment-inl.h" // JS::Compartment::wrap, js::UnwrapAndDowncastObject +#include "vm/JSObject-inl.h" // js::NewBuiltinClassInstance +#include "vm/Realm-inl.h" // js::AutoRealm + +using js::ArrayBufferViewObject; +using js::AssertHeapIsIdle; +using js::AutoRealm; +using js::CheckReadableStreamControllerCanCloseOrEnqueue; +using js::ForAuthorCodeBool; +using js::GetErrorMessage; +using js::IsObjectInContextCompartment; +using js::NewBuiltinClassInstance; +using js::PlainObject; +using js::ReadableByteStreamController; +using js::ReadableByteStreamControllerClose; +using js::ReadableStream; +using js::ReadableStreamController; +using js::ReadableStreamControllerError; +using js::ReadableStreamControllerGetDesiredSizeUnchecked; +using js::ReadableStreamDefaultController; +using js::ReadableStreamDefaultControllerClose; +using js::ReadableStreamDefaultReader; +using js::ReadableStreamFulfillReadOrReadIntoRequest; +using js::ReadableStreamGetNumReadRequests; +using js::ReadableStreamHasDefaultReader; +using js::ReadableStreamReader; +using js::ReadableStreamTee; +using js::SetUpReadableStreamDefaultControllerFromUnderlyingSource; +using js::StreamController; +using js::UnwrapAndDowncastObject; +using js::UnwrapStreamFromReader; + +JS_FRIEND_API JSObject* js::UnwrapReadableStream(JSObject* obj) { + return obj->maybeUnwrapIf<ReadableStream>(); +} + +JS_PUBLIC_API JSObject* JS::NewReadableDefaultStreamObject( + JSContext* cx, JS::Handle<JSObject*> underlyingSource /* = nullptr */, + JS::Handle<JSFunction*> size /* = nullptr */, + double highWaterMark /* = 1 */, + JS::Handle<JSObject*> proto /* = nullptr */) { + MOZ_ASSERT(!cx->zone()->isAtomsZone()); + AssertHeapIsIdle(); + CHECK_THREAD(cx); + cx->check(underlyingSource, size, proto); + MOZ_ASSERT(highWaterMark >= 0); + + // A copy of ReadableStream::constructor, with most of the + // argument-checking done implicitly by C++ type checking. + Rooted<ReadableStream*> stream(cx, ReadableStream::create(cx)); + if (!stream) { + return nullptr; + } + Rooted<Value> sourceVal(cx); + if (underlyingSource) { + sourceVal.setObject(*underlyingSource); + } else { + JSObject* source = NewBuiltinClassInstance<PlainObject>(cx); + if (!source) { + return nullptr; + } + sourceVal.setObject(*source); + } + Rooted<Value> sizeVal(cx, size ? ObjectValue(*size) : UndefinedValue()); + + if (!SetUpReadableStreamDefaultControllerFromUnderlyingSource( + cx, stream, sourceVal, highWaterMark, sizeVal)) { + return nullptr; + } + + return stream; +} + +JS_PUBLIC_API JSObject* JS::NewReadableExternalSourceStreamObject( + JSContext* cx, JS::ReadableStreamUnderlyingSource* underlyingSource, + void* nsISupportsObject_alreadyAddreffed /* = nullptr */, + Handle<JSObject*> proto /* = nullptr */) { + MOZ_ASSERT(!cx->zone()->isAtomsZone()); + AssertHeapIsIdle(); + CHECK_THREAD(cx); + MOZ_ASSERT(underlyingSource); + MOZ_ASSERT((uintptr_t(underlyingSource) & 1) == 0, + "external underlying source pointers must be aligned"); + cx->check(proto); + + return ReadableStream::createExternalSourceStream( + cx, underlyingSource, nsISupportsObject_alreadyAddreffed, proto); +} + +JS_PUBLIC_API bool JS::IsReadableStream(JSObject* obj) { + return obj->canUnwrapAs<ReadableStream>(); +} + +JS_PUBLIC_API bool JS::IsReadableStreamReader(JSObject* obj) { + return obj->canUnwrapAs<ReadableStreamDefaultReader>(); +} + +JS_PUBLIC_API bool JS::IsReadableStreamDefaultReader(JSObject* obj) { + return obj->canUnwrapAs<ReadableStreamDefaultReader>(); +} + +template <class T> +static MOZ_MUST_USE T* APIUnwrapAndDowncast(JSContext* cx, JSObject* obj) { + cx->check(obj); + return UnwrapAndDowncastObject<T>(cx, obj); +} + +JS_PUBLIC_API bool JS::ReadableStreamIsReadable(JSContext* cx, + Handle<JSObject*> streamObj, + bool* result) { + ReadableStream* unwrappedStream = + APIUnwrapAndDowncast<ReadableStream>(cx, streamObj); + if (!unwrappedStream) { + return false; + } + + *result = unwrappedStream->readable(); + return true; +} + +JS_PUBLIC_API bool JS::ReadableStreamIsLocked(JSContext* cx, + Handle<JSObject*> streamObj, + bool* result) { + ReadableStream* unwrappedStream = + APIUnwrapAndDowncast<ReadableStream>(cx, streamObj); + if (!unwrappedStream) { + return false; + } + + *result = unwrappedStream->locked(); + return true; +} + +JS_PUBLIC_API bool JS::ReadableStreamIsDisturbed(JSContext* cx, + Handle<JSObject*> streamObj, + bool* result) { + ReadableStream* unwrappedStream = + APIUnwrapAndDowncast<ReadableStream>(cx, streamObj); + if (!unwrappedStream) { + return false; + } + + *result = unwrappedStream->disturbed(); + return true; +} + +JS_PUBLIC_API JSObject* JS::ReadableStreamCancel(JSContext* cx, + Handle<JSObject*> streamObj, + Handle<Value> reason) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + cx->check(reason); + + Rooted<ReadableStream*> unwrappedStream( + cx, APIUnwrapAndDowncast<ReadableStream>(cx, streamObj)); + if (!unwrappedStream) { + return nullptr; + } + + return js::ReadableStreamCancel(cx, unwrappedStream, reason); +} + +JS_PUBLIC_API bool JS::ReadableStreamGetMode(JSContext* cx, + Handle<JSObject*> streamObj, + JS::ReadableStreamMode* mode) { + ReadableStream* unwrappedStream = + APIUnwrapAndDowncast<ReadableStream>(cx, streamObj); + if (!unwrappedStream) { + return false; + } + + *mode = unwrappedStream->mode(); + return true; +} + +JS_PUBLIC_API JSObject* JS::ReadableStreamGetReader( + JSContext* cx, Handle<JSObject*> streamObj, ReadableStreamReaderMode mode) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + + Rooted<ReadableStream*> unwrappedStream( + cx, APIUnwrapAndDowncast<ReadableStream>(cx, streamObj)); + if (!unwrappedStream) { + return nullptr; + } + + JSObject* result = CreateReadableStreamDefaultReader(cx, unwrappedStream, + ForAuthorCodeBool::No); + MOZ_ASSERT_IF(result, IsObjectInContextCompartment(result, cx)); + return result; +} + +JS_PUBLIC_API bool JS::ReadableStreamGetExternalUnderlyingSource( + JSContext* cx, Handle<JSObject*> streamObj, + JS::ReadableStreamUnderlyingSource** source) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + + Rooted<ReadableStream*> unwrappedStream( + cx, APIUnwrapAndDowncast<ReadableStream>(cx, streamObj)); + if (!unwrappedStream) { + return false; + } + + MOZ_ASSERT(unwrappedStream->mode() == JS::ReadableStreamMode::ExternalSource); + if (unwrappedStream->locked()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_LOCKED); + return false; + } + if (!unwrappedStream->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, + "ReadableStreamGetExternalUnderlyingSource"); + return false; + } + + auto unwrappedController = + &unwrappedStream->controller()->as<ReadableByteStreamController>(); + unwrappedController->setSourceLocked(); + *source = unwrappedController->externalSource(); + return true; +} + +JS_PUBLIC_API bool JS::ReadableStreamReleaseExternalUnderlyingSource( + JSContext* cx, Handle<JSObject*> streamObj) { + ReadableStream* unwrappedStream = + APIUnwrapAndDowncast<ReadableStream>(cx, streamObj); + if (!unwrappedStream) { + return false; + } + + MOZ_ASSERT(unwrappedStream->mode() == JS::ReadableStreamMode::ExternalSource); + MOZ_ASSERT(unwrappedStream->locked()); + MOZ_ASSERT(unwrappedStream->controller()->sourceLocked()); + unwrappedStream->controller()->clearSourceLocked(); + return true; +} + +JS_PUBLIC_API bool JS::ReadableStreamUpdateDataAvailableFromSource( + JSContext* cx, JS::Handle<JSObject*> streamObj, uint32_t availableData) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + + Rooted<ReadableStream*> unwrappedStream( + cx, APIUnwrapAndDowncast<ReadableStream>(cx, streamObj)); + if (!unwrappedStream) { + return false; + } + + // This is based on Streams spec 3.11.4.4. enqueue(chunk) steps 1-3 and + // 3.13.9. ReadableByteStreamControllerEnqueue(controller, chunk) 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. + + Rooted<ReadableByteStreamController*> unwrappedController( + cx, &unwrappedStream->controller()->as<ReadableByteStreamController>()); + + // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception. + if (unwrappedController->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 (!unwrappedController->stream()->readable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, + "enqueue"); + return false; + } + + unwrappedController->clearPullFlags(); + +#if DEBUG + uint32_t oldAvailableData = + unwrappedController->getFixedSlot(StreamController::Slot_TotalSize) + .toInt32(); +#endif // DEBUG + unwrappedController->setQueueTotalSize(availableData); + + // 3.139. ReadableByteStreamControllerEnqueue + // Step 8.a: If ! ReadableStreamGetNumReadRequests(stream) is 0, + // Reordered because for externally-sourced streams it applies regardless + // of reader type. + if (ReadableStreamGetNumReadRequests(unwrappedStream) == 0) { + return true; + } + + // Step 8: If ! ReadableStreamHasDefaultReader(stream) is true + bool hasDefaultReader; + if (!ReadableStreamHasDefaultReader(cx, unwrappedStream, &hasDefaultReader)) { + return false; + } + if (hasDefaultReader) { + // 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); + if (!viewObj) { + return false; + } + Rooted<ArrayBufferViewObject*> transferredView( + cx, &viewObj->as<ArrayBufferViewObject>()); + if (!transferredView) { + return false; + } + + JS::ReadableStreamUnderlyingSource* source = + unwrappedController->externalSource(); + + size_t bytesWritten; + { + AutoRealm ar(cx, unwrappedStream); + JS::AutoSuppressGCAnalysis suppressGC(cx); + JS::AutoCheckCannotGC noGC; + bool dummy; + void* buffer = JS_GetArrayBufferViewData(transferredView, &dummy, noGC); + source->writeIntoReadRequestBuffer(cx, unwrappedStream, buffer, + availableData, &bytesWritten); + } + + // Step iii: Perform ! ReadableStreamFulfillReadRequest(stream, + // transferredView, + // false). + Rooted<Value> chunk(cx, ObjectValue(*transferredView)); + if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, unwrappedStream, chunk, + false)) { + return false; + } + + unwrappedController->setQueueTotalSize(availableData - bytesWritten); + } else { + // Step 9: Otherwise, if ! ReadableStreamHasBYOBReader(stream) is true, + // [...] + // (Omitted. BYOB readers are not implemented.) + + // Step 10: Otherwise, + // Step a: Assert: ! IsReadableStreamLocked(stream) is false. + MOZ_ASSERT(!unwrappedStream->locked()); + + // Step b: Perform ! ReadableByteStreamControllerEnqueueChunkToQueue( + // controller, transferredBuffer, byteOffset, byteLength). + // (Not needed for external underlying sources.) + } + + return true; +} + +JS_PUBLIC_API void JS::ReadableStreamReleaseCCObject(JSObject* streamObj) { + MOZ_ASSERT(JS::IsReadableStream(streamObj)); + JS::SetPrivate(streamObj, nullptr); +} + +JS_PUBLIC_API bool JS::ReadableStreamTee(JSContext* cx, + Handle<JSObject*> streamObj, + MutableHandle<JSObject*> branch1Obj, + MutableHandle<JSObject*> branch2Obj) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + + Rooted<ReadableStream*> unwrappedStream( + cx, APIUnwrapAndDowncast<ReadableStream>(cx, streamObj)); + if (!unwrappedStream) { + return false; + } + + Rooted<ReadableStream*> branch1Stream(cx); + Rooted<ReadableStream*> branch2Stream(cx); + if (!ReadableStreamTee(cx, unwrappedStream, false, &branch1Stream, + &branch2Stream)) { + return false; + } + + branch1Obj.set(branch1Stream); + branch2Obj.set(branch2Stream); + return true; +} + +JS_PUBLIC_API bool JS::ReadableStreamGetDesiredSize(JSContext* cx, + JSObject* streamObj, + bool* hasValue, + double* value) { + ReadableStream* unwrappedStream = + APIUnwrapAndDowncast<ReadableStream>(cx, streamObj); + if (!unwrappedStream) { + return false; + } + + if (unwrappedStream->errored()) { + *hasValue = false; + return true; + } + + *hasValue = true; + + if (unwrappedStream->closed()) { + *value = 0; + return true; + } + + *value = ReadableStreamControllerGetDesiredSizeUnchecked( + unwrappedStream->controller()); + return true; +} + +JS_PUBLIC_API bool JS::ReadableStreamClose(JSContext* cx, + Handle<JSObject*> streamObj) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + + Rooted<ReadableStream*> unwrappedStream( + cx, APIUnwrapAndDowncast<ReadableStream>(cx, streamObj)); + if (!unwrappedStream) { + return false; + } + + Rooted<ReadableStreamController*> unwrappedControllerObj( + cx, unwrappedStream->controller()); + if (!CheckReadableStreamControllerCanCloseOrEnqueue( + cx, unwrappedControllerObj, "close")) { + return false; + } + + if (unwrappedControllerObj->is<ReadableStreamDefaultController>()) { + Rooted<ReadableStreamDefaultController*> unwrappedController(cx); + unwrappedController = + &unwrappedControllerObj->as<ReadableStreamDefaultController>(); + return ReadableStreamDefaultControllerClose(cx, unwrappedController); + } + + Rooted<ReadableByteStreamController*> unwrappedController(cx); + unwrappedController = + &unwrappedControllerObj->as<ReadableByteStreamController>(); + return ReadableByteStreamControllerClose(cx, unwrappedController); +} + +JS_PUBLIC_API bool JS::ReadableStreamEnqueue(JSContext* cx, + Handle<JSObject*> streamObj, + Handle<Value> chunk) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + cx->check(chunk); + + Rooted<ReadableStream*> unwrappedStream( + cx, APIUnwrapAndDowncast<ReadableStream>(cx, streamObj)); + if (!unwrappedStream) { + return false; + } + + if (unwrappedStream->mode() != JS::ReadableStreamMode::Default) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_NOT_DEFAULT_CONTROLLER, + "JS::ReadableStreamEnqueue"); + return false; + } + + Rooted<ReadableStreamDefaultController*> unwrappedController(cx); + unwrappedController = + &unwrappedStream->controller()->as<ReadableStreamDefaultController>(); + + MOZ_ASSERT(!unwrappedController->closeRequested()); + MOZ_ASSERT(unwrappedStream->readable()); + + return ReadableStreamDefaultControllerEnqueue(cx, unwrappedController, chunk); +} + +JS_PUBLIC_API bool JS::ReadableStreamError(JSContext* cx, + Handle<JSObject*> streamObj, + Handle<Value> error) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + cx->check(error); + + Rooted<ReadableStream*> unwrappedStream( + cx, APIUnwrapAndDowncast<ReadableStream>(cx, streamObj)); + if (!unwrappedStream) { + return false; + } + + Rooted<ReadableStreamController*> unwrappedController( + cx, unwrappedStream->controller()); + return ReadableStreamControllerError(cx, unwrappedController, error); +} + +JS_PUBLIC_API bool JS::ReadableStreamReaderIsClosed(JSContext* cx, + Handle<JSObject*> readerObj, + bool* result) { + Rooted<ReadableStreamReader*> unwrappedReader( + cx, APIUnwrapAndDowncast<ReadableStreamReader>(cx, readerObj)); + if (!unwrappedReader) { + return false; + } + + *result = unwrappedReader->isClosed(); + return true; +} + +JS_PUBLIC_API bool JS::ReadableStreamReaderCancel(JSContext* cx, + Handle<JSObject*> readerObj, + Handle<Value> reason) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + cx->check(reason); + + Rooted<ReadableStreamReader*> unwrappedReader( + cx, APIUnwrapAndDowncast<ReadableStreamReader>(cx, readerObj)); + if (!unwrappedReader) { + return false; + } + MOZ_ASSERT(unwrappedReader->forAuthorCode() == ForAuthorCodeBool::No, + "C++ code should not touch readers created by scripts"); + + return ReadableStreamReaderGenericCancel(cx, unwrappedReader, reason); +} + +JS_PUBLIC_API bool JS::ReadableStreamReaderReleaseLock( + JSContext* cx, Handle<JSObject*> readerObj) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + + Rooted<ReadableStreamReader*> unwrappedReader( + cx, APIUnwrapAndDowncast<ReadableStreamReader>(cx, readerObj)); + if (!unwrappedReader) { + return false; + } + MOZ_ASSERT(unwrappedReader->forAuthorCode() == ForAuthorCodeBool::No, + "C++ code should not touch readers created by scripts"); + +#ifdef DEBUG + Rooted<ReadableStream*> unwrappedStream( + cx, UnwrapStreamFromReader(cx, unwrappedReader)); + if (!unwrappedStream) { + return false; + } + MOZ_ASSERT(ReadableStreamGetNumReadRequests(unwrappedStream) == 0); +#endif // DEBUG + + return ReadableStreamReaderGenericRelease(cx, unwrappedReader); +} + +JS_PUBLIC_API JSObject* JS::ReadableStreamDefaultReaderRead( + JSContext* cx, Handle<JSObject*> readerObj) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + + Rooted<ReadableStreamDefaultReader*> unwrappedReader( + cx, APIUnwrapAndDowncast<ReadableStreamDefaultReader>(cx, readerObj)); + if (!unwrappedReader) { + return nullptr; + } + MOZ_ASSERT(unwrappedReader->forAuthorCode() == ForAuthorCodeBool::No, + "C++ code should not touch readers created by scripts"); + + return js::ReadableStreamDefaultReaderRead(cx, unwrappedReader); +} + +void JS::InitPipeToHandling(const JSClass* abortSignalClass, + AbortSignalIsAborted isAborted, JSContext* cx) { + cx->runtime()->initPipeToHandling(abortSignalClass, isAborted); +} diff --git a/js/src/builtin/streams/StreamController-inl.h b/js/src/builtin/streams/StreamController-inl.h new file mode 100644 index 0000000000..4672004772 --- /dev/null +++ b/js/src/builtin/streams/StreamController-inl.h @@ -0,0 +1,24 @@ +/* -*- 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/. */ + +/* Base stream controller inlines. */ + +#ifndef builtin_streams_StreamController_inl_h +#define builtin_streams_StreamController_inl_h + +#include "builtin/streams/StreamController.h" // js::StreamController +#include "builtin/streams/ReadableStreamController.h" // js::Readable{ByteStream,StreamDefault}Controller +#include "builtin/streams/WritableStreamDefaultController.h" // js::WritableStreamDefaultController +#include "vm/JSObject.h" // JSObject + +template <> +inline bool JSObject::is<js::StreamController>() const { + return is<js::ReadableStreamDefaultController>() || + is<js::ReadableByteStreamController>() || + is<js::WritableStreamDefaultController>(); +} + +#endif // builtin_streams_ReadableStreamController_inl_h diff --git a/js/src/builtin/streams/StreamController.h b/js/src/builtin/streams/StreamController.h new file mode 100644 index 0000000000..4986421ab0 --- /dev/null +++ b/js/src/builtin/streams/StreamController.h @@ -0,0 +1,51 @@ +/* -*- 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/. */ + +/* Base class for readable and writable stream controllers. */ + +#ifndef builtin_streams_StreamController_h +#define builtin_streams_StreamController_h + +#include "js/Value.h" // JS::Value, JS::NumberValue +#include "vm/JSObject.h" // JSObject +#include "vm/List.h" // js::ListObject +#include "vm/NativeObject.h" // js::NativeObject + +namespace js { + +/** + * Common base class of both readable and writable stream controllers. + */ +class StreamController : public NativeObject { + public: + /** + * Memory layout for stream controllers. + * + * Both ReadableStreamDefaultController and ReadableByteStreamController + * are queue containers and must have these slots at identical offsets. + * + * The queue is guaranteed to be in the same compartment as the container, + * but might contain wrappers for objects from other compartments. + */ + enum Slots { Slot_Queue, Slot_TotalSize, SlotCount }; + + ListObject* queue() const { + return &getFixedSlot(Slot_Queue).toObject().as<ListObject>(); + } + double queueTotalSize() const { + return getFixedSlot(Slot_TotalSize).toNumber(); + } + void setQueueTotalSize(double size) { + setFixedSlot(Slot_TotalSize, JS::NumberValue(size)); + } +}; + +} // namespace js + +template <> +inline bool JSObject::is<js::StreamController>() const; + +#endif // builtin_streams_StreamController_h diff --git a/js/src/builtin/streams/TeeState.cpp b/js/src/builtin/streams/TeeState.cpp new file mode 100644 index 0000000000..c8c151fd8a --- /dev/null +++ b/js/src/builtin/streams/TeeState.cpp @@ -0,0 +1,52 @@ +/* -*- 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/. */ + +/* Stream teeing state. */ + +#include "builtin/streams/TeeState.h" + +#include "builtin/streams/ReadableStream.h" // js::ReadableStream +#include "js/Class.h" // JSClass, JSCLASS_HAS_RESERVED_SLOTS +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted +#include "vm/JSContext.h" // JSContext +#include "vm/PromiseObject.h" // js::PromiseObject + +#include "vm/JSObject-inl.h" // js::NewBuiltinClassInstance + +using js::ReadableStream; +using js::TeeState; + +using JS::Handle; +using JS::Int32Value; +using JS::ObjectValue; +using JS::Rooted; + +/* static */ TeeState* TeeState::create( + JSContext* cx, Handle<ReadableStream*> unwrappedStream) { + Rooted<TeeState*> state(cx, NewBuiltinClassInstance<TeeState>(cx)); + if (!state) { + return nullptr; + } + + Rooted<PromiseObject*> cancelPromise( + cx, PromiseObject::createSkippingExecutor(cx)); + if (!cancelPromise) { + return nullptr; + } + + state->setFixedSlot(Slot_Flags, Int32Value(0)); + state->setFixedSlot(Slot_CancelPromise, ObjectValue(*cancelPromise)); + Rooted<JSObject*> wrappedStream(cx, unwrappedStream); + if (!cx->compartment()->wrap(cx, &wrappedStream)) { + return nullptr; + } + state->setFixedSlot(Slot_Stream, JS::ObjectValue(*wrappedStream)); + + return state; +} + +const JSClass TeeState::class_ = {"TeeState", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount)}; diff --git a/js/src/builtin/streams/TeeState.h b/js/src/builtin/streams/TeeState.h new file mode 100644 index 0000000000..beb42ff8e6 --- /dev/null +++ b/js/src/builtin/streams/TeeState.h @@ -0,0 +1,155 @@ +/* -*- 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/. */ + +/* Stream teeing state. */ + +#ifndef builtin_streams_TeeState_h +#define builtin_streams_TeeState_h + +#include "mozilla/Assertions.h" // MOZ_ASSERT + +#include <stdint.h> // uint32_t + +#include "builtin/streams/ReadableStreamController.h" // js::ReadableStreamDefaultController +#include "js/Class.h" // JSClass +#include "js/Value.h" // JS::{Int32,Object}Value +#include "vm/NativeObject.h" // js::NativeObject +#include "vm/PromiseObject.h" // js::PromiseObject + +namespace js { + +/** + * TeeState objects implement the local variables in Streams spec 3.3.9 + * ReadableStreamTee, which are accessed by several algorithms. + */ +class TeeState : public NativeObject { + public: + /** + * Memory layout for TeeState instances. + * + * The Reason1 and Reason2 slots store opaque values, which might be + * wrapped objects from other compartments. Since we don't treat them as + * objects in Streams-specific code, we don't have to worry about that + * apart from ensuring that the values are properly wrapped before storing + * them. + * + * CancelPromise is always created in TeeState::create below, so is + * guaranteed to be in the same compartment as the TeeState instance + * itself. + * + * Stream can be from another compartment. It is automatically wrapped + * before storing it and unwrapped upon retrieval. That means that + * TeeState consumers need to be able to deal with unwrapped + * ReadableStream instances from non-current compartments. + * + * Branch1 and Branch2 are always created in the same compartment as the + * TeeState instance, so cannot be from another compartment. + */ + enum Slots { + Slot_Flags = 0, + Slot_Reason1, + Slot_Reason2, + Slot_CancelPromise, + Slot_Stream, + Slot_Branch1, + Slot_Branch2, + SlotCount + }; + + private: + enum Flags { + Flag_Reading = 1 << 0, + Flag_Canceled1 = 1 << 1, + Flag_Canceled2 = 1 << 2, + + // No internal user ever sets the cloneForBranch2 flag to true, and the + // streams spec doesn't expose a way to set the flag to true. So for the + // moment, don't even reserve flag-space to store it. + // Flag_CloneForBranch2 = 1 << 3, + }; + uint32_t flags() const { return getFixedSlot(Slot_Flags).toInt32(); } + void setFlags(uint32_t flags) { + setFixedSlot(Slot_Flags, JS::Int32Value(flags)); + } + + public: + static const JSClass class_; + + // Consistent with not even storing this always-false flag, expose it as + // compile-time constant false. + static constexpr bool cloneForBranch2() { return false; } + + bool reading() const { return flags() & Flag_Reading; } + void setReading() { + MOZ_ASSERT(!(flags() & Flag_Reading)); + setFlags(flags() | Flag_Reading); + } + void unsetReading() { + MOZ_ASSERT(flags() & Flag_Reading); + setFlags(flags() & ~Flag_Reading); + } + + 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); + } + + JS::Value reason1() const { + MOZ_ASSERT(canceled1()); + return getFixedSlot(Slot_Reason1); + } + + JS::Value reason2() const { + MOZ_ASSERT(canceled2()); + return getFixedSlot(Slot_Reason2); + } + + PromiseObject* cancelPromise() { + return &getFixedSlot(Slot_CancelPromise).toObject().as<PromiseObject>(); + } + + ReadableStreamDefaultController* branch1() { + ReadableStreamDefaultController* controller = + &getFixedSlot(Slot_Branch1) + .toObject() + .as<ReadableStreamDefaultController>(); + MOZ_ASSERT(controller->isTeeBranch1()); + return controller; + } + void setBranch1(ReadableStreamDefaultController* controller) { + MOZ_ASSERT(controller->isTeeBranch1()); + setFixedSlot(Slot_Branch1, JS::ObjectValue(*controller)); + } + + ReadableStreamDefaultController* branch2() { + ReadableStreamDefaultController* controller = + &getFixedSlot(Slot_Branch2) + .toObject() + .as<ReadableStreamDefaultController>(); + MOZ_ASSERT(controller->isTeeBranch2()); + return controller; + } + void setBranch2(ReadableStreamDefaultController* controller) { + MOZ_ASSERT(controller->isTeeBranch2()); + setFixedSlot(Slot_Branch2, JS::ObjectValue(*controller)); + } + + static TeeState* create(JSContext* cx, + Handle<ReadableStream*> unwrappedStream); +}; + +} // namespace js + +#endif // builtin_streams_TeeState_h diff --git a/js/src/builtin/streams/WritableStream-inl.h b/js/src/builtin/streams/WritableStream-inl.h new file mode 100644 index 0000000000..f2832d1ec1 --- /dev/null +++ b/js/src/builtin/streams/WritableStream-inl.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +/* Class WritableStream. */ + +#ifndef builtin_streams_WritableStream_inl_h +#define builtin_streams_WritableStream_inl_h + +#include "builtin/streams/WritableStream.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jstypes.h" // JS_PUBLIC_API + +#include "builtin/streams/WritableStreamDefaultWriter.h" // js::WritableStreamDefaultWriter +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::{,Object}Value + +#include "vm/Compartment-inl.h" // js::UnwrapInternalSlot + +struct JS_PUBLIC_API JSContext; + +namespace js { + +/** + * Returns the writer associated with the given stream. + * + * Must only be called on WritableStreams that already have a writer + * associated with them. + * + * If the writer is a wrapper, it will be unwrapped, so the result might not be + * an object from the currently active compartment. + */ +inline MOZ_MUST_USE WritableStreamDefaultWriter* UnwrapWriterFromStream( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream) { + MOZ_ASSERT(unwrappedStream->hasWriter()); + return UnwrapInternalSlot<WritableStreamDefaultWriter>( + cx, unwrappedStream, WritableStream::Slot_Writer); +} + +} // namespace js + +#endif // builtin_streams_WritableStream_inl_h diff --git a/js/src/builtin/streams/WritableStream.cpp b/js/src/builtin/streams/WritableStream.cpp new file mode 100644 index 0000000000..2a021334ad --- /dev/null +++ b/js/src/builtin/streams/WritableStream.cpp @@ -0,0 +1,282 @@ +/* -*- 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/. */ + +/* Class WritableStream. */ + +#include "builtin/streams/WritableStream.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jsapi.h" // JS_ReportErrorNumberASCII +#include "jspubtd.h" // JSProto_WritableStream + +#include "builtin/streams/ClassSpecMacro.h" // JS_STREAMS_CLASS_SPEC +#include "builtin/streams/MiscellaneousOperations.h" // js::MakeSizeAlgorithmFromSizeFunction, js::ReturnPromiseRejectedWithPendingError, js::ValidateAndNormalizeHighWaterMark +#include "builtin/streams/WritableStreamDefaultControllerOperations.h" // js::SetUpWritableStreamDefaultControllerFromUnderlyingSink +#include "builtin/streams/WritableStreamDefaultWriter.h" // js::CreateWritableStreamDefaultWriter +#include "builtin/streams/WritableStreamOperations.h" // js::WritableStream{Abort,Close{,QueuedOrInFlight}} +#include "js/CallArgs.h" // JS::CallArgs{,FromVp} +#include "js/Class.h" // JS{Function,Property}Spec, JS_{FS,PS}_END, JSCLASS_PRIVATE_IS_NSISUPPORTS, JSCLASS_HAS_PRIVATE, JS_NULL_CLASS_OPS +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/RealmOptions.h" // JS::RealmCreationOptions +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted +#include "js/Value.h" // JS::{,Object}Value +#include "vm/JSContext.h" // JSContext +#include "vm/JSObject.h" // js::GetPrototypeFromBuiltinConstructor +#include "vm/ObjectOperations.h" // js::GetProperty +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/Realm.h" // JS::Realm + +#include "vm/Compartment-inl.h" // js::UnwrapAndTypeCheckThis +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/JSObject-inl.h" // js::NewBuiltinClassInstance +#include "vm/NativeObject-inl.h" // js::ThrowIfNotConstructing + +using js::CreateWritableStreamDefaultWriter; +using js::GetErrorMessage; +using js::ReturnPromiseRejectedWithPendingError; +using js::UnwrapAndTypeCheckThis; +using js::WritableStream; +using js::WritableStreamAbort; +using js::WritableStreamClose; +using js::WritableStreamCloseQueuedOrInFlight; + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::ObjectValue; +using JS::Rooted; +using JS::Value; + +/*** 4.2. Class WritableStream **********************************************/ + +/** + * Streams spec, 4.2.3. new WritableStream(underlyingSink = {}, strategy = {}) + */ +bool WritableStream::constructor(JSContext* cx, unsigned argc, Value* vp) { + MOZ_ASSERT(cx->realm()->creationOptions().getWritableStreamsEnabled(), + "WritableStream should be enabled in this realm if we reach here"); + + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "WritableStream")) { + return false; + } + + // Implicit in the spec: argument default values. + Rooted<Value> underlyingSink(cx, args.get(0)); + if (underlyingSink.isUndefined()) { + JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx); + if (!emptyObj) { + return false; + } + underlyingSink = ObjectValue(*emptyObj); + } + + Rooted<Value> strategy(cx, args.get(1)); + if (strategy.isUndefined()) { + JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx); + if (!emptyObj) { + return false; + } + strategy = ObjectValue(*emptyObj); + } + + // Implicit in the spec: Set this to + // OrdinaryCreateFromConstructor(NewTarget, ...). + // Step 1: Perform ! InitializeWritableStream(this). + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_WritableStream, + &proto)) { + return false; + } + Rooted<WritableStream*> stream(cx, + WritableStream::create(cx, nullptr, proto)); + if (!stream) { + return false; + } + + // Step 2: Let size be ? GetV(strategy, "size"). + Rooted<Value> size(cx); + if (!GetProperty(cx, strategy, cx->names().size, &size)) { + return false; + } + + // Step 3: Let highWaterMark be ? GetV(strategy, "highWaterMark"). + Rooted<Value> highWaterMarkVal(cx); + if (!GetProperty(cx, strategy, cx->names().highWaterMark, + &highWaterMarkVal)) { + return false; + } + + // Step 4: Let type be ? GetV(underlyingSink, "type"). + Rooted<Value> type(cx); + if (!GetProperty(cx, underlyingSink, cx->names().type, &type)) { + return false; + } + + // Step 5: If type is not undefined, throw a RangeError exception. + if (!type.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLESTREAM_UNDERLYINGSINK_TYPE_WRONG); + return false; + } + + // Step 6: Let sizeAlgorithm be ? MakeSizeAlgorithmFromSizeFunction(size). + if (!MakeSizeAlgorithmFromSizeFunction(cx, size)) { + return false; + } + + // Step 7: If highWaterMark is undefined, let highWaterMark be 1. + double highWaterMark; + if (highWaterMarkVal.isUndefined()) { + highWaterMark = 1; + } else { + // Step 8: Set highWaterMark to ? + // ValidateAndNormalizeHighWaterMark(highWaterMark). + if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal, + &highWaterMark)) { + return false; + } + } + + // Step 9: Perform + // ? SetUpWritableStreamDefaultControllerFromUnderlyingSink( + // this, underlyingSink, highWaterMark, sizeAlgorithm). + if (!SetUpWritableStreamDefaultControllerFromUnderlyingSink( + cx, stream, underlyingSink, highWaterMark, size)) { + return false; + } + + args.rval().setObject(*stream); + return true; +} + +/** + * Streams spec, 4.2.5.1. get locked + */ +static bool WritableStream_locked(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! WritableStream(this) is false, throw a TypeError exception. + Rooted<WritableStream*> unwrappedStream( + cx, UnwrapAndTypeCheckThis<WritableStream>(cx, args, "get locked")); + if (!unwrappedStream) { + return false; + } + + // Step 2: Return ! IsWritableStreamLocked(this). + args.rval().setBoolean(unwrappedStream->isLocked()); + return true; +} + +/** + * Streams spec, 4.2.5.2. abort(reason) + */ +static bool WritableStream_abort(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsWritableStream(this) is false, return a promise rejected + // with a TypeError exception. + Rooted<WritableStream*> unwrappedStream( + cx, UnwrapAndTypeCheckThis<WritableStream>(cx, args, "abort")); + if (!unwrappedStream) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: If ! IsWritableStreamLocked(this) is true, return a promise + // rejected with a TypeError exception. + if (unwrappedStream->isLocked()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_CANT_USE_LOCKED_WRITABLESTREAM, "abort"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: Return ! WritableStreamAbort(this, reason). + JSObject* promise = WritableStreamAbort(cx, unwrappedStream, args.get(0)); + if (!promise) { + return false; + } + cx->check(promise); + + args.rval().setObject(*promise); + return true; +} + +/** + * Streams spec, 4.2.5.3. close() + */ +static bool WritableStream_close(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsWritableStream(this) is false, return a promise rejected + // with a TypeError exception. + Rooted<WritableStream*> unwrappedStream( + cx, UnwrapAndTypeCheckThis<WritableStream>(cx, args, "close")); + if (!unwrappedStream) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: If ! IsWritableStreamLocked(this) is true, return a promise + // rejected with a TypeError exception. + if (unwrappedStream->isLocked()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_CANT_USE_LOCKED_WRITABLESTREAM, "close"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: If ! WritableStreamCloseQueuedOrInFlight(this) is true, return a + // promise rejected with a TypeError exception. + if (WritableStreamCloseQueuedOrInFlight(unwrappedStream)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAM_CLOSE_CLOSING_OR_CLOSED); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 4: Return ! WritableStreamClose(this). + JSObject* promise = WritableStreamClose(cx, unwrappedStream); + if (!promise) { + return false; + } + + args.rval().setObject(*promise); + return true; +} + +/** + * Streams spec, 4.2.5.4. getWriter() + */ +static bool WritableStream_getWriter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! WritableStream(this) is false, throw a TypeError exception. + Rooted<WritableStream*> unwrappedStream( + cx, UnwrapAndTypeCheckThis<WritableStream>(cx, args, "getWriter")); + if (!unwrappedStream) { + return false; + } + + auto* writer = CreateWritableStreamDefaultWriter(cx, unwrappedStream); + if (!writer) { + return false; + } + + args.rval().setObject(*writer); + return true; +} + +static const JSFunctionSpec WritableStream_methods[] = { + JS_FN("abort", WritableStream_abort, 1, 0), + JS_FN("close", WritableStream_close, 0, 0), + JS_FN("getWriter", WritableStream_getWriter, 0, 0), JS_FS_END}; + +static const JSPropertySpec WritableStream_properties[] = { + JS_PSG("locked", WritableStream_locked, 0), JS_PS_END}; + +JS_STREAMS_CLASS_SPEC(WritableStream, 0, SlotCount, 0, + JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE, + JS_NULL_CLASS_OPS); diff --git a/js/src/builtin/streams/WritableStream.h b/js/src/builtin/streams/WritableStream.h new file mode 100644 index 0000000000..3ed6cd12f4 --- /dev/null +++ b/js/src/builtin/streams/WritableStream.h @@ -0,0 +1,425 @@ +/* -*- 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/. */ + +/* Class WritableStream. */ + +#ifndef builtin_streams_WritableStream_h +#define builtin_streams_WritableStream_h + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE +#include "mozilla/Casting.h" // mozilla::AssertedCast +#include "mozilla/MathAlgorithms.h" // mozilla::IsPowerOfTwo + +#include <stdint.h> // uint32_t + +#include "jstypes.h" // JS_PUBLIC_API +#include "js/Class.h" // JSClass, js::ClassSpec +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::{,Int32,Object,Undefined}Value +#include "vm/List.h" // js::ListObject +#include "vm/NativeObject.h" // js::NativeObject + +struct JS_PUBLIC_API JSContext; + +namespace js { + +class PromiseObject; +class WritableStreamDefaultController; +class WritableStreamDefaultWriter; + +class WritableStream : public NativeObject { + public: + enum Slots { + /** + * A WritableStream's associated controller is always created from under the + * stream's constructor and thus cannot be in a different compartment. + */ + Slot_Controller, + + /** + * Either |undefined| if no writer has been created yet for |this|, or a + * |WritableStreamDefaultWriter| object that writes to this. Writers are + * created under |WritableStream.prototype.getWriter|, which may not be + * same-compartment with |this|, so this object may be a wrapper. + */ + Slot_Writer, + + /** + * A bit field that stores both [[state]] and the [[backpressure]] spec + * fields in a WritableStream::State 32-bit integer. + */ + Slot_State, + + /** + * Either |undefined| if this stream hasn't yet started erroring, or an + * arbitrary value indicating the reason for the error (e.g. the + * reason-value passed to a related |abort(reason)| or |error(e)| function). + * + * This value can be an arbitrary user-provided value, so it might be a + * cross-comaprtment wrapper. + */ + Slot_StoredError, + + /** + * Very briefly for newborn writable streams before they are initialized, + * |undefined|. + * + * After initialization, a |ListObject| consisting of the value of the + * [[inFlightWriteRequest]] spec field (if it is not |undefined|) followed + * by the elements of the [[queue]] List. |this| and the |ListObject| are + * same-compartment. + * + * After a stream has gone irrevocably into an error state (specifically, + * |stream.[[state]]| is "errored") and requests can no longer be enqueued, + * |undefined| yet again. + * + * If the |HaveInFlightWriteRequest| flag is set, the first element of this + * List is the non-|undefined| value of [[inFlightWriteRequest]]. If it is + * unset, [[inFlightWriteRequest]] has the value |undefined|. + */ + Slot_WriteRequests, + + /** + * A slot storing both [[closeRequest]] and [[inFlightCloseRequest]]. This + * value is created under |WritableStreamDefaultWriterClose|, so it may be a + * wrapper around a promise rather than directly a |PromiseObject|. + * + * If this slot has the value |undefined|, then [[inFlightCloseRequest]] + * and [[closeRequest]] are both |undefined|. Otherwise one field has the + * value |undefined| and the other has the value of this slot, and the value + * of the |HaveInFlightCloseRequest| flag indicates which field is set. + */ + Slot_CloseRequest, + + /** + * In the spec the [[pendingAbortRequest]] field is either |undefined| or + * Record { [[promise]]: Object, [[reason]]: value, [[wasAlreadyErroring]]: + * boolean }. We represent this as follows: + * + * 1) If Slot_PendingAbortRequestPromise contains |undefined|, then the + * spec field is |undefined|; + * 2) Otherwise Slot_PendingAbortRequestPromise contains the value of + * [[pendingAbortRequest]].[[promise]], Slot_PendingAbortRequestReason + * contains the value of [[pendingAbortRequest]].[[reason]], and the + * |PendingAbortRequestWasAlreadyErroring| flag stores the value of + * [[pendingAbortRequest]].[[wasAlreadyErroring]]. + */ + Slot_PendingAbortRequestPromise, + Slot_PendingAbortRequestReason, + + SlotCount + }; + + private: + enum State : uint32_t { + Writable = 0x0000'0000, + Closed = 0x0000'0001, + Erroring = 0x0000'0002, + Errored = 0x0000'0003, + StateBits = 0x0000'0003, + StateMask = 0x0000'00ff, + + Backpressure = 0x0000'0100, + HaveInFlightWriteRequest = 0x0000'0200, + HaveInFlightCloseRequest = 0x0000'0400, + PendingAbortRequestWasAlreadyErroring = 0x0000'0800, + FlagBits = Backpressure | HaveInFlightWriteRequest | + HaveInFlightCloseRequest | PendingAbortRequestWasAlreadyErroring, + FlagMask = 0x0000'ff00, + + SettableBits = uint32_t(StateBits | FlagBits) + }; + + bool stateIsInitialized() const { return getFixedSlot(Slot_State).isInt32(); } + + State state() const { + MOZ_ASSERT(stateIsInitialized()); + + uint32_t v = getFixedSlot(Slot_State).toInt32(); + MOZ_ASSERT((v & ~SettableBits) == 0); + + return static_cast<State>(v & StateMask); + } + + State flags() const { + MOZ_ASSERT(stateIsInitialized()); + + uint32_t v = getFixedSlot(Slot_State).toInt32(); + MOZ_ASSERT((v & ~SettableBits) == 0); + + return static_cast<State>(v & FlagMask); + } + + void initWritableState() { + MOZ_ASSERT(!stateIsInitialized()); + + setFixedSlot(Slot_State, JS::Int32Value(Writable)); + + MOZ_ASSERT(writable()); + MOZ_ASSERT(!backpressure()); + } + + void setState(State newState) { + MOZ_ASSERT(stateIsInitialized()); + MOZ_ASSERT((newState & ~StateBits) == 0); + MOZ_ASSERT(newState <= Errored); + +#ifdef DEBUG + { + auto current = state(); + if (current == Writable) { + MOZ_ASSERT(newState == Closed || newState == Erroring); + } else if (current == Erroring) { + MOZ_ASSERT(newState == Errored || newState == Closed); + } else if (current == Closed || current == Errored) { + MOZ_ASSERT_UNREACHABLE( + "closed/errored stream shouldn't undergo state transitions"); + } else { + MOZ_ASSERT_UNREACHABLE("smashed state bits?"); + } + } +#endif + + uint32_t newValue = static_cast<uint32_t>(newState) | + (getFixedSlot(Slot_State).toInt32() & FlagMask); + setFixedSlot(Slot_State, + JS::Int32Value(mozilla::AssertedCast<int32_t>(newValue))); + } + + void setFlag(State flag, bool set) { + MOZ_ASSERT(stateIsInitialized()); + MOZ_ASSERT(mozilla::IsPowerOfTwo(uint32_t(flag))); + MOZ_ASSERT((flag & FlagBits) != 0); + + uint32_t v = getFixedSlot(Slot_State).toInt32(); + MOZ_ASSERT((v & ~SettableBits) == 0); + + uint32_t newValue = set ? (v | flag) : (v & ~flag); + setFixedSlot(Slot_State, + JS::Int32Value(mozilla::AssertedCast<int32_t>(newValue))); + } + + public: + bool writable() const { return state() == Writable; } + + bool closed() const { return state() == Closed; } + void setClosed() { setState(Closed); } + + bool erroring() const { return state() == Erroring; } + void setErroring() { setState(Erroring); } + + bool errored() const { return state() == Errored; } + void setErrored() { setState(Errored); } + + bool backpressure() const { return flags() & Backpressure; } + void setBackpressure(bool pressure) { setFlag(Backpressure, pressure); } + + bool haveInFlightWriteRequest() const { + return flags() & HaveInFlightWriteRequest; + } + void setHaveInFlightWriteRequest() { + MOZ_ASSERT(!haveInFlightWriteRequest()); + MOZ_ASSERT(writeRequests()->length() > 0); + setFlag(HaveInFlightWriteRequest, true); + } + + bool haveInFlightCloseRequest() const { + return flags() & HaveInFlightCloseRequest; + } + + bool hasController() const { + return !getFixedSlot(Slot_Controller).isUndefined(); + } + inline WritableStreamDefaultController* controller() const; + inline void setController(WritableStreamDefaultController* controller); + void clearController() { + setFixedSlot(Slot_Controller, JS::UndefinedValue()); + } + + bool hasWriter() const { return !getFixedSlot(Slot_Writer).isUndefined(); } + bool isLocked() const { return hasWriter(); } + void setWriter(JSObject* writer) { + MOZ_ASSERT(!hasWriter()); + setFixedSlot(Slot_Writer, JS::ObjectValue(*writer)); + } + void clearWriter() { setFixedSlot(Slot_Writer, JS::UndefinedValue()); } + + JS::Value storedError() const { return getFixedSlot(Slot_StoredError); } + void setStoredError(JS::Handle<JS::Value> value) { + setFixedSlot(Slot_StoredError, value); + } + void clearStoredError() { + setFixedSlot(Slot_StoredError, JS::UndefinedValue()); + } + + JS::Value inFlightWriteRequest() const { + MOZ_ASSERT(stateIsInitialized()); + + // The in-flight write request is the first element of |writeRequests()| -- + // if there is a request in flight. + if (haveInFlightWriteRequest()) { + MOZ_ASSERT(writeRequests()->length() > 0); + return writeRequests()->get(0); + } + + return JS::UndefinedValue(); + } + + void clearInFlightWriteRequest(JSContext* cx); + + JS::Value closeRequest() const { + JS::Value v = getFixedSlot(Slot_CloseRequest); + if (v.isUndefined()) { + // In principle |haveInFlightCloseRequest()| only distinguishes whether + // the close-request slot is [[closeRequest]] or [[inFlightCloseRequest]]. + // In practice, for greater implementation strictness to try to head off + // more bugs, we require that the HaveInFlightCloseRequest flag be unset + // when [[closeRequest]] and [[inFlightCloseRequest]] are both undefined. + MOZ_ASSERT(!haveInFlightCloseRequest()); + return JS::UndefinedValue(); + } + + if (!haveInFlightCloseRequest()) { + return v; + } + + return JS::UndefinedValue(); + } + + void setCloseRequest(JSObject* closeRequest) { + MOZ_ASSERT(!haveCloseRequestOrInFlightCloseRequest()); + setFixedSlot(Slot_CloseRequest, JS::ObjectValue(*closeRequest)); + MOZ_ASSERT(!haveInFlightCloseRequest()); + } + + void clearCloseRequest() { + MOZ_ASSERT(!haveInFlightCloseRequest()); + MOZ_ASSERT(!getFixedSlot(Slot_CloseRequest).isUndefined()); + setFixedSlot(Slot_CloseRequest, JS::UndefinedValue()); + } + + JS::Value inFlightCloseRequest() const { + JS::Value v = getFixedSlot(Slot_CloseRequest); + if (v.isUndefined()) { + // In principle |haveInFlightCloseRequest()| only distinguishes whether + // the close-request slot is [[closeRequest]] or [[inFlightCloseRequest]]. + // In practice, for greater implementation strictness to try to head off + // more bugs, we require that the HaveInFlightCloseRequest flag be unset + // when [[closeRequest]] and [[inFlightCloseRequest]] are both undefined. + MOZ_ASSERT(!haveInFlightCloseRequest()); + return JS::UndefinedValue(); + } + + if (haveInFlightCloseRequest()) { + return v; + } + + return JS::UndefinedValue(); + } + + bool haveCloseRequestOrInFlightCloseRequest() const { + // Slot_CloseRequest suffices to store both [[closeRequest]] and + // [[inFlightCloseRequest]], with the precisely-set field determined by + // |haveInFlightCloseRequest()|. If both are undefined, then per above, for + // extra implementation rigor, |haveInFlightCloseRequest()| will be false, + // so additionally assert that. + if (getFixedSlot(Slot_CloseRequest).isUndefined()) { + MOZ_ASSERT(!haveInFlightCloseRequest()); + return false; + } + + return true; + } + + void convertCloseRequestToInFlightCloseRequest() { + MOZ_ASSERT(stateIsInitialized()); + MOZ_ASSERT(!haveInFlightCloseRequest()); + setFlag(HaveInFlightCloseRequest, true); + MOZ_ASSERT(haveInFlightCloseRequest()); + } + + void clearInFlightCloseRequest() { + MOZ_ASSERT(stateIsInitialized()); + MOZ_ASSERT(haveInFlightCloseRequest()); + MOZ_ASSERT(!getFixedSlot(Slot_CloseRequest).isUndefined()); + + // As noted above, for greater rigor we require HaveInFlightCloseRequest be + // unset when [[closeRequest]] and [[inFlightCloseRequest]] are both + // undefined. + setFlag(HaveInFlightCloseRequest, false); + setFixedSlot(Slot_CloseRequest, JS::UndefinedValue()); + } + + ListObject* writeRequests() const { + MOZ_ASSERT(!getFixedSlot(Slot_WriteRequests).isUndefined(), + "shouldn't be accessing [[writeRequests]] on a newborn and " + "uninitialized stream, or on a stream that's errored and no " + "longer has any write requests"); + return &getFixedSlot(Slot_WriteRequests).toObject().as<ListObject>(); + } + void clearWriteRequests() { + // Setting [[writeRequests]] to an empty List in the irrevocably-in-error + // case (in which [[writeRequests]] is never again accessed) is optimized to + // just clearing the field. See the comment on the slot constant above. + MOZ_ASSERT(stateIsInitialized()); + MOZ_ASSERT(!haveInFlightWriteRequest(), + "must clear the in-flight request flag before clearing " + "requests"); + setFixedSlot(Slot_WriteRequests, JS::UndefinedValue()); + } + + bool hasPendingAbortRequest() const { + MOZ_ASSERT(stateIsInitialized()); + return !getFixedSlot(Slot_PendingAbortRequestPromise).isUndefined(); + } + JSObject* pendingAbortRequestPromise() const { + MOZ_ASSERT(hasPendingAbortRequest()); + return &getFixedSlot(Slot_PendingAbortRequestPromise).toObject(); + } + JS::Value pendingAbortRequestReason() const { + MOZ_ASSERT(hasPendingAbortRequest()); + return getFixedSlot(Slot_PendingAbortRequestReason); + } + bool pendingAbortRequestWasAlreadyErroring() const { + MOZ_ASSERT(hasPendingAbortRequest()); + return flags() & PendingAbortRequestWasAlreadyErroring; + } + + void setPendingAbortRequest(JSObject* promise, const JS::Value& reason, + bool wasAlreadyErroring) { + MOZ_ASSERT(!hasPendingAbortRequest()); + MOZ_ASSERT(!(flags() & PendingAbortRequestWasAlreadyErroring)); + setFixedSlot(Slot_PendingAbortRequestPromise, JS::ObjectValue(*promise)); + setFixedSlot(Slot_PendingAbortRequestReason, reason); + setFlag(PendingAbortRequestWasAlreadyErroring, wasAlreadyErroring); + } + + void clearPendingAbortRequest() { + MOZ_ASSERT(stateIsInitialized()); + MOZ_ASSERT(hasPendingAbortRequest()); + + // [[pendingAbortRequest]] is { [[promise]], [[reason]] } in the spec but + // separate slots in our implementation, so both must be cleared. + setFixedSlot(Slot_PendingAbortRequestPromise, JS::UndefinedValue()); + setFixedSlot(Slot_PendingAbortRequestReason, JS::UndefinedValue()); + } + + static MOZ_MUST_USE WritableStream* create( + JSContext* cx, void* nsISupportsObject_alreadyAddreffed = nullptr, + JS::Handle<JSObject*> proto = nullptr); + + static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp); + + static const ClassSpec classSpec_; + static const JSClass class_; + static const ClassSpec protoClassSpec_; + static const JSClass protoClass_; +}; + +} // namespace js + +#endif // builtin_streams_WritableStream_h diff --git a/js/src/builtin/streams/WritableStreamDefaultController.cpp b/js/src/builtin/streams/WritableStreamDefaultController.cpp new file mode 100644 index 0000000000..8f9022459f --- /dev/null +++ b/js/src/builtin/streams/WritableStreamDefaultController.cpp @@ -0,0 +1,83 @@ +/* -*- 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/. */ + +/* Class WritableStreamDefaultController. */ + +#include "builtin/streams/WritableStreamDefaultController.h" + +#include "builtin/streams/ClassSpecMacro.h" // JS_STREAMS_CLASS_SPEC +#include "builtin/streams/WritableStream.h" // js::WritableStream +#include "builtin/streams/WritableStreamDefaultControllerOperations.h" // js::WritableStreamDefaultControllerError +#include "js/CallArgs.h" // JS::CallArgs{,FromVp} +#include "js/Class.h" // js::ClassSpec, JS_NULL_CLASS_OPS +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" // JS{Function,Property}Spec, JS_{FS,PS}_END +#include "js/Value.h" // JS::Value + +#include "vm/Compartment-inl.h" // js::UnwrapAndTypeCheckThis + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Rooted; +using JS::Value; + +using js::ClassSpec; +using js::UnwrapAndTypeCheckThis; +using js::WritableStreamDefaultController; +using js::WritableStreamDefaultControllerError; + +/*** 4.7. Class WritableStreamDefaultController *****************************/ + +/** + * Streams spec, 4.7.3. + * new WritableStreamDefaultController() + */ +bool WritableStreamDefaultController::constructor(JSContext* cx, unsigned argc, + Value* vp) { + // Step 1: Throw a TypeError. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BOGUS_CONSTRUCTOR, + "WritableStreamDefaultController"); + return false; +} + +/** + * Streams spec, 4.7.4.1. error(e) + */ +static bool WritableStreamDefaultController_error(JSContext* cx, unsigned argc, + Value* vp) { + // Step 1: If ! IsWritableStreamDefaultController(this) is false, throw a + // TypeError exception. + CallArgs args = CallArgsFromVp(argc, vp); + Rooted<WritableStreamDefaultController*> unwrappedController( + cx, UnwrapAndTypeCheckThis<WritableStreamDefaultController>(cx, args, + "error")); + if (!unwrappedController) { + return false; + } + + // Step 2: Let state be this.[[controlledWritableStream]].[[state]]. + // Step 3: If state is not "writable", return. + if (unwrappedController->stream()->writable()) { + // Step 4: Perform ! WritableStreamDefaultControllerError(this, e). + if (!WritableStreamDefaultControllerError(cx, unwrappedController, + args.get(0))) { + return false; + } + } + + args.rval().setUndefined(); + return true; +} + +static const JSPropertySpec WritableStreamDefaultController_properties[] = { + JS_PS_END}; + +static const JSFunctionSpec WritableStreamDefaultController_methods[] = { + JS_FN("error", WritableStreamDefaultController_error, 1, 0), JS_FS_END}; + +JS_STREAMS_CLASS_SPEC(WritableStreamDefaultController, 0, SlotCount, + ClassSpec::DontDefineConstructor, 0, JS_NULL_CLASS_OPS); diff --git a/js/src/builtin/streams/WritableStreamDefaultController.h b/js/src/builtin/streams/WritableStreamDefaultController.h new file mode 100644 index 0000000000..44f3d237a9 --- /dev/null +++ b/js/src/builtin/streams/WritableStreamDefaultController.h @@ -0,0 +1,186 @@ +/* -*- 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/. */ + +/* WritableStream controller classes and functions. */ + +#ifndef builtin_streams_WritableStreamDefaultController_h +#define builtin_streams_WritableStreamDefaultController_h + +#include "mozilla/Assertions.h" // MOZ_ASSERT + +#include <stdint.h> // uint32_t + +#include "builtin/streams/StreamController.h" // js::StreamController +#include "builtin/streams/WritableStream.h" // js::WritableStream +#include "js/Class.h" // JSClass, js::ClassSpec +#include "js/RootingAPI.h" // JS::Handle +#include "js/Stream.h" // JS::WritableStreamUnderlyingSink +#include "js/Value.h" // JS::Value, JS::{Number,Object,Private,Undefined}Value, JS::UndefinedHandleValue +#include "vm/NativeObject.h" // js::NativeObject + +namespace js { + +class WritableStreamDefaultController : public StreamController { + public: + /** + * Memory layout for WritableStream default controllers, starting after the + * slots reserved for queue container usage. (Note that this is the only + * writable stream controller class in the spec: ReadableByteStreamController + * exists, but WritableByteStreamController does not.) + */ + enum Slots { + /** + * The stream that this controller controls. Stream and controller are + * initialized at the same time underneath the |WritableStream| constructor, + * so they are same-compartment with each other. + */ + Slot_Stream = StreamController::SlotCount, + + /** + * The underlying sink object that this controller and its associated stream + * write to. + * + * This is a user-provided value, the first argument passed to + * |new WritableStream|, so it may be a cross-compartment wrapper around an + * object from another realm. + */ + Slot_UnderlyingSink, + + /** Number stored as DoubleValue. */ + Slot_StrategyHWM, + + /** + * Either undefined if each chunk has size 1, or a callable object to be + * invoked on each chunk to determine its size. See + * MakeSizeAlgorithmFromSizeFunction. + */ + Slot_StrategySize, + + /** + * Slots containing the core of each of the write/close/abort algorithms the + * spec creates from the underlying sink passed in when creating a + * |WritableStream|. ("core", as in the value produced by + * |CreateAlgorithmFromUnderlyingMethod| after validating the user-provided + * input.) + * + * These slots are initialized underneath the |WritableStream| constructor, + * so they are same-compartment with both stream and controller. (They + * could be wrappers around arbitrary callable objects from other + * compartments, tho.) + */ + Slot_WriteMethod, + Slot_CloseMethod, + Slot_AbortMethod, + + /** Bit field stored as Int32Value. */ + Slot_Flags, + + SlotCount + }; + + enum ControllerFlags { + Flag_Started = 0b0001, + Flag_ExternalSink = 0b0010, + }; + + WritableStream* stream() const { + return &getFixedSlot(Slot_Stream).toObject().as<WritableStream>(); + } + void setStream(WritableStream* stream) { + setFixedSlot(Slot_Stream, JS::ObjectValue(*stream)); + } + + JS::Value underlyingSink() const { return getFixedSlot(Slot_UnderlyingSink); } + void setUnderlyingSink(const JS::Value& underlyingSink) { + setFixedSlot(Slot_UnderlyingSink, underlyingSink); + } + + JS::WritableStreamUnderlyingSink* externalSink() const { + static_assert(alignof(JS::WritableStreamUnderlyingSink) >= 2, + "external underling sinks are stored as PrivateValues, so " + "they must have even addresses"); + MOZ_ASSERT(hasExternalSink()); + return static_cast<JS::WritableStreamUnderlyingSink*>( + underlyingSink().toPrivate()); + } + void setExternalSink(JS::WritableStreamUnderlyingSink* underlyingSink) { + setUnderlyingSink(JS::PrivateValue(underlyingSink)); + addFlags(Flag_ExternalSink); + } + static void clearUnderlyingSink( + JS::Handle<WritableStreamDefaultController*> controller, + bool finalizeSink = true) { + if (controller->hasExternalSink()) { + if (finalizeSink) { + controller->externalSink()->finalize(); + } + controller->setFlags(controller->flags() & ~Flag_ExternalSink); + } + controller->setUnderlyingSink(JS::UndefinedHandleValue); + } + + JS::Value writeMethod() const { return getFixedSlot(Slot_WriteMethod); } + void setWriteMethod(const JS::Value& writeMethod) { + setFixedSlot(Slot_WriteMethod, writeMethod); + } + void clearWriteMethod() { setWriteMethod(JS::UndefinedValue()); } + + JS::Value closeMethod() const { return getFixedSlot(Slot_CloseMethod); } + void setCloseMethod(const JS::Value& closeMethod) { + setFixedSlot(Slot_CloseMethod, closeMethod); + } + void clearCloseMethod() { setCloseMethod(JS::UndefinedValue()); } + + JS::Value abortMethod() const { return getFixedSlot(Slot_AbortMethod); } + void setAbortMethod(const JS::Value& abortMethod) { + setFixedSlot(Slot_AbortMethod, abortMethod); + } + void clearAbortMethod() { setAbortMethod(JS::UndefinedValue()); } + + double strategyHWM() const { + return getFixedSlot(Slot_StrategyHWM).toDouble(); + } + void setStrategyHWM(double highWaterMark) { + setFixedSlot(Slot_StrategyHWM, DoubleValue(highWaterMark)); + } + + JS::Value strategySize() const { return getFixedSlot(Slot_StrategySize); } + void setStrategySize(const JS::Value& size) { + setFixedSlot(Slot_StrategySize, size); + } + void clearStrategySize() { setStrategySize(JS::UndefinedValue()); } + + uint32_t flags() const { return getFixedSlot(Slot_Flags).toInt32(); } + void setFlags(uint32_t flags) { setFixedSlot(Slot_Flags, Int32Value(flags)); } + void addFlags(uint32_t flags) { setFlags(this->flags() | flags); } + void removeFlags(uint32_t flags) { setFlags(this->flags() & ~flags); } + + bool started() const { return flags() & Flag_Started; } + void setStarted() { addFlags(Flag_Started); } + + bool hasExternalSink() const { return flags() & Flag_ExternalSink; } + + static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp); + static const ClassSpec classSpec_; + static const JSClass class_; + static const ClassSpec protoClassSpec_; + static const JSClass protoClass_; +}; + +inline WritableStreamDefaultController* WritableStream::controller() const { + return &getFixedSlot(Slot_Controller) + .toObject() + .as<WritableStreamDefaultController>(); +} + +inline void WritableStream::setController( + WritableStreamDefaultController* controller) { + setFixedSlot(Slot_Controller, JS::ObjectValue(*controller)); +} + +} // namespace js + +#endif // builtin_streams_WritableStreamDefaultController_h diff --git a/js/src/builtin/streams/WritableStreamDefaultControllerOperations.cpp b/js/src/builtin/streams/WritableStreamDefaultControllerOperations.cpp new file mode 100644 index 0000000000..704d4e4dac --- /dev/null +++ b/js/src/builtin/streams/WritableStreamDefaultControllerOperations.cpp @@ -0,0 +1,1014 @@ +/* -*- 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/. */ + +/* Writable stream default controller abstract operations. */ + +#include "builtin/streams/WritableStreamDefaultControllerOperations.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jsapi.h" // JS_ReportErrorASCII + +#include "builtin/streams/MiscellaneousOperations.h" // js::CreateAlgorithmFromUnderlyingMethod, js::InvokeOrNoop +#include "builtin/streams/QueueWithSizes.h" // js::{EnqueueValueWithSize,QueueIsEmpty,ResetQueue} +#include "builtin/streams/WritableStream.h" // js::WritableStream +#include "builtin/streams/WritableStreamDefaultController.h" // js::WritableStreamDefaultController +#include "builtin/streams/WritableStreamOperations.h" // js::WritableStream{CloseQueuedOrInFlight,DealWithRejection,{Start,Finish}Erroring,UpdateBackpressure,Mark{Close,FirstWrite}RequestInFlight} +#include "js/CallArgs.h" // JS::CallArgs{,FromVp} +#include "js/Promise.h" // JS::AddPromiseReactions +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted +#include "js/Value.h" // JS::{,Int32,Magic,Object}Value, JS::UndefinedHandleValue, JS_WRITABLESTREAM_CLOSE_RECORD +#include "vm/Compartment.h" // JS::Compartment +#include "vm/JSContext.h" // JSContext +#include "vm/JSObject.h" // JSObject +#include "vm/List.h" // js::ListObject +#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseResolvedWithUndefined +#include "vm/Runtime.h" // JSAtomState + +#include "builtin/HandlerFunction-inl.h" // js::TargetFromHandler +#include "builtin/streams/MiscellaneousOperations-inl.h" // js::PromiseCall +#include "builtin/streams/QueueWithSizes-inl.h" // js::PeekQueueValue +#include "vm/Compartment-inl.h" // JS::Compartment::wrap +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/JSObject-inl.h" // js::IsCallable, js::NewBuiltinClassInstance, js::NewObjectWithClassProto +#include "vm/Realm-inl.h" // js::AutoRealm + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::Int32Value; +using JS::MagicValue; +using JS::ObjectValue; +using JS::Rooted; +using JS::UndefinedHandleValue; +using JS::Value; + +using js::IsCallable; +using js::ListObject; +using js::NewHandler; +using js::PeekQueueValue; +using js::PromiseObject; +using js::PromiseResolvedWithUndefined; +using js::TargetFromHandler; +using js::WritableStream; +using js::WritableStreamCloseQueuedOrInFlight; +using js::WritableStreamDefaultController; +using js::WritableStreamFinishErroring; +using js::WritableStreamMarkCloseRequestInFlight; +using js::WritableStreamMarkFirstWriteRequestInFlight; +using js::WritableStreamUpdateBackpressure; + +/*** 4.7. Writable stream default controller internal methods ***************/ + +/** + * Streams spec, 4.7.5.1. + * [[AbortSteps]]( reason ) + */ +JSObject* js::WritableStreamControllerAbortSteps( + JSContext* cx, Handle<WritableStreamDefaultController*> unwrappedController, + Handle<Value> reason) { + cx->check(reason); + + // Step 1: Let result be the result of performing this.[[abortAlgorithm]], + // passing reason. + // CreateAlgorithmFromUnderlyingMethod(underlyingSink, "abort", 1, « ») + Rooted<Value> unwrappedAbortMethod(cx, unwrappedController->abortMethod()); + Rooted<JSObject*> result(cx); + if (unwrappedAbortMethod.isUndefined()) { + // CreateAlgorithmFromUnderlyingMethod step 7. + result = PromiseResolvedWithUndefined(cx); + if (!result) { + return nullptr; + } + } else { + // CreateAlgorithmFromUnderlyingMethod step 6.c.i-ii. + { + AutoRealm ar(cx, unwrappedController); + cx->check(unwrappedAbortMethod); + + Rooted<Value> underlyingSink(cx, unwrappedController->underlyingSink()); + cx->check(underlyingSink); + + Rooted<Value> wrappedReason(cx, reason); + if (!cx->compartment()->wrap(cx, &wrappedReason)) { + return nullptr; + } + + result = + PromiseCall(cx, unwrappedAbortMethod, underlyingSink, wrappedReason); + if (!result) { + return nullptr; + } + } + if (!cx->compartment()->wrap(cx, &result)) { + return nullptr; + } + } + + // Step 2: Perform ! WritableStreamDefaultControllerClearAlgorithms(this). + WritableStreamDefaultControllerClearAlgorithms(unwrappedController); + + // Step 3: Return result. + return result; +} + +/** + * Streams spec, 4.7.5.2. + * [[ErrorSteps]]() + */ +bool js::WritableStreamControllerErrorSteps( + JSContext* cx, + Handle<WritableStreamDefaultController*> unwrappedController) { + // Step 1: Perform ! ResetQueue(this). + return ResetQueue(cx, unwrappedController); +} + +/*** 4.8. Writable stream default controller abstract operations ************/ + +static MOZ_MUST_USE bool WritableStreamDefaultControllerAdvanceQueueIfNeeded( + JSContext* cx, + Handle<WritableStreamDefaultController*> unwrappedController); + +/** + * Streams spec, 4.8.2. SetUpWritableStreamDefaultController, step 16: + * Upon fulfillment of startPromise, [...] + */ +bool js::WritableStreamControllerStartHandler(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + Rooted<WritableStreamDefaultController*> unwrappedController( + cx, TargetFromHandler<WritableStreamDefaultController>(args)); + + // Step a: Assert: stream.[[state]] is "writable" or "erroring". +#ifdef DEBUG + const auto* unwrappedStream = unwrappedController->stream(); + MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); +#endif + + // Step b: Set controller.[[started]] to true. + unwrappedController->setStarted(); + + // Step c: Perform + // ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + if (!WritableStreamDefaultControllerAdvanceQueueIfNeeded( + cx, unwrappedController)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 4.8.2. SetUpWritableStreamDefaultController, step 17: + * Upon rejection of startPromise with reason r, [...] + */ +bool js::WritableStreamControllerStartFailedHandler(JSContext* cx, + unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + Rooted<WritableStreamDefaultController*> unwrappedController( + cx, TargetFromHandler<WritableStreamDefaultController>(args)); + + Rooted<WritableStream*> unwrappedStream(cx, unwrappedController->stream()); + + // Step a: Assert: stream.[[state]] is "writable" or "erroring". + MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); + + // Step b: Set controller.[[started]] to true. + unwrappedController->setStarted(); + + // Step c: Perform ! WritableStreamDealWithRejection(stream, r). + if (!WritableStreamDealWithRejection(cx, unwrappedStream, args.get(0))) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 4.8.2. + * SetUpWritableStreamDefaultController(stream, controller, + * startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, + * highWaterMark, sizeAlgorithm ) + * + * The standard algorithm takes a `controller` argument which must be a new, + * blank object. This implementation creates a new controller instead. + * + * In the spec, four algorithms (startAlgorithm, writeAlgorithm, closeAlgorithm, + * abortAlgorithm) are passed as arguments to this routine. This implementation + * passes these "algorithms" as data, using five arguments: sinkAlgorithms, + * underlyingSink, writeMethod, closeMethod, and abortMethod. The sinkAlgorithms + * argument tells how to interpret the other three: + * + * - SinkAlgorithms::Script - We're creating a stream from a JS source. The + * caller is `new WritableStream(underlyingSink)` or + * `JS::NewWritableDefaultStreamObject`. `underlyingSink` is the sink; + * `writeMethod`, `closeMethod`, and `abortMethod` are its .write, .close, + * and .abort methods, which the caller has already extracted and + * type-checked: each one must be either a callable JS object or undefined. + * + * Script streams use the start/write/close/abort algorithms defined in + * 4.8.3. SetUpWritableStreamDefaultControllerFromUnderlyingSink, which + * call JS methods of the underlyingSink. + * + * - SinkAlgorithms::Transform - We're creating a transform stream. + * `underlyingSink` is a Transform object. `writeMethod`, `closeMethod, and + * `abortMethod` are undefined. + * + * Transform streams use the write/close/abort algorithms given in + * 5.3.2 InitializeTransformStream. + * + * An additional sizeAlgorithm in the spec is an algorithm used to compute the + * size of a chunk. Per MakeSizeAlgorithmFromSizeFunction, we just save the + * |size| value used to create that algorithm, then -- inline -- perform the + * requisite algorithm steps. (Hence the unadorned name |size|.) + * + * Note: All arguments must be same-compartment with cx. WritableStream + * controllers are always created in the same compartment as the stream. + */ +MOZ_MUST_USE bool js::SetUpWritableStreamDefaultController( + JSContext* cx, Handle<WritableStream*> stream, + SinkAlgorithms sinkAlgorithms, Handle<Value> underlyingSink, + Handle<Value> writeMethod, Handle<Value> closeMethod, + Handle<Value> abortMethod, double highWaterMark, Handle<Value> size) { + cx->check(stream); + cx->check(underlyingSink); + cx->check(writeMethod); + MOZ_ASSERT(writeMethod.isUndefined() || IsCallable(writeMethod)); + cx->check(closeMethod); + MOZ_ASSERT(closeMethod.isUndefined() || IsCallable(closeMethod)); + cx->check(abortMethod); + MOZ_ASSERT(abortMethod.isUndefined() || IsCallable(abortMethod)); + MOZ_ASSERT(highWaterMark >= 0); + cx->check(size); + MOZ_ASSERT(size.isUndefined() || IsCallable(size)); + + // Done elsewhere in the standard: Create the new controller. + Rooted<WritableStreamDefaultController*> controller( + cx, NewBuiltinClassInstance<WritableStreamDefaultController>(cx)); + if (!controller) { + return false; + } + + // Step 1: Assert: ! IsWritableStream(stream) is true. + // (guaranteed by |stream|'s type) + + // Step 2: Assert: stream.[[writableStreamController]] is undefined. + MOZ_ASSERT(!stream->hasController()); + + // Step 3: Set controller.[[controlledWritableStream]] to stream. + controller->setStream(stream); + + // Step 4: Set stream.[[writableStreamController]] to controller. + stream->setController(controller); + + // Step 5: Perform ! ResetQueue(controller). + if (!ResetQueue(cx, controller)) { + return false; + } + + // Step 6: Set controller.[[started]] to false. + controller->setFlags(0); + MOZ_ASSERT(!controller->started()); + + // Step 7: Set controller.[[strategySizeAlgorithm]] to sizeAlgorithm. + controller->setStrategySize(size); + + // Step 8: Set controller.[[strategyHWM]] to highWaterMark. + controller->setStrategyHWM(highWaterMark); + + // Step 9: Set controller.[[writeAlgorithm]] to writeAlgorithm. + // Step 10: Set controller.[[closeAlgorithm]] to closeAlgorithm. + // Step 11: Set controller.[[abortAlgorithm]] to abortAlgorithm. + // (In this implementation, all [[*Algorithm]] are determined by the + // underlyingSink in combination with the corresponding *Method field.) + controller->setUnderlyingSink(underlyingSink); + controller->setWriteMethod(writeMethod); + controller->setCloseMethod(closeMethod); + controller->setAbortMethod(abortMethod); + + // Step 12: Let backpressure be + // ! WritableStreamDefaultControllerGetBackpressure(controller). + bool backpressure = + WritableStreamDefaultControllerGetBackpressure(controller); + + // Step 13: Perform ! WritableStreamUpdateBackpressure(stream, backpressure). + if (!WritableStreamUpdateBackpressure(cx, stream, backpressure)) { + return false; + } + + // Step 14: Let startResult be the result of performing startAlgorithm. (This + // may throw an exception.) + Rooted<Value> startResult(cx); + if (sinkAlgorithms == SinkAlgorithms::Script) { + Rooted<Value> controllerVal(cx, ObjectValue(*controller)); + if (!InvokeOrNoop(cx, underlyingSink, cx->names().start, controllerVal, + &startResult)) { + return false; + } + } + + // Step 15: Let startPromise be a promise resolved with startResult. + Rooted<JSObject*> startPromise( + cx, PromiseObject::unforgeableResolve(cx, startResult)); + if (!startPromise) { + return false; + } + + // Step 16: Upon fulfillment of startPromise, + // Assert: stream.[[state]] is "writable" or "erroring". + // Set controller.[[started]] to true. + // Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + // Step 17: Upon rejection of startPromise with reason r, + // Assert: stream.[[state]] is "writable" or "erroring". + // Set controller.[[started]] to true. + // Perform ! WritableStreamDealWithRejection(stream, r). + Rooted<JSObject*> onStartFulfilled( + cx, NewHandler(cx, WritableStreamControllerStartHandler, controller)); + if (!onStartFulfilled) { + return false; + } + Rooted<JSObject*> onStartRejected( + cx, + NewHandler(cx, WritableStreamControllerStartFailedHandler, controller)); + if (!onStartRejected) { + return false; + } + + return JS::AddPromiseReactions(cx, startPromise, onStartFulfilled, + onStartRejected); +} + +/** + * Streams spec, 4.8.3. + * SetUpWritableStreamDefaultControllerFromUnderlyingSink( stream, + * underlyingSink, highWaterMark, sizeAlgorithm ) + */ +MOZ_MUST_USE bool js::SetUpWritableStreamDefaultControllerFromUnderlyingSink( + JSContext* cx, Handle<WritableStream*> stream, Handle<Value> underlyingSink, + double highWaterMark, Handle<Value> sizeAlgorithm) { + cx->check(stream); + cx->check(underlyingSink); + cx->check(sizeAlgorithm); + + // Step 1: Assert: underlyingSink is not undefined. + MOZ_ASSERT(!underlyingSink.isUndefined()); + + // Step 2: Let controller be ObjectCreate(the original value of + // WritableStreamDefaultController's prototype property). + // (Deferred to SetUpWritableStreamDefaultController.) + + // Step 3: Let startAlgorithm be the following steps: + // a. Return ? InvokeOrNoop(underlyingSink, "start", + // « controller »). + SinkAlgorithms sinkAlgorithms = SinkAlgorithms::Script; + + // Step 4: Let writeAlgorithm be + // ? CreateAlgorithmFromUnderlyingMethod(underlyingSink, "write", 1, + // « controller »). + Rooted<Value> writeMethod(cx); + if (!CreateAlgorithmFromUnderlyingMethod(cx, underlyingSink, + "WritableStream sink.write method", + cx->names().write, &writeMethod)) { + return false; + } + + // Step 5: Let closeAlgorithm be + // ? CreateAlgorithmFromUnderlyingMethod(underlyingSink, "close", 0, + // « »). + Rooted<Value> closeMethod(cx); + if (!CreateAlgorithmFromUnderlyingMethod(cx, underlyingSink, + "WritableStream sink.close method", + cx->names().close, &closeMethod)) { + return false; + } + + // Step 6: Let abortAlgorithm be + // ? CreateAlgorithmFromUnderlyingMethod(underlyingSink, "abort", 1, + // « »). + Rooted<Value> abortMethod(cx); + if (!CreateAlgorithmFromUnderlyingMethod(cx, underlyingSink, + "WritableStream sink.abort method", + cx->names().abort, &abortMethod)) { + return false; + } + + // Step 6. Perform ? SetUpWritableStreamDefaultController(stream, + // controller, startAlgorithm, writeAlgorithm, closeAlgorithm, + // abortAlgorithm, highWaterMark, sizeAlgorithm). + return SetUpWritableStreamDefaultController( + cx, stream, sinkAlgorithms, underlyingSink, writeMethod, closeMethod, + abortMethod, highWaterMark, sizeAlgorithm); +} + +/** + * Streams spec, 4.8.4. + * WritableStreamDefaultControllerClearAlgorithms ( controller ) + */ +void js::WritableStreamDefaultControllerClearAlgorithms( + WritableStreamDefaultController* unwrappedController) { + // Note: This operation will be performed multiple times in some edge cases, + // so it can't assert that the various algorithms initially haven't been + // cleared. + + // Step 1: Set controller.[[writeAlgorithm]] to undefined. + unwrappedController->clearWriteMethod(); + + // Step 2: Set controller.[[closeAlgorithm]] to undefined. + unwrappedController->clearCloseMethod(); + + // Step 3: Set controller.[[abortAlgorithm]] to undefined. + unwrappedController->clearAbortMethod(); + + // Step 4: Set controller.[[strategySizeAlgorithm]] to undefined. + unwrappedController->clearStrategySize(); +} + +/** + * Streams spec, 4.8.5. + * WritableStreamDefaultControllerClose ( controller ) + */ +bool js::WritableStreamDefaultControllerClose( + JSContext* cx, + Handle<WritableStreamDefaultController*> unwrappedController) { + // Step 1: Perform ! EnqueueValueWithSize(controller, "close", 0). + { + Rooted<Value> v(cx, MagicValue(JS_WRITABLESTREAM_CLOSE_RECORD)); + Rooted<Value> size(cx, Int32Value(0)); + if (!EnqueueValueWithSize(cx, unwrappedController, v, size)) { + return false; + } + } + + // Step 2: Perform + // ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + return WritableStreamDefaultControllerAdvanceQueueIfNeeded( + cx, unwrappedController); +} + +/** + * Streams spec, 4.8.6. + * WritableStreamDefaultControllerGetChunkSize ( controller, chunk ) + */ +bool js::WritableStreamDefaultControllerGetChunkSize( + JSContext* cx, Handle<WritableStreamDefaultController*> unwrappedController, + Handle<Value> chunk, MutableHandle<Value> returnValue) { + cx->check(chunk); + + // Step 1: Let returnValue be the result of performing + // controller.[[strategySizeAlgorithm]], passing in chunk, and + // interpreting the result as an ECMAScript completion value. + + // We don't store a literal [[strategySizeAlgorithm]], only the value that if + // passed through |MakeSizeAlgorithmFromSizeFunction| wouldn't have triggered + // an error. Perform the algorithm that function would return. + Rooted<Value> unwrappedStrategySize(cx, unwrappedController->strategySize()); + if (unwrappedStrategySize.isUndefined()) { + // 6.3.8 step 1: If size is undefined, return an algorithm that returns 1. + // ...and then from this function... + // Step 3: Return returnValue.[[Value]]. + returnValue.setInt32(1); + return true; + } + + MOZ_ASSERT(IsCallable(unwrappedStrategySize)); + + { + bool success; + { + AutoRealm ar(cx, unwrappedController); + cx->check(unwrappedStrategySize); + + Rooted<Value> wrappedChunk(cx, chunk); + if (!cx->compartment()->wrap(cx, &wrappedChunk)) { + return false; + } + + // 6.3.8 step 3 (of |MakeSizeAlgorithmFromSizeFunction|): + // Return an algorithm that performs the following steps, taking a + // chunk argument: + // a. Return ? Call(size, undefined, « chunk »). + success = Call(cx, unwrappedStrategySize, UndefinedHandleValue, + wrappedChunk, returnValue); + } + + // Step 3: (If returnValue is [not] an abrupt completion, ) + // Return returnValue.[[Value]]. (reordered for readability) + if (success) { + return cx->compartment()->wrap(cx, returnValue); + } + } + + // Step 2: If returnValue is an abrupt completion, + if (!cx->isExceptionPending() || !cx->getPendingException(returnValue)) { + // Uncatchable error. Die immediately without erroring the stream. + return false; + } + cx->check(returnValue); + + cx->clearPendingException(); + + // Step 2.a: Perform + // ! WritableStreamDefaultControllerErrorIfNeeded( + // controller, returnValue.[[Value]]). + if (!WritableStreamDefaultControllerErrorIfNeeded(cx, unwrappedController, + returnValue)) { + return false; + } + + // Step 2.b: Return 1. + returnValue.setInt32(1); + return true; +} + +/** + * Streams spec, 4.8.7. + * WritableStreamDefaultControllerGetDesiredSize ( controller ) + */ +double js::WritableStreamDefaultControllerGetDesiredSize( + const WritableStreamDefaultController* controller) { + return controller->strategyHWM() - controller->queueTotalSize(); +} + +/** + * Streams spec, 4.8.8. + * WritableStreamDefaultControllerWrite ( controller, chunk, chunkSize ) + */ +bool js::WritableStreamDefaultControllerWrite( + JSContext* cx, Handle<WritableStreamDefaultController*> unwrappedController, + Handle<Value> chunk, Handle<Value> chunkSize) { + MOZ_ASSERT(!chunk.isMagic()); + cx->check(chunk); + cx->check(chunkSize); + + // Step 1: Let writeRecord be Record {[[chunk]]: chunk}. + // Step 2: Let enqueueResult be + // EnqueueValueWithSize(controller, writeRecord, chunkSize). + bool succeeded = + EnqueueValueWithSize(cx, unwrappedController, chunk, chunkSize); + + // Step 3: If enqueueResult is an abrupt completion, + if (!succeeded) { + Rooted<Value> enqueueResult(cx); + if (!cx->isExceptionPending() || !cx->getPendingException(&enqueueResult)) { + // Uncatchable error. Die immediately without erroring the stream. + return false; + } + cx->check(enqueueResult); + + cx->clearPendingException(); + + // Step 3.a: Perform ! WritableStreamDefaultControllerErrorIfNeeded( + // controller, enqueueResult.[[Value]]). + // Step 3.b: Return. + return WritableStreamDefaultControllerErrorIfNeeded(cx, unwrappedController, + enqueueResult); + } + + // Step 4: Let stream be controller.[[controlledWritableStream]]. + Rooted<WritableStream*> unwrappedStream(cx, unwrappedController->stream()); + + // Step 5: If ! WritableStreamCloseQueuedOrInFlight(stream) is false and + // stream.[[state]] is "writable", + if (!WritableStreamCloseQueuedOrInFlight(unwrappedStream) && + unwrappedStream->writable()) { + // Step 5.a: Let backpressure be + // ! WritableStreamDefaultControllerGetBackpressure(controller). + bool backpressure = + WritableStreamDefaultControllerGetBackpressure(unwrappedController); + + // Step 5.b: Perform + // ! WritableStreamUpdateBackpressure(stream, backpressure). + if (!WritableStreamUpdateBackpressure(cx, unwrappedStream, backpressure)) { + return false; + } + } + + // Step 6: Perform + // ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + return WritableStreamDefaultControllerAdvanceQueueIfNeeded( + cx, unwrappedController); +} + +static MOZ_MUST_USE bool WritableStreamDefaultControllerProcessIfNeeded( + JSContext* cx, + Handle<WritableStreamDefaultController*> unwrappedController); + +/** + * Streams spec, 4.8.9. + * WritableStreamDefaultControllerAdvanceQueueIfNeeded ( controller ) + */ +MOZ_MUST_USE bool WritableStreamDefaultControllerAdvanceQueueIfNeeded( + JSContext* cx, + Handle<WritableStreamDefaultController*> unwrappedController) { + // Step 2: If controller.[[started]] is false, return. + if (!unwrappedController->started()) { + return true; + } + + // Step 1: Let stream be controller.[[controlledWritableStream]]. + Rooted<WritableStream*> unwrappedStream(cx, unwrappedController->stream()); + + // Step 3: If stream.[[inFlightWriteRequest]] is not undefined, return. + if (!unwrappedStream->inFlightWriteRequest().isUndefined()) { + return true; + } + + // Step 4: Let state be stream.[[state]]. + // Step 5: Assert: state is not "closed" or "errored". + // Step 6: If state is "erroring", + MOZ_ASSERT(!unwrappedStream->closed()); + MOZ_ASSERT(!unwrappedStream->errored()); + if (unwrappedStream->erroring()) { + // Step 6a: Perform ! WritableStreamFinishErroring(stream). + // Step 6b: Return. + return WritableStreamFinishErroring(cx, unwrappedStream); + } + + // Step 7: If controller.[[queue]] is empty, return. + // Step 8: Let writeRecord be ! PeekQueueValue(controller). + // Step 9: If writeRecord is "close", perform + // ! WritableStreamDefaultControllerProcessClose(controller). + // Step 10: Otherwise, perform + // ! WritableStreamDefaultControllerProcessWrite( + // controller, writeRecord.[[chunk]]). + return WritableStreamDefaultControllerProcessIfNeeded(cx, + unwrappedController); +} + +/** + * Streams spec, 4.8.10. + * WritableStreamDefaultControllerErrorIfNeeded ( controller, error ) + */ +bool js::WritableStreamDefaultControllerErrorIfNeeded( + JSContext* cx, Handle<WritableStreamDefaultController*> unwrappedController, + Handle<Value> error) { + cx->check(error); + + // Step 1: If controller.[[controlledWritableStream]].[[state]] is "writable", + // perform ! WritableStreamDefaultControllerError(controller, error). + if (unwrappedController->stream()->writable()) { + if (!WritableStreamDefaultControllerError(cx, unwrappedController, error)) { + return false; + } + } + + return true; +} + +// 4.8.11 step 5: Let sinkClosePromise be the result of performing +// controller.[[closeAlgorithm]]. +static MOZ_MUST_USE JSObject* PerformCloseAlgorithm( + JSContext* cx, + Handle<WritableStreamDefaultController*> unwrappedController) { + // 4.8.3 step 5: Let closeAlgorithm be + // ? CreateAlgorithmFromUnderlyingMethod(underlyingSink, + // "close", 0, « »). + + // Step 1: Assert: underlyingObject is not undefined. + // Step 2: Assert: ! IsPropertyKey(methodName) is true (implicit). + // Step 3: Assert: algoArgCount is 0 or 1 (omitted). + // Step 4: Assert: extraArgs is a List (omitted). + // Step 5: Let method be ? GetV(underlyingObject, methodName). + // + // These steps were performed in |CreateAlgorithmFromUnderlyingMethod|. The + // spec stores away algorithms for later invocation; we instead store the + // value that determines the algorithm to be created -- either |undefined|, or + // a callable object that's called with context-specific arguments. + + // Step 7: (If method is undefined,) Return an algorithm which returns a + // promise resolved with undefined (implicit). + if (unwrappedController->closeMethod().isUndefined()) { + return PromiseResolvedWithUndefined(cx); + } + + // Step 6: If method is not undefined, + + // Step 6.a: If ! IsCallable(method) is false, throw a TypeError exception. + MOZ_ASSERT(IsCallable(unwrappedController->closeMethod())); + + // Step 6.b: If algoArgCount is 0, return an algorithm that performs the + // following steps: + // Step 6.b.ii: Return ! PromiseCall(method, underlyingObject, extraArgs). + Rooted<Value> closeMethod(cx, unwrappedController->closeMethod()); + if (!cx->compartment()->wrap(cx, &closeMethod)) { + return nullptr; + } + + Rooted<Value> underlyingSink(cx, unwrappedController->underlyingSink()); + if (!cx->compartment()->wrap(cx, &underlyingSink)) { + return nullptr; + } + + return PromiseCall(cx, closeMethod, underlyingSink); +} + +// 4.8.12 step 3: Let sinkWritePromise be the result of performing +// controller.[[writeAlgorithm]], passing in chunk. +static MOZ_MUST_USE JSObject* PerformWriteAlgorithm( + JSContext* cx, Handle<WritableStreamDefaultController*> unwrappedController, + Handle<Value> chunk) { + cx->check(chunk); + + // 4.8.3 step 4: Let writeAlgorithm be + // ? CreateAlgorithmFromUnderlyingMethod(underlyingSink, + // "write", 1, + // « controller »). + + // Step 1: Assert: underlyingObject is not undefined. + // Step 2: Assert: ! IsPropertyKey(methodName) is true (implicit). + // Step 3: Assert: algoArgCount is 0 or 1 (omitted). + // Step 4: Assert: extraArgs is a List (omitted). + // Step 5: Let method be ? GetV(underlyingObject, methodName). + // + // These steps were performed in |CreateAlgorithmFromUnderlyingMethod|. The + // spec stores away algorithms for later invocation; we instead store the + // value that determines the algorithm to be created -- either |undefined|, or + // a callable object that's called with context-specific arguments. + + // Step 7: (If method is undefined,) Return an algorithm which returns a + // promise resolved with undefined (implicit). + if (unwrappedController->writeMethod().isUndefined()) { + return PromiseResolvedWithUndefined(cx); + } + + // Step 6: If method is not undefined, + + // Step 6.a: If ! IsCallable(method) is false, throw a TypeError exception. + MOZ_ASSERT(IsCallable(unwrappedController->writeMethod())); + + // Step 6.c: Otherwise (if algoArgCount is not 0), return an algorithm that + // performs the following steps, taking an arg argument: + // Step 6.c.i: Let fullArgs be a List consisting of arg followed by the + // elements of extraArgs in order. + // Step 6.c.ii: Return ! PromiseCall(method, underlyingObject, fullArgs). + Rooted<Value> writeMethod(cx, unwrappedController->writeMethod()); + if (!cx->compartment()->wrap(cx, &writeMethod)) { + return nullptr; + } + + Rooted<Value> underlyingSink(cx, unwrappedController->underlyingSink()); + if (!cx->compartment()->wrap(cx, &underlyingSink)) { + return nullptr; + } + + Rooted<Value> controller(cx, ObjectValue(*unwrappedController)); + if (!cx->compartment()->wrap(cx, &controller)) { + return nullptr; + } + + return PromiseCall(cx, writeMethod, underlyingSink, chunk, controller); +} + +/** + * Streams spec, 4.8.11 step 7: + * Upon fulfillment of sinkClosePromise, + */ +static MOZ_MUST_USE bool WritableStreamCloseHandler(JSContext* cx, + unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<WritableStream*> unwrappedStream( + cx, TargetFromHandler<WritableStream>(args)); + + // Step 7.a: Perform ! WritableStreamFinishInFlightClose(stream). + if (!WritableStreamFinishInFlightClose(cx, unwrappedStream)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 4.8.11 step 8: + * Upon rejection of sinkClosePromise with reason reason, + */ +static MOZ_MUST_USE bool WritableStreamCloseFailedHandler(JSContext* cx, + unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<WritableStream*> unwrappedStream( + cx, TargetFromHandler<WritableStream>(args)); + + // Step 8.a: Perform + // ! WritableStreamFinishInFlightCloseWithError(stream, reason). + if (!WritableStreamFinishInFlightCloseWithError(cx, unwrappedStream, + args.get(0))) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 4.8.12 step 4: + * Upon fulfillment of sinkWritePromise, + */ +static MOZ_MUST_USE bool WritableStreamWriteHandler(JSContext* cx, + unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<WritableStream*> unwrappedStream( + cx, TargetFromHandler<WritableStream>(args)); + + // Step 4.a: Perform ! WritableStreamFinishInFlightWrite(stream). + if (!WritableStreamFinishInFlightWrite(cx, unwrappedStream)) { + return false; + } + + // Step 4.b: Let state be stream.[[state]]. + // Step 4.c: Assert: state is "writable" or "erroring". + MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); + + // Step 4.d: Perform ! DequeueValue(controller). + DequeueValue(unwrappedStream->controller(), cx); + + // Step 4.e: If ! WritableStreamCloseQueuedOrInFlight(stream) is false and + // state is "writable", + if (!WritableStreamCloseQueuedOrInFlight(unwrappedStream) && + unwrappedStream->writable()) { + // Step 4.e.i: Let backpressure be + // ! WritableStreamDefaultControllerGetBackpressure( + // controller). + bool backpressure = WritableStreamDefaultControllerGetBackpressure( + unwrappedStream->controller()); + + // Step 4.e.ii: Perform + // ! WritableStreamUpdateBackpressure(stream, backpressure). + if (!WritableStreamUpdateBackpressure(cx, unwrappedStream, backpressure)) { + return false; + } + } + + // Step 4.f: Perform + // ! WritableStreamDefaultControllerAdvanceQueueIfNeeded( + // controller). + Rooted<WritableStreamDefaultController*> unwrappedController( + cx, unwrappedStream->controller()); + if (!WritableStreamDefaultControllerAdvanceQueueIfNeeded( + cx, unwrappedController)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 4.8.12 step 5: + * Upon rejection of sinkWritePromise with reason, + */ +static MOZ_MUST_USE bool WritableStreamWriteFailedHandler(JSContext* cx, + unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<WritableStream*> unwrappedStream( + cx, TargetFromHandler<WritableStream>(args)); + + // Step 5.a: If stream.[[state]] is "writable", perform + // ! WritableStreamDefaultControllerClearAlgorithms(controller). + if (unwrappedStream->writable()) { + WritableStreamDefaultControllerClearAlgorithms( + unwrappedStream->controller()); + } + + // Step 5.b: Perform + // ! WritableStreamFinishInFlightWriteWithError(stream, reason). + if (!WritableStreamFinishInFlightWriteWithError(cx, unwrappedStream, + args.get(0))) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 4.8.9 (steps 7-10), + * WritableStreamDefaultControllerAdvanceQueueIfNeeded ( controller ) + * Streams spec, 4.8.11. + * WritableStreamDefaultControllerProcessClose ( controller ) + * Streams spec, 4.8.12. + * WritableStreamDefaultControllerProcessWrite ( controller, chunk ) + */ +bool WritableStreamDefaultControllerProcessIfNeeded( + JSContext* cx, + Handle<WritableStreamDefaultController*> unwrappedController) { + // Step 7: If controller.[[queue]] is empty, return. + ListObject* unwrappedQueue = unwrappedController->queue(); + if (QueueIsEmpty(unwrappedQueue)) { + return true; + } + + // Step 8: Let writeRecord be ! PeekQueueValue(controller). + // Step 9: If writeRecord is "close", perform + // ! WritableStreamDefaultControllerProcessClose(controller). + // Step 10: Otherwise, perform + // ! WritableStreamDefaultControllerProcessWrite( + // controller, writeRecord.[[chunk]]). + Rooted<JSObject*> sinkWriteOrClosePromise(cx); + JSNative onFulfilledFunc, onRejectedFunc; + if (PeekQueueValue(unwrappedQueue).isMagic(JS_WRITABLESTREAM_CLOSE_RECORD)) { + MOZ_ASSERT(unwrappedQueue->length() == 2); + + onFulfilledFunc = WritableStreamCloseHandler; + onRejectedFunc = WritableStreamCloseFailedHandler; + + // 4.8.11 step 1: Let stream be controller.[[controlledWritableStream]]. + // 4.8.11 step 2: Perform ! WritableStreamMarkCloseRequestInFlight(stream). + WritableStreamMarkCloseRequestInFlight(unwrappedController->stream()); + + // 4.8.11 step 3: Perform ! DequeueValue(controller). + DequeueValue(unwrappedController, cx); + + // 4.8.11 step 4: Assert: controller.[[queue]] is empty. + MOZ_ASSERT(unwrappedQueue->isEmpty()); + + // 4.8.11 step 5: Let sinkClosePromise be the result of performing + // controller.[[closeAlgorithm]]. + sinkWriteOrClosePromise = PerformCloseAlgorithm(cx, unwrappedController); + } else { + onFulfilledFunc = WritableStreamWriteHandler; + onRejectedFunc = WritableStreamWriteFailedHandler; + + Rooted<Value> chunk(cx, PeekQueueValue(unwrappedQueue)); + if (!cx->compartment()->wrap(cx, &chunk)) { + return false; + } + + // 4.8.12 step 1: Let stream be controller.[[controlledWritableStream]]. + // 4.8.12 step 2: Perform + // ! WritableStreamMarkFirstWriteRequestInFlight(stream). + WritableStreamMarkFirstWriteRequestInFlight(unwrappedController->stream()); + + // 4.8.12 step 3: Let sinkWritePromise be the result of performing + // controller.[[writeAlgorithm]], passing in chunk. + sinkWriteOrClosePromise = + PerformWriteAlgorithm(cx, unwrappedController, chunk); + } + if (!sinkWriteOrClosePromise) { + return false; + } + + Rooted<JSObject*> stream(cx, unwrappedController->stream()); + if (!cx->compartment()->wrap(cx, &stream)) { + return false; + } + + // Step 7: Upon fulfillment of sinkClosePromise, + // Step 4: Upon fulfillment of sinkWritePromise, + // Step 8: Upon rejection of sinkClosePromise with reason reason, + // Step 5: Upon rejection of sinkWritePromise with reason, + Rooted<JSObject*> onFulfilled(cx, NewHandler(cx, onFulfilledFunc, stream)); + if (!onFulfilled) { + return false; + } + Rooted<JSObject*> onRejected(cx, NewHandler(cx, onRejectedFunc, stream)); + if (!onRejected) { + return false; + } + return JS::AddPromiseReactions(cx, sinkWriteOrClosePromise, onFulfilled, + onRejected); +} + +/** + * Streams spec, 4.8.13. + * WritableStreamDefaultControllerGetBackpressure ( controller ) + */ +bool js::WritableStreamDefaultControllerGetBackpressure( + const WritableStreamDefaultController* unwrappedController) { + return WritableStreamDefaultControllerGetDesiredSize(unwrappedController) <= + 0.0; +} + +/** + * Streams spec, 4.8.14. + * WritableStreamDefaultControllerError ( controller, error ) + */ +bool js::WritableStreamDefaultControllerError( + JSContext* cx, Handle<WritableStreamDefaultController*> unwrappedController, + Handle<Value> error) { + cx->check(error); + + // Step 1: Let stream be controller.[[controlledWritableStream]]. + Rooted<WritableStream*> unwrappedStream(cx, unwrappedController->stream()); + + // Step 2: Assert: stream.[[state]] is "writable". + MOZ_ASSERT(unwrappedStream->writable()); + + // Step 3: Perform + // ! WritableStreamDefaultControllerClearAlgorithms(controller). + WritableStreamDefaultControllerClearAlgorithms(unwrappedController); + + // Step 4: Perform ! WritableStreamStartErroring(stream, error). + return WritableStreamStartErroring(cx, unwrappedStream, error); +} diff --git a/js/src/builtin/streams/WritableStreamDefaultControllerOperations.h b/js/src/builtin/streams/WritableStreamDefaultControllerOperations.h new file mode 100644 index 0000000000..90756b25bc --- /dev/null +++ b/js/src/builtin/streams/WritableStreamDefaultControllerOperations.h @@ -0,0 +1,108 @@ +/* -*- 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/. */ + +/* Writable stream default controller abstract operations. */ + +#ifndef builtin_streams_WritableStreamDefaultControllerOperations_h +#define builtin_streams_WritableStreamDefaultControllerOperations_h + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jstypes.h" // JS_PUBLIC_API +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::Value + +struct JS_PUBLIC_API JSContext; + +namespace js { + +class WritableStream; +class WritableStreamDefaultController; + +extern JSObject* WritableStreamControllerAbortSteps( + JSContext* cx, + JS::Handle<WritableStreamDefaultController*> unwrappedController, + JS::Handle<JS::Value> reason); + +extern MOZ_MUST_USE bool WritableStreamControllerErrorSteps( + JSContext* cx, + JS::Handle<WritableStreamDefaultController*> unwrappedController); + +extern MOZ_MUST_USE bool WritableStreamControllerStartHandler(JSContext* cx, + unsigned argc, + JS::Value* vp); + +extern MOZ_MUST_USE bool WritableStreamControllerStartFailedHandler( + JSContext* cx, unsigned argc, JS::Value* vp); + +/** + * Characterizes the family of algorithms, (startAlgorithm, writeAlgorithm, + * closeAlgorithm, abortAlgorithm), associated with a writable stream. + * + * See the comment on SetUpWritableStreamDefaultController(). + */ +enum class SinkAlgorithms { + Script, + Transform, +}; + +extern MOZ_MUST_USE bool SetUpWritableStreamDefaultController( + JSContext* cx, JS::Handle<WritableStream*> stream, + SinkAlgorithms algorithms, JS::Handle<JS::Value> underlyingSink, + JS::Handle<JS::Value> writeMethod, JS::Handle<JS::Value> closeMethod, + JS::Handle<JS::Value> abortMethod, double highWaterMark, + JS::Handle<JS::Value> size); + +extern MOZ_MUST_USE bool SetUpWritableStreamDefaultControllerFromUnderlyingSink( + JSContext* cx, JS::Handle<WritableStream*> stream, + JS::Handle<JS::Value> underlyingSink, double highWaterMark, + JS::Handle<JS::Value> sizeAlgorithm); + +extern void WritableStreamDefaultControllerClearAlgorithms( + WritableStreamDefaultController* unwrappedController); + +extern MOZ_MUST_USE bool WritableStreamDefaultControllerClose( + JSContext* cx, + JS::Handle<WritableStreamDefaultController*> unwrappedController); + +extern MOZ_MUST_USE bool WritableStreamDefaultControllerGetChunkSize( + JSContext* cx, + JS::Handle<WritableStreamDefaultController*> unwrappedController, + JS::Handle<JS::Value> chunk, JS::MutableHandle<JS::Value> returnValue); + +extern double WritableStreamDefaultControllerGetDesiredSize( + const WritableStreamDefaultController* controller); + +extern MOZ_MUST_USE bool WritableStreamDefaultControllerWrite( + JSContext* cx, + JS::Handle<WritableStreamDefaultController*> unwrappedController, + JS::Handle<JS::Value> chunk, JS::Handle<JS::Value> chunkSize); + +extern MOZ_MUST_USE bool WritableStreamDefaultControllerErrorIfNeeded( + JSContext* cx, + JS::Handle<WritableStreamDefaultController*> unwrappedController, + JS::Handle<JS::Value> error); + +extern MOZ_MUST_USE bool WritableStreamDefaultControllerProcessClose( + JSContext* cx, + JS::Handle<WritableStreamDefaultController*> unwrappedController); + +extern MOZ_MUST_USE bool WritableStreamDefaultControllerProcessWrite( + JSContext* cx, + JS::Handle<WritableStreamDefaultController*> unwrappedController, + JS::Handle<JS::Value> chunk); + +extern bool WritableStreamDefaultControllerGetBackpressure( + const WritableStreamDefaultController* unwrappedController); + +extern MOZ_MUST_USE bool WritableStreamDefaultControllerError( + JSContext* cx, + JS::Handle<WritableStreamDefaultController*> unwrappedController, + JS::Handle<JS::Value> error); + +} // namespace js + +#endif // builtin_streams_WritableStreamDefaultControllerOperations_h diff --git a/js/src/builtin/streams/WritableStreamDefaultWriter-inl.h b/js/src/builtin/streams/WritableStreamDefaultWriter-inl.h new file mode 100644 index 0000000000..fe85f35bf8 --- /dev/null +++ b/js/src/builtin/streams/WritableStreamDefaultWriter-inl.h @@ -0,0 +1,40 @@ +/* -*- 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/. */ + +/* Class WritableStreamDefaultWriter. */ + +#ifndef builtin_streams_WritableStreamDefaultWriter_inl_h +#define builtin_streams_WritableStreamDefaultWriter_inl_h + +#include "builtin/streams/WritableStreamDefaultWriter.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "builtin/streams/WritableStream.h" // js::WritableStream +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::ObjectValue +#include "vm/NativeObject.h" // js::NativeObject + +#include "vm/Compartment-inl.h" // js::UnwrapInternalSlot + +struct JS_PUBLIC_API JSContext; + +namespace js { + +/** + * Returns the stream associated with the given reader. + */ +inline MOZ_MUST_USE WritableStream* UnwrapStreamFromWriter( + JSContext* cx, JS::Handle<WritableStreamDefaultWriter*> unwrappedWriter) { + MOZ_ASSERT(unwrappedWriter->hasStream()); + return UnwrapInternalSlot<WritableStream>( + cx, unwrappedWriter, WritableStreamDefaultWriter::Slot_Stream); +} + +} // namespace js + +#endif // builtin_streams_WritableStreamDefaultWriter_inl_h diff --git a/js/src/builtin/streams/WritableStreamDefaultWriter.cpp b/js/src/builtin/streams/WritableStreamDefaultWriter.cpp new file mode 100644 index 0000000000..c45948c64d --- /dev/null +++ b/js/src/builtin/streams/WritableStreamDefaultWriter.cpp @@ -0,0 +1,529 @@ +/* -*- 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/. */ + +/* Class WritableStreamDefaultWriter. */ + +#include "builtin/streams/WritableStreamDefaultWriter-inl.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jsapi.h" // JS_ReportErrorASCII, JS_ReportErrorNumberASCII + +#include "builtin/streams/ClassSpecMacro.h" // JS_STREAMS_CLASS_SPEC +#include "builtin/streams/MiscellaneousOperations.h" // js::ReturnPromiseRejectedWithPendingError +#include "builtin/streams/WritableStream.h" // js::WritableStream +#include "builtin/streams/WritableStreamOperations.h" // js::WritableStreamCloseQueuedOrInFlight +#include "builtin/streams/WritableStreamWriterOperations.h" // js::WritableStreamDefaultWriter{Abort,GetDesiredSize,Release,Write} +#include "js/CallArgs.h" // JS::CallArgs{,FromVp} +#include "js/Class.h" // js::ClassSpec, JS_NULL_CLASS_OPS +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" // JS{Function,Property}Spec, JS_{FS,PS}_END, JS_{FN,PSG} +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::Value +#include "vm/Compartment.h" // JS::Compartment +#include "vm/JSContext.h" // JSContext +#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseResolvedWithUndefined + +#include "builtin/Promise-inl.h" // js::SetSettledPromiseIsHandled +#include "vm/Compartment-inl.h" // JS::Compartment::wrap, js::UnwrapAndTypeCheck{Argument,This} +#include "vm/JSObject-inl.h" // js::NewObjectWithClassProto +#include "vm/NativeObject-inl.h" // js::ThrowIfNotConstructing +#include "vm/Realm-inl.h" // js::AutoRealm + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::Rooted; +using JS::Value; + +using js::ClassSpec; +using js::GetErrorMessage; +using js::PromiseObject; +using js::ReturnPromiseRejectedWithPendingError; +using js::UnwrapAndTypeCheckArgument; +using js::UnwrapAndTypeCheckThis; +using js::WritableStream; +using js::WritableStreamCloseQueuedOrInFlight; +using js::WritableStreamDefaultWriter; +using js::WritableStreamDefaultWriterGetDesiredSize; +using js::WritableStreamDefaultWriterRelease; +using js::WritableStreamDefaultWriterWrite; + +/*** 4.5. Class WritableStreamDefaultWriter *********************************/ + +/** + * Stream spec, 4.5.3. new WritableStreamDefaultWriter(stream) + * Steps 3-9. + */ +MOZ_MUST_USE WritableStreamDefaultWriter* js::CreateWritableStreamDefaultWriter( + JSContext* cx, Handle<WritableStream*> unwrappedStream, + Handle<JSObject*> proto /* = nullptr */) { + Rooted<WritableStreamDefaultWriter*> writer( + cx, NewObjectWithClassProto<WritableStreamDefaultWriter>(cx, proto)); + if (!writer) { + return nullptr; + } + + // Step 3: Set this.[[ownerWritableStream]] to stream. + { + Rooted<JSObject*> stream(cx, unwrappedStream); + if (!cx->compartment()->wrap(cx, &stream)) { + return nullptr; + } + writer->setStream(stream); + } + + // Step 4 is moved to the end. + + // Step 5: Let state be stream.[[state]]. + // Step 6: If state is "writable", + if (unwrappedStream->writable()) { + // Step 6.a: If ! WritableStreamCloseQueuedOrInFlight(stream) is false and + // stream.[[backpressure]] is true, set this.[[readyPromise]] to a + // new promise. + PromiseObject* promise; + if (!WritableStreamCloseQueuedOrInFlight(unwrappedStream) && + unwrappedStream->backpressure()) { + promise = PromiseObject::createSkippingExecutor(cx); + } + // Step 6.b: Otherwise, set this.[[readyPromise]] to a promise resolved with + // undefined. + else { + promise = PromiseResolvedWithUndefined(cx); + } + if (!promise) { + return nullptr; + } + writer->setReadyPromise(promise); + + // Step 6.c: Set this.[[closedPromise]] to a new promise. + promise = PromiseObject::createSkippingExecutor(cx); + if (!promise) { + return nullptr; + } + + writer->setClosedPromise(promise); + } + // Step 8: Otherwise, if state is "closed", + else if (unwrappedStream->closed()) { + // Step 8.a: Set this.[[readyPromise]] to a promise resolved with undefined. + PromiseObject* readyPromise = PromiseResolvedWithUndefined(cx); + if (!readyPromise) { + return nullptr; + } + + writer->setReadyPromise(readyPromise); + + // Step 8.b: Set this.[[closedPromise]] to a promise resolved with + // undefined. + PromiseObject* closedPromise = PromiseResolvedWithUndefined(cx); + if (!closedPromise) { + return nullptr; + } + + writer->setClosedPromise(closedPromise); + } else { + // Wrap stream.[[StoredError]] just once for either step 7 or step 9. + Rooted<Value> storedError(cx, unwrappedStream->storedError()); + if (!cx->compartment()->wrap(cx, &storedError)) { + return nullptr; + } + + // Step 7: Otherwise, if state is "erroring", + if (unwrappedStream->erroring()) { + // Step 7.a: Set this.[[readyPromise]] to a promise rejected with + // stream.[[storedError]]. + Rooted<JSObject*> promise( + cx, PromiseObject::unforgeableReject(cx, storedError)); + if (!promise) { + return nullptr; + } + + writer->setReadyPromise(promise); + + // Step 7.b: Set this.[[readyPromise]].[[PromiseIsHandled]] to true. + js::SetSettledPromiseIsHandled(cx, promise.as<PromiseObject>()); + + // Step 7.c: Set this.[[closedPromise]] to a new promise. + JSObject* closedPromise = PromiseObject::createSkippingExecutor(cx); + if (!closedPromise) { + return nullptr; + } + + writer->setClosedPromise(closedPromise); + } + // Step 9: Otherwise, + else { + // Step 9.a: Assert: state is "errored". + MOZ_ASSERT(unwrappedStream->errored()); + + Rooted<JSObject*> promise(cx); + + // Step 9.b: Let storedError be stream.[[storedError]]. + // Step 9.c: Set this.[[readyPromise]] to a promise rejected with + // storedError. + promise = PromiseObject::unforgeableReject(cx, storedError); + if (!promise) { + return nullptr; + } + + writer->setReadyPromise(promise); + + // Step 9.d: Set this.[[readyPromise]].[[PromiseIsHandled]] to true. + js::SetSettledPromiseIsHandled(cx, promise.as<PromiseObject>()); + + // Step 9.e: Set this.[[closedPromise]] to a promise rejected with + // storedError. + promise = PromiseObject::unforgeableReject(cx, storedError); + if (!promise) { + return nullptr; + } + + writer->setClosedPromise(promise); + + // Step 9.f: Set this.[[closedPromise]].[[PromiseIsHandled]] to true. + js::SetSettledPromiseIsHandled(cx, promise.as<PromiseObject>()); + } + } + + // Step 4 (reordered): Set stream.[[writer]] to this. + // Doing this last prevents a partially-initialized writer from being attached + // to the stream (and possibly left there on OOM). + { + AutoRealm ar(cx, unwrappedStream); + Rooted<JSObject*> wrappedWriter(cx, writer); + if (!cx->compartment()->wrap(cx, &wrappedWriter)) { + return nullptr; + } + unwrappedStream->setWriter(wrappedWriter); + } + + return writer; +} + +/** + * Streams spec, 4.5.3. + * new WritableStreamDefaultWriter(stream) + */ +bool WritableStreamDefaultWriter::constructor(JSContext* cx, unsigned argc, + Value* vp) { + MOZ_ASSERT(cx->realm()->creationOptions().getWritableStreamsEnabled(), + "WritableStream should be enabled in this realm if we reach here"); + + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "WritableStreamDefaultWriter")) { + return false; + } + + // Step 1: If ! IsWritableStream(stream) is false, throw a TypeError + // exception. + Rooted<WritableStream*> unwrappedStream( + cx, UnwrapAndTypeCheckArgument<WritableStream>( + cx, args, "WritableStreamDefaultWriter constructor", 0)); + if (!unwrappedStream) { + return false; + } + + // Step 2: If ! IsWritableStreamLocked(stream) is true, throw a TypeError + // exception. + if (unwrappedStream->isLocked()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAM_ALREADY_LOCKED); + return false; + } + + // Implicit in the spec: Find the prototype object to use. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Null, &proto)) { + return false; + } + + // Steps 3-9. + Rooted<WritableStreamDefaultWriter*> writer( + cx, CreateWritableStreamDefaultWriter(cx, unwrappedStream, proto)); + if (!writer) { + return false; + } + + args.rval().setObject(*writer); + return true; +} + +/** + * Streams spec, 4.5.4.1. get closed + */ +static MOZ_MUST_USE bool WritableStreamDefaultWriter_closed(JSContext* cx, + unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise + // rejected with a TypeError exception. + Rooted<WritableStreamDefaultWriter*> unwrappedWriter( + cx, UnwrapAndTypeCheckThis<WritableStreamDefaultWriter>(cx, args, + "get closed")); + if (!unwrappedWriter) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: Return this.[[closedPromise]]. + Rooted<JSObject*> closedPromise(cx, unwrappedWriter->closedPromise()); + if (!cx->compartment()->wrap(cx, &closedPromise)) { + return false; + } + + args.rval().setObject(*closedPromise); + return true; +} + +/** + * Streams spec, 4.5.4.2. get desiredSize + */ +static MOZ_MUST_USE bool WritableStreamDefaultWriter_desiredSize(JSContext* cx, + unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, throw a + // TypeError exception. + Rooted<WritableStreamDefaultWriter*> unwrappedWriter( + cx, UnwrapAndTypeCheckThis<WritableStreamDefaultWriter>( + cx, args, "get desiredSize")); + if (!unwrappedWriter) { + return false; + } + + // Step 2: If this.[[ownerWritableStream]] is undefined, throw a TypeError + // exception. + if (!unwrappedWriter->hasStream()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAMWRITER_NOT_OWNED, + "get desiredSize"); + return false; + } + + // Step 3: Return ! WritableStreamDefaultWriterGetDesiredSize(this). + if (!WritableStreamDefaultWriterGetDesiredSize(cx, unwrappedWriter, + args.rval())) { + return false; + } + + MOZ_ASSERT(args.rval().isNull() || args.rval().isNumber(), + "expected a type that'll never require wrapping"); + return true; +} + +/** + * Streams spec, 4.5.4.3. get ready + */ +static MOZ_MUST_USE bool WritableStreamDefaultWriter_ready(JSContext* cx, + unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise + // rejected with a TypeError exception. + Rooted<WritableStreamDefaultWriter*> unwrappedWriter( + cx, UnwrapAndTypeCheckThis<WritableStreamDefaultWriter>(cx, args, + "get ready")); + if (!unwrappedWriter) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: Return this.[[readyPromise]]. + Rooted<JSObject*> readyPromise(cx, unwrappedWriter->readyPromise()); + if (!cx->compartment()->wrap(cx, &readyPromise)) { + return false; + } + + args.rval().setObject(*readyPromise); + return true; +} + +/** + * Streams spec, 4.5.4.4. abort(reason) + */ +static MOZ_MUST_USE bool WritableStreamDefaultWriter_abort(JSContext* cx, + unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise + // rejected with a TypeError exception. + Rooted<WritableStreamDefaultWriter*> unwrappedWriter( + cx, + UnwrapAndTypeCheckThis<WritableStreamDefaultWriter>(cx, args, "abort")); + if (!unwrappedWriter) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: If this.[[ownerWritableStream]] is undefined, return a promise + // rejected with a TypeError exception. + if (!unwrappedWriter->hasStream()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAMWRITER_NOT_OWNED, "abort"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: Return ! WritableStreamDefaultWriterAbort(this, reason). + JSObject* promise = + WritableStreamDefaultWriterAbort(cx, unwrappedWriter, args.get(0)); + if (!promise) { + return false; + } + cx->check(promise); + + args.rval().setObject(*promise); + return true; +} + +/** + * Streams spec, 4.5.4.5. close() + */ +static MOZ_MUST_USE bool WritableStreamDefaultWriter_close(JSContext* cx, + unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise + // rejected with a TypeError exception. + Rooted<WritableStreamDefaultWriter*> unwrappedWriter( + cx, + UnwrapAndTypeCheckThis<WritableStreamDefaultWriter>(cx, args, "close")); + if (!unwrappedWriter) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: Let stream be this.[[ownerWritableStream]]. + // Step 3: If stream is undefined, return a promise rejected with a TypeError + // exception. + if (!unwrappedWriter->hasStream()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAMWRITER_NOT_OWNED, "write"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + WritableStream* unwrappedStream = UnwrapStreamFromWriter(cx, unwrappedWriter); + if (!unwrappedStream) { + return false; + } + + // Step 4: If ! WritableStreamCloseQueuedOrInFlight(stream) is true, return a + // promise rejected with a TypeError exception. + if (WritableStreamCloseQueuedOrInFlight(unwrappedStream)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAM_CLOSE_CLOSING_OR_CLOSED); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 5: Return ! WritableStreamDefaultWriterClose(this). + JSObject* promise = WritableStreamDefaultWriterClose(cx, unwrappedWriter); + if (!promise) { + return false; + } + cx->check(promise); + + args.rval().setObject(*promise); + return true; +} + +/** + * Streams spec, 4.5.4.6. releaseLock() + */ +static MOZ_MUST_USE bool WritableStreamDefaultWriter_releaseLock(JSContext* cx, + unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise + // rejected with a TypeError exception. + Rooted<WritableStreamDefaultWriter*> unwrappedWriter( + cx, + UnwrapAndTypeCheckThis<WritableStreamDefaultWriter>(cx, args, "close")); + if (!unwrappedWriter) { + return false; + } + + // Step 2: Let stream be this.[[ownerWritableStream]]. + // Step 3: If stream is undefined, return. + if (!unwrappedWriter->hasStream()) { + args.rval().setUndefined(); + return true; + } + + // Step 4: Assert: stream.[[writer]] is not undefined. +#ifdef DEBUG + { + WritableStream* unwrappedStream = + UnwrapStreamFromWriter(cx, unwrappedWriter); + if (!unwrappedStream) { + return false; + } + MOZ_ASSERT(unwrappedStream->hasWriter()); + } +#endif + + // Step 5: Perform ! WritableStreamDefaultWriterRelease(this). + if (!WritableStreamDefaultWriterRelease(cx, unwrappedWriter)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/** + * Streams spec, 4.5.4.7. write(chunk) + */ +static MOZ_MUST_USE bool WritableStreamDefaultWriter_write(JSContext* cx, + unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise + // rejected with a TypeError exception. + Rooted<WritableStreamDefaultWriter*> unwrappedWriter( + cx, + UnwrapAndTypeCheckThis<WritableStreamDefaultWriter>(cx, args, "write")); + if (!unwrappedWriter) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 2: If this.[[ownerWritableStream]] is undefined, return a promise + // rejected with a TypeError exception. + if (!unwrappedWriter->hasStream()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAMWRITER_NOT_OWNED, "write"); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Step 3: Return this.[[readyPromise]]. + PromiseObject* promise = + WritableStreamDefaultWriterWrite(cx, unwrappedWriter, args.get(0)); + if (!promise) { + return false; + } + cx->check(promise); + + args.rval().setObject(*promise); + return true; +} + +static const JSPropertySpec WritableStreamDefaultWriter_properties[] = { + JS_PSG("closed", WritableStreamDefaultWriter_closed, 0), + JS_PSG("desiredSize", WritableStreamDefaultWriter_desiredSize, 0), + JS_PSG("ready", WritableStreamDefaultWriter_ready, 0), JS_PS_END}; + +static const JSFunctionSpec WritableStreamDefaultWriter_methods[] = { + JS_FN("abort", WritableStreamDefaultWriter_abort, 1, 0), + JS_FN("close", WritableStreamDefaultWriter_close, 0, 0), + JS_FN("releaseLock", WritableStreamDefaultWriter_releaseLock, 0, 0), + JS_FN("write", WritableStreamDefaultWriter_write, 1, 0), JS_FS_END}; + +JS_STREAMS_CLASS_SPEC(WritableStreamDefaultWriter, 1, SlotCount, + ClassSpec::DontDefineConstructor, 0, JS_NULL_CLASS_OPS); diff --git a/js/src/builtin/streams/WritableStreamDefaultWriter.h b/js/src/builtin/streams/WritableStreamDefaultWriter.h new file mode 100644 index 0000000000..e818cec57b --- /dev/null +++ b/js/src/builtin/streams/WritableStreamDefaultWriter.h @@ -0,0 +1,113 @@ +/* -*- 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/. */ + +/* Class WritableStreamDefaultWriter. */ + +#ifndef builtin_streams_WritableStreamDefaultWriter_h +#define builtin_streams_WritableStreamDefaultWriter_h + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jstypes.h" // JS_PUBLIC_API +#include "js/Class.h" // JSClass, js::ClassSpec +#include "js/Value.h" // JS::{,Object,Undefined}Value +#include "vm/NativeObject.h" // js::NativeObject + +struct JS_PUBLIC_API JSContext; +class JS_PUBLIC_API JSObject; + +namespace js { + +class PromiseObject; +class WritableStream; + +class WritableStreamDefaultWriter : public NativeObject { + public: + /** + * Memory layout of Stream Writer instances. + * + * See https://streams.spec.whatwg.org/#default-writer-internal-slots for + * details. + */ + enum Slots { + /** + * A promise that is resolved when the stream this writes to becomes closed. + * + * This promise is ordinarily created while this writer is being created; in + * this case this promise is not a wrapper and is same-compartment with + * this. However, if the writer is closed and then this writer releases its + * lock on the stream, this promise will be recreated within whatever realm + * is in force when the lock is released: + * + * var ws = new WritableStream({}); + * var w = ws.getWriter(); + * var c = w.closed; + * w.close().then(() => { + * w.releaseLock(); // changes this slot, and |w.closed| + * assertEq(c === w.closed, false); + * }); + * + * So this field *may* potentially contain a wrapper around a promise. + */ + Slot_ClosedPromise, + + /** + * The stream that this writer writes to. Because writers are created under + * |WritableStream.prototype.getWriter| which may not be same-compartment + * with the stream, this is potentially a wrapper. + */ + Slot_Stream, + + /** + * The promise returned by the |writer.ready| getter property, a promise + * signaling that the related stream is accepting writes. + * + * This value repeatedly changes as the related stream changes back and + * forth between being writable and temporarily filled (or, ultimately, + * errored or aborted). These changes are invoked by a number of user- + * visible functions, so this may be a wrapper around a promise in another + * realm. + */ + Slot_ReadyPromise, + + SlotCount, + }; + + JSObject* closedPromise() const { + return &getFixedSlot(Slot_ClosedPromise).toObject(); + } + void setClosedPromise(JSObject* wrappedPromise) { + setFixedSlot(Slot_ClosedPromise, JS::ObjectValue(*wrappedPromise)); + } + + bool hasStream() const { return !getFixedSlot(Slot_Stream).isUndefined(); } + void setStream(JSObject* stream) { + setFixedSlot(Slot_Stream, JS::ObjectValue(*stream)); + } + void clearStream() { setFixedSlot(Slot_Stream, JS::UndefinedValue()); } + + JSObject* readyPromise() const { + return &getFixedSlot(Slot_ReadyPromise).toObject(); + } + void setReadyPromise(JSObject* wrappedPromise) { + setFixedSlot(Slot_ReadyPromise, JS::ObjectValue(*wrappedPromise)); + } + + static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp); + static const ClassSpec classSpec_; + static const JSClass class_; + static const ClassSpec protoClassSpec_; + static const JSClass protoClass_; +}; + +extern MOZ_MUST_USE WritableStreamDefaultWriter* +CreateWritableStreamDefaultWriter(JSContext* cx, + JS::Handle<WritableStream*> unwrappedStream, + JS::Handle<JSObject*> proto = nullptr); + +} // namespace js + +#endif // builtin_streams_WritableStreamDefaultWriter_h diff --git a/js/src/builtin/streams/WritableStreamOperations.cpp b/js/src/builtin/streams/WritableStreamOperations.cpp new file mode 100644 index 0000000000..bc11bf4b39 --- /dev/null +++ b/js/src/builtin/streams/WritableStreamOperations.cpp @@ -0,0 +1,923 @@ +/* -*- 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/. */ + +/* Writable stream abstract operations. */ + +#include "builtin/streams/WritableStreamOperations.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include <stdint.h> // uint32_t + +#include "jsapi.h" // JS_ReportErrorASCII + +#include "builtin/streams/MiscellaneousOperations.h" // js::PromiseRejectedWithPendingError +#include "builtin/streams/WritableStream.h" // js::WritableStream +#include "builtin/streams/WritableStreamDefaultController.h" // js::WritableStreamDefaultController{,Close}, js::WritableStream::controller +#include "builtin/streams/WritableStreamDefaultControllerOperations.h" // js::WritableStreamControllerErrorSteps +#include "builtin/streams/WritableStreamWriterOperations.h" // js::WritableStreamDefaultWriterEnsureReadyPromiseRejected +#include "js/CallArgs.h" // JS::CallArgs{,FromVp} +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/Promise.h" // JS::{Reject,Resolve}Promise +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted +#include "js/Value.h" // JS::Value, JS::ObjecValue, JS::UndefinedHandleValue +#include "vm/Compartment.h" // JS::Compartment +#include "vm/JSContext.h" // JSContext +#include "vm/List.h" // js::ListObject +#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseResolvedWithUndefined + +#include "builtin/HandlerFunction-inl.h" // js::NewHandler, js::TargetFromHandler +#include "builtin/Promise-inl.h" // js::SetSettledPromiseIsHandled +#include "builtin/streams/MiscellaneousOperations-inl.h" // js::ResolveUnwrappedPromiseWithUndefined, js::RejectUnwrappedPromiseWithError +#include "builtin/streams/WritableStream-inl.h" // js::UnwrapWriterFromStream +#include "builtin/streams/WritableStreamDefaultWriter-inl.h" // js::WritableStreamDefaultWriter::closedPromise +#include "vm/Compartment-inl.h" // JS::Compartment::wrap, js::UnwrapAndDowncastObject +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/JSObject-inl.h" // js::NewObjectWithClassProto +#include "vm/List-inl.h" // js::{AppendTo,StoreNew}ListInFixedSlot +#include "vm/Realm-inl.h" // js::AutoRealm + +using js::ExtraFromHandler; +using js::PromiseObject; +using js::TargetFromHandler; +using js::UnwrapAndDowncastObject; +using js::WritableStream; +using js::WritableStreamDefaultController; +using js::WritableStreamRejectCloseAndClosedPromiseIfNeeded; + +using JS::CallArgs; +using JS::CallArgsFromVp; +using JS::Handle; +using JS::ObjectValue; +using JS::RejectPromise; +using JS::ResolvePromise; +using JS::Rooted; +using JS::UndefinedHandleValue; +using JS::Value; + +/*** 4.3. General writable stream abstract operations. **********************/ + +/** + * Streams spec, 4.3.4. InitializeWritableStream ( stream ) + */ +/* static */ MOZ_MUST_USE +WritableStream* WritableStream::create( + JSContext* cx, void* nsISupportsObject_alreadyAddreffed /* = nullptr */, + Handle<JSObject*> proto /* = nullptr */) { + cx->check(proto); + + // In the spec, InitializeWritableStream is always passed a newly created + // WritableStream object. We instead create it here and return it below. + Rooted<WritableStream*> stream( + cx, NewObjectWithClassProto<WritableStream>(cx, proto)); + if (!stream) { + return nullptr; + } + + stream->setPrivate(nsISupportsObject_alreadyAddreffed); + + stream->initWritableState(); + + // Step 1: Set stream.[[state]] to "writable". + MOZ_ASSERT(stream->writable()); + + // Step 2: Set stream.[[storedError]], stream.[[writer]], + // stream.[[writableStreamController]], + // stream.[[inFlightWriteRequest]], stream.[[closeRequest]], + // stream.[[inFlightCloseRequest]] and stream.[[pendingAbortRequest]] + // to undefined. + MOZ_ASSERT(stream->storedError().isUndefined()); + MOZ_ASSERT(!stream->hasWriter()); + MOZ_ASSERT(!stream->hasController()); + MOZ_ASSERT(!stream->haveInFlightWriteRequest()); + MOZ_ASSERT(stream->inFlightWriteRequest().isUndefined()); + MOZ_ASSERT(stream->closeRequest().isUndefined()); + MOZ_ASSERT(stream->inFlightCloseRequest().isUndefined()); + MOZ_ASSERT(!stream->hasPendingAbortRequest()); + + // Step 3: Set stream.[[writeRequests]] to a new empty List. + if (!StoreNewListInFixedSlot(cx, stream, + WritableStream::Slot_WriteRequests)) { + return nullptr; + } + + // Step 4: Set stream.[[backpressure]] to false. + MOZ_ASSERT(!stream->backpressure()); + + return stream; +} + +void WritableStream::clearInFlightWriteRequest(JSContext* cx) { + MOZ_ASSERT(stateIsInitialized()); + MOZ_ASSERT(haveInFlightWriteRequest()); + + writeRequests()->popFirst(cx); + setFlag(HaveInFlightWriteRequest, false); + + MOZ_ASSERT(!haveInFlightWriteRequest()); + MOZ_ASSERT(inFlightWriteRequest().isUndefined()); +} + +/** + * Streams spec, 4.3.6. + * WritableStreamAbort ( stream, reason ) + * + * Note: The object (a promise) returned by this function is in the current + * compartment and does not require special wrapping to be put to use. + */ +JSObject* js::WritableStreamAbort(JSContext* cx, + Handle<WritableStream*> unwrappedStream, + Handle<Value> reason) { + cx->check(reason); + + // Step 1: Let state be stream.[[state]]. + // Step 2: If state is "closed" or "errored", return a promise resolved with + // undefined. + if (unwrappedStream->closed() || unwrappedStream->errored()) { + return PromiseResolvedWithUndefined(cx); + } + + // Step 3: If stream.[[pendingAbortRequest]] is not undefined, return + // stream.[[pendingAbortRequest]].[[promise]]. + if (unwrappedStream->hasPendingAbortRequest()) { + Rooted<JSObject*> pendingPromise( + cx, unwrappedStream->pendingAbortRequestPromise()); + if (!cx->compartment()->wrap(cx, &pendingPromise)) { + return nullptr; + } + return pendingPromise; + } + + // Step 4: Assert: state is "writable" or "erroring". + MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); + + // Step 7: Let promise be a new promise (reordered). + Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) { + return nullptr; + } + + // Step 5: Let wasAlreadyErroring be false. + // Step 6: If state is "erroring", + // Step 6.a: Set wasAlreadyErroring to true. + // Step 6.b: Set reason to undefined. + bool wasAlreadyErroring = unwrappedStream->erroring(); + Handle<Value> pendingReason = + wasAlreadyErroring ? UndefinedHandleValue : reason; + + // Step 8: Set stream.[[pendingAbortRequest]] to + // Record {[[promise]]: promise, [[reason]]: reason, + // [[wasAlreadyErroring]]: wasAlreadyErroring}. + { + AutoRealm ar(cx, unwrappedStream); + + Rooted<JSObject*> wrappedPromise(cx, promise); + Rooted<Value> wrappedPendingReason(cx, pendingReason); + + JS::Compartment* comp = cx->compartment(); + if (!comp->wrap(cx, &wrappedPromise) || + !comp->wrap(cx, &wrappedPendingReason)) { + return nullptr; + } + + unwrappedStream->setPendingAbortRequest( + wrappedPromise, wrappedPendingReason, wasAlreadyErroring); + } + + // Step 9: If wasAlreadyErroring is false, perform + // ! WritableStreamStartErroring(stream, reason). + if (!wasAlreadyErroring) { + if (!WritableStreamStartErroring(cx, unwrappedStream, pendingReason)) { + return nullptr; + } + } + + // Step 10: Return promise. + return promise; +} + +/** + * Streams spec, 4.3.7. + * WritableStreamClose ( stream ) + * + * Note: The object (a promise) returned by this function is in the current + * compartment and does not require special wrapping to be put to use. + */ +JSObject* js::WritableStreamClose(JSContext* cx, + Handle<WritableStream*> unwrappedStream) { + // Step 1: Let state be stream.[[state]]. + // Step 2: If state is "closed" or "errored", return a promise rejected with a + // TypeError exception. + if (unwrappedStream->closed() || unwrappedStream->errored()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAM_CLOSED_OR_ERRORED); + return PromiseRejectedWithPendingError(cx); + } + + // Step 3: Assert: state is "writable" or "erroring". + MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); + + // Step 4: Assert: ! WritableStreamCloseQueuedOrInFlight(stream) is false. + MOZ_ASSERT(!WritableStreamCloseQueuedOrInFlight(unwrappedStream)); + + // Step 5: Let promise be a new promise. + Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) { + return nullptr; + } + + // Step 6: Set stream.[[closeRequest]] to promise. + { + AutoRealm ar(cx, unwrappedStream); + Rooted<JSObject*> wrappedPromise(cx, promise); + if (!cx->compartment()->wrap(cx, &wrappedPromise)) { + return nullptr; + } + + unwrappedStream->setCloseRequest(promise); + } + + // Step 7: Let writer be stream.[[writer]]. + // Step 8: If writer is not undefined, and stream.[[backpressure]] is true, + // and state is "writable", resolve writer.[[readyPromise]] with + // undefined. + if (unwrappedStream->hasWriter() && unwrappedStream->backpressure() && + unwrappedStream->writable()) { + Rooted<WritableStreamDefaultWriter*> unwrappedWriter( + cx, UnwrapWriterFromStream(cx, unwrappedStream)); + if (!unwrappedWriter) { + return nullptr; + } + + if (!ResolveUnwrappedPromiseWithUndefined( + cx, unwrappedWriter->readyPromise())) { + return nullptr; + } + } + + // Step 9: Perform + // ! WritableStreamDefaultControllerClose( + // stream.[[writableStreamController]]). + Rooted<WritableStreamDefaultController*> unwrappedController( + cx, unwrappedStream->controller()); + if (!WritableStreamDefaultControllerClose(cx, unwrappedController)) { + return nullptr; + } + + // Step 10: Return promise. + return promise; +} + +/*** 4.4. Writable stream abstract operations used by controllers ***********/ + +/** + * Streams spec, 4.4.1. + * WritableStreamAddWriteRequest ( stream ) + */ +MOZ_MUST_USE PromiseObject* js::WritableStreamAddWriteRequest( + JSContext* cx, Handle<WritableStream*> unwrappedStream) { + // Step 1: Assert: ! IsWritableStreamLocked(stream) is true. + MOZ_ASSERT(unwrappedStream->isLocked()); + + // Step 2: Assert: stream.[[state]] is "writable". + MOZ_ASSERT(unwrappedStream->writable()); + + // Step 3: Let promise be a new promise. + Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) { + return nullptr; + } + + // Step 4: Append promise as the last element of stream.[[writeRequests]]. + if (!AppendToListInFixedSlot(cx, unwrappedStream, + WritableStream::Slot_WriteRequests, promise)) { + return nullptr; + } + + // Step 5: Return promise. + return promise; +} + +/** + * Streams spec, 4.4.2. + * WritableStreamDealWithRejection ( stream, error ) + */ +MOZ_MUST_USE bool js::WritableStreamDealWithRejection( + JSContext* cx, Handle<WritableStream*> unwrappedStream, + Handle<Value> error) { + cx->check(error); + + // Step 1: Let state be stream.[[state]]. + // Step 2: If state is "writable", + if (unwrappedStream->writable()) { + // Step 2a: Perform ! WritableStreamStartErroring(stream, error). + // Step 2b: Return. + return WritableStreamStartErroring(cx, unwrappedStream, error); + } + + // Step 3: Assert: state is "erroring". + MOZ_ASSERT(unwrappedStream->erroring()); + + // Step 4: Perform ! WritableStreamFinishErroring(stream). + return WritableStreamFinishErroring(cx, unwrappedStream); +} + +static bool WritableStreamHasOperationMarkedInFlight( + const WritableStream* unwrappedStream); + +/** + * Streams spec, 4.4.3. + * WritableStreamStartErroring ( stream, reason ) + */ +MOZ_MUST_USE bool js::WritableStreamStartErroring( + JSContext* cx, Handle<WritableStream*> unwrappedStream, + Handle<Value> reason) { + cx->check(reason); + + // Step 1: Assert: stream.[[storedError]] is undefined. + MOZ_ASSERT(unwrappedStream->storedError().isUndefined()); + + // Step 2: Assert: stream.[[state]] is "writable". + MOZ_ASSERT(unwrappedStream->writable()); + + // Step 3: Let controller be stream.[[writableStreamController]]. + // Step 4: Assert: controller is not undefined. + MOZ_ASSERT(unwrappedStream->hasController()); + Rooted<WritableStreamDefaultController*> unwrappedController( + cx, unwrappedStream->controller()); + + // Step 5: Set stream.[[state]] to "erroring". + unwrappedStream->setErroring(); + + // Step 6: Set stream.[[storedError]] to reason. + { + AutoRealm ar(cx, unwrappedStream); + Rooted<Value> wrappedReason(cx, reason); + if (!cx->compartment()->wrap(cx, &wrappedReason)) { + return false; + } + unwrappedStream->setStoredError(wrappedReason); + } + + // Step 7: Let writer be stream.[[writer]]. + // Step 8: If writer is not undefined, perform + // ! WritableStreamDefaultWriterEnsureReadyPromiseRejected( + // writer, reason). + if (unwrappedStream->hasWriter()) { + Rooted<WritableStreamDefaultWriter*> unwrappedWriter( + cx, UnwrapWriterFromStream(cx, unwrappedStream)); + if (!unwrappedWriter) { + return false; + } + + if (!WritableStreamDefaultWriterEnsureReadyPromiseRejected( + cx, unwrappedWriter, reason)) { + return false; + } + } + + // Step 9: If ! WritableStreamHasOperationMarkedInFlight(stream) is false and + // controller.[[started]] is true, perform + // ! WritableStreamFinishErroring(stream). + if (!WritableStreamHasOperationMarkedInFlight(unwrappedStream) && + unwrappedController->started()) { + if (!WritableStreamFinishErroring(cx, unwrappedStream)) { + return false; + } + } + + return true; +} + +/** + * Streams spec, 4.4.4 WritableStreamFinishErroring ( stream ) + * Step 13: Upon fulfillment of promise, [...] + */ +static bool AbortRequestPromiseFulfilledHandler(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 13.a: Resolve abortRequest.[[promise]] with undefined. + Rooted<JSObject*> abortRequestPromise(cx, TargetFromHandler<JSObject>(args)); + if (!ResolvePromise(cx, abortRequestPromise, UndefinedHandleValue)) { + return false; + } + + // Step 13.b: Perform + // ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + Rooted<WritableStream*> unwrappedStream( + cx, UnwrapAndDowncastObject<WritableStream>( + cx, ExtraFromHandler<JSObject>(args))); + if (!unwrappedStream) { + return false; + } + + if (!WritableStreamRejectCloseAndClosedPromiseIfNeeded(cx, unwrappedStream)) { + return false; + } + + args.rval().setUndefined(); + return false; +} + +/** + * Streams spec, 4.4.4 WritableStreamFinishErroring ( stream ) + * Step 14: Upon rejection of promise with reason reason, [...] + */ +static bool AbortRequestPromiseRejectedHandler(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 14.a: Reject abortRequest.[[promise]] with reason. + Rooted<JSObject*> abortRequestPromise(cx, TargetFromHandler<JSObject>(args)); + if (!RejectPromise(cx, abortRequestPromise, args.get(0))) { + return false; + } + + // Step 14.b: Perform + // ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + Rooted<WritableStream*> unwrappedStream( + cx, UnwrapAndDowncastObject<WritableStream>( + cx, ExtraFromHandler<JSObject>(args))); + if (!unwrappedStream) { + return false; + } + + if (!WritableStreamRejectCloseAndClosedPromiseIfNeeded(cx, unwrappedStream)) { + return false; + } + + args.rval().setUndefined(); + return false; +} + +/** + * Streams spec, 4.4.4. + * WritableStreamFinishErroring ( stream ) + */ +MOZ_MUST_USE bool js::WritableStreamFinishErroring( + JSContext* cx, Handle<WritableStream*> unwrappedStream) { + // Step 1: Assert: stream.[[state]] is "erroring". + MOZ_ASSERT(unwrappedStream->erroring()); + + // Step 2: Assert: ! WritableStreamHasOperationMarkedInFlight(stream) is + // false. + MOZ_ASSERT(!WritableStreamHasOperationMarkedInFlight(unwrappedStream)); + + // Step 3: Set stream.[[state]] to "errored". + unwrappedStream->setErrored(); + + // Step 4: Perform ! stream.[[writableStreamController]].[[ErrorSteps]](). + { + Rooted<WritableStreamDefaultController*> unwrappedController( + cx, unwrappedStream->controller()); + if (!WritableStreamControllerErrorSteps(cx, unwrappedController)) { + return false; + } + } + + // Step 5: Let storedError be stream.[[storedError]]. + Rooted<Value> storedError(cx, unwrappedStream->storedError()); + if (!cx->compartment()->wrap(cx, &storedError)) { + return false; + } + + // Step 6: Repeat for each writeRequest that is an element of + // stream.[[writeRequests]], + { + Rooted<ListObject*> unwrappedWriteRequests( + cx, unwrappedStream->writeRequests()); + Rooted<JSObject*> writeRequest(cx); + uint32_t len = unwrappedWriteRequests->length(); + for (uint32_t i = 0; i < len; i++) { + // Step 6.a: Reject writeRequest with storedError. + writeRequest = &unwrappedWriteRequests->get(i).toObject(); + if (!RejectUnwrappedPromiseWithError(cx, &writeRequest, storedError)) { + return false; + } + } + } + + // Step 7: Set stream.[[writeRequests]] to an empty List. + // We optimize this to discard the list entirely. (A brief scan of the + // streams spec should verify that [[writeRequests]] is never accessed on a + // stream when |stream.[[state]] === "errored"|, set in step 3 above.) + unwrappedStream->clearWriteRequests(); + + // Step 8: If stream.[[pendingAbortRequest]] is undefined, + if (!unwrappedStream->hasPendingAbortRequest()) { + // Step 8.a: Perform + // ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + // Step 8.b: Return. + return WritableStreamRejectCloseAndClosedPromiseIfNeeded(cx, + unwrappedStream); + } + + // Step 9: Let abortRequest be stream.[[pendingAbortRequest]]. + // Step 10: Set stream.[[pendingAbortRequest]] to undefined. + Rooted<Value> abortRequestReason( + cx, unwrappedStream->pendingAbortRequestReason()); + if (!cx->compartment()->wrap(cx, &abortRequestReason)) { + return false; + } + Rooted<JSObject*> abortRequestPromise( + cx, unwrappedStream->pendingAbortRequestPromise()); + bool wasAlreadyErroring = + unwrappedStream->pendingAbortRequestWasAlreadyErroring(); + unwrappedStream->clearPendingAbortRequest(); + + // Step 11: If abortRequest.[[wasAlreadyErroring]] is true, + if (wasAlreadyErroring) { + // Step 11.a: Reject abortRequest.[[promise]] with storedError. + if (!RejectUnwrappedPromiseWithError(cx, &abortRequestPromise, + storedError)) { + return false; + } + + // Step 11.b: Perform + // ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + // Step 11.c: Return. + return WritableStreamRejectCloseAndClosedPromiseIfNeeded(cx, + unwrappedStream); + } + + // Step 12: Let promise be + // ! stream.[[writableStreamController]].[[AbortSteps]]( + // abortRequest.[[reason]]). + Rooted<WritableStreamDefaultController*> unwrappedController( + cx, unwrappedStream->controller()); + Rooted<JSObject*> promise( + cx, WritableStreamControllerAbortSteps(cx, unwrappedController, + abortRequestReason)); + if (!promise) { + return false; + } + cx->check(promise); + + if (!cx->compartment()->wrap(cx, &abortRequestPromise)) { + return false; + } + + Rooted<JSObject*> stream(cx, unwrappedStream); + if (!cx->compartment()->wrap(cx, &stream)) { + return false; + } + + // Step 13: Upon fulfillment of promise, [...] + // Step 14: Upon rejection of promise with reason reason, [...] + Rooted<JSObject*> onFulfilled( + cx, NewHandlerWithExtra(cx, AbortRequestPromiseFulfilledHandler, + abortRequestPromise, stream)); + if (!onFulfilled) { + return false; + } + Rooted<JSObject*> onRejected( + cx, NewHandlerWithExtra(cx, AbortRequestPromiseRejectedHandler, + abortRequestPromise, stream)); + if (!onRejected) { + return false; + } + + return JS::AddPromiseReactions(cx, promise, onFulfilled, onRejected); +} + +/** + * Streams spec, 4.4.5. + * WritableStreamFinishInFlightWrite ( stream ) + */ +MOZ_MUST_USE bool js::WritableStreamFinishInFlightWrite( + JSContext* cx, Handle<WritableStream*> unwrappedStream) { + // Step 1: Assert: stream.[[inFlightWriteRequest]] is not undefined. + MOZ_ASSERT(unwrappedStream->haveInFlightWriteRequest()); + + // Step 2: Resolve stream.[[inFlightWriteRequest]] with undefined. + if (!ResolveUnwrappedPromiseWithUndefined( + cx, &unwrappedStream->inFlightWriteRequest().toObject())) { + return false; + } + + // Step 3: Set stream.[[inFlightWriteRequest]] to undefined. + unwrappedStream->clearInFlightWriteRequest(cx); + MOZ_ASSERT(!unwrappedStream->haveInFlightWriteRequest()); + + return true; +} + +/** + * Streams spec, 4.4.6. + * WritableStreamFinishInFlightWriteWithError ( stream, error ) + */ +MOZ_MUST_USE bool js::WritableStreamFinishInFlightWriteWithError( + JSContext* cx, Handle<WritableStream*> unwrappedStream, + Handle<Value> error) { + cx->check(error); + + // Step 1: Assert: stream.[[inFlightWriteRequest]] is not undefined. + MOZ_ASSERT(unwrappedStream->haveInFlightWriteRequest()); + + // Step 2: Reject stream.[[inFlightWriteRequest]] with error. + if (!RejectUnwrappedPromiseWithError( + cx, &unwrappedStream->inFlightWriteRequest().toObject(), error)) { + return false; + } + + // Step 3: Set stream.[[inFlightWriteRequest]] to undefined. + unwrappedStream->clearInFlightWriteRequest(cx); + + // Step 4: Assert: stream.[[state]] is "writable" or "erroring". + MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); + + // Step 5: Perform ! WritableStreamDealWithRejection(stream, error). + return WritableStreamDealWithRejection(cx, unwrappedStream, error); +} + +/** + * Streams spec, 4.4.7. + * WritableStreamFinishInFlightClose ( stream ) + */ +MOZ_MUST_USE bool js::WritableStreamFinishInFlightClose( + JSContext* cx, Handle<WritableStream*> unwrappedStream) { + // Step 1: Assert: stream.[[inFlightCloseRequest]] is not undefined. + MOZ_ASSERT(unwrappedStream->haveInFlightCloseRequest()); + + // Step 2: Resolve stream.[[inFlightCloseRequest]] with undefined. + if (!ResolveUnwrappedPromiseWithUndefined( + cx, &unwrappedStream->inFlightCloseRequest().toObject())) { + return false; + } + + // Step 3: Set stream.[[inFlightCloseRequest]] to undefined. + unwrappedStream->clearInFlightCloseRequest(); + MOZ_ASSERT(unwrappedStream->inFlightCloseRequest().isUndefined()); + + // Step 4: Let state be stream.[[state]]. + // Step 5: Assert: stream.[[state]] is "writable" or "erroring". + MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); + + // Step 6: If state is "erroring", + if (unwrappedStream->erroring()) { + // Step 6.a: Set stream.[[storedError]] to undefined. + unwrappedStream->clearStoredError(); + + // Step 6.b: If stream.[[pendingAbortRequest]] is not undefined, + if (unwrappedStream->hasPendingAbortRequest()) { + // Step 6.b.i: Resolve stream.[[pendingAbortRequest]].[[promise]] with + // undefined. + if (!ResolveUnwrappedPromiseWithUndefined( + cx, unwrappedStream->pendingAbortRequestPromise())) { + return false; + } + + // Step 6.b.ii: Set stream.[[pendingAbortRequest]] to undefined. + unwrappedStream->clearPendingAbortRequest(); + } + } + + // Step 7: Set stream.[[state]] to "closed". + unwrappedStream->setClosed(); + + // Step 8: Let writer be stream.[[writer]]. + // Step 9: If writer is not undefined, resolve writer.[[closedPromise]] with + // undefined. + if (unwrappedStream->hasWriter()) { + WritableStreamDefaultWriter* unwrappedWriter = + UnwrapWriterFromStream(cx, unwrappedStream); + if (!unwrappedWriter) { + return false; + } + + if (!ResolveUnwrappedPromiseWithUndefined( + cx, unwrappedWriter->closedPromise())) { + return false; + } + } + + // Step 10: Assert: stream.[[pendingAbortRequest]] is undefined. + MOZ_ASSERT(!unwrappedStream->hasPendingAbortRequest()); + + // Step 11: Assert: stream.[[storedError]] is undefined. + MOZ_ASSERT(unwrappedStream->storedError().isUndefined()); + + return true; +} + +/** + * Streams spec, 4.4.8. + * WritableStreamFinishInFlightCloseWithError ( stream, error ) + */ +MOZ_MUST_USE bool js::WritableStreamFinishInFlightCloseWithError( + JSContext* cx, Handle<WritableStream*> unwrappedStream, + Handle<Value> error) { + cx->check(error); + + // Step 1: Assert: stream.[[inFlightCloseRequest]] is not undefined. + MOZ_ASSERT(unwrappedStream->haveInFlightCloseRequest()); + MOZ_ASSERT(!unwrappedStream->inFlightCloseRequest().isUndefined()); + + // Step 2: Reject stream.[[inFlightCloseRequest]] with error. + if (!RejectUnwrappedPromiseWithError( + cx, &unwrappedStream->inFlightCloseRequest().toObject(), error)) { + return false; + } + + // Step 3: Set stream.[[inFlightCloseRequest]] to undefined. + unwrappedStream->clearInFlightCloseRequest(); + + // Step 4: Assert: stream.[[state]] is "writable" or "erroring". + MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); + + // Step 5: If stream.[[pendingAbortRequest]] is not undefined, + if (unwrappedStream->hasPendingAbortRequest()) { + // Step 5.a: Reject stream.[[pendingAbortRequest]].[[promise]] with error. + if (!RejectUnwrappedPromiseWithError( + cx, unwrappedStream->pendingAbortRequestPromise(), error)) { + return false; + } + + // Step 5.b: Set stream.[[pendingAbortRequest]] to undefined. + unwrappedStream->clearPendingAbortRequest(); + } + + // Step 6: Perform ! WritableStreamDealWithRejection(stream, error). + return WritableStreamDealWithRejection(cx, unwrappedStream, error); +} + +/** + * Streams spec, 4.4.9. + * WritableStreamCloseQueuedOrInFlight ( stream ) + */ +bool js::WritableStreamCloseQueuedOrInFlight( + const WritableStream* unwrappedStream) { + // Step 1: If stream.[[closeRequest]] is undefined and + // stream.[[inFlightCloseRequest]] is undefined, return false. + // Step 2: Return true. + return unwrappedStream->haveCloseRequestOrInFlightCloseRequest(); +} + +/** + * Streams spec, 4.4.10. + * WritableStreamHasOperationMarkedInFlight ( stream ) + */ +bool WritableStreamHasOperationMarkedInFlight( + const WritableStream* unwrappedStream) { + // Step 1: If stream.[[inFlightWriteRequest]] is undefined and + // controller.[[inFlightCloseRequest]] is undefined, return false. + // Step 2: Return true. + return unwrappedStream->haveInFlightWriteRequest() || + unwrappedStream->haveInFlightCloseRequest(); +} + +/** + * Streams spec, 4.4.11. + * WritableStreamMarkCloseRequestInFlight ( stream ) + */ +void js::WritableStreamMarkCloseRequestInFlight( + WritableStream* unwrappedStream) { + // Step 1: Assert: stream.[[inFlightCloseRequest]] is undefined. + MOZ_ASSERT(!unwrappedStream->haveInFlightCloseRequest()); + + // Step 2: Assert: stream.[[closeRequest]] is not undefined. + MOZ_ASSERT(!unwrappedStream->closeRequest().isUndefined()); + + // Step 3: Set stream.[[inFlightCloseRequest]] to stream.[[closeRequest]]. + // Step 4: Set stream.[[closeRequest]] to undefined. + unwrappedStream->convertCloseRequestToInFlightCloseRequest(); +} + +/** + * Streams spec, 4.4.12. + * WritableStreamMarkFirstWriteRequestInFlight ( stream ) + */ +void js::WritableStreamMarkFirstWriteRequestInFlight( + WritableStream* unwrappedStream) { + // Step 1: Assert: stream.[[inFlightWriteRequest]] is undefined. + MOZ_ASSERT(!unwrappedStream->haveInFlightWriteRequest()); + + // Step 2: Assert: stream.[[writeRequests]] is not empty. + MOZ_ASSERT(unwrappedStream->writeRequests()->length() > 0); + + // Step 3: Let writeRequest be the first element of stream.[[writeRequests]]. + // Step 4: Remove writeRequest from stream.[[writeRequests]], shifting all + // other elements downward (so that the second becomes the first, and + // so on). + // Step 5: Set stream.[[inFlightWriteRequest]] to writeRequest. + // In our implementation, we model [[inFlightWriteRequest]] as merely the + // first element of [[writeRequests]], plus a flag indicating there's an + // in-flight request. Set the flag and be done with it. + unwrappedStream->setHaveInFlightWriteRequest(); +} + +/** + * Streams spec, 4.4.13. + * WritableStreamRejectCloseAndClosedPromiseIfNeeded ( stream ) + */ +MOZ_MUST_USE bool js::WritableStreamRejectCloseAndClosedPromiseIfNeeded( + JSContext* cx, Handle<WritableStream*> unwrappedStream) { + // Step 1: Assert: stream.[[state]] is "errored". + MOZ_ASSERT(unwrappedStream->errored()); + + Rooted<Value> storedError(cx, unwrappedStream->storedError()); + if (!cx->compartment()->wrap(cx, &storedError)) { + return false; + } + + // Step 2: If stream.[[closeRequest]] is not undefined, + if (!unwrappedStream->closeRequest().isUndefined()) { + // Step 2.a: Assert: stream.[[inFlightCloseRequest]] is undefined. + MOZ_ASSERT(unwrappedStream->inFlightCloseRequest().isUndefined()); + + // Step 2.b: Reject stream.[[closeRequest]] with stream.[[storedError]]. + if (!RejectUnwrappedPromiseWithError( + cx, &unwrappedStream->closeRequest().toObject(), storedError)) { + return false; + } + + // Step 2.c: Set stream.[[closeRequest]] to undefined. + unwrappedStream->clearCloseRequest(); + } + + // Step 3: Let writer be stream.[[writer]]. + // Step 4: If writer is not undefined, + if (unwrappedStream->hasWriter()) { + Rooted<WritableStreamDefaultWriter*> unwrappedWriter( + cx, UnwrapWriterFromStream(cx, unwrappedStream)); + if (!unwrappedWriter) { + return false; + } + + // Step 4.a: Reject writer.[[closedPromise]] with stream.[[storedError]]. + if (!RejectUnwrappedPromiseWithError(cx, unwrappedWriter->closedPromise(), + storedError)) { + return false; + } + + // Step 4.b: Set writer.[[closedPromise]].[[PromiseIsHandled]] to true. + Rooted<PromiseObject*> unwrappedClosedPromise( + cx, UnwrapAndDowncastObject<PromiseObject>( + cx, unwrappedWriter->closedPromise())); + if (!unwrappedClosedPromise) { + return false; + } + + js::SetSettledPromiseIsHandled(cx, unwrappedClosedPromise); + } + + return true; +} + +/** + * Streams spec, 4.4.14. + * WritableStreamUpdateBackpressure ( stream, backpressure ) + */ +MOZ_MUST_USE bool js::WritableStreamUpdateBackpressure( + JSContext* cx, Handle<WritableStream*> unwrappedStream, bool backpressure) { + // Step 1: Assert: stream.[[state]] is "writable". + MOZ_ASSERT(unwrappedStream->writable()); + + // Step 2: Assert: ! WritableStreamCloseQueuedOrInFlight(stream) is false. + MOZ_ASSERT(!WritableStreamCloseQueuedOrInFlight(unwrappedStream)); + + // Step 3: Let writer be stream.[[writer]]. + // Step 4: If writer is not undefined and backpressure is not + // stream.[[backpressure]], + if (unwrappedStream->hasWriter() && + backpressure != unwrappedStream->backpressure()) { + Rooted<WritableStreamDefaultWriter*> unwrappedWriter( + cx, UnwrapWriterFromStream(cx, unwrappedStream)); + if (!unwrappedWriter) { + return false; + } + + // Step 4.a: If backpressure is true, set writer.[[readyPromise]] to a new + // promise. + if (backpressure) { + Rooted<JSObject*> promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) { + return false; + } + + AutoRealm ar(cx, unwrappedWriter); + if (!cx->compartment()->wrap(cx, &promise)) { + return false; + } + unwrappedWriter->setReadyPromise(promise); + } else { + // Step 4.b: Otherwise, + // Step 4.b.i: Assert: backpressure is false. (guaranteed by type) + // Step 4.b.ii: Resolve writer.[[readyPromise]] with undefined. + if (!ResolveUnwrappedPromiseWithUndefined( + cx, unwrappedWriter->readyPromise())) { + return false; + } + } + } + + // Step 5: Set stream.[[backpressure]] to backpressure. + unwrappedStream->setBackpressure(backpressure); + + return true; +} diff --git a/js/src/builtin/streams/WritableStreamOperations.h b/js/src/builtin/streams/WritableStreamOperations.h new file mode 100644 index 0000000000..149caca401 --- /dev/null +++ b/js/src/builtin/streams/WritableStreamOperations.h @@ -0,0 +1,78 @@ +/* -*- 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/. */ + +/* Writable stream abstract operations. */ + +#ifndef builtin_streams_WritableStreamOperations_h +#define builtin_streams_WritableStreamOperations_h + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jstypes.h" // JS_PUBLIC_API +#include "js/RootingAPI.h" // JS::Handle +#include "js/Value.h" // JS::Value + +struct JS_PUBLIC_API JSContext; + +namespace js { + +class PromiseObject; +class WritableStream; + +extern JSObject* WritableStreamAbort( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream, + JS::Handle<JS::Value> reason); + +extern JSObject* WritableStreamClose( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream); + +extern MOZ_MUST_USE PromiseObject* WritableStreamAddWriteRequest( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream); + +extern MOZ_MUST_USE bool WritableStreamDealWithRejection( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream, + JS::Handle<JS::Value> error); + +extern MOZ_MUST_USE bool WritableStreamStartErroring( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream, + JS::Handle<JS::Value> reason); + +extern MOZ_MUST_USE bool WritableStreamFinishErroring( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream); + +extern MOZ_MUST_USE bool WritableStreamFinishInFlightWrite( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream); + +extern MOZ_MUST_USE bool WritableStreamFinishInFlightWriteWithError( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream, + JS::Handle<JS::Value> error); + +extern MOZ_MUST_USE bool WritableStreamFinishInFlightClose( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream); + +extern MOZ_MUST_USE bool WritableStreamFinishInFlightCloseWithError( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream, + JS::Handle<JS::Value> error); + +extern bool WritableStreamCloseQueuedOrInFlight( + const WritableStream* unwrappedStream); + +extern void WritableStreamMarkCloseRequestInFlight( + WritableStream* unwrappedStream); + +extern void WritableStreamMarkFirstWriteRequestInFlight( + WritableStream* unwrappedStream); + +extern MOZ_MUST_USE bool WritableStreamRejectCloseAndClosedPromiseIfNeeded( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream); + +extern MOZ_MUST_USE bool WritableStreamUpdateBackpressure( + JSContext* cx, JS::Handle<WritableStream*> unwrappedStream, + bool backpressure); + +} // namespace js + +#endif // builtin_streams_WritableStreamOperations_h diff --git a/js/src/builtin/streams/WritableStreamWriterOperations.cpp b/js/src/builtin/streams/WritableStreamWriterOperations.cpp new file mode 100644 index 0000000000..cf8a6d1c84 --- /dev/null +++ b/js/src/builtin/streams/WritableStreamWriterOperations.cpp @@ -0,0 +1,446 @@ +/* -*- 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/. */ + +/* Writable stream writer abstract operations. */ + +#include "builtin/streams/WritableStreamWriterOperations.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "jsapi.h" // JS_ReportErrorNumberASCII, JS_ReportErrorASCII + +#include "builtin/streams/MiscellaneousOperations.h" // js::PromiseRejectedWithPendingError +#include "builtin/streams/WritableStream.h" // js::WritableStream +#include "builtin/streams/WritableStreamDefaultController.h" // js::WritableStream::controller +#include "builtin/streams/WritableStreamDefaultControllerOperations.h" // js::WritableStreamDefaultController{Close,GetDesiredSize} +#include "builtin/streams/WritableStreamDefaultWriter.h" // js::WritableStreamDefaultWriter +#include "builtin/streams/WritableStreamOperations.h" // js::WritableStream{Abort,CloseQueuedOrInFlight} +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/Promise.h" // JS::PromiseState +#include "js/Value.h" // JS::Value, JS::{Int32,Null}Value +#include "vm/Compartment.h" // JS::Compartment +#include "vm/Interpreter.h" // js::GetAndClearException +#include "vm/JSContext.h" // JSContext +#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseResolvedWithUndefined + +#include "builtin/Promise-inl.h" // js::SetSettledPromiseIsHandled +#include "builtin/streams/MiscellaneousOperations-inl.h" // js::ResolveUnwrappedPromiseWithUndefined +#include "builtin/streams/WritableStream-inl.h" // js::WritableStream::setCloseRequest +#include "builtin/streams/WritableStreamDefaultWriter-inl.h" // js::UnwrapStreamFromWriter +#include "vm/Compartment-inl.h" // js::UnwrapAnd{DowncastObject,TypeCheckThis} +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/Realm-inl.h" // js::AutoRealm + +using JS::Handle; +using JS::Int32Value; +using JS::MutableHandle; +using JS::NullValue; +using JS::NumberValue; +using JS::Rooted; +using JS::Value; + +using js::AutoRealm; +using js::PromiseObject; +using js::UnwrapAndDowncastObject; +using js::WritableStreamDefaultWriter; + +/*** 4.6. Writable stream writer abstract operations ************************/ + +/** + * Streams spec, 4.6.2. + * WritableStreamDefaultWriterAbort ( writer, reason ) + */ +JSObject* js::WritableStreamDefaultWriterAbort( + JSContext* cx, Handle<WritableStreamDefaultWriter*> unwrappedWriter, + Handle<Value> reason) { + cx->check(reason); + + // Step 1: Let stream be writer.[[ownerWritableStream]]. + // Step 2: Assert: stream is not undefined. + MOZ_ASSERT(unwrappedWriter->hasStream()); + Rooted<WritableStream*> unwrappedStream( + cx, UnwrapStreamFromWriter(cx, unwrappedWriter)); + if (!unwrappedStream) { + return nullptr; + } + + // Step 3: Return ! WritableStreamAbort(stream, reason). + return WritableStreamAbort(cx, unwrappedStream, reason); +} + +/** + * Streams spec, 4.6.3. + * WritableStreamDefaultWriterClose ( writer ) + */ +PromiseObject* js::WritableStreamDefaultWriterClose( + JSContext* cx, Handle<WritableStreamDefaultWriter*> unwrappedWriter) { + // Step 1: Let stream be writer.[[ownerWritableStream]]. + // Step 2: Assert: stream is not undefined. + MOZ_ASSERT(unwrappedWriter->hasStream()); + Rooted<WritableStream*> unwrappedStream( + cx, UnwrapStreamFromWriter(cx, unwrappedWriter)); + if (!unwrappedStream) { + return PromiseRejectedWithPendingError(cx); + } + + // Step 3: Let state be stream.[[state]]. + // Step 4: If state is "closed" or "errored", return a promise rejected with a + // TypeError exception. + if (unwrappedStream->closed() || unwrappedStream->errored()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAM_CLOSED_OR_ERRORED); + return PromiseRejectedWithPendingError(cx); + } + + // Step 5: Assert: state is "writable" or "erroring". + MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); + + // Step 6: Assert: ! WritableStreamCloseQueuedOrInFlight(stream) is false. + MOZ_ASSERT(!WritableStreamCloseQueuedOrInFlight(unwrappedStream)); + + // Step 7: Let promise be a new promise. + Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) { + return nullptr; + } + + // Step 8: Set stream.[[closeRequest]] to promise. + { + AutoRealm ar(cx, unwrappedStream); + Rooted<JSObject*> closeRequest(cx, promise); + if (!cx->compartment()->wrap(cx, &closeRequest)) { + return nullptr; + } + + unwrappedStream->setCloseRequest(closeRequest); + } + + // Step 9: If stream.[[backpressure]] is true and state is "writable", resolve + // writer.[[readyPromise]] with undefined. + if (unwrappedStream->backpressure() && unwrappedStream->writable()) { + if (!ResolveUnwrappedPromiseWithUndefined( + cx, unwrappedWriter->readyPromise())) { + return nullptr; + } + } + + // Step 10: Perform + // ! WritableStreamDefaultControllerClose( + // stream.[[writableStreamController]]). + Rooted<WritableStreamDefaultController*> unwrappedController( + cx, unwrappedStream->controller()); + if (!WritableStreamDefaultControllerClose(cx, unwrappedController)) { + return nullptr; + } + + // Step 11: Return promise. + return promise; +} + +/** + * Streams spec. + * WritableStreamDefaultWriterCloseWithErrorPropagation ( writer ) + */ +PromiseObject* js::WritableStreamDefaultWriterCloseWithErrorPropagation( + JSContext* cx, Handle<WritableStreamDefaultWriter*> unwrappedWriter) { + // Step 1: Let stream be writer.[[ownerWritableStream]]. + // Step 2: Assert: stream is not undefined. + WritableStream* unwrappedStream = UnwrapStreamFromWriter(cx, unwrappedWriter); + if (!unwrappedStream) { + return nullptr; + } + + // Step 3: Let state be stream.[[state]]. + // Step 4: If ! WritableStreamCloseQueuedOrInFlight(stream) is true or state + // is "closed", return a promise resolved with undefined. + if (WritableStreamCloseQueuedOrInFlight(unwrappedStream) || + unwrappedStream->closed()) { + return PromiseResolvedWithUndefined(cx); + } + + // Step 5: If state is "errored", return a promise rejected with + // stream.[[storedError]]. + if (unwrappedStream->errored()) { + Rooted<Value> storedError(cx, unwrappedStream->storedError()); + if (!cx->compartment()->wrap(cx, &storedError)) { + return nullptr; + } + + return PromiseObject::unforgeableReject(cx, storedError); + } + + // Step 6: Assert: state is "writable" or "erroring". + MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); + + // Step 7: Return ! WritableStreamDefaultWriterClose(writer). + return WritableStreamDefaultWriterClose(cx, unwrappedWriter); +} + +using GetField = JSObject* (WritableStreamDefaultWriter::*)() const; +using SetField = void (WritableStreamDefaultWriter::*)(JSObject*); + +static bool EnsurePromiseRejected( + JSContext* cx, Handle<WritableStreamDefaultWriter*> unwrappedWriter, + GetField getField, SetField setField, Handle<Value> error) { + cx->check(error); + + Rooted<PromiseObject*> unwrappedPromise( + cx, UnwrapAndDowncastObject<PromiseObject>( + cx, (unwrappedWriter->*getField)())); + if (!unwrappedPromise) { + return false; + } + + // 4.6.{5,6} step 1: If writer.[[<field>]].[[PromiseState]] is "pending", + // reject writer.[[<field>]] with error. + if (unwrappedPromise->state() == JS::PromiseState::Pending) { + if (!RejectUnwrappedPromiseWithError(cx, unwrappedPromise, error)) { + return false; + } + } else { + // 4.6.{5,6} step 2: Otherwise, set writer.[[<field>]] to a promise rejected + // with error. + Rooted<JSObject*> rejectedWithError( + cx, PromiseObject::unforgeableReject(cx, error)); + if (!rejectedWithError) { + return false; + } + + { + AutoRealm ar(cx, unwrappedWriter); + if (!cx->compartment()->wrap(cx, &rejectedWithError)) { + return false; + } + (unwrappedWriter->*setField)(rejectedWithError); + } + + // Directly-unobservable rejected promises aren't collapsed like resolved + // promises, and this promise is created in the current realm, so it's + // always an actual Promise. + unwrappedPromise = &rejectedWithError->as<PromiseObject>(); + } + + // 4.6.{5,6} step 3: Set writer.[[<field>]].[[PromiseIsHandled]] to true. + js::SetSettledPromiseIsHandled(cx, unwrappedPromise); + return true; +} + +/** + * Streams spec, 4.6.5. + * WritableStreamDefaultWriterEnsureClosedPromiseRejected( writer, error ) + */ +MOZ_MUST_USE bool js::WritableStreamDefaultWriterEnsureClosedPromiseRejected( + JSContext* cx, Handle<WritableStreamDefaultWriter*> unwrappedWriter, + Handle<Value> error) { + return EnsurePromiseRejected( + cx, unwrappedWriter, &WritableStreamDefaultWriter::closedPromise, + &WritableStreamDefaultWriter::setClosedPromise, error); +} + +/** + * Streams spec, 4.6.6. + * WritableStreamDefaultWriterEnsureReadyPromiseRejected( writer, error ) + */ +MOZ_MUST_USE bool js::WritableStreamDefaultWriterEnsureReadyPromiseRejected( + JSContext* cx, Handle<WritableStreamDefaultWriter*> unwrappedWriter, + Handle<Value> error) { + return EnsurePromiseRejected( + cx, unwrappedWriter, &WritableStreamDefaultWriter::readyPromise, + &WritableStreamDefaultWriter::setReadyPromise, error); +} + +/** + * Streams spec, 4.6.7. + * WritableStreamDefaultWriterGetDesiredSize ( writer ) + */ +bool js::WritableStreamDefaultWriterGetDesiredSize( + JSContext* cx, Handle<WritableStreamDefaultWriter*> unwrappedWriter, + MutableHandle<Value> size) { + // Step 1: Let stream be writer.[[ownerWritableStream]]. + const WritableStream* unwrappedStream = + UnwrapStreamFromWriter(cx, unwrappedWriter); + if (!unwrappedStream) { + return false; + } + + // Step 2: Let state be stream.[[state]]. + // Step 3: If state is "errored" or "erroring", return null. + if (unwrappedStream->errored() || unwrappedStream->erroring()) { + size.setNull(); + } + // Step 4: If state is "closed", return 0. + else if (unwrappedStream->closed()) { + size.setInt32(0); + } + // Step 5: Return + // ! WritableStreamDefaultControllerGetDesiredSize( + // stream.[[writableStreamController]]). + else { + size.setNumber(WritableStreamDefaultControllerGetDesiredSize( + unwrappedStream->controller())); + } + + return true; +} + +/** + * Streams spec, 4.6.8. + * WritableStreamDefaultWriterRelease ( writer ) + */ +bool js::WritableStreamDefaultWriterRelease( + JSContext* cx, Handle<WritableStreamDefaultWriter*> unwrappedWriter) { + // Step 1: Let stream be writer.[[ownerWritableStream]]. + // Step 2: Assert: stream is not undefined. + MOZ_ASSERT(unwrappedWriter->hasStream()); + Rooted<WritableStream*> unwrappedStream( + cx, UnwrapStreamFromWriter(cx, unwrappedWriter)); + if (!unwrappedStream) { + return false; + } + + // Step 3: Assert: stream.[[writer]] is writer. +#ifdef DEBUG + { + WritableStreamDefaultWriter* unwrappedStreamWriter = + UnwrapWriterFromStream(cx, unwrappedStream); + if (!unwrappedStreamWriter) { + return false; + } + + MOZ_ASSERT(unwrappedStreamWriter == unwrappedWriter); + } +#endif + + // Step 4: Let releasedError be a new TypeError. + Rooted<Value> releasedError(cx); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAM_CANT_RELEASE_ALREADY_CLOSED); + if (!cx->isExceptionPending() || !GetAndClearException(cx, &releasedError)) { + return false; + } + + // Step 5: Perform + // ! WritableStreamDefaultWriterEnsureReadyPromiseRejected( + // writer, releasedError). + if (!WritableStreamDefaultWriterEnsureReadyPromiseRejected( + cx, unwrappedWriter, releasedError)) { + return false; + } + + // Step 6: Perform + // ! WritableStreamDefaultWriterEnsureClosedPromiseRejected( + // writer, releasedError). + if (!WritableStreamDefaultWriterEnsureClosedPromiseRejected( + cx, unwrappedWriter, releasedError)) { + return false; + } + + // Step 7: Set stream.[[writer]] to undefined. + unwrappedStream->clearWriter(); + + // Step 8: Set writer.[[ownerWritableStream]] to undefined. + unwrappedWriter->clearStream(); + return true; +} + +/** + * Streams spec, 4.6.9. + * WritableStreamDefaultWriterWrite ( writer, chunk ) + */ +PromiseObject* js::WritableStreamDefaultWriterWrite( + JSContext* cx, Handle<WritableStreamDefaultWriter*> unwrappedWriter, + Handle<Value> chunk) { + cx->check(chunk); + + // Step 1: Let stream be writer.[[ownerWritableStream]]. + // Step 2: Assert: stream is not undefined. + MOZ_ASSERT(unwrappedWriter->hasStream()); + Rooted<WritableStream*> unwrappedStream( + cx, UnwrapStreamFromWriter(cx, unwrappedWriter)); + if (!unwrappedStream) { + return nullptr; + } + + // Step 3: Let controller be stream.[[writableStreamController]]. + Rooted<WritableStreamDefaultController*> unwrappedController( + cx, unwrappedStream->controller()); + + // Step 4: Let chunkSize be + // ! WritableStreamDefaultControllerGetChunkSize(controller, chunk). + Rooted<Value> chunkSize(cx); + if (!WritableStreamDefaultControllerGetChunkSize(cx, unwrappedController, + chunk, &chunkSize)) { + return nullptr; + } + cx->check(chunkSize); + + // Step 5: If stream is not equal to writer.[[ownerWritableStream]], return a + // promise rejected with a TypeError exception. + // (This is just an obscure way of saying "If step 4 caused writer's lock on + // stream to be released", or concretely, "If writer.[[ownerWritableStream]] + // is [now, newly] undefined".) + if (!unwrappedWriter->hasStream()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAM_RELEASED_DURING_WRITE); + return PromiseRejectedWithPendingError(cx); + } + + auto RejectWithStoredError = + [](JSContext* cx, + Handle<WritableStream*> unwrappedStream) -> PromiseObject* { + Rooted<Value> storedError(cx, unwrappedStream->storedError()); + if (!cx->compartment()->wrap(cx, &storedError)) { + return nullptr; + } + + return PromiseObject::unforgeableReject(cx, storedError); + }; + + // Step 6: Let state be stream.[[state]]. + // Step 7: If state is "errored", return a promise rejected with + // stream.[[storedError]]. + if (unwrappedStream->errored()) { + return RejectWithStoredError(cx, unwrappedStream); + } + + // Step 8: If ! WritableStreamCloseQueuedOrInFlight(stream) is true or state + // is "closed", return a promise rejected with a TypeError exception + // indicating that the stream is closing or closed. + if (WritableStreamCloseQueuedOrInFlight(unwrappedStream) || + unwrappedStream->closed()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_WRITABLESTREAM_WRITE_CLOSING_OR_CLOSED); + return PromiseRejectedWithPendingError(cx); + } + + // Step 9: If state is "erroring", return a promise rejected with + // stream.[[storedError]]. + if (unwrappedStream->erroring()) { + return RejectWithStoredError(cx, unwrappedStream); + } + + // Step 10: Assert: state is "writable". + MOZ_ASSERT(unwrappedStream->writable()); + + // Step 11: Let promise be ! WritableStreamAddWriteRequest(stream). + Rooted<PromiseObject*> promise( + cx, WritableStreamAddWriteRequest(cx, unwrappedStream)); + if (!promise) { + return nullptr; + } + + // Step 12: Perform + // ! WritableStreamDefaultControllerWrite(controller, chunk, + // chunkSize). + if (!WritableStreamDefaultControllerWrite(cx, unwrappedController, chunk, + chunkSize)) { + return nullptr; + } + + // Step 13: Return promise. + return promise; +} diff --git a/js/src/builtin/streams/WritableStreamWriterOperations.h b/js/src/builtin/streams/WritableStreamWriterOperations.h new file mode 100644 index 0000000000..241b166354 --- /dev/null +++ b/js/src/builtin/streams/WritableStreamWriterOperations.h @@ -0,0 +1,56 @@ +/* -*- 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/. */ + +/* Writable stream writer abstract operations. */ + +#ifndef builtin_streams_WritableStreamWriterOperations_h +#define builtin_streams_WritableStreamWriterOperations_h + +#include "mozilla/Attributes.h" // MOZ_MUST_USE + +#include "js/RootingAPI.h" // JS::{,Mutable}Handle +#include "js/Value.h" // JS::Value + +struct JS_PUBLIC_API JSContext; +class JS_PUBLIC_API JSObject; + +namespace js { + +class PromiseObject; +class WritableStreamDefaultWriter; + +extern JSObject* WritableStreamDefaultWriterAbort( + JSContext* cx, JS::Handle<WritableStreamDefaultWriter*> unwrappedWriter, + JS::Handle<JS::Value> reason); + +extern PromiseObject* WritableStreamDefaultWriterClose( + JSContext* cx, JS::Handle<WritableStreamDefaultWriter*> unwrappedWriter); + +extern PromiseObject* WritableStreamDefaultWriterCloseWithErrorPropagation( + JSContext* cx, JS::Handle<WritableStreamDefaultWriter*> unwrappedWriter); + +extern MOZ_MUST_USE bool WritableStreamDefaultWriterEnsureClosedPromiseRejected( + JSContext* cx, JS::Handle<WritableStreamDefaultWriter*> unwrappedWriter, + JS::Handle<JS::Value> error); + +extern MOZ_MUST_USE bool WritableStreamDefaultWriterEnsureReadyPromiseRejected( + JSContext* cx, JS::Handle<WritableStreamDefaultWriter*> unwrappedWriter, + JS::Handle<JS::Value> error); + +extern MOZ_MUST_USE bool WritableStreamDefaultWriterGetDesiredSize( + JSContext* cx, JS::Handle<WritableStreamDefaultWriter*> unwrappedWriter, + JS::MutableHandle<JS::Value> size); + +extern MOZ_MUST_USE bool WritableStreamDefaultWriterRelease( + JSContext* cx, JS::Handle<WritableStreamDefaultWriter*> unwrappedWriter); + +extern PromiseObject* WritableStreamDefaultWriterWrite( + JSContext* cx, JS::Handle<WritableStreamDefaultWriter*> unwrappedWriter, + JS::Handle<JS::Value> chunk); + +} // namespace js + +#endif // builtin_streams_WritableStreamWriterOperations_h |