From 2aa4a82499d4becd2284cdb482213d541b8804dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 16:29:10 +0200 Subject: Adding upstream version 86.0.1. Signed-off-by: Daniel Baumann --- js/src/builtin/streams/ReadableStream.cpp | 552 ++++++++++++++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 js/src/builtin/streams/ReadableStream.cpp (limited to 'js/src/builtin/streams/ReadableStream.cpp') 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()) { + return JS::ReadableStreamMode::Default; + } + return controller->as().hasExternalSource() + ? JS::ReadableStreamMode::ExternalSource + : JS::ReadableStreamMode::Byte; +} + +ReadableStream* ReadableStream::createExternalSourceStream( + JSContext* cx, JS::ReadableStreamUnderlyingSource* source, + void* nsISupportsObject_alreadyAddreffed /* = nullptr */, + Handle proto /* = nullptr */) { + Rooted 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 underlyingSource(cx, args.get(0)); + if (underlyingSource.isUndefined()) { + JSObject* emptyObj = NewBuiltinClassInstance(cx); + if (!emptyObj) { + return false; + } + underlyingSource = ObjectValue(*emptyObj); + } + + Rooted strategy(cx, args.get(1)); + if (strategy.isUndefined()) { + JSObject* emptyObj = NewBuiltinClassInstance(cx); + if (!emptyObj) { + return false; + } + strategy = ObjectValue(*emptyObj); + } + + // Implicit in the spec: Set this to + // OrdinaryCreateFromConstructor(NewTarget, ...). + // Step 1: Perform ! InitializeReadableStream(this). + Rooted proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_ReadableStream, + &proto)) { + return false; + } + Rooted stream(cx, + ReadableStream::create(cx, nullptr, proto)); + if (!stream) { + return false; + } + + // Step 2: Let size be ? GetV(strategy, "size"). + Rooted size(cx); + if (!GetProperty(cx, strategy, cx->names().size, &size)) { + return false; + } + + // Step 3: Let highWaterMark be ? GetV(strategy, "highWaterMark"). + Rooted highWaterMarkVal(cx); + if (!GetProperty(cx, strategy, cx->names().highWaterMark, + &highWaterMarkVal)) { + return false; + } + + // Step 4: Let type be ? GetV(underlyingSource, "type"). + Rooted type(cx); + if (!GetProperty(cx, underlyingSource, cx->names().type, &type)) { + return false; + } + + // Step 5: Let typeString be ? ToString(type). + Rooted typeString(cx, ToString(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 unwrappedStream( + cx, UnwrapAndTypeCheckThis(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 unwrappedStream( + cx, UnwrapAndTypeCheckThis(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 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 optionsVal(cx, args.get(0)); + if (optionsVal.isUndefined()) { + JSObject* emptyObj = NewBuiltinClassInstance(cx); + if (!emptyObj) { + return false; + } + optionsVal.setObject(*emptyObj); + } + Rooted modeVal(cx); + if (!GetProperty(cx, optionsVal, cx->names().mode, &modeVal)) { + return false; + } + + // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception. + Rooted unwrappedStream( + cx, UnwrapAndTypeCheckThis(cx, args, "getReader")); + if (!unwrappedStream) { + return false; + } + + // Step 2: If mode is undefined, return + // ? AcquireReadableStreamDefaultReader(this, true). + Rooted reader(cx); + if (modeVal.isUndefined()) { + reader = CreateReadableStreamDefaultReader(cx, unwrappedStream, + ForAuthorCodeBool::Yes); + } else { + // Step 3: Set mode to ? ToString(mode) (implicit). + Rooted mode(cx, ToString(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 options(cx, args.get(1)); + if (options.isUndefined()) { + JSObject* emptyObj = NewBuiltinClassInstance(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 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 unwrappedThis( + cx, UnwrapAndTypeCheckThis(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 unwrappedDest( + cx, UnwrapAndTypeCheckArgument(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 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 unwrappedStream( + cx, UnwrapAndTypeCheckThis(cx, args, "tee")); + if (!unwrappedStream) { + return false; + } + + // Step 2: Let branches be ? ReadableStreamTee(this, false). + Rooted branch1(cx); + Rooted branch2(cx); + if (!ReadableStreamTee(cx, unwrappedStream, false, &branch1, &branch2)) { + return false; + } + + // Step 3: Return ! CreateArrayFromList(branches). + Rooted 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 ctor, + Handle 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 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, + js::GenericCreatePrototype, + 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_}; -- cgit v1.2.3